@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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/events.cjs +276 -0
- package/dist/events.d.cts +596 -0
- package/dist/events.d.ts +596 -0
- package/dist/events.js +258 -0
- package/dist/index.cjs +171 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +169 -0
- package/package.json +75 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|