Alert Webhook improvements (#1703)

* WIP with webhook SDK function and types

* JSDocs added to the schema

* Webhooks are working

* Expanded the alert docs

* Remove duplicate export of waitUntil.js

* Use uncrypto

* Don’t rate limit webhooks

* Create slow-olives-fix.md
This commit is contained in:
Matt Aitken
2025-02-13 13:44:36 +00:00
committed by GitHub
parent a2c70b450a
commit 440d413ce8
17 changed files with 1023 additions and 328 deletions
+16
View File
@@ -0,0 +1,16 @@
---
"@trigger.dev/sdk": patch
---
You can add Alerts in the dashboard. One of these is a webhook, which this change greatly improves.
The main change is that there's now an SDK function to verify and parse them (similar to Stripe SDK).
```ts
const event = await webhooks.constructEvent(request, process.env.ALERT_WEBHOOK_SECRET!);
```
If the signature you provide matches the one from the dashboard when you create the webhook, you will get a nicely typed object back for these three types:
- "alert.run.failed"
- "alert.deployment.success"
- "alert.deployment.failed"
@@ -4,6 +4,7 @@ import { EncryptedSecretValueSchema } from "~/services/secrets/secretStore.serve
export const ProjectAlertWebhookProperties = z.object({
secret: EncryptedSecretValueSchema,
url: z.string(),
version: z.string().optional().default("v1"),
});
export type ProjectAlertWebhookProperties = z.infer<typeof ProjectAlertWebhookProperties>;
@@ -0,0 +1,50 @@
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { webhooks } from "@trigger.dev/sdk/v3";
import { WebhookError } from "@trigger.dev/sdk/v3";
import { logger } from "~/services/logger.server";
/*
This route is for testing our webhooks
*/
export async function action({ request }: ActionFunctionArgs) {
// Make sure this is a POST request
if (request.method !== "POST") {
return json({ error: "[Webhook Internal Test] Method not allowed" }, { status: 405 });
}
const clonedRequest = request.clone();
const rawBody = await clonedRequest.text();
logger.log("[Webhook Internal Test] Raw body:", { rawBody });
try {
// Construct and verify the webhook event
const event = await webhooks.constructEvent(request, process.env.INTERNAL_TEST_WEBHOOK_SECRET!);
// Handle the webhook event
logger.log("[Webhook Internal Test] Received verified webhook:", event);
// Process the event based on its type
switch (event.type) {
default:
logger.log(`[Webhook Internal Test] Unhandled event type: ${event.type}`);
}
// Return a success response
return json({ received: true }, { status: 200 });
} catch (err) {
// Handle webhook errors
if (err instanceof WebhookError) {
logger.error("[Webhook Internal Test] Webhook error:", { message: err.message });
return json({ error: err.message }, { status: 400 });
}
if (err instanceof Error) {
logger.error("[Webhook Internal Test] Error processing webhook:", { message: err.message });
return json({ error: err.message }, { status: 400 });
}
// Handle other errors
logger.error("[Webhook Internal Test] Error processing webhook:", { err });
return json({ error: "Internal server error" }, { status: 500 });
}
}
@@ -100,6 +100,7 @@ export class CreateAlertChannelService extends BaseService {
return {
url: channel.url,
secret: await encryptSecret(env.ENCRYPTION_KEY, channel.secret ?? nanoid()),
version: "v2",
};
case "SLACK":
return {
@@ -6,7 +6,14 @@ import {
WebAPIRateLimitedError,
WebAPIRequestError,
} from "@slack/web-api";
import { TaskRunError, createJsonErrorObject } from "@trigger.dev/core/v3";
import {
Webhook,
TaskRunError,
createJsonErrorObject,
RunFailedWebhook,
DeploymentFailedWebhook,
DeploymentSuccessWebhook,
} from "@trigger.dev/core/v3";
import assertNever from "assert-never";
import { subtle } from "crypto";
import { Prisma, prisma, PrismaClientOrTransaction } from "~/db.server";
@@ -29,8 +36,11 @@ import { commonWorker } from "~/v3/commonWorker.server";
import { FINAL_ATTEMPT_STATUSES } from "~/v3/taskStatus";
import { BaseService } from "../baseService.server";
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
import { ProjectAlertType } from "@trigger.dev/database";
import { ProjectAlertChannelType, ProjectAlertType } from "@trigger.dev/database";
import { alertsRateLimiter } from "~/v3/alertsRateLimiter.server";
import { v3RunPath } from "~/utils/pathBuilder";
import { isOOMError } from "../completeAttempt.server";
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
type FoundAlert = Prisma.Result<
typeof prisma.projectAlert,
@@ -193,43 +203,7 @@ export class DeliverAlertService extends BaseService {
switch (alert.type) {
case "TASK_RUN_ATTEMPT": {
if (alert.taskRunAttempt) {
const parseError = TaskRunError.safeParse(alert.taskRunAttempt.error);
let taskRunError: TaskRunError;
if (!parseError.success) {
logger.error("[DeliverAlert] Attempt: Failed to parse task run error", {
issues: parseError.error.issues,
taskAttemptError: alert.taskRunAttempt.error,
});
taskRunError = {
type: "STRING_ERROR" as const,
raw: "No error on task",
};
} else {
taskRunError = parseError.data;
}
await sendAlertEmail({
email: "alert-attempt",
to: emailProperties.data.email,
taskIdentifier: alert.taskRunAttempt.taskRun.taskIdentifier,
fileName: alert.taskRunAttempt.backgroundWorkerTask.filePath,
exportName: alert.taskRunAttempt.backgroundWorkerTask.exportName,
version: alert.taskRunAttempt.backgroundWorker.version,
environment: alert.environment.slug,
error: createJsonErrorObject(taskRunError),
attemptLink: `${env.APP_ORIGIN}/projects/v3/${alert.project.externalRef}/runs/${alert.taskRunAttempt.taskRun.friendlyId}`,
organization: alert.project.organization.title,
});
} else {
logger.error("[DeliverAlert] Task run attempt not found", {
alert,
});
}
logger.error("[DeliverAlert] Task run attempt alerts are deprecated, not sending anything");
break;
}
case "TASK_RUN": {
@@ -332,102 +306,110 @@ export class DeliverAlertService extends BaseService {
switch (alert.type) {
case "TASK_RUN_ATTEMPT": {
if (alert.taskRunAttempt) {
const taskRunError = TaskRunError.safeParse(alert.taskRunAttempt.error);
if (!taskRunError.success) {
logger.error("[DeliverAlert] Attempt: Failed to parse task run error", {
issues: taskRunError.error.issues,
taskAttemptError: alert.taskRunAttempt.error,
});
return;
}
const error = createJsonErrorObject(taskRunError.data);
const payload = {
task: {
id: alert.taskRunAttempt.taskRun.taskIdentifier,
filePath: alert.taskRunAttempt.backgroundWorkerTask.filePath,
exportName: alert.taskRunAttempt.backgroundWorkerTask.exportName,
},
attempt: {
id: alert.taskRunAttempt.friendlyId,
number: alert.taskRunAttempt.number,
startedAt: alert.taskRunAttempt.startedAt,
status: alert.taskRunAttempt.status,
},
run: {
id: alert.taskRunAttempt.taskRun.friendlyId,
isTest: alert.taskRunAttempt.taskRun.isTest,
createdAt: alert.taskRunAttempt.taskRun.createdAt,
idempotencyKey: alert.taskRunAttempt.taskRun.idempotencyKey,
},
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
error,
};
await this.#deliverWebhook(payload, webhookProperties.data);
} else {
logger.error("[DeliverAlert] Task run attempt not found", {
alert,
});
}
logger.error("[DeliverAlert] Task run attempt alerts are deprecated, not sending anything");
break;
}
case "TASK_RUN": {
if (alert.taskRun) {
const error = this.#getRunError(alert);
const payload = {
task: {
id: alert.taskRun.taskIdentifier,
fileName: alert.taskRun.lockedBy?.filePath ?? "Unknown",
exportName: alert.taskRun.lockedBy?.exportName ?? "Unknown",
},
run: {
id: alert.taskRun.friendlyId,
isTest: alert.taskRun.isTest,
createdAt: alert.taskRun.createdAt,
idempotencyKey: alert.taskRun.idempotencyKey,
},
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
error,
};
switch (webhookProperties.data.version) {
case "v1": {
const payload = {
task: {
id: alert.taskRun.taskIdentifier,
fileName: alert.taskRun.lockedBy?.filePath ?? "Unknown",
exportName: alert.taskRun.lockedBy?.exportName ?? "Unknown",
},
run: {
id: alert.taskRun.friendlyId,
isTest: alert.taskRun.isTest,
createdAt: alert.taskRun.createdAt,
idempotencyKey: alert.taskRun.idempotencyKey,
},
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
error,
};
await this.#deliverWebhook(payload, webhookProperties.data);
await this.#deliverWebhook(payload, webhookProperties.data);
break;
}
case "v2": {
const payload: RunFailedWebhook = {
id: alert.id,
created: alert.createdAt,
webhookVersion: "v1",
type: "alert.run.failed",
object: {
task: {
id: alert.taskRun.taskIdentifier,
filePath: alert.taskRun.lockedBy?.filePath ?? "Unknown",
exportName: alert.taskRun.lockedBy?.exportName ?? "Unknown",
version: alert.taskRun.taskVersion ?? "Unknown",
sdkVersion: alert.taskRun.sdkVersion ?? "Unknown",
cliVersion: alert.taskRun.cliVersion ?? "Unknown",
},
run: {
id: alert.taskRun.friendlyId,
number: alert.taskRun.number,
status: ApiRetrieveRunPresenter.apiStatusFromRunStatus(alert.taskRun.status),
createdAt: alert.taskRun.createdAt,
startedAt: alert.taskRun.startedAt ?? undefined,
completedAt: alert.taskRun.completedAt ?? undefined,
isTest: alert.taskRun.isTest,
idempotencyKey: alert.taskRun.idempotencyKey ?? undefined,
tags: alert.taskRun.runTags,
error,
isOutOfMemoryError: isOOMError(error),
machine: alert.taskRun.machinePreset ?? "Unknown",
dashboardUrl: `${env.APP_ORIGIN}${v3RunPath(
alert.project.organization,
alert.project,
alert.taskRun
)}`,
},
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
},
};
await this.#deliverWebhook(payload, webhookProperties.data);
break;
}
default: {
throw new Error(`Unknown webhook version: ${webhookProperties.data.version}`);
}
}
} else {
logger.error("[DeliverAlert] Task run not found", {
alert,
@@ -450,34 +432,80 @@ export class DeliverAlertService extends BaseService {
return;
}
const payload = {
deployment: {
id: alert.workerDeployment.friendlyId,
status: alert.workerDeployment.status,
version: alert.workerDeployment.version,
shortCode: alert.workerDeployment.shortCode,
failedAt: alert.workerDeployment.failedAt ?? new Date(),
},
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
error: preparedError,
};
switch (webhookProperties.data.version) {
case "v1": {
const payload = {
deployment: {
id: alert.workerDeployment.friendlyId,
status: alert.workerDeployment.status,
version: alert.workerDeployment.version,
shortCode: alert.workerDeployment.shortCode,
failedAt: alert.workerDeployment.failedAt ?? new Date(),
},
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
error: preparedError,
};
await this.#deliverWebhook(payload, webhookProperties.data);
await this.#deliverWebhook(payload, webhookProperties.data);
break;
}
case "v2": {
const payload: DeploymentFailedWebhook = {
id: alert.id,
created: alert.createdAt,
webhookVersion: "v1",
type: "alert.deployment.failed",
object: {
deployment: {
id: alert.workerDeployment.friendlyId,
status: alert.workerDeployment.status,
version: alert.workerDeployment.version,
shortCode: alert.workerDeployment.shortCode,
failedAt: alert.workerDeployment.failedAt ?? new Date(),
},
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
error: preparedError,
},
};
await this.#deliverWebhook(payload, webhookProperties.data);
break;
}
default: {
throw new Error(`Unknown webhook version: ${webhookProperties.data.version}`);
}
}
} else {
logger.error("[DeliverAlert] Worker deployment not found", {
alert,
@@ -488,40 +516,92 @@ export class DeliverAlertService extends BaseService {
}
case "DEPLOYMENT_SUCCESS": {
if (alert.workerDeployment) {
const payload = {
deployment: {
id: alert.workerDeployment.friendlyId,
status: alert.workerDeployment.status,
version: alert.workerDeployment.version,
shortCode: alert.workerDeployment.shortCode,
deployedAt: alert.workerDeployment.deployedAt ?? new Date(),
},
tasks:
alert.workerDeployment.worker?.tasks.map((task) => ({
id: task.slug,
filePath: task.filePath,
exportName: task.exportName,
triggerSource: task.triggerSource,
})) ?? [],
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
};
switch (webhookProperties.data.version) {
case "v1": {
const payload = {
deployment: {
id: alert.workerDeployment.friendlyId,
status: alert.workerDeployment.status,
version: alert.workerDeployment.version,
shortCode: alert.workerDeployment.shortCode,
deployedAt: alert.workerDeployment.deployedAt ?? new Date(),
},
tasks:
alert.workerDeployment.worker?.tasks.map((task) => ({
id: task.slug,
filePath: task.filePath,
exportName: task.exportName,
triggerSource: task.triggerSource,
})) ?? [],
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
};
await this.#deliverWebhook(payload, webhookProperties.data);
await this.#deliverWebhook(payload, webhookProperties.data);
break;
}
case "v2": {
const payload: DeploymentSuccessWebhook = {
id: alert.id,
created: alert.createdAt,
webhookVersion: "v1",
type: "alert.deployment.success",
object: {
deployment: {
id: alert.workerDeployment.friendlyId,
status: alert.workerDeployment.status,
version: alert.workerDeployment.version,
shortCode: alert.workerDeployment.shortCode,
deployedAt: alert.workerDeployment.deployedAt! ?? new Date(),
},
tasks:
alert.workerDeployment.worker?.tasks.map((task) => ({
id: task.slug,
filePath: task.filePath,
exportName: task.exportName,
triggerSource: task.triggerSource,
})) ?? [],
environment: {
id: alert.environment.id,
type: alert.environment.type,
slug: alert.environment.slug,
},
organization: {
id: alert.project.organizationId,
slug: alert.project.organization.slug,
name: alert.project.organization.title,
},
project: {
id: alert.project.id,
ref: alert.project.externalRef,
slug: alert.project.slug,
name: alert.project.name,
},
},
};
await this.#deliverWebhook(payload, webhookProperties.data);
break;
}
default: {
throw new Error(`Unknown webhook version: ${webhookProperties.data.version}`);
}
}
} else {
logger.error("[DeliverAlert] Worker deployment not found", {
alert,
@@ -582,126 +662,7 @@ export class DeliverAlertService extends BaseService {
switch (alert.type) {
case "TASK_RUN_ATTEMPT": {
if (alert.taskRunAttempt) {
// Find existing storage by the run ID
const storage = await this._prisma.projectAlertStorage.findFirst({
where: {
alertChannelId: alert.channel.id,
alertType: alert.type,
storageId: alert.taskRunAttempt.taskRunId,
},
});
const storageData = storage
? ProjectAlertSlackStorage.safeParse(storage.storageData)
: undefined;
const thread_ts =
storageData && storageData.success ? storageData.data.message_ts : undefined;
const taskRunError = TaskRunError.safeParse(alert.taskRunAttempt.error);
if (!taskRunError.success) {
logger.error("[DeliverAlert] Attempt: Failed to parse task run error", {
issues: taskRunError.error.issues,
taskAttemptError: alert.taskRunAttempt.error,
});
return;
}
const error = createJsonErrorObject(taskRunError.data);
const exportName = alert.taskRunAttempt.backgroundWorkerTask.exportName;
const version = alert.taskRunAttempt.backgroundWorker.version;
const environment = alert.environment.slug;
const taskIdentifier = alert.taskRunAttempt.backgroundWorkerTask.slug;
const timestamp = alert.taskRunAttempt.completedAt ?? new Date();
const runId = alert.taskRunAttempt.taskRun.friendlyId;
const attemptNumber = alert.taskRunAttempt.number;
const message = await this.#postSlackMessage(integration, {
thread_ts,
channel: slackProperties.data.channelId,
text: `Task error in ${alert.taskRunAttempt.backgroundWorkerTask.exportName} [${alert.taskRunAttempt.backgroundWorker.version}.${alert.environment.slug}]`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `:rotating_light: Error in *${exportName}* _<!date^${Math.round(
timestamp.getTime() / 1000
)}^at {date_num} {time_secs}|${timestamp.toLocaleString()}>_`,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: this.#wrapInCodeBlock(error.stackTrace ?? error.message),
},
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `${runId}.${attemptNumber} | ${taskIdentifier} | ${version}.${environment} | ${alert.project.name}`,
},
],
},
{
type: "divider",
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "Investigate",
},
url: `${env.APP_ORIGIN}/projects/v3/${alert.project.externalRef}/runs/${alert.taskRunAttempt.taskRun.friendlyId}`,
},
],
},
],
});
// Upsert the storage
if (message.ts) {
if (storage) {
await this._prisma.projectAlertStorage.update({
where: {
id: storage.id,
},
data: {
storageData: {
message_ts: message.ts,
},
},
});
} else {
await this._prisma.projectAlertStorage.create({
data: {
alertChannelId: alert.channel.id,
alertType: alert.type,
storageId: alert.taskRunAttempt.taskRunId,
storageData: {
message_ts: message.ts,
},
projectId: alert.project.id,
},
});
}
}
} else {
logger.error("[DeliverAlert] Task run attempt not found", {
alert,
});
}
logger.error("[DeliverAlert] Task run attempt alerts are deprecated, not sending anything");
break;
}
case "TASK_RUN": {
@@ -945,7 +906,7 @@ export class DeliverAlertService extends BaseService {
}
}
async #deliverWebhook(payload: any, webhook: ProjectAlertWebhookProperties) {
async #deliverWebhook<T>(payload: T, webhook: ProjectAlertWebhookProperties) {
const rawPayload = JSON.stringify(payload);
const hashPayload = Buffer.from(rawPayload, "utf-8");
@@ -1107,6 +1068,7 @@ export class DeliverAlertService extends BaseService {
static async createAndSendAlert(
{
channelId,
channelType,
projectId,
environmentId,
alertType,
@@ -1114,6 +1076,7 @@ export class DeliverAlertService extends BaseService {
taskRunId,
}: {
channelId: string;
channelType: ProjectAlertChannelType;
projectId: string;
environmentId: string;
alertType: ProjectAlertType;
@@ -1122,7 +1085,7 @@ export class DeliverAlertService extends BaseService {
},
db: PrismaClientOrTransaction
) {
if (taskRunId) {
if (taskRunId && channelType !== "WEBHOOK") {
try {
const result = await alertsRateLimiter.check(channelId);
@@ -49,6 +49,7 @@ export class PerformDeploymentAlertsService extends BaseService {
await DeliverAlertService.createAndSendAlert(
{
channelId: alertChannel.id,
channelType: alertChannel.type,
projectId: deployment.projectId,
environmentId: deployment.environmentId,
alertType,
@@ -49,6 +49,7 @@ export class PerformTaskRunAlertsService extends BaseService {
await DeliverAlertService.createAndSendAlert(
{
channelId: alertChannel.id,
channelType: alertChannel.type,
projectId: run.projectId,
environmentId: run.runtimeEnvironmentId,
alertType: "TASK_RUN",
@@ -738,7 +738,7 @@ async function findAttempt(prismaClient: PrismaClientOrTransaction, friendlyId:
});
}
function isOOMError(error: TaskRunError) {
export function isOOMError(error: TaskRunError) {
if (error.type === "INTERNAL_ERROR") {
if (
error.code === "TASK_PROCESS_OOM_KILLED" ||
+67
View File
@@ -3,6 +3,13 @@ title: "Alerts"
description: "Get alerted when runs or deployments fail, or when deployments succeed."
---
We support receiving alerts for the following events:
- Run fails
- Deployment fails
- Deployment succeeds
## How to setup alerts
<Steps>
<Step title="Create a new alert">
@@ -27,3 +34,63 @@ Click on the triple dot menu on the right side of the table row and select "Disa
</Step>
</Steps>
## Alert webhooks
For the alert webhooks you can use the SDK to parse them. Here is an example of how to parse the webhook payload in Remix:
```ts
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { webhooks, WebhookError } from "@trigger.dev/sdk/v3";
export async function action({ request }: ActionFunctionArgs) {
// Make sure this is a POST request
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}
try {
// Construct and verify the webhook event
// This secret can be found on your Alerts page when you create a webhook alert
const event = await webhooks.constructEvent(request, process.env.ALERT_WEBHOOK_SECRET!);
// Process the event based on its type
switch (event.type) {
case "alert.run.failed": {
console.log("[Webhook Internal Test] Run failed alert webhook received", { event });
break;
}
case "alert.deployment.success": {
console.log("[Webhook Internal Test] Deployment success alert webhook received", { event });
break;
}
case "alert.deployment.failed": {
console.log("[Webhook Internal Test] Deployment failed alert webhook received", { event });
break;
}
default: {
console.log("[Webhook Internal Test] Unhandled webhook type", { event });
}
}
// Return a success response
return json({ received: true }, { status: 200 });
} catch (err) {
// Handle webhook errors
if (err instanceof WebhookError) {
console.error("Webhook error:", { message: err.message });
return json({ error: err.message }, { status: 400 });
}
if (err instanceof Error) {
console.error("Error processing webhook:", { message: err.message });
return json({ error: err.message }, { status: 400 });
}
// Handle other errors
console.error("Error processing webhook:", { err });
return json({ error: "Internal server error" }, { status: 500 });
}
}
```
+1
View File
@@ -10,3 +10,4 @@ export * from "./eventFilter.js";
export * from "./openTelemetry.js";
export * from "./config.js";
export * from "./build.js";
export * from "./webhooks.js";
+205
View File
@@ -0,0 +1,205 @@
import { z } from "zod";
import { RuntimeEnvironmentTypeSchema } from "../../schemas/api.js";
import { RunStatus } from "./api.js";
import { TaskRunError } from "./common.js";
/** Represents a failed run alert webhook payload */
const AlertWebhookRunFailedObject = z.object({
/** Task information */
task: z.object({
/** Unique identifier for the task */
id: z.string(),
/** File path where the task is defined */
filePath: z.string(),
/** Name of the exported task function */
exportName: z.string(),
/** Version of the task */
version: z.string(),
/** Version of the SDK used */
sdkVersion: z.string(),
/** Version of the CLI used */
cliVersion: z.string(),
}),
/** Run information */
run: z.object({
/** Unique identifier for the run */
id: z.string(),
/** Run number */
number: z.number(),
/** Current status of the run */
status: RunStatus,
/** When the run was created */
createdAt: z.coerce.date(),
/** When the run started executing */
startedAt: z.coerce.date().optional(),
/** When the run finished executing */
completedAt: z.coerce.date().optional(),
/** Whether this is a test run */
isTest: z.boolean(),
/** Idempotency key for the run */
idempotencyKey: z.string().optional(),
/** Associated tags */
tags: z.array(z.string()),
/** Error information */
error: TaskRunError,
/** Whether the run was an out-of-memory error */
isOutOfMemoryError: z.boolean(),
/** Machine preset used for the run */
machine: z.string(),
/** URL to view the run in the dashboard */
dashboardUrl: z.string(),
}),
/** Environment information */
environment: z.object({
/** Environment ID */
id: z.string(),
/** Environment type */
type: RuntimeEnvironmentTypeSchema,
/** Environment slug */
slug: z.string(),
}),
/** Organization information */
organization: z.object({
/** Organization ID */
id: z.string(),
/** Organization slug */
slug: z.string(),
/** Organization name */
name: z.string(),
}),
/** Project information */
project: z.object({
/** Project ID */
id: z.string(),
/** Project reference */
ref: z.string(),
/** Project slug */
slug: z.string(),
/** Project name */
name: z.string(),
}),
});
export type AlertWebhookRunFailedObject = z.infer<typeof AlertWebhookRunFailedObject>;
/** Represents a deployment error */
export const DeployError = z.object({
/** Error name */
name: z.string(),
/** Error message */
message: z.string(),
/** Error stack trace */
stack: z.string().optional(),
/** Standard error output */
stderr: z.string().optional(),
});
export type DeployError = z.infer<typeof DeployError>;
const deploymentCommonProperties = {
/** Environment information */
environment: z.object({
id: z.string(),
type: RuntimeEnvironmentTypeSchema,
slug: z.string(),
}),
/** Organization information */
organization: z.object({
id: z.string(),
slug: z.string(),
name: z.string(),
}),
/** Project information */
project: z.object({
id: z.string(),
ref: z.string(),
slug: z.string(),
name: z.string(),
}),
};
const deploymentDeploymentCommonProperties = {
/** Deployment ID */
id: z.string(),
/** Deployment status */
status: z.string(),
/** Deployment version */
version: z.string(),
/** Short code identifier */
shortCode: z.string(),
};
/** Represents a successful deployment alert webhook payload */
export const AlertWebhookDeploymentSuccessObject = z.object({
...deploymentCommonProperties,
deployment: z.object({
...deploymentDeploymentCommonProperties,
/** When the deployment completed */
deployedAt: z.coerce.date(),
}),
/** Deployed tasks */
tasks: z.array(
z.object({
/** Task ID */
id: z.string(),
/** File path where the task is defined */
filePath: z.string(),
/** Name of the exported task function */
exportName: z.string(),
/** Source of the trigger */
triggerSource: z.string(),
})
),
});
/** Represents a failed deployment alert webhook payload */
export const AlertWebhookDeploymentFailedObject = z.object({
...deploymentCommonProperties,
deployment: z.object({
...deploymentDeploymentCommonProperties,
/** When the deployment failed */
failedAt: z.coerce.date(),
}),
/** Error information */
error: DeployError,
});
export type AlertWebhookDeploymentSuccessObject = z.infer<
typeof AlertWebhookDeploymentSuccessObject
>;
export type AlertWebhookDeploymentFailedObject = z.infer<typeof AlertWebhookDeploymentFailedObject>;
/** Common properties for all webhooks */
const commonProperties = {
/** Webhook ID */
id: z.string(),
/** When the webhook was created */
created: z.coerce.date(),
/** Version of the webhook */
webhookVersion: z.string(),
};
/** Represents all possible webhook types */
export const Webhook = z.discriminatedUnion("type", [
/** Run failed alert webhook */
z.object({
...commonProperties,
type: z.literal("alert.run.failed"),
object: AlertWebhookRunFailedObject,
}),
/** Deployment success alert webhook */
z.object({
...commonProperties,
type: z.literal("alert.deployment.success"),
object: AlertWebhookDeploymentSuccessObject,
}),
/** Deployment failed alert webhook */
z.object({
...commonProperties,
type: z.literal("alert.deployment.failed"),
object: AlertWebhookDeploymentFailedObject,
}),
]);
export type Webhook = z.infer<typeof Webhook>;
export type RunFailedWebhook = Extract<Webhook, { type: "alert.run.failed" }>;
export type DeploymentSuccessWebhook = Extract<Webhook, { type: "alert.deployment.success" }>;
export type DeploymentFailedWebhook = Extract<Webhook, { type: "alert.deployment.failed" }>;
+1
View File
@@ -56,6 +56,7 @@
"slug": "^6.0.0",
"terminal-link": "^3.0.0",
"ulid": "^2.3.0",
"uncrypto": "^0.1.3",
"uuid": "^9.0.0",
"ws": "^8.11.0"
},
@@ -0,0 +1,5 @@
// @ts-ignore
const { subtle } = require("uncrypto");
// @ts-ignore
module.exports.subtle = subtle;
@@ -0,0 +1,5 @@
// @ts-ignore
import { subtle } from "uncrypto";
// @ts-ignore
export { subtle };
+1 -1
View File
@@ -11,7 +11,7 @@ export * from "./idempotencyKeys.js";
export * from "./tags.js";
export * from "./metadata.js";
export * from "./timeout.js";
export * from "./waitUntil.js";
export * from "./webhooks.js";
export type { Context };
import type { Context } from "./shared.js";
+171
View File
@@ -0,0 +1,171 @@
import { Webhook } from "@trigger.dev/core/v3";
import { subtle } from "../imports/uncrypto.js";
/**
* The type of error thrown when a webhook fails to parse or verify
*/
export class WebhookError extends Error {
constructor(message: string) {
super(message);
this.name = "WebhookError";
}
}
/** Header name used for webhook signatures */
const SIGNATURE_HEADER_NAME = "x-trigger-signature-hmacsha256";
/**
* Options for constructing a webhook event
*/
type ConstructEventOptions = {
/** Raw payload as string or Buffer */
payload: string | Buffer;
/** Signature header as string, Buffer, or string array */
header: string | Buffer | Array<string>;
};
/**
* Interface describing the webhook utilities
*/
interface Webhooks {
/**
* Constructs and validates a webhook event from an incoming request
* @param request - Either a Request object or ConstructEventOptions containing the payload and signature
* @param secret - Secret key used to verify the webhook signature
* @returns Promise resolving to a validated AlertWebhook object
* @throws {WebhookError} If validation fails or payload can't be parsed
*
* @example
* // Using with Request object
* const event = await webhooks.constructEvent(request, "webhook_secret");
*
* @example
* // Using with manual options
* const event = await webhooks.constructEvent({
* payload: rawBody,
* header: signatureHeader
* }, "webhook_secret");
*/
constructEvent(request: ConstructEventOptions | Request, secret: string): Promise<Webhook>;
/** Header name used for webhook signatures */
SIGNATURE_HEADER_NAME: string;
}
/**
* Webhook utilities for handling incoming webhook requests
*/
export const webhooks: Webhooks = {
constructEvent,
SIGNATURE_HEADER_NAME,
};
async function constructEvent(
request: ConstructEventOptions | Request,
secret: string
): Promise<Webhook> {
let payload: string;
let signature: string;
if (request instanceof Request) {
if (!secret) {
throw new WebhookError("Secret is required when passing a Request object");
}
const signatureHeader = request.headers.get(SIGNATURE_HEADER_NAME);
if (!signatureHeader) {
throw new WebhookError("No signature header found");
}
signature = signatureHeader;
payload = await request.text();
} else {
payload = request.payload.toString();
if (Array.isArray(request.header)) {
throw new WebhookError("Signature header cannot be an array");
}
signature = request.header.toString();
}
// Verify the signature
const isValid = await verifySignature(payload, signature, secret);
if (!isValid) {
throw new WebhookError("Invalid signature");
}
// Parse and validate the payload
try {
const jsonPayload = JSON.parse(payload);
const parsedPayload = Webhook.parse(jsonPayload);
return parsedPayload;
} catch (error) {
if (error instanceof Error) {
throw new WebhookError(`Webhook parsing failed: ${error.message}`);
}
throw new WebhookError("Webhook parsing failed");
}
}
/**
* Verifies the signature of a webhook payload
* @param payload - Raw payload string to verify
* @param signature - Expected signature to check against
* @param secret - Secret key used to generate the signature
* @returns Promise resolving to boolean indicating if signature is valid
* @throws {WebhookError} If signature verification process fails
*
* @example
* const isValid = await verifySignature(
* '{"event": "test"}',
* "abc123signature",
* "webhook_secret"
* );
*/
async function verifySignature(
payload: string,
signature: string,
secret: string
): Promise<boolean> {
try {
if (!secret) {
throw new WebhookError("Secret is required for signature verification");
}
// Convert the payload and secret to buffers
const hashPayload = Buffer.from(payload, "utf-8");
const hmacSecret = Buffer.from(secret, "utf-8");
// Import the secret key
const key = await subtle.importKey(
"raw",
hmacSecret,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign", "verify"]
);
// Calculate the expected signature
const actualSignature = await subtle.sign("HMAC", key, hashPayload);
const actualSignatureHex = Buffer.from(actualSignature).toString("hex");
// Compare signatures using timing-safe comparison
return timingSafeEqual(signature, actualSignatureHex);
} catch (error) {
throw new WebhookError("Signature verification failed");
}
}
// Timing-safe comparison to prevent timing attacks
function timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
+223 -16
View File
@@ -410,7 +410,7 @@ importers:
version: 8.6.6
'@uiw/react-codemirror':
specifier: ^4.19.5
version: 4.19.5(@babel/runtime@7.24.5)(@codemirror/autocomplete@6.4.0)(@codemirror/language@6.3.2)(@codemirror/lint@6.4.2)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/theme-one-dark@6.1.0)(@codemirror/view@6.7.2)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0)
version: 4.19.5(@babel/runtime@7.26.7)(@codemirror/autocomplete@6.4.0)(@codemirror/language@6.3.2)(@codemirror/lint@6.4.2)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/theme-one-dark@6.1.0)(@codemirror/view@6.7.2)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0)
'@unkey/cache':
specifier: ^1.5.0
version: 1.5.0
@@ -774,7 +774,7 @@ importers:
version: 10.4.13(postcss@8.4.44)
babel-loader:
specifier: ^9.1.3
version: 9.1.3(@babel/core@7.24.5)(webpack@5.88.2)
version: 9.1.3(@babel/core@7.26.8)(webpack@5.88.2)
babel-preset-react-app:
specifier: ^10.0.1
version: 10.0.1
@@ -1535,6 +1535,9 @@ importers:
ulid:
specifier: ^2.3.0
version: 2.3.0
uncrypto:
specifier: ^0.1.3
version: 0.1.3
uuid:
specifier: ^9.0.0
version: 9.0.0
@@ -3329,6 +3332,15 @@ packages:
'@babel/highlight': 7.24.7
picocolors: 1.0.1
/@babel/code-frame@7.26.2:
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.25.9
js-tokens: 4.0.0
picocolors: 1.1.1
dev: true
/@babel/compat-data@7.22.9:
resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==}
engines: {node: '>=6.9.0'}
@@ -3336,6 +3348,12 @@ packages:
/@babel/compat-data@7.25.4:
resolution: {integrity: sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==}
engines: {node: '>=6.9.0'}
dev: false
/@babel/compat-data@7.26.8:
resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/core@7.22.17:
resolution: {integrity: sha512-2EENLmhpwplDux5PSsZnSbnSkB3tZ6QTksgO25xwEL7pIDcNOMhF5v/s6RzwjMZzZzw9Ofc30gHv5ChCC8pifQ==}
@@ -3380,6 +3398,31 @@ packages:
semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: false
/@babel/core@7.26.8:
resolution: {integrity: sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.3.0
'@babel/code-frame': 7.26.2
'@babel/generator': 7.26.8
'@babel/helper-compilation-targets': 7.26.5
'@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.8)
'@babel/helpers': 7.26.7
'@babel/parser': 7.26.8
'@babel/template': 7.26.8
'@babel/traverse': 7.26.8
'@babel/types': 7.26.8
'@types/gensync': 1.0.4
convert-source-map: 2.0.0
debug: 4.3.7
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
dev: true
/@babel/eslint-parser@7.21.8(@babel/core@7.22.17)(eslint@8.31.0):
resolution: {integrity: sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==}
@@ -3421,6 +3464,18 @@ packages:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
jsesc: 2.5.2
dev: false
/@babel/generator@7.26.8:
resolution: {integrity: sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/parser': 7.26.8
'@babel/types': 7.26.8
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.0.2
dev: true
/@babel/helper-annotate-as-pure@7.22.5:
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
@@ -3456,6 +3511,18 @@ packages:
browserslist: 4.23.3
lru-cache: 5.1.1
semver: 6.3.1
dev: false
/@babel/helper-compilation-targets@7.26.5:
resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/compat-data': 7.26.8
'@babel/helper-validator-option': 7.25.9
browserslist: 4.24.4
lru-cache: 5.1.1
semver: 6.3.1
dev: true
/@babel/helper-create-class-features-plugin@7.21.8(@babel/core@7.22.17):
resolution: {integrity: sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw==}
@@ -3623,6 +3690,17 @@ packages:
'@babel/types': 7.24.7
transitivePeerDependencies:
- supports-color
dev: false
/@babel/helper-module-imports@7.25.9:
resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/traverse': 7.26.8
'@babel/types': 7.26.8
transitivePeerDependencies:
- supports-color
dev: true
/@babel/helper-module-transforms@7.22.17(@babel/core@7.22.17):
resolution: {integrity: sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ==}
@@ -3650,6 +3728,21 @@ packages:
'@babel/traverse': 7.25.6
transitivePeerDependencies:
- supports-color
dev: false
/@babel/helper-module-transforms@7.26.0(@babel/core@7.26.8):
resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.26.8
'@babel/helper-module-imports': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/traverse': 7.26.8
transitivePeerDependencies:
- supports-color
dev: true
/@babel/helper-optimise-call-expression@7.18.6:
resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==}
@@ -3730,6 +3823,7 @@ packages:
'@babel/types': 7.24.7
transitivePeerDependencies:
- supports-color
dev: false
/@babel/helper-skip-transparent-expression-wrappers@7.20.0:
resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==}
@@ -3764,11 +3858,20 @@ packages:
/@babel/helper-string-parser@7.24.8:
resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==}
engines: {node: '>=6.9.0'}
dev: false
/@babel/helper-string-parser@7.25.9:
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'}
/@babel/helper-validator-identifier@7.24.7:
resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==}
engines: {node: '>=6.9.0'}
/@babel/helper-validator-identifier@7.25.9:
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'}
/@babel/helper-validator-option@7.22.15:
resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==}
engines: {node: '>=6.9.0'}
@@ -3776,6 +3879,12 @@ packages:
/@babel/helper-validator-option@7.24.8:
resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==}
engines: {node: '>=6.9.0'}
dev: false
/@babel/helper-validator-option@7.25.9:
resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-wrap-function@7.20.5:
resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==}
@@ -3805,6 +3914,15 @@ packages:
dependencies:
'@babel/template': 7.25.0
'@babel/types': 7.25.6
dev: false
/@babel/helpers@7.26.7:
resolution: {integrity: sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.26.8
'@babel/types': 7.26.8
dev: true
/@babel/highlight@7.22.13:
resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==}
@@ -3860,6 +3978,14 @@ packages:
hasBin: true
dependencies:
'@babel/types': 7.25.6
dev: false
/@babel/parser@7.26.8:
resolution: {integrity: sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.26.8
/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.22.17):
resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
@@ -4872,6 +4998,13 @@ packages:
dependencies:
regenerator-runtime: 0.14.1
/@babel/runtime@7.26.7:
resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: false
/@babel/template@7.22.15:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
@@ -4895,6 +5028,16 @@ packages:
'@babel/code-frame': 7.24.7
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
dev: false
/@babel/template@7.26.8:
resolution: {integrity: sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.26.2
'@babel/parser': 7.26.8
'@babel/types': 7.26.8
dev: true
/@babel/traverse@7.22.17:
resolution: {integrity: sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg==}
@@ -4944,6 +5087,22 @@ packages:
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: false
/@babel/traverse@7.26.8:
resolution: {integrity: sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.26.2
'@babel/generator': 7.26.8
'@babel/parser': 7.26.8
'@babel/template': 7.26.8
'@babel/types': 7.26.8
debug: 4.3.7
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: true
/@babel/types@7.24.0:
resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
@@ -4968,6 +5127,14 @@ packages:
'@babel/helper-string-parser': 7.24.8
'@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0
dev: false
/@babel/types@7.26.8:
resolution: {integrity: sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
/@balena/dockerignore@1.0.2:
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
@@ -16674,6 +16841,10 @@ packages:
'@types/node': 18.19.20
dev: true
/@types/gensync@1.0.4:
resolution: {integrity: sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==}
dev: true
/@types/gradient-string@1.1.2:
resolution: {integrity: sha512-zIet2KvHr2dkOCPI5ggQQ+WJVyfBSFaqK9sNelhgDjlE2K3Fu2muuPJwu5aKM3xoWuc3WXudVEMUwI1QWhykEQ==}
dependencies:
@@ -17288,7 +17459,7 @@ packages:
'@codemirror/view': 6.7.2
dev: false
/@uiw/react-codemirror@4.19.5(@babel/runtime@7.24.5)(@codemirror/autocomplete@6.4.0)(@codemirror/language@6.3.2)(@codemirror/lint@6.4.2)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/theme-one-dark@6.1.0)(@codemirror/view@6.7.2)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0):
/@uiw/react-codemirror@4.19.5(@babel/runtime@7.26.7)(@codemirror/autocomplete@6.4.0)(@codemirror/language@6.3.2)(@codemirror/lint@6.4.2)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/theme-one-dark@6.1.0)(@codemirror/view@6.7.2)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==}
peerDependencies:
'@babel/runtime': '>=7.11.0'
@@ -17299,7 +17470,7 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@babel/runtime': 7.24.5
'@babel/runtime': 7.26.7
'@codemirror/commands': 6.1.3
'@codemirror/state': 6.2.0
'@codemirror/theme-one-dark': 6.1.0
@@ -17590,7 +17761,7 @@ packages:
/@vue/compiler-core@3.4.38:
resolution: {integrity: sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==}
dependencies:
'@babel/parser': 7.25.6
'@babel/parser': 7.26.8
'@vue/shared': 3.4.38
entities: 4.5.0
estree-walker: 2.0.2
@@ -17605,7 +17776,7 @@ packages:
/@vue/compiler-sfc@3.4.38:
resolution: {integrity: sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==}
dependencies:
'@babel/parser': 7.25.6
'@babel/parser': 7.26.8
'@vue/compiler-core': 3.4.38
'@vue/compiler-dom': 3.4.38
'@vue/compiler-ssr': 3.4.38
@@ -18598,14 +18769,14 @@ packages:
/b4a@1.6.6:
resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==}
/babel-loader@9.1.3(@babel/core@7.24.5)(webpack@5.88.2):
/babel-loader@9.1.3(@babel/core@7.26.8)(webpack@5.88.2):
resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==}
engines: {node: '>= 14.15.0'}
peerDependencies:
'@babel/core': ^7.12.0
webpack: '>=5'
dependencies:
'@babel/core': 7.24.5
'@babel/core': 7.26.8
find-cache-dir: 4.0.0
schema-utils: 4.0.1
webpack: 5.88.2(@swc/core@1.3.26)(esbuild@0.15.18)
@@ -18893,6 +19064,17 @@ packages:
node-releases: 2.0.18
update-browserslist-db: 1.1.0(browserslist@4.23.3)
/browserslist@4.24.4:
resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001699
electron-to-chromium: 1.5.98
node-releases: 2.0.19
update-browserslist-db: 1.1.2(browserslist@4.24.4)
dev: true
/buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
@@ -19089,6 +19271,10 @@ packages:
/caniuse-lite@1.0.30001655:
resolution: {integrity: sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==}
/caniuse-lite@1.0.30001699:
resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==}
dev: true
/capnp-ts@0.7.0:
resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
dependencies:
@@ -20617,6 +20803,10 @@ packages:
/electron-to-chromium@1.5.13:
resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==}
/electron-to-chromium@1.5.98:
resolution: {integrity: sha512-bI/LbtRBxU2GzK7KK5xxFd2y9Lf9XguHooPYbcXWy6wUoT8NMnffsvRhPmSeUHLSDKAEtKuTaEtK4Ms15zkIEA==}
dev: true
/email-reply-parser@1.8.0:
resolution: {integrity: sha512-hiie/4vNxT5NYBux8m/jZccUQdtQqiq5ytwJwbA9QIkTWbTNPQasJXfsfdetZo23hBn4ZJleRDyOA5nV4+ssSQ==}
engines: {node: '>= 10.0.0'}
@@ -25726,6 +25916,10 @@ packages:
/node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
/node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
dev: true
/nodemailer@6.9.16:
resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==}
engines: {node: '>=6.0.0'}
@@ -26265,14 +26459,14 @@ packages:
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
yocto-queue: 1.0.0
yocto-queue: 1.1.1
dev: true
/p-limit@5.0.0:
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
engines: {node: '>=18'}
dependencies:
yocto-queue: 1.0.0
yocto-queue: 1.1.1
dev: true
/p-limit@6.2.0:
@@ -26729,6 +26923,10 @@ packages:
/picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
/picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
dev: true
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
@@ -31211,6 +31409,10 @@ packages:
through: 2.3.8
dev: false
/uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
dev: false
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
@@ -31398,6 +31600,17 @@ packages:
escalade: 3.2.0
picocolors: 1.0.1
/update-browserslist-db@1.1.2(browserslist@4.24.4):
resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
browserslist: 4.24.4
escalade: 3.2.0
picocolors: 1.1.1
dev: true
/uploadthing@7.1.0(next@14.2.15)(tailwindcss@3.4.1):
resolution: {integrity: sha512-l1bRHs+q/YLx3XwBav98t4Bl1wLWaskhPEwopxtYgiRrxX5nW3uUuSP0RJ9eKwx0+6ZhHWxHDvShf7ZLledqmQ==}
engines: {node: '>=18.13.0'}
@@ -32857,15 +33070,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
/yocto-queue@1.0.0:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
dev: true
/yocto-queue@1.1.1:
resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
engines: {node: '>=12.20'}
dev: false
/youch@3.3.3:
resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==}