@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Endalkachew Biruk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @better-webhook/resend
2
+
3
+ [![npm](https://img.shields.io/npm/v/@better-webhook/resend?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@better-webhook/resend)
4
+
5
+ Type-safe Resend webhook handling for `better-webhook`.
6
+
7
+ ## Features
8
+
9
+ - Typed webhook envelopes for all 17 documented Resend event types
10
+ - Automatic Svix-compatible signature verification using `svix-*` headers
11
+ - Default signed timestamp freshness checks (`300` seconds)
12
+ - Replay key support via `svix-id`
13
+ - Tree-shakeable event exports from `@better-webhook/resend/events`
14
+ - Verified but unhandled events acknowledge with `200` to match Resend's delivery contract
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @better-webhook/resend @better-webhook/core
20
+ # or
21
+ pnpm add @better-webhook/resend @better-webhook/core
22
+ # or
23
+ yarn add @better-webhook/resend @better-webhook/core
24
+ ```
25
+
26
+ Install one adapter package too:
27
+
28
+ ```bash
29
+ # Pick one:
30
+ npm install @better-webhook/nextjs
31
+ npm install @better-webhook/express
32
+ npm install @better-webhook/nestjs
33
+ npm install @better-webhook/hono
34
+ npm install @better-webhook/gcp-functions
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```ts
40
+ import { resend } from "@better-webhook/resend";
41
+ import {
42
+ email_bounced,
43
+ email_delivered,
44
+ email_received,
45
+ } from "@better-webhook/resend/events";
46
+ import { toNextJS } from "@better-webhook/nextjs";
47
+
48
+ const webhook = resend({
49
+ secret: process.env.RESEND_WEBHOOK_SECRET,
50
+ })
51
+ .event(email_delivered, async (payload) => {
52
+ console.log("delivered:", payload.data.email_id);
53
+ })
54
+ .event(email_bounced, async (payload) => {
55
+ console.log("bounce type:", payload.data.bounce.type);
56
+ })
57
+ .event(email_received, async (payload) => {
58
+ console.log("inbound message:", payload.data.message_id);
59
+ });
60
+
61
+ export const POST = toNextJS(webhook);
62
+ ```
63
+
64
+ ## Supported Events
65
+
66
+ ### Email Events
67
+
68
+ - `email_sent` (`email.sent`)
69
+ - `email_scheduled` (`email.scheduled`)
70
+ - `email_delivered` (`email.delivered`)
71
+ - `email_delivery_delayed` (`email.delivery_delayed`)
72
+ - `email_complained` (`email.complained`)
73
+ - `email_bounced` (`email.bounced`)
74
+ - `email_opened` (`email.opened`)
75
+ - `email_clicked` (`email.clicked`)
76
+ - `email_received` (`email.received`)
77
+ - `email_failed` (`email.failed`)
78
+ - `email_suppressed` (`email.suppressed`)
79
+
80
+ ### Domain Events
81
+
82
+ - `domain_created` (`domain.created`)
83
+ - `domain_updated` (`domain.updated`)
84
+ - `domain_deleted` (`domain.deleted`)
85
+
86
+ ### Contact Events
87
+
88
+ - `contact_created` (`contact.created`)
89
+ - `contact_updated` (`contact.updated`)
90
+ - `contact_deleted` (`contact.deleted`)
91
+
92
+ ## Signature Verification
93
+
94
+ Resend signs webhook requests with Svix-compatible headers:
95
+
96
+ - `svix-id`
97
+ - `svix-timestamp`
98
+ - `svix-signature`
99
+
100
+ This package verifies the exact raw body using HMAC-SHA256 over:
101
+
102
+ ```text
103
+ ${svixId}.${svixTimestamp}.${rawBody}
104
+ ```
105
+
106
+ using the base64-decoded portion of the `whsec_...` signing secret.
107
+
108
+ Verification behavior:
109
+
110
+ - rejects missing `svix-*` headers
111
+ - rejects malformed timestamps
112
+ - rejects stale or far-future timestamps outside the configured tolerance window
113
+ - accepts any valid `v1` entry in a multi-signature `svix-signature` header
114
+
115
+ Use the raw request body exactly as Resend sent it. Re-serializing JSON before
116
+ verification will break the signature.
117
+
118
+ ## Replay Protection and Idempotency
119
+
120
+ Resend delivers webhooks with at-least-once semantics and may retry or replay
121
+ the same message. The provider exposes:
122
+
123
+ - `context.deliveryId` from `svix-id`
124
+ - replay metadata via `svix-id` + `svix-timestamp`
125
+
126
+ With core replay protection enabled, duplicate `svix-id` values return `409` by
127
+ default:
128
+
129
+ ```ts
130
+ import { createInMemoryReplayStore } from "@better-webhook/core";
131
+
132
+ const webhook = resend({ secret: process.env.RESEND_WEBHOOK_SECRET })
133
+ .withReplayProtection({
134
+ store: createInMemoryReplayStore(),
135
+ })
136
+ .event(email_delivered, async (payload) => {
137
+ await persistEvent(payload);
138
+ });
139
+ ```
140
+
141
+ For production, use a shared durable replay store so deduplication works across
142
+ instances and restarts.
143
+
144
+ ## Payload Notes
145
+
146
+ - Handlers receive the full Resend webhook envelope: `{ type, created_at, data }`.
147
+ - `email.received` webhooks contain metadata only. Fetch the full inbound body,
148
+ headers, and attachments through Resend's receiving APIs if you need message
149
+ content.
150
+ - `email.received` payloads may omit `data.subject`; the schema normalizes a
151
+ missing subject to `""`.
152
+ - `data.tags` follows Resend's documented `Record<string, string>` shape.
153
+
154
+ ## Environment Variables
155
+
156
+ When no explicit secret is provided, Better Webhook resolves:
157
+
158
+ 1. `RESEND_WEBHOOK_SECRET`
159
+ 2. `WEBHOOK_SECRET`
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ var core = require('@better-webhook/core');
4
+ var zod = require('zod');
5
+
6
+ // src/events.ts
7
+ var ResendTagsMapSchema = zod.z.record(zod.z.string(), zod.z.string());
8
+ var ResendEmailEventDataSchema = zod.z.object({
9
+ broadcast_id: zod.z.string().optional(),
10
+ created_at: zod.z.string(),
11
+ email_id: zod.z.string(),
12
+ from: zod.z.string(),
13
+ to: zod.z.array(zod.z.string()),
14
+ subject: zod.z.string(),
15
+ template_id: zod.z.string().optional(),
16
+ tags: ResendTagsMapSchema.optional()
17
+ }).passthrough();
18
+ var ResendBounceSchema = zod.z.object({
19
+ diagnosticCode: zod.z.array(zod.z.string()).optional(),
20
+ message: zod.z.string(),
21
+ subType: zod.z.string(),
22
+ type: zod.z.string()
23
+ }).passthrough();
24
+ var ResendClickSchema = zod.z.object({
25
+ ipAddress: zod.z.string(),
26
+ link: zod.z.string(),
27
+ timestamp: zod.z.string(),
28
+ userAgent: zod.z.string()
29
+ }).passthrough();
30
+ var ResendFailedSchema = zod.z.object({
31
+ reason: zod.z.string()
32
+ }).passthrough();
33
+ var ResendSuppressedSchema = zod.z.object({
34
+ message: zod.z.string(),
35
+ type: zod.z.string()
36
+ }).passthrough();
37
+ var ResendReceivedAttachmentSchema = zod.z.object({
38
+ id: zod.z.string(),
39
+ filename: zod.z.string().nullable(),
40
+ content_type: zod.z.string(),
41
+ content_disposition: zod.z.string().nullable(),
42
+ content_id: zod.z.string().nullable()
43
+ }).passthrough();
44
+ var ResendReceivedEmailEventDataSchema = zod.z.object({
45
+ email_id: zod.z.string(),
46
+ created_at: zod.z.string(),
47
+ from: zod.z.string(),
48
+ to: zod.z.array(zod.z.string()),
49
+ bcc: zod.z.array(zod.z.string()).optional(),
50
+ cc: zod.z.array(zod.z.string()).optional(),
51
+ message_id: zod.z.string(),
52
+ subject: zod.z.string().default(""),
53
+ attachments: zod.z.array(ResendReceivedAttachmentSchema).optional()
54
+ }).passthrough();
55
+ var ResendContactEventDataSchema = zod.z.object({
56
+ id: zod.z.string(),
57
+ audience_id: zod.z.string().optional(),
58
+ segment_ids: zod.z.array(zod.z.string()),
59
+ created_at: zod.z.string(),
60
+ updated_at: zod.z.string(),
61
+ email: zod.z.string(),
62
+ first_name: zod.z.string().optional(),
63
+ last_name: zod.z.string().optional(),
64
+ unsubscribed: zod.z.boolean()
65
+ }).passthrough();
66
+ var ResendContactDeletedEventDataSchema = ResendContactEventDataSchema.extend(
67
+ {
68
+ segment_ids: zod.z.array(zod.z.string()).optional(),
69
+ unsubscribed: zod.z.boolean().optional()
70
+ }
71
+ ).passthrough();
72
+ var ResendDomainRecordSchema = zod.z.object({
73
+ record: zod.z.string(),
74
+ name: zod.z.string(),
75
+ type: zod.z.string(),
76
+ value: zod.z.string(),
77
+ ttl: zod.z.string(),
78
+ status: zod.z.string(),
79
+ priority: zod.z.number().optional()
80
+ }).passthrough();
81
+ var ResendDomainEventDataSchema = zod.z.object({
82
+ id: zod.z.string(),
83
+ name: zod.z.string(),
84
+ status: zod.z.string(),
85
+ created_at: zod.z.string(),
86
+ region: zod.z.string(),
87
+ records: zod.z.array(ResendDomainRecordSchema)
88
+ }).passthrough();
89
+ function createResendEventSchema(eventType, dataSchema) {
90
+ return zod.z.object({
91
+ type: zod.z.literal(eventType),
92
+ created_at: zod.z.string(),
93
+ data: dataSchema
94
+ }).passthrough();
95
+ }
96
+ var ResendEmailSentEventSchema = createResendEventSchema(
97
+ "email.sent",
98
+ ResendEmailEventDataSchema
99
+ );
100
+ var ResendEmailScheduledEventSchema = createResendEventSchema(
101
+ "email.scheduled",
102
+ ResendEmailEventDataSchema
103
+ );
104
+ var ResendEmailDeliveredEventSchema = createResendEventSchema(
105
+ "email.delivered",
106
+ ResendEmailEventDataSchema
107
+ );
108
+ var ResendEmailDeliveryDelayedEventSchema = createResendEventSchema(
109
+ "email.delivery_delayed",
110
+ ResendEmailEventDataSchema
111
+ );
112
+ var ResendEmailComplainedEventSchema = createResendEventSchema(
113
+ "email.complained",
114
+ ResendEmailEventDataSchema
115
+ );
116
+ var ResendEmailBouncedEventSchema = createResendEventSchema(
117
+ "email.bounced",
118
+ ResendEmailEventDataSchema.extend({
119
+ bounce: ResendBounceSchema
120
+ })
121
+ );
122
+ var ResendEmailOpenedEventSchema = createResendEventSchema(
123
+ "email.opened",
124
+ ResendEmailEventDataSchema
125
+ );
126
+ var ResendEmailClickedEventSchema = createResendEventSchema(
127
+ "email.clicked",
128
+ ResendEmailEventDataSchema.extend({
129
+ click: ResendClickSchema
130
+ })
131
+ );
132
+ var ResendEmailReceivedEventSchema = createResendEventSchema(
133
+ "email.received",
134
+ ResendReceivedEmailEventDataSchema
135
+ );
136
+ var ResendEmailFailedEventSchema = createResendEventSchema(
137
+ "email.failed",
138
+ ResendEmailEventDataSchema.extend({
139
+ failed: ResendFailedSchema
140
+ })
141
+ );
142
+ var ResendEmailSuppressedEventSchema = createResendEventSchema(
143
+ "email.suppressed",
144
+ ResendEmailEventDataSchema.extend({
145
+ suppressed: ResendSuppressedSchema
146
+ })
147
+ );
148
+ var ResendContactCreatedEventSchema = createResendEventSchema(
149
+ "contact.created",
150
+ ResendContactEventDataSchema
151
+ );
152
+ var ResendContactUpdatedEventSchema = createResendEventSchema(
153
+ "contact.updated",
154
+ ResendContactEventDataSchema
155
+ );
156
+ var ResendContactDeletedEventSchema = createResendEventSchema(
157
+ "contact.deleted",
158
+ ResendContactDeletedEventDataSchema
159
+ );
160
+ var ResendDomainCreatedEventSchema = createResendEventSchema(
161
+ "domain.created",
162
+ ResendDomainEventDataSchema
163
+ );
164
+ var ResendDomainUpdatedEventSchema = createResendEventSchema(
165
+ "domain.updated",
166
+ ResendDomainEventDataSchema
167
+ );
168
+ var ResendDomainDeletedEventSchema = createResendEventSchema(
169
+ "domain.deleted",
170
+ ResendDomainEventDataSchema
171
+ );
172
+
173
+ // src/events.ts
174
+ var email_sent = core.defineEvent({
175
+ name: "email.sent",
176
+ schema: ResendEmailSentEventSchema,
177
+ provider: "resend"
178
+ });
179
+ var email_scheduled = core.defineEvent({
180
+ name: "email.scheduled",
181
+ schema: ResendEmailScheduledEventSchema,
182
+ provider: "resend"
183
+ });
184
+ var email_delivered = core.defineEvent({
185
+ name: "email.delivered",
186
+ schema: ResendEmailDeliveredEventSchema,
187
+ provider: "resend"
188
+ });
189
+ var email_delivery_delayed = core.defineEvent({
190
+ name: "email.delivery_delayed",
191
+ schema: ResendEmailDeliveryDelayedEventSchema,
192
+ provider: "resend"
193
+ });
194
+ var email_complained = core.defineEvent({
195
+ name: "email.complained",
196
+ schema: ResendEmailComplainedEventSchema,
197
+ provider: "resend"
198
+ });
199
+ var email_bounced = core.defineEvent({
200
+ name: "email.bounced",
201
+ schema: ResendEmailBouncedEventSchema,
202
+ provider: "resend"
203
+ });
204
+ var email_opened = core.defineEvent({
205
+ name: "email.opened",
206
+ schema: ResendEmailOpenedEventSchema,
207
+ provider: "resend"
208
+ });
209
+ var email_clicked = core.defineEvent({
210
+ name: "email.clicked",
211
+ schema: ResendEmailClickedEventSchema,
212
+ provider: "resend"
213
+ });
214
+ var email_received = core.defineEvent({
215
+ name: "email.received",
216
+ schema: ResendEmailReceivedEventSchema,
217
+ provider: "resend"
218
+ });
219
+ var email_failed = core.defineEvent({
220
+ name: "email.failed",
221
+ schema: ResendEmailFailedEventSchema,
222
+ provider: "resend"
223
+ });
224
+ var email_suppressed = core.defineEvent({
225
+ name: "email.suppressed",
226
+ schema: ResendEmailSuppressedEventSchema,
227
+ provider: "resend"
228
+ });
229
+ var contact_created = core.defineEvent({
230
+ name: "contact.created",
231
+ schema: ResendContactCreatedEventSchema,
232
+ provider: "resend"
233
+ });
234
+ var contact_updated = core.defineEvent({
235
+ name: "contact.updated",
236
+ schema: ResendContactUpdatedEventSchema,
237
+ provider: "resend"
238
+ });
239
+ var contact_deleted = core.defineEvent({
240
+ name: "contact.deleted",
241
+ schema: ResendContactDeletedEventSchema,
242
+ provider: "resend"
243
+ });
244
+ var domain_created = core.defineEvent({
245
+ name: "domain.created",
246
+ schema: ResendDomainCreatedEventSchema,
247
+ provider: "resend"
248
+ });
249
+ var domain_updated = core.defineEvent({
250
+ name: "domain.updated",
251
+ schema: ResendDomainUpdatedEventSchema,
252
+ provider: "resend"
253
+ });
254
+ var domain_deleted = core.defineEvent({
255
+ name: "domain.deleted",
256
+ schema: ResendDomainDeletedEventSchema,
257
+ provider: "resend"
258
+ });
259
+
260
+ exports.contact_created = contact_created;
261
+ exports.contact_deleted = contact_deleted;
262
+ exports.contact_updated = contact_updated;
263
+ exports.domain_created = domain_created;
264
+ exports.domain_deleted = domain_deleted;
265
+ exports.domain_updated = domain_updated;
266
+ exports.email_bounced = email_bounced;
267
+ exports.email_clicked = email_clicked;
268
+ exports.email_complained = email_complained;
269
+ exports.email_delivered = email_delivered;
270
+ exports.email_delivery_delayed = email_delivery_delayed;
271
+ exports.email_failed = email_failed;
272
+ exports.email_opened = email_opened;
273
+ exports.email_received = email_received;
274
+ exports.email_scheduled = email_scheduled;
275
+ exports.email_sent = email_sent;
276
+ exports.email_suppressed = email_suppressed;