@keystrokehq/resend 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +184 -0
  2. package/dist/_official/index.d.mts +2 -0
  3. package/dist/_official/index.mjs +3 -0
  4. package/dist/_runtime/index.d.mts +1 -0
  5. package/dist/_runtime/index.mjs +1 -0
  6. package/dist/api-keys.d.mts +67 -0
  7. package/dist/api-keys.mjs +90 -0
  8. package/dist/broadcasts.d.mts +238 -0
  9. package/dist/broadcasts.mjs +178 -0
  10. package/dist/client.d.mts +27 -0
  11. package/dist/client.mjs +40 -0
  12. package/dist/connection.d.mts +2 -0
  13. package/dist/connection.mjs +3 -0
  14. package/dist/contact-properties.d.mts +139 -0
  15. package/dist/contact-properties.mjs +115 -0
  16. package/dist/contacts.d.mts +218 -0
  17. package/dist/contacts.mjs +225 -0
  18. package/dist/domains.d.mts +293 -0
  19. package/dist/domains.mjs +160 -0
  20. package/dist/emails-receiving.d.mts +177 -0
  21. package/dist/emails-receiving.mjs +114 -0
  22. package/dist/emails.d.mts +307 -0
  23. package/dist/emails.mjs +241 -0
  24. package/dist/events.d.mts +571 -0
  25. package/dist/events.mjs +178 -0
  26. package/dist/factory-B3VyPRsL.mjs +8 -0
  27. package/dist/index.d.mts +1 -0
  28. package/dist/index.mjs +1 -0
  29. package/dist/integration-BR1nTAnU.mjs +28 -0
  30. package/dist/integration-ClH0F7x-.d.mts +49 -0
  31. package/dist/logs.d.mts +71 -0
  32. package/dist/logs.mjs +61 -0
  33. package/dist/schemas.d.mts +77 -0
  34. package/dist/schemas.mjs +68 -0
  35. package/dist/segments.d.mts +112 -0
  36. package/dist/segments.mjs +120 -0
  37. package/dist/templates.d.mts +193 -0
  38. package/dist/templates.mjs +151 -0
  39. package/dist/topics.d.mts +125 -0
  40. package/dist/topics.mjs +113 -0
  41. package/dist/triggers.d.mts +68 -0
  42. package/dist/triggers.mjs +141 -0
  43. package/dist/verification.d.mts +49 -0
  44. package/dist/verification.mjs +139 -0
  45. package/dist/webhooks.d.mts +285 -0
  46. package/dist/webhooks.mjs +124 -0
  47. package/package.json +137 -0
