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:
@@ -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" ||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from "./eventFilter.js";
|
||||
export * from "./openTelemetry.js";
|
||||
export * from "./config.js";
|
||||
export * from "./build.js";
|
||||
export * from "./webhooks.js";
|
||||
|
||||
@@ -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" }>;
|
||||
@@ -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 };
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Generated
+223
-16
@@ -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==}
|
||||
|
||||
Reference in New Issue
Block a user