@better-webhook/resend 0.1.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.
package/dist/events.js ADDED
@@ -0,0 +1,258 @@
1
+ import { defineEvent } from '@better-webhook/core';
2
+ import { z } from 'zod';
3
+
4
+ // src/events.ts
5
+ var ResendTagsMapSchema = z.record(z.string(), z.string());
6
+ var ResendEmailEventDataSchema = z.object({
7
+ broadcast_id: z.string().optional(),
8
+ created_at: z.string(),
9
+ email_id: z.string(),
10
+ from: z.string(),
11
+ to: z.array(z.string()),
12
+ subject: z.string(),
13
+ template_id: z.string().optional(),
14
+ tags: ResendTagsMapSchema.optional()
15
+ }).passthrough();
16
+ var ResendBounceSchema = z.object({
17
+ diagnosticCode: z.array(z.string()).optional(),
18
+ message: z.string(),
19
+ subType: z.string(),
20
+ type: z.string()
21
+ }).passthrough();
22
+ var ResendClickSchema = z.object({
23
+ ipAddress: z.string(),
24
+ link: z.string(),
25
+ timestamp: z.string(),
26
+ userAgent: z.string()
27
+ }).passthrough();
28
+ var ResendFailedSchema = z.object({
29
+ reason: z.string()
30
+ }).passthrough();
31
+ var ResendSuppressedSchema = z.object({
32
+ message: z.string(),
33
+ type: z.string()
34
+ }).passthrough();
35
+ var ResendReceivedAttachmentSchema = z.object({
36
+ id: z.string(),
37
+ filename: z.string().nullable(),
38
+ content_type: z.string(),
39
+ content_disposition: z.string().nullable(),
40
+ content_id: z.string().nullable()
41
+ }).passthrough();
42
+ var ResendReceivedEmailEventDataSchema = z.object({
43
+ email_id: z.string(),
44
+ created_at: z.string(),
45
+ from: z.string(),
46
+ to: z.array(z.string()),
47
+ bcc: z.array(z.string()).optional(),
48
+ cc: z.array(z.string()).optional(),
49
+ message_id: z.string(),
50
+ subject: z.string().default(""),
51
+ attachments: z.array(ResendReceivedAttachmentSchema).optional()
52
+ }).passthrough();
53
+ var ResendContactEventDataSchema = z.object({
54
+ id: z.string(),
55
+ audience_id: z.string().optional(),
56
+ segment_ids: z.array(z.string()),
57
+ created_at: z.string(),
58
+ updated_at: z.string(),
59
+ email: z.string(),
60
+ first_name: z.string().optional(),
61
+ last_name: z.string().optional(),
62
+ unsubscribed: z.boolean()
63
+ }).passthrough();
64
+ var ResendContactDeletedEventDataSchema = ResendContactEventDataSchema.extend(
65
+ {
66
+ segment_ids: z.array(z.string()).optional(),
67
+ unsubscribed: z.boolean().optional()
68
+ }
69
+ ).passthrough();
70
+ var ResendDomainRecordSchema = z.object({
71
+ record: z.string(),
72
+ name: z.string(),
73
+ type: z.string(),
74
+ value: z.string(),
75
+ ttl: z.string(),
76
+ status: z.string(),
77
+ priority: z.number().optional()
78
+ }).passthrough();
79
+ var ResendDomainEventDataSchema = z.object({
80
+ id: z.string(),
81
+ name: z.string(),
82
+ status: z.string(),
83
+ created_at: z.string(),
84
+ region: z.string(),
85
+ records: z.array(ResendDomainRecordSchema)
86
+ }).passthrough();
87
+ function createResendEventSchema(eventType, dataSchema) {
88
+ return z.object({
89
+ type: z.literal(eventType),
90
+ created_at: z.string(),
91
+ data: dataSchema
92
+ }).passthrough();
93
+ }
94
+ var ResendEmailSentEventSchema = createResendEventSchema(
95
+ "email.sent",
96
+ ResendEmailEventDataSchema
97
+ );
98
+ var ResendEmailScheduledEventSchema = createResendEventSchema(
99
+ "email.scheduled",
100
+ ResendEmailEventDataSchema
101
+ );
102
+ var ResendEmailDeliveredEventSchema = createResendEventSchema(
103
+ "email.delivered",
104
+ ResendEmailEventDataSchema
105
+ );
106
+ var ResendEmailDeliveryDelayedEventSchema = createResendEventSchema(
107
+ "email.delivery_delayed",
108
+ ResendEmailEventDataSchema
109
+ );
110
+ var ResendEmailComplainedEventSchema = createResendEventSchema(
111
+ "email.complained",
112
+ ResendEmailEventDataSchema
113
+ );
114
+ var ResendEmailBouncedEventSchema = createResendEventSchema(
115
+ "email.bounced",
116
+ ResendEmailEventDataSchema.extend({
117
+ bounce: ResendBounceSchema
118
+ })
119
+ );
120
+ var ResendEmailOpenedEventSchema = createResendEventSchema(
121
+ "email.opened",
122
+ ResendEmailEventDataSchema
123
+ );
124
+ var ResendEmailClickedEventSchema = createResendEventSchema(
125
+ "email.clicked",
126
+ ResendEmailEventDataSchema.extend({
127
+ click: ResendClickSchema
128
+ })
129
+ );
130
+ var ResendEmailReceivedEventSchema = createResendEventSchema(
131
+ "email.received",
132
+ ResendReceivedEmailEventDataSchema
133
+ );
134
+ var ResendEmailFailedEventSchema = createResendEventSchema(
135
+ "email.failed",
136
+ ResendEmailEventDataSchema.extend({
137
+ failed: ResendFailedSchema
138
+ })
139
+ );
140
+ var ResendEmailSuppressedEventSchema = createResendEventSchema(
141
+ "email.suppressed",
142
+ ResendEmailEventDataSchema.extend({
143
+ suppressed: ResendSuppressedSchema
144
+ })
145
+ );
146
+ var ResendContactCreatedEventSchema = createResendEventSchema(
147
+ "contact.created",
148
+ ResendContactEventDataSchema
149
+ );
150
+ var ResendContactUpdatedEventSchema = createResendEventSchema(
151
+ "contact.updated",
152
+ ResendContactEventDataSchema
153
+ );
154
+ var ResendContactDeletedEventSchema = createResendEventSchema(
155
+ "contact.deleted",
156
+ ResendContactDeletedEventDataSchema
157
+ );
158
+ var ResendDomainCreatedEventSchema = createResendEventSchema(
159
+ "domain.created",
160
+ ResendDomainEventDataSchema
161
+ );
162
+ var ResendDomainUpdatedEventSchema = createResendEventSchema(
163
+ "domain.updated",
164
+ ResendDomainEventDataSchema
165
+ );
166
+ var ResendDomainDeletedEventSchema = createResendEventSchema(
167
+ "domain.deleted",
168
+ ResendDomainEventDataSchema
169
+ );
170
+
171
+ // src/events.ts
172
+ var email_sent = defineEvent({
173
+ name: "email.sent",
174
+ schema: ResendEmailSentEventSchema,
175
+ provider: "resend"
176
+ });
177
+ var email_scheduled = defineEvent({
178
+ name: "email.scheduled",
179
+ schema: ResendEmailScheduledEventSchema,
180
+ provider: "resend"
181
+ });
182
+ var email_delivered = defineEvent({
183
+ name: "email.delivered",
184
+ schema: ResendEmailDeliveredEventSchema,
185
+ provider: "resend"
186
+ });
187
+ var email_delivery_delayed = defineEvent({
188
+ name: "email.delivery_delayed",
189
+ schema: ResendEmailDeliveryDelayedEventSchema,
190
+ provider: "resend"
191
+ });
192
+ var email_complained = defineEvent({
193
+ name: "email.complained",
194
+ schema: ResendEmailComplainedEventSchema,
195
+ provider: "resend"
196
+ });
197
+ var email_bounced = defineEvent({
198
+ name: "email.bounced",
199
+ schema: ResendEmailBouncedEventSchema,
200
+ provider: "resend"
201
+ });
202
+ var email_opened = defineEvent({
203
+ name: "email.opened",
204
+ schema: ResendEmailOpenedEventSchema,
205
+ provider: "resend"
206
+ });
207
+ var email_clicked = defineEvent({
208
+ name: "email.clicked",
209
+ schema: ResendEmailClickedEventSchema,
210
+ provider: "resend"
211
+ });
212
+ var email_received = defineEvent({
213
+ name: "email.received",
214
+ schema: ResendEmailReceivedEventSchema,
215
+ provider: "resend"
216
+ });
217
+ var email_failed = defineEvent({
218
+ name: "email.failed",
219
+ schema: ResendEmailFailedEventSchema,
220
+ provider: "resend"
221
+ });
222
+ var email_suppressed = defineEvent({
223
+ name: "email.suppressed",
224
+ schema: ResendEmailSuppressedEventSchema,
225
+ provider: "resend"
226
+ });
227
+ var contact_created = defineEvent({
228
+ name: "contact.created",
229
+ schema: ResendContactCreatedEventSchema,
230
+ provider: "resend"
231
+ });
232
+ var contact_updated = defineEvent({
233
+ name: "contact.updated",
234
+ schema: ResendContactUpdatedEventSchema,
235
+ provider: "resend"
236
+ });
237
+ var contact_deleted = defineEvent({
238
+ name: "contact.deleted",
239
+ schema: ResendContactDeletedEventSchema,
240
+ provider: "resend"
241
+ });
242
+ var domain_created = defineEvent({
243
+ name: "domain.created",
244
+ schema: ResendDomainCreatedEventSchema,
245
+ provider: "resend"
246
+ });
247
+ var domain_updated = defineEvent({
248
+ name: "domain.updated",
249
+ schema: ResendDomainUpdatedEventSchema,
250
+ provider: "resend"
251
+ });
252
+ var domain_deleted = defineEvent({
253
+ name: "domain.deleted",
254
+ schema: ResendDomainDeletedEventSchema,
255
+ provider: "resend"
256
+ });
257
+
258
+ export { contact_created, contact_deleted, contact_updated, domain_created, domain_deleted, domain_updated, email_bounced, email_clicked, email_complained, email_delivered, email_delivery_delayed, email_failed, email_opened, email_received, email_scheduled, email_sent, email_suppressed };
package/dist/index.cjs ADDED
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ var buffer = require('buffer');
4
+ var crypto = require('crypto');
5
+ var core = require('@better-webhook/core');
6
+
7
+ // src/index.ts
8
+ var DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 60 * 5;
9
+ var WEBHOOK_SECRET_PREFIX = "whsec_";
10
+ var STRICT_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
11
+ var STRICT_BASE64_UNPADDED_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}|[A-Za-z0-9+/]{3})?$/;
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function normalizeBody(rawBody) {
16
+ return typeof rawBody === "string" ? rawBody : rawBody.toString("utf-8");
17
+ }
18
+ function normalizeStrictBase64(value, options) {
19
+ if (value.length === 0) {
20
+ return void 0;
21
+ }
22
+ if (STRICT_BASE64_PATTERN.test(value)) {
23
+ return value;
24
+ }
25
+ if (!options?.allowUnpadded || !STRICT_BASE64_UNPADDED_PATTERN.test(value)) {
26
+ return void 0;
27
+ }
28
+ return value.padEnd(value.length + (4 - value.length % 4) % 4, "=");
29
+ }
30
+ function parseUnixTimestamp(value) {
31
+ if (!/^\d+$/.test(value)) {
32
+ return void 0;
33
+ }
34
+ const parsedTimestamp = Number(value);
35
+ if (!Number.isSafeInteger(parsedTimestamp) || parsedTimestamp <= 0) {
36
+ return void 0;
37
+ }
38
+ return parsedTimestamp;
39
+ }
40
+ function decodeWebhookSecret(secret) {
41
+ if (!secret.startsWith(WEBHOOK_SECRET_PREFIX)) {
42
+ return void 0;
43
+ }
44
+ const encodedSecret = secret.slice(WEBHOOK_SECRET_PREFIX.length);
45
+ const normalizedSecret = normalizeStrictBase64(encodedSecret, {
46
+ allowUnpadded: true
47
+ });
48
+ if (!normalizedSecret) {
49
+ return void 0;
50
+ }
51
+ try {
52
+ const decoded = buffer.Buffer.from(normalizedSecret, "base64");
53
+ if (decoded.length === 0) {
54
+ return void 0;
55
+ }
56
+ return decoded;
57
+ } catch {
58
+ return void 0;
59
+ }
60
+ }
61
+ function secureCompareBase64(left, right) {
62
+ const normalizedLeft = normalizeStrictBase64(left, { allowUnpadded: true });
63
+ const normalizedRight = normalizeStrictBase64(right, {
64
+ allowUnpadded: true
65
+ });
66
+ if (!normalizedLeft || !normalizedRight) {
67
+ return false;
68
+ }
69
+ try {
70
+ const leftBuffer = buffer.Buffer.from(normalizedLeft, "base64");
71
+ const rightBuffer = buffer.Buffer.from(normalizedRight, "base64");
72
+ if (leftBuffer.length !== rightBuffer.length) {
73
+ return false;
74
+ }
75
+ return crypto.timingSafeEqual(leftBuffer, rightBuffer);
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+ function parseSvixSignatures(signatureHeader) {
81
+ const signatures = [];
82
+ for (const versionedSignature of signatureHeader.split(" ")) {
83
+ const trimmedSegment = versionedSignature.trim();
84
+ if (!trimmedSegment) {
85
+ continue;
86
+ }
87
+ const [version, signature] = trimmedSegment.split(",", 2);
88
+ if (version === "v1" && signature) {
89
+ signatures.push(signature);
90
+ }
91
+ }
92
+ return signatures;
93
+ }
94
+ function extractReplayContext(headers) {
95
+ const replayKey = headers["svix-id"]?.trim();
96
+ const timestampHeader = headers["svix-timestamp"];
97
+ const timestamp = timestampHeader ? parseUnixTimestamp(timestampHeader) : void 0;
98
+ return {
99
+ replayKey: replayKey && replayKey.length > 0 ? replayKey : void 0,
100
+ timestamp
101
+ };
102
+ }
103
+ function verifyResendSignature(rawBody, headers, secret, timestampToleranceSeconds) {
104
+ const messageId = headers["svix-id"];
105
+ const messageTimestamp = headers["svix-timestamp"];
106
+ const signatureHeader = headers["svix-signature"];
107
+ if (!messageId || !messageTimestamp || !signatureHeader) {
108
+ return false;
109
+ }
110
+ const parsedTimestamp = parseUnixTimestamp(messageTimestamp);
111
+ if (!parsedTimestamp) {
112
+ return false;
113
+ }
114
+ const nowSeconds = Math.floor(Date.now() / 1e3);
115
+ const ageInSeconds = Math.abs(nowSeconds - parsedTimestamp);
116
+ if (timestampToleranceSeconds > 0 && ageInSeconds > timestampToleranceSeconds) {
117
+ return false;
118
+ }
119
+ const signingKey = decodeWebhookSecret(secret.trim());
120
+ if (!signingKey) {
121
+ return false;
122
+ }
123
+ const signatures = parseSvixSignatures(signatureHeader);
124
+ if (signatures.length === 0) {
125
+ return false;
126
+ }
127
+ const signedPayload = `${messageId}.${messageTimestamp}.${normalizeBody(rawBody)}`;
128
+ const expectedSignature = crypto.createHmac("sha256", signingKey).update(signedPayload).digest("base64");
129
+ for (const candidateSignature of signatures) {
130
+ if (secureCompareBase64(expectedSignature, candidateSignature)) {
131
+ return true;
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+ function createResendProvider(options) {
137
+ const configuredTimestampTolerance = options?.timestampToleranceSeconds;
138
+ const timestampToleranceSeconds = configuredTimestampTolerance !== void 0 && Number.isFinite(configuredTimestampTolerance) ? configuredTimestampTolerance : DEFAULT_TIMESTAMP_TOLERANCE_SECONDS;
139
+ return {
140
+ name: "resend",
141
+ secret: options?.secret,
142
+ verification: "required",
143
+ verifiedUnhandledStatus: 200,
144
+ getEventType(_headers, body) {
145
+ if (!isRecord(body)) {
146
+ return void 0;
147
+ }
148
+ return typeof body.type === "string" ? body.type : void 0;
149
+ },
150
+ getDeliveryId(headers) {
151
+ return headers["svix-id"];
152
+ },
153
+ getReplayContext(headers) {
154
+ return extractReplayContext(headers);
155
+ },
156
+ verify(rawBody, headers, secret) {
157
+ return verifyResendSignature(
158
+ rawBody,
159
+ headers,
160
+ secret,
161
+ timestampToleranceSeconds
162
+ );
163
+ }
164
+ };
165
+ }
166
+ function resend(options) {
167
+ const provider = createResendProvider(options);
168
+ return new core.WebhookBuilder(provider);
169
+ }
170
+
171
+ exports.resend = resend;
@@ -0,0 +1,12 @@
1
+ import { WebhookBuilder } from '@better-webhook/core';
2
+ export { ResendContactCreatedEvent, ResendContactDeletedEvent, ResendContactUpdatedEvent, ResendDomainCreatedEvent, ResendDomainDeletedEvent, ResendDomainUpdatedEvent, ResendEmailBouncedEvent, ResendEmailClickedEvent, ResendEmailComplainedEvent, ResendEmailDeliveredEvent, ResendEmailDeliveryDelayedEvent, ResendEmailFailedEvent, ResendEmailOpenedEvent, ResendEmailReceivedEvent, ResendEmailScheduledEvent, ResendEmailSentEvent, ResendEmailSuppressedEvent, ResendProvider } from './events.cjs';
3
+ import 'zod/v4/core';
4
+ import 'zod';
5
+
6
+ interface ResendOptions {
7
+ secret?: string;
8
+ timestampToleranceSeconds?: number;
9
+ }
10
+ declare function resend(options?: ResendOptions): WebhookBuilder<"resend">;
11
+
12
+ export { type ResendOptions, resend };
@@ -0,0 +1,12 @@
1
+ import { WebhookBuilder } from '@better-webhook/core';
2
+ export { ResendContactCreatedEvent, ResendContactDeletedEvent, ResendContactUpdatedEvent, ResendDomainCreatedEvent, ResendDomainDeletedEvent, ResendDomainUpdatedEvent, ResendEmailBouncedEvent, ResendEmailClickedEvent, ResendEmailComplainedEvent, ResendEmailDeliveredEvent, ResendEmailDeliveryDelayedEvent, ResendEmailFailedEvent, ResendEmailOpenedEvent, ResendEmailReceivedEvent, ResendEmailScheduledEvent, ResendEmailSentEvent, ResendEmailSuppressedEvent, ResendProvider } from './events.js';
3
+ import 'zod/v4/core';
4
+ import 'zod';
5
+
6
+ interface ResendOptions {
7
+ secret?: string;
8
+ timestampToleranceSeconds?: number;
9
+ }
10
+ declare function resend(options?: ResendOptions): WebhookBuilder<"resend">;
11
+
12
+ export { type ResendOptions, resend };
package/dist/index.js ADDED
@@ -0,0 +1,169 @@
1
+ import { Buffer } from 'buffer';
2
+ import { createHmac, timingSafeEqual } from 'crypto';
3
+ import { WebhookBuilder } from '@better-webhook/core';
4
+
5
+ // src/index.ts
6
+ var DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 60 * 5;
7
+ var WEBHOOK_SECRET_PREFIX = "whsec_";
8
+ var STRICT_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
9
+ var STRICT_BASE64_UNPADDED_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}|[A-Za-z0-9+/]{3})?$/;
10
+ function isRecord(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ function normalizeBody(rawBody) {
14
+ return typeof rawBody === "string" ? rawBody : rawBody.toString("utf-8");
15
+ }
16
+ function normalizeStrictBase64(value, options) {
17
+ if (value.length === 0) {
18
+ return void 0;
19
+ }
20
+ if (STRICT_BASE64_PATTERN.test(value)) {
21
+ return value;
22
+ }
23
+ if (!options?.allowUnpadded || !STRICT_BASE64_UNPADDED_PATTERN.test(value)) {
24
+ return void 0;
25
+ }
26
+ return value.padEnd(value.length + (4 - value.length % 4) % 4, "=");
27
+ }
28
+ function parseUnixTimestamp(value) {
29
+ if (!/^\d+$/.test(value)) {
30
+ return void 0;
31
+ }
32
+ const parsedTimestamp = Number(value);
33
+ if (!Number.isSafeInteger(parsedTimestamp) || parsedTimestamp <= 0) {
34
+ return void 0;
35
+ }
36
+ return parsedTimestamp;
37
+ }
38
+ function decodeWebhookSecret(secret) {
39
+ if (!secret.startsWith(WEBHOOK_SECRET_PREFIX)) {
40
+ return void 0;
41
+ }
42
+ const encodedSecret = secret.slice(WEBHOOK_SECRET_PREFIX.length);
43
+ const normalizedSecret = normalizeStrictBase64(encodedSecret, {
44
+ allowUnpadded: true
45
+ });
46
+ if (!normalizedSecret) {
47
+ return void 0;
48
+ }
49
+ try {
50
+ const decoded = Buffer.from(normalizedSecret, "base64");
51
+ if (decoded.length === 0) {
52
+ return void 0;
53
+ }
54
+ return decoded;
55
+ } catch {
56
+ return void 0;
57
+ }
58
+ }
59
+ function secureCompareBase64(left, right) {
60
+ const normalizedLeft = normalizeStrictBase64(left, { allowUnpadded: true });
61
+ const normalizedRight = normalizeStrictBase64(right, {
62
+ allowUnpadded: true
63
+ });
64
+ if (!normalizedLeft || !normalizedRight) {
65
+ return false;
66
+ }
67
+ try {
68
+ const leftBuffer = Buffer.from(normalizedLeft, "base64");
69
+ const rightBuffer = Buffer.from(normalizedRight, "base64");
70
+ if (leftBuffer.length !== rightBuffer.length) {
71
+ return false;
72
+ }
73
+ return timingSafeEqual(leftBuffer, rightBuffer);
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ function parseSvixSignatures(signatureHeader) {
79
+ const signatures = [];
80
+ for (const versionedSignature of signatureHeader.split(" ")) {
81
+ const trimmedSegment = versionedSignature.trim();
82
+ if (!trimmedSegment) {
83
+ continue;
84
+ }
85
+ const [version, signature] = trimmedSegment.split(",", 2);
86
+ if (version === "v1" && signature) {
87
+ signatures.push(signature);
88
+ }
89
+ }
90
+ return signatures;
91
+ }
92
+ function extractReplayContext(headers) {
93
+ const replayKey = headers["svix-id"]?.trim();
94
+ const timestampHeader = headers["svix-timestamp"];
95
+ const timestamp = timestampHeader ? parseUnixTimestamp(timestampHeader) : void 0;
96
+ return {
97
+ replayKey: replayKey && replayKey.length > 0 ? replayKey : void 0,
98
+ timestamp
99
+ };
100
+ }
101
+ function verifyResendSignature(rawBody, headers, secret, timestampToleranceSeconds) {
102
+ const messageId = headers["svix-id"];
103
+ const messageTimestamp = headers["svix-timestamp"];
104
+ const signatureHeader = headers["svix-signature"];
105
+ if (!messageId || !messageTimestamp || !signatureHeader) {
106
+ return false;
107
+ }
108
+ const parsedTimestamp = parseUnixTimestamp(messageTimestamp);
109
+ if (!parsedTimestamp) {
110
+ return false;
111
+ }
112
+ const nowSeconds = Math.floor(Date.now() / 1e3);
113
+ const ageInSeconds = Math.abs(nowSeconds - parsedTimestamp);
114
+ if (timestampToleranceSeconds > 0 && ageInSeconds > timestampToleranceSeconds) {
115
+ return false;
116
+ }
117
+ const signingKey = decodeWebhookSecret(secret.trim());
118
+ if (!signingKey) {
119
+ return false;
120
+ }
121
+ const signatures = parseSvixSignatures(signatureHeader);
122
+ if (signatures.length === 0) {
123
+ return false;
124
+ }
125
+ const signedPayload = `${messageId}.${messageTimestamp}.${normalizeBody(rawBody)}`;
126
+ const expectedSignature = createHmac("sha256", signingKey).update(signedPayload).digest("base64");
127
+ for (const candidateSignature of signatures) {
128
+ if (secureCompareBase64(expectedSignature, candidateSignature)) {
129
+ return true;
130
+ }
131
+ }
132
+ return false;
133
+ }
134
+ function createResendProvider(options) {
135
+ const configuredTimestampTolerance = options?.timestampToleranceSeconds;
136
+ const timestampToleranceSeconds = configuredTimestampTolerance !== void 0 && Number.isFinite(configuredTimestampTolerance) ? configuredTimestampTolerance : DEFAULT_TIMESTAMP_TOLERANCE_SECONDS;
137
+ return {
138
+ name: "resend",
139
+ secret: options?.secret,
140
+ verification: "required",
141
+ verifiedUnhandledStatus: 200,
142
+ getEventType(_headers, body) {
143
+ if (!isRecord(body)) {
144
+ return void 0;
145
+ }
146
+ return typeof body.type === "string" ? body.type : void 0;
147
+ },
148
+ getDeliveryId(headers) {
149
+ return headers["svix-id"];
150
+ },
151
+ getReplayContext(headers) {
152
+ return extractReplayContext(headers);
153
+ },
154
+ verify(rawBody, headers, secret) {
155
+ return verifyResendSignature(
156
+ rawBody,
157
+ headers,
158
+ secret,
159
+ timestampToleranceSeconds
160
+ );
161
+ }
162
+ };
163
+ }
164
+ function resend(options) {
165
+ const provider = createResendProvider(options);
166
+ return new WebhookBuilder(provider);
167
+ }
168
+
169
+ export { resend };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@better-webhook/resend",
3
+ "version": "0.1.1",
4
+ "description": "Resend module for better-webhook",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ },
16
+ "./events": {
17
+ "types": "./dist/events.d.ts",
18
+ "import": "./dist/events.js",
19
+ "require": "./dist/events.cjs"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "keywords": [
28
+ "webhook",
29
+ "resend",
30
+ "email",
31
+ "webhook-handler",
32
+ "module",
33
+ "api"
34
+ ],
35
+ "license": "MIT",
36
+ "author": "Endalk <endalk200>",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/endalk200/better-webhook.git",
40
+ "directory": "packages/resend"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/endalk200/better-webhook/issues"
44
+ },
45
+ "homepage": "https://github.com/endalk200/better-webhook#readme",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "dependencies": {
53
+ "zod": "^4.0.0",
54
+ "@better-webhook/core": "0.11.4"
55
+ },
56
+ "peerDependencies": {},
57
+ "devDependencies": {
58
+ "@types/node": "^24.3.1",
59
+ "eslint": "^9.35.0",
60
+ "tsup": "^8.5.0",
61
+ "typescript": "^5.6.3",
62
+ "vitest": "^4.0.16",
63
+ "@better-webhook/eslint-config": "0.0.0",
64
+ "@better-webhook/typescript-config": "0.0.0"
65
+ },
66
+ "scripts": {
67
+ "build": "tsup",
68
+ "dev": "tsup --watch",
69
+ "lint": "eslint .",
70
+ "check-types": "tsc --noEmit",
71
+ "test": "vitest run",
72
+ "test:watch": "vitest",
73
+ "clean": "rm -rf dist .turbo"
74
+ }
75
+ }