@@ -0,0 +1,113 @@
1
+ import { createResendClient } from "./client.mjs";
2
+ import { paginationQuerySchema, resendListEnvelope } from "./schemas.mjs";
3
+ import { t as resendOperation } from "./factory-B3VyPRsL.mjs";
4
+ import { z } from "zod";
5
+
6
+ //#region src/topics.ts
7
+ /**
8
+ * resend/topics.ts
9
+ *
10
+ * Operations backing the Resend `/topics` endpoints.
11
+ * PLAN.md § 6.9: 5 operations.
12
+ *
13
+ * Topics model recipient subscription preferences. A broadcast sent
14
+ * with `topic_id` honours each recipient's opt-in/opt-out state.
15
+ */
16
+ const topicDefaultSubscriptionSchema = z.enum(["opt_in", "opt_out"]);
17
+ const topicSchema = z.object({
18
+ id: z.string(),
19
+ name: z.string().optional(),
20
+ description: z.string().optional().nullable(),
21
+ default_subscription: topicDefaultSubscriptionSchema.optional(),
22
+ created_at: z.string().optional(),
23
+ updated_at: z.string().optional()
24
+ }).passthrough();
25
+ const listTopicsResponseSchema = resendListEnvelope(topicSchema);
26
+ const createTopic = resendOperation({
27
+ id: "create_resend_topic",
28
+ name: "Create Resend Topic",
29
+ description: "Create a new subscription topic.",
30
+ input: z.object({
31
+ name: z.string().min(1),
32
+ default_subscription: topicDefaultSubscriptionSchema,
33
+ description: z.string().optional()
34
+ }),
35
+ output: topicSchema,
36
+ needsApproval: true,
37
+ run: async (input, credentials) => {
38
+ return createResendClient(credentials).request({
39
+ method: "POST",
40
+ path: "/topics",
41
+ body: input
42
+ });
43
+ }
44
+ });
45
+ const listTopics = resendOperation({
46
+ id: "list_resend_topics",
47
+ name: "List Resend Topics",
48
+ description: "Retrieve a cursor-paginated list of topics.",
49
+ input: paginationQuerySchema,
50
+ output: listTopicsResponseSchema,
51
+ run: async (input, credentials) => {
52
+ return createResendClient(credentials).request({
53
+ method: "GET",
54
+ path: "/topics",
55
+ query: input
56
+ });
57
+ }
58
+ });
59
+ const retrieveTopic = resendOperation({
60
+ id: "retrieve_resend_topic",
61
+ name: "Retrieve Resend Topic",
62
+ description: "Retrieve a single topic by id.",
63
+ input: z.object({ id: z.string().min(1) }),
64
+ output: topicSchema,
65
+ run: async (input, credentials) => {
66
+ return createResendClient(credentials).request({
67
+ method: "GET",
68
+ path: `/topics/${encodeURIComponent(input.id)}`
69
+ });
70
+ }
71
+ });
72
+ const updateTopic = resendOperation({
73
+ id: "update_resend_topic",
74
+ name: "Update Resend Topic",
75
+ description: "Update an existing topic.",
76
+ input: z.object({
77
+ id: z.string().min(1),
78
+ name: z.string().optional(),
79
+ description: z.string().optional(),
80
+ default_subscription: topicDefaultSubscriptionSchema.optional()
81
+ }),
82
+ output: topicSchema,
83
+ needsApproval: true,
84
+ run: async (input, credentials) => {
85
+ const { id, ...body } = input;
86
+ return createResendClient(credentials).request({
87
+ method: "PATCH",
88
+ path: `/topics/${encodeURIComponent(id)}`,
89
+ body
90
+ });
91
+ }
92
+ });
93
+ const deleteTopic = resendOperation({
94
+ id: "delete_resend_topic",
95
+ name: "Delete Resend Topic",
96
+ description: "Delete a topic.",
97
+ input: z.object({ id: z.string().min(1) }),
98
+ output: z.object({
99
+ object: z.literal("topic").optional(),
100
+ id: z.string(),
101
+ deleted: z.boolean().optional()
102
+ }).passthrough(),
103
+ needsApproval: true,
104
+ run: async (input, credentials) => {
105
+ return createResendClient(credentials).request({
106
+ method: "DELETE",
107
+ path: `/topics/${encodeURIComponent(input.id)}`
108
+ });
109
+ }
110
+ });
111
+
112
+ //#endregion
113
+ export { createTopic, deleteTopic, listTopics, retrieveTopic, updateTopic };
@@ -0,0 +1,68 @@
1
+ import { n as resend, t as ResendCredentials } from "./integration-ClH0F7x-.mjs";
2
+ import { ResendGenericWebhookEvent, ResendWebhookEventName } from "./events.mjs";
3
+ import { IntegrationTriggerBindingOptions } from "@keystrokehq/integration-authoring";
4
+ import { BoundTrigger, WebhookRequest } from "@keystrokehq/core";
5
+ import { WebhookTriggerManifest } from "@keystrokehq/core/trigger";
6
+
7
+ //#region src/triggers.d.ts
8
+ type ResendTriggerCredentialSets = readonly [typeof resend];
9
+ type ResendWebhookBoundTrigger<TOutput = ResendGenericWebhookEvent> = BoundTrigger<ResendGenericWebhookEvent, ResendTriggerCredentialSets, WebhookTriggerManifest, TOutput, WebhookRequest>;
10
+ type ResendWebhookBindingOptions<TOutput = ResendGenericWebhookEvent> = IntegrationTriggerBindingOptions<ResendGenericWebhookEvent, TOutput, WebhookRequest>;
11
+ type ResendWebhookBindingSurface = <TOutput = ResendGenericWebhookEvent>(options?: ResendWebhookBindingOptions<TOutput>) => ResendWebhookBoundTrigger<TOutput>;
12
+ interface ResendTriggersNamespace {
13
+ readonly emailSent: ResendWebhookBindingSurface;
14
+ readonly emailDelivered: ResendWebhookBindingSurface;
15
+ readonly emailDeliveryDelayed: ResendWebhookBindingSurface;
16
+ readonly emailBounced: ResendWebhookBindingSurface;
17
+ readonly emailComplained: ResendWebhookBindingSurface;
18
+ /** MPP-opens are filtered by default; pass your own `filter` to opt in. */
19
+ readonly emailOpened: ResendWebhookBindingSurface;
20
+ readonly emailClicked: ResendWebhookBindingSurface;
21
+ readonly emailFailed: ResendWebhookBindingSurface;
22
+ readonly emailReceived: ResendWebhookBindingSurface;
23
+ readonly emailScheduled: ResendWebhookBindingSurface;
24
+ readonly emailSuppressed: ResendWebhookBindingSurface;
25
+ readonly domainCreated: ResendWebhookBindingSurface;
26
+ readonly domainUpdated: ResendWebhookBindingSurface;
27
+ readonly domainDeleted: ResendWebhookBindingSurface;
28
+ readonly contactCreated: ResendWebhookBindingSurface;
29
+ readonly contactUpdated: ResendWebhookBindingSurface;
30
+ readonly contactDeleted: ResendWebhookBindingSurface;
31
+ }
32
+ declare const triggers: ResendTriggersNamespace;
33
+ /**
34
+ * Idempotently register (or update) a Resend webhook endpoint and
35
+ * return its id + signing secret. Workflows that bind triggers
36
+ * typically call this once to persist the `whsec_…` into the
37
+ * connection credentials before a trigger can verify inbound payloads.
38
+ *
39
+ * Behavior:
40
+ *
41
+ * 1. Look up existing webhooks via `GET /webhooks`.
42
+ * 2. If one already points to `endpointUrl`, reuse it. If its event
43
+ * list drifted, `PATCH` to reconcile.
44
+ * 3. Otherwise `POST /webhooks` with the requested events.
45
+ * 4. Return `{ webhookId, signingSecret, endpointUrl, events }`.
46
+ *
47
+ * Does NOT mutate the vault. Callers are responsible for writing
48
+ * the returned `signingSecret` into
49
+ * `credentials.RESEND_WEBHOOK_SIGNING_SECRETS[webhookId]`.
50
+ */
51
+ interface EnsureResendWebhookParams {
52
+ readonly credentials: ResendCredentials;
53
+ readonly endpointUrl: string;
54
+ /** Events to subscribe to. Pass `'all'` to subscribe to every event. */
55
+ readonly events: 'all' | readonly ResendWebhookEventName[];
56
+ readonly status?: 'enabled' | 'disabled';
57
+ }
58
+ interface EnsureResendWebhookResult {
59
+ readonly webhookId: string;
60
+ readonly signingSecret: string;
61
+ readonly endpointUrl: string;
62
+ readonly events: readonly ResendWebhookEventName[] | 'all';
63
+ readonly created: boolean;
64
+ readonly updated: boolean;
65
+ }
66
+ declare function ensureResendWebhook(params: EnsureResendWebhookParams): Promise<EnsureResendWebhookResult>;
67
+ //#endregion
68
+ export { EnsureResendWebhookParams, EnsureResendWebhookResult, ResendTriggersNamespace, ensureResendWebhook, triggers };
@@ -0,0 +1,141 @@
1
+ import { t as resend } from "./integration-BR1nTAnU.mjs";
2
+ import { createResendClient } from "./client.mjs";
3
+ import { RESEND_WEBHOOK_EVENT_NAMES, resendGenericWebhookEventSchema } from "./events.mjs";
4
+ import { verifyResendWebhookRequest } from "./verification.mjs";
5
+ import { createWebhookTriggerBindingFactory } from "@keystrokehq/integration-authoring";
6
+
7
+ //#region src/triggers.ts
8
+ function resolveSigningSecret(credentials) {
9
+ const map = credentials.RESEND_WEBHOOK_SIGNING_SECRETS;
10
+ if (map === void 0) return [];
11
+ return Object.values(map);
12
+ }
13
+ function idempotencyFromPayload(payload) {
14
+ const data = payload.data;
15
+ for (const key of ["email_id", "id"]) {
16
+ const value = data[key];
17
+ if (typeof value === "string" && value.length > 0) return `${payload.type}:${value}`;
18
+ }
19
+ return `${payload.type}:unknown`;
20
+ }
21
+ function isAppleMppOpen(payload) {
22
+ if (payload.type !== "email.opened") return false;
23
+ return payload.data.is_machine_opened === true;
24
+ }
25
+ function defaultDefinitionFilter(eventType) {
26
+ if (eventType === "email.opened") return (payload) => payload.type === eventType && !isAppleMppOpen(payload);
27
+ return (payload) => payload.type === eventType;
28
+ }
29
+ function buildBinding(eventType) {
30
+ return createWebhookTriggerBindingFactory({
31
+ defaultName: `Resend ${eventType}`,
32
+ defaultDescription: `Fires when Resend emits the ${eventType} webhook event.`,
33
+ path: "/resend",
34
+ method: "POST",
35
+ payload: resendGenericWebhookEventSchema,
36
+ credentialSets: [resend],
37
+ verify: (request, ctx) => {
38
+ const credentials = ctx.credentials.resend;
39
+ const secrets = resolveSigningSecret(credentials);
40
+ if (secrets.length === 0) throw new Error("Resend webhook signing secret is not configured. Populate `RESEND_WEBHOOK_SIGNING_SECRETS` on the connection (e.g. via `ensureResendWebhook`) before binding webhook triggers.");
41
+ for (const signingSecret of secrets) if (verifyResendWebhookRequest({
42
+ rawBody: request.rawBody,
43
+ headers: request.headers,
44
+ signingSecret
45
+ }).ok) return;
46
+ throw new Error("Invalid Resend webhook signature.");
47
+ },
48
+ definitionFilter: defaultDefinitionFilter(eventType),
49
+ idempotencyKey: (payload) => idempotencyFromPayload(payload),
50
+ response: { successStatus: 200 }
51
+ });
52
+ }
53
+ const bindingTable = Object.fromEntries(RESEND_WEBHOOK_EVENT_NAMES.map((eventName) => [eventName, buildBinding(eventName)]));
54
+ function bind(eventType) {
55
+ return (options) => bindingTable[eventType](options);
56
+ }
57
+ const triggers = Object.freeze({
58
+ emailSent: bind("email.sent"),
59
+ emailDelivered: bind("email.delivered"),
60
+ emailDeliveryDelayed: bind("email.delivery_delayed"),
61
+ emailBounced: bind("email.bounced"),
62
+ emailComplained: bind("email.complained"),
63
+ emailOpened: bind("email.opened"),
64
+ emailClicked: bind("email.clicked"),
65
+ emailFailed: bind("email.failed"),
66
+ emailReceived: bind("email.received"),
67
+ emailScheduled: bind("email.scheduled"),
68
+ emailSuppressed: bind("email.suppressed"),
69
+ domainCreated: bind("domain.created"),
70
+ domainUpdated: bind("domain.updated"),
71
+ domainDeleted: bind("domain.deleted"),
72
+ contactCreated: bind("contact.created"),
73
+ contactUpdated: bind("contact.updated"),
74
+ contactDeleted: bind("contact.deleted")
75
+ });
76
+ async function ensureResendWebhook(params) {
77
+ const client = createResendClient(params.credentials);
78
+ const existing = (await client.request({
79
+ method: "GET",
80
+ path: "/webhooks"
81
+ })).data.find((w) => w.endpoint_url === params.endpointUrl);
82
+ if (existing) {
83
+ const existingEvents = new Set(existing.events ?? []);
84
+ const desiredEvents = params.events === "all" ? /* @__PURE__ */ new Set() : new Set(params.events);
85
+ const eventsChanged = params.events === "all" ? false : existingEvents.size !== desiredEvents.size || [...desiredEvents].some((e) => !existingEvents.has(e));
86
+ const statusChanged = params.status !== void 0 && existing.status !== params.status;
87
+ if (eventsChanged || statusChanged) {
88
+ const secret = (await client.request({
89
+ method: "PATCH",
90
+ path: `/webhooks/${encodeURIComponent(existing.id)}`,
91
+ body: {
92
+ ...eventsChanged ? { events: params.events } : {},
93
+ ...statusChanged ? { status: params.status } : {}
94
+ }
95
+ })).signing_secret ?? (await client.request({
96
+ method: "GET",
97
+ path: `/webhooks/${encodeURIComponent(existing.id)}`
98
+ })).signing_secret;
99
+ return {
100
+ webhookId: existing.id,
101
+ signingSecret: secret,
102
+ endpointUrl: params.endpointUrl,
103
+ events: params.events,
104
+ created: false,
105
+ updated: true
106
+ };
107
+ }
108
+ const refreshed = await client.request({
109
+ method: "GET",
110
+ path: `/webhooks/${encodeURIComponent(existing.id)}`
111
+ });
112
+ return {
113
+ webhookId: existing.id,
114
+ signingSecret: refreshed.signing_secret,
115
+ endpointUrl: params.endpointUrl,
116
+ events: params.events,
117
+ created: false,
118
+ updated: false
119
+ };
120
+ }
121
+ const created = await client.request({
122
+ method: "POST",
123
+ path: "/webhooks",
124
+ body: {
125
+ endpoint_url: params.endpointUrl,
126
+ events: params.events,
127
+ ...params.status ? { status: params.status } : {}
128
+ }
129
+ });
130
+ return {
131
+ webhookId: created.id,
132
+ signingSecret: created.signing_secret,
133
+ endpointUrl: params.endpointUrl,
134
+ events: params.events,
135
+ created: true,
136
+ updated: false
137
+ };
138
+ }
139
+
140
+ //#endregion
141
+ export { ensureResendWebhook, triggers };
@@ -0,0 +1,49 @@
1
+ import { WebhookRequest } from "@keystrokehq/core";
2
+
3
+ //#region src/verification.d.ts
4
+ declare const RESEND_SVIX_ID_HEADER = "svix-id";
5
+ declare const RESEND_SVIX_TIMESTAMP_HEADER = "svix-timestamp";
6
+ declare const RESEND_SVIX_SIGNATURE_HEADER = "svix-signature";
7
+ /** Default replay window: 5 minutes either side of `now`. */
8
+ declare const RESEND_DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 300;
9
+ type ResendWebhookVerificationResult = {
10
+ readonly ok: true;
11
+ readonly svixId: string;
12
+ readonly svixTimestamp: number;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly reason: 'missing_headers' | 'bad_secret_format' | 'bad_timestamp' | 'stale_timestamp' | 'bad_signature';
16
+ };
17
+ interface VerifyResendWebhookOptions {
18
+ /** Raw, UTF-8 request body. Must be preserved byte-for-byte. */
19
+ readonly rawBody: string | Buffer;
20
+ /**
21
+ * Inbound request headers. Case-insensitive; values may be string
22
+ * or string[] (HTTP/2 / Node req.headers).
23
+ */
24
+ readonly headers: Headers | Record<string, string | string[] | undefined> | Pick<WebhookRequest, 'headers'>['headers'];
25
+ /** `whsec_<base64>` as returned by Resend. */
26
+ readonly signingSecret: string;
27
+ /** Max allowed age of the request in seconds. Defaults to 300. */
28
+ readonly toleranceSeconds?: number;
29
+ /** Override the clock for testing. Returns milliseconds since epoch. */
30
+ readonly now?: () => number;
31
+ }
32
+ /**
33
+ * Verify a Resend webhook request using Svix v1 signatures.
34
+ *
35
+ * Returns a structured result instead of a boolean so that callers
36
+ * (and tests) can differentiate replay-window failures from signature
37
+ * failures from bad configuration.
38
+ */
39
+ declare function verifyResendWebhookRequest(options: VerifyResendWebhookOptions): ResendWebhookVerificationResult;
40
+ /**
41
+ * Parse the raw (verified) webhook body as JSON.
42
+ *
43
+ * Returns `undefined` on parse failure. Callers are expected to run
44
+ * the result through `resendWebhookEventSchema.parse(...)` (from
45
+ * `./events`) to get a discriminated-union-typed event.
46
+ */
47
+ declare function parseResendWebhookBody(rawBody: string | Buffer): unknown;
48
+ //#endregion
49
+ export { RESEND_DEFAULT_TIMESTAMP_TOLERANCE_SECONDS, RESEND_SVIX_ID_HEADER, RESEND_SVIX_SIGNATURE_HEADER, RESEND_SVIX_TIMESTAMP_HEADER, ResendWebhookVerificationResult, VerifyResendWebhookOptions, parseResendWebhookBody, verifyResendWebhookRequest };
@@ -0,0 +1,139 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+
3
+ //#region src/verification.ts
4
+ /**
5
+ * resend/verification.ts
6
+ *
7
+ * Svix v1 webhook signature verification for Resend webhooks.
8
+ *
9
+ * Resend signs every webhook POST with Svix. Every request carries:
10
+ *
11
+ * svix-id: unique message id (also used as the idempotency key)
12
+ * svix-timestamp: unix-seconds send time
13
+ * svix-signature: space-separated list of `v1,<base64>` tokens
14
+ *
15
+ * The signing secret is per-endpoint and returned as `whsec_<base64>`
16
+ * on `POST /webhooks` and `GET /webhooks/{id}`.
17
+ *
18
+ * Wire format (Svix v1):
19
+ *
20
+ * signing_input = `${svixId}.${svixTimestamp}.${rawBody}`
21
+ * hmac_key_bytes = base64_decode(secret.slice('whsec_'.length))
22
+ * expected_token = `v1,${base64(HMAC_SHA256(hmac_key_bytes, signing_input))}`
23
+ *
24
+ * A request is valid when at least one `v1,*` token in
25
+ * `svix-signature` matches `expected_token`, verified with a
26
+ * constant-time comparison, and `svix-timestamp` is within the
27
+ * configurable tolerance (default: 300 s) of the current time.
28
+ *
29
+ * Why hand-rolled:
30
+ *
31
+ * - Runtime: no `svix` npm dependency, keeping the bundle
32
+ * edge-friendly. See `IMPLEMENTATION_NOTES.md` § 3.
33
+ * - The shared `verifyHmacWebhook` helper in
34
+ * `@keystrokehq/integration-authoring/webhooks` digests as hex + strips a prefix;
35
+ * Svix uses base64, multiple tokens, and a base64-decoded secret, so
36
+ * we need a Svix-specific path.
37
+ */
38
+ const RESEND_SVIX_ID_HEADER = "svix-id";
39
+ const RESEND_SVIX_TIMESTAMP_HEADER = "svix-timestamp";
40
+ const RESEND_SVIX_SIGNATURE_HEADER = "svix-signature";
41
+ /** Secrets Resend returns on webhook create/retrieve. Always `whsec_<base64>`. */
42
+ const WEBHOOK_SECRET_PREFIX = "whsec_";
43
+ /** Default replay window: 5 minutes either side of `now`. */
44
+ const RESEND_DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 300;
45
+ function getHeader(headers, name) {
46
+ if (headers === void 0) return void 0;
47
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(name) ?? void 0;
48
+ const lower = name.toLowerCase();
49
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === lower) {
50
+ if (Array.isArray(value)) return value[0];
51
+ return value ?? void 0;
52
+ }
53
+ }
54
+ /**
55
+ * Verify a Resend webhook request using Svix v1 signatures.
56
+ *
57
+ * Returns a structured result instead of a boolean so that callers
58
+ * (and tests) can differentiate replay-window failures from signature
59
+ * failures from bad configuration.
60
+ */
61
+ function verifyResendWebhookRequest(options) {
62
+ const svixId = getHeader(options.headers, RESEND_SVIX_ID_HEADER);
63
+ const svixTimestampRaw = getHeader(options.headers, RESEND_SVIX_TIMESTAMP_HEADER);
64
+ const svixSignature = getHeader(options.headers, RESEND_SVIX_SIGNATURE_HEADER);
65
+ if (!svixId || !svixTimestampRaw || !svixSignature) return {
66
+ ok: false,
67
+ reason: "missing_headers"
68
+ };
69
+ if (!options.signingSecret.startsWith(WEBHOOK_SECRET_PREFIX)) return {
70
+ ok: false,
71
+ reason: "bad_secret_format"
72
+ };
73
+ const svixTimestamp = Number(svixTimestampRaw);
74
+ if (!Number.isFinite(svixTimestamp) || !Number.isInteger(svixTimestamp)) return {
75
+ ok: false,
76
+ reason: "bad_timestamp"
77
+ };
78
+ const nowSeconds = Math.floor((options.now?.() ?? Date.now()) / 1e3);
79
+ const tolerance = options.toleranceSeconds ?? RESEND_DEFAULT_TIMESTAMP_TOLERANCE_SECONDS;
80
+ if (Math.abs(nowSeconds - svixTimestamp) > tolerance) return {
81
+ ok: false,
82
+ reason: "stale_timestamp"
83
+ };
84
+ let secretBytes;
85
+ try {
86
+ secretBytes = Buffer.from(options.signingSecret.slice(6), "base64");
87
+ if (secretBytes.length === 0) return {
88
+ ok: false,
89
+ reason: "bad_secret_format"
90
+ };
91
+ } catch {
92
+ return {
93
+ ok: false,
94
+ reason: "bad_secret_format"
95
+ };
96
+ }
97
+ const signingInput = `${svixId}.${svixTimestamp}.${typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8")}`;
98
+ const expected = createHmac("sha256", secretBytes).update(signingInput).digest();
99
+ const expectedBuffer = Buffer.from(expected);
100
+ const tokens = svixSignature.split(" ");
101
+ for (const token of tokens) {
102
+ if (!token.startsWith("v1,")) continue;
103
+ const candidate = token.slice(3);
104
+ let candidateBytes;
105
+ try {
106
+ candidateBytes = Buffer.from(candidate, "base64");
107
+ } catch {
108
+ continue;
109
+ }
110
+ if (candidateBytes.length !== expectedBuffer.length) continue;
111
+ if (timingSafeEqual(candidateBytes, expectedBuffer)) return {
112
+ ok: true,
113
+ svixId,
114
+ svixTimestamp
115
+ };
116
+ }
117
+ return {
118
+ ok: false,
119
+ reason: "bad_signature"
120
+ };
121
+ }
122
+ /**
123
+ * Parse the raw (verified) webhook body as JSON.
124
+ *
125
+ * Returns `undefined` on parse failure. Callers are expected to run
126
+ * the result through `resendWebhookEventSchema.parse(...)` (from
127
+ * `./events`) to get a discriminated-union-typed event.
128
+ */
129
+ function parseResendWebhookBody(rawBody) {
130
+ const text = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
131
+ try {
132
+ return JSON.parse(text);
133
+ } catch {
134
+ return;
135
+ }
136
+ }
137
+
138
+ //#endregion
139
+ export { RESEND_DEFAULT_TIMESTAMP_TOLERANCE_SECONDS, RESEND_SVIX_ID_HEADER, RESEND_SVIX_SIGNATURE_HEADER, RESEND_SVIX_TIMESTAMP_HEADER, parseResendWebhookBody, verifyResendWebhookRequest };