@kizunajs/resend 2.0.0-beta.12
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.MD +9 -0
- package/README.md +41 -0
- package/dist/index.cjs +434 -0
- package/dist/index.d.cts +262 -0
- package/dist/index.d.mts +262 -0
- package/dist/index.mjs +429 -0
- package/package.json +77 -0
package/LICENSE.MD
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sondre Ørland
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @kizunajs/resend
|
|
2
|
+
|
|
3
|
+
`@kizunajs/resend` connects your API to Resend. Your route and job handlers send email and newsletters, and Resend's webhook events run functions you write.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @kizunajs/resend resend
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// kizuna.config.ts
|
|
15
|
+
import { resendPlugin } from '@kizunajs/resend';
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
adapter: expressAdapter(),
|
|
19
|
+
routes,
|
|
20
|
+
plugins: [
|
|
21
|
+
resendPlugin({
|
|
22
|
+
apiKey: process.env.RESEND_API_KEY,
|
|
23
|
+
from: 'Kizuna <hello@example.com>',
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Every route and job handler reaches it at `plugins.resend`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
await plugins.resend.sendEmail({
|
|
33
|
+
to: user.email,
|
|
34
|
+
subject: 'Welcome',
|
|
35
|
+
html: `<p>Welcome, ${user.name}.</p>`,
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
[Resend](https://kizunajs.com/docs/resend)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let resend = require("resend");
|
|
3
|
+
let kizunajs_plugin = require("kizunajs/plugin");
|
|
4
|
+
let zod = require("zod");
|
|
5
|
+
let kizunajs_schemas = require("kizunajs/schemas");
|
|
6
|
+
//#region src/options.ts
|
|
7
|
+
/**
|
|
8
|
+
* A list an app sends to: the Resend segment its contacts sit in, and the topic
|
|
9
|
+
* they opt in and out of.
|
|
10
|
+
*/
|
|
11
|
+
const ResendListSchema = zod.z.object({
|
|
12
|
+
/**
|
|
13
|
+
* The segment the list's contacts are added to, and broadcasts go to.
|
|
14
|
+
*/
|
|
15
|
+
segmentId: zod.z.string(),
|
|
16
|
+
/**
|
|
17
|
+
* The topic a contact opts in and out of, so unsubscribing from one list
|
|
18
|
+
* leaves the others alone.
|
|
19
|
+
*/
|
|
20
|
+
topicId: zod.z.string().optional()
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* The loosest of Zod's email patterns, so the plugin never refuses an address
|
|
24
|
+
* Resend would accept.
|
|
25
|
+
*/
|
|
26
|
+
const EmailSchema = zod.z.email({ pattern: zod.z.regexes.unicodeEmail });
|
|
27
|
+
/**
|
|
28
|
+
* A sender as Resend takes it: an address, or a name and an address in angle
|
|
29
|
+
* brackets.
|
|
30
|
+
*/
|
|
31
|
+
const isSender = (value) => {
|
|
32
|
+
const named = /^[^<>]+<([^<>]+)>$/.exec(value.trim());
|
|
33
|
+
return EmailSchema.safeParse(named === null ? value.trim() : named[1]).success;
|
|
34
|
+
};
|
|
35
|
+
const SenderSchema = zod.z.string().refine(isSender, { error: "must be an address, or a name and an address like `Kizuna <hello@example.com>`" });
|
|
36
|
+
const ResendPluginOptionsSchema = zod.z.object({
|
|
37
|
+
/**
|
|
38
|
+
* The Resend API key.
|
|
39
|
+
*/
|
|
40
|
+
apiKey: zod.z.string(),
|
|
41
|
+
/**
|
|
42
|
+
* The sender every email and broadcast uses unless it names its own.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* from: 'Kizuna <hello@example.com>',
|
|
46
|
+
*/
|
|
47
|
+
from: SenderSchema,
|
|
48
|
+
/**
|
|
49
|
+
* The lists `subscribe`, `unsubscribe` and `broadcast` name, keyed by
|
|
50
|
+
* the name handlers use.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* lists: {
|
|
54
|
+
* weekly: {
|
|
55
|
+
* segmentId: 'seg_123',
|
|
56
|
+
* topicId: 'top_456',
|
|
57
|
+
* },
|
|
58
|
+
* },
|
|
59
|
+
*/
|
|
60
|
+
lists: zod.z.record(zod.z.string(), ResendListSchema).optional(),
|
|
61
|
+
/**
|
|
62
|
+
* The webhook's signing secret. The webhook route is served when it's set.
|
|
63
|
+
*/
|
|
64
|
+
webhookSecret: zod.z.string().optional(),
|
|
65
|
+
/**
|
|
66
|
+
* What runs for each webhook event, keyed by its type.
|
|
67
|
+
*/
|
|
68
|
+
on: zod.z.custom((value) => typeof value === "object" && value !== null, { error: "must map event types to functions" }).optional(),
|
|
69
|
+
/**
|
|
70
|
+
* Catch every email outside production and forward it to `forwardTo`
|
|
71
|
+
* instead of its real recipients.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* intercept: {
|
|
75
|
+
* enabled: process.env.NODE_ENV !== 'production',
|
|
76
|
+
* forwardTo: process.env.EMAIL_FORWARD_TO,
|
|
77
|
+
* deliverTo: ['@example.com'],
|
|
78
|
+
* },
|
|
79
|
+
*/
|
|
80
|
+
intercept: zod.z.object({
|
|
81
|
+
/**
|
|
82
|
+
* Whether emails are intercepted.
|
|
83
|
+
*/
|
|
84
|
+
enabled: zod.z.boolean(),
|
|
85
|
+
/**
|
|
86
|
+
* Where every email is forwarded, one address or several.
|
|
87
|
+
* Required when `enabled` is true.
|
|
88
|
+
*/
|
|
89
|
+
forwardTo: zod.z.union([EmailSchema, zod.z.array(EmailSchema).min(1)]).optional(),
|
|
90
|
+
/**
|
|
91
|
+
* Addresses, or whole domains like `@example.com`, whose emails are
|
|
92
|
+
* delivered as normal, with `forwardTo` in bcc.
|
|
93
|
+
*/
|
|
94
|
+
deliverTo: zod.z.array(zod.z.string()).optional(),
|
|
95
|
+
/**
|
|
96
|
+
* Put in front of every intercepted email's subject.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* subjectPrefix: '[dev]',
|
|
100
|
+
*/
|
|
101
|
+
subjectPrefix: zod.z.string().optional()
|
|
102
|
+
}).refine((intercept) => !intercept.enabled || intercept.forwardTo !== void 0, {
|
|
103
|
+
error: "is required when intercept is enabled",
|
|
104
|
+
path: ["forwardTo"]
|
|
105
|
+
}).optional(),
|
|
106
|
+
/**
|
|
107
|
+
* Passed to the Resend client as it is.
|
|
108
|
+
*/
|
|
109
|
+
resend: zod.z.custom().optional()
|
|
110
|
+
}).refine((options) => options.on === void 0 || options.webhookSecret !== void 0, {
|
|
111
|
+
error: "is required when `on` is set",
|
|
112
|
+
path: ["webhookSecret"]
|
|
113
|
+
});
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/requests.ts
|
|
116
|
+
/**
|
|
117
|
+
* A call to Resend that answered with an error.
|
|
118
|
+
*/
|
|
119
|
+
var ResendRequestError = class extends Error {
|
|
120
|
+
action;
|
|
121
|
+
error;
|
|
122
|
+
constructor(action, error) {
|
|
123
|
+
super(`[kizuna/resend] ${action} failed: ${error.message}`);
|
|
124
|
+
this.action = action;
|
|
125
|
+
this.error = error;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* How many times a rate-limited call is tried again.
|
|
130
|
+
*/
|
|
131
|
+
const RATE_LIMIT_RETRIES = 3;
|
|
132
|
+
const wait = (milliseconds) => new Promise((resolve) => {
|
|
133
|
+
setTimeout(resolve, milliseconds);
|
|
134
|
+
});
|
|
135
|
+
/**
|
|
136
|
+
* Call Resend, retrying when it's rate limited, and throw its error.
|
|
137
|
+
*/
|
|
138
|
+
const call = async (action, request) => {
|
|
139
|
+
let response = await request();
|
|
140
|
+
for (let retry = 0; retry < RATE_LIMIT_RETRIES && response.error?.name === "rate_limit_exceeded"; retry++) {
|
|
141
|
+
const seconds = Number(response.headers?.["retry-after"] ?? 1);
|
|
142
|
+
await wait((Number.isFinite(seconds) ? seconds : 1) * 1e3);
|
|
143
|
+
response = await request();
|
|
144
|
+
}
|
|
145
|
+
if (response.error !== null) throw new ResendRequestError(action, response.error);
|
|
146
|
+
return response.data;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Every item of a paginated Resend list.
|
|
150
|
+
*/
|
|
151
|
+
const everyPage = async (action, request) => {
|
|
152
|
+
const items = [];
|
|
153
|
+
let after;
|
|
154
|
+
for (;;) {
|
|
155
|
+
const page = await call(action, () => request(after));
|
|
156
|
+
items.push(...page.data);
|
|
157
|
+
const last = page.data.at(-1);
|
|
158
|
+
if (!page.has_more || last === void 0) return items;
|
|
159
|
+
after = last.id;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const listOf = (addresses) => addresses === void 0 ? [] : Array.isArray(addresses) ? addresses : [addresses];
|
|
163
|
+
/**
|
|
164
|
+
* The email as `intercept` sends it: to the `deliverTo` addresses it names, and
|
|
165
|
+
* to `forwardTo` in their place or in bcc. Headers keep who it was for.
|
|
166
|
+
*/
|
|
167
|
+
const intercepted = (email, intercept) => {
|
|
168
|
+
if (!intercept.enabled) return email;
|
|
169
|
+
const forwardTo = listOf(intercept.forwardTo);
|
|
170
|
+
const deliverTo = (intercept.deliverTo ?? []).map((entry) => entry.toLowerCase());
|
|
171
|
+
const delivered = (address) => {
|
|
172
|
+
const lowered = address.toLowerCase();
|
|
173
|
+
return deliverTo.some((entry) => entry.startsWith("@") ? lowered.endsWith(entry) : lowered === entry);
|
|
174
|
+
};
|
|
175
|
+
const to = listOf(email.to).filter(delivered);
|
|
176
|
+
const cc = listOf(email.cc).filter(delivered);
|
|
177
|
+
const bcc = listOf(email.bcc).filter(delivered);
|
|
178
|
+
const originals = {
|
|
179
|
+
"X-Intercepted-To": listOf(email.to),
|
|
180
|
+
"X-Intercepted-Cc": listOf(email.cc),
|
|
181
|
+
"X-Intercepted-Bcc": listOf(email.bcc)
|
|
182
|
+
};
|
|
183
|
+
const headers = {
|
|
184
|
+
...email.headers,
|
|
185
|
+
...Object.fromEntries(Object.entries(originals).filter(([, addresses]) => addresses.length > 0).map(([name, addresses]) => [name, addresses.join(", ")]))
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
...email,
|
|
189
|
+
to: to.length > 0 ? to : forwardTo,
|
|
190
|
+
cc,
|
|
191
|
+
bcc: to.length > 0 ? [...bcc, ...forwardTo] : bcc,
|
|
192
|
+
subject: intercept.subjectPrefix === void 0 ? email.subject : `${intercept.subjectPrefix} ${email.subject}`,
|
|
193
|
+
headers
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* What handlers reach at `plugins.resend`.
|
|
198
|
+
*/
|
|
199
|
+
const resendExports = (resend, options) => {
|
|
200
|
+
const listNamed = (name) => {
|
|
201
|
+
const list = options.lists?.[name];
|
|
202
|
+
if (list === void 0) throw new Error(`[kizuna/resend] No list is named '${name}'. Declare it under \`lists\`.`);
|
|
203
|
+
return list;
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
/**
|
|
207
|
+
* The Resend client, for anything the plugin doesn't cover.
|
|
208
|
+
*/
|
|
209
|
+
client: resend,
|
|
210
|
+
/**
|
|
211
|
+
* Send one email.
|
|
212
|
+
*/
|
|
213
|
+
sendEmail: (email) => {
|
|
214
|
+
const addressed = options.intercept === void 0 ? email : intercepted(email, options.intercept);
|
|
215
|
+
return call("Sending the email", () => resend.emails.send({
|
|
216
|
+
from: options.from,
|
|
217
|
+
...addressed
|
|
218
|
+
}));
|
|
219
|
+
},
|
|
220
|
+
/**
|
|
221
|
+
* Add a contact to a list, creating the contact when it's new.
|
|
222
|
+
*/
|
|
223
|
+
subscribe: async ({ email, list: name, firstName, lastName }) => {
|
|
224
|
+
const list = listNamed(name);
|
|
225
|
+
const topics = list.topicId === void 0 ? [] : [{
|
|
226
|
+
id: list.topicId,
|
|
227
|
+
subscription: "opt_in"
|
|
228
|
+
}];
|
|
229
|
+
const found = await resend.contacts.get({ email });
|
|
230
|
+
if (found.error?.name === "not_found") return { contactId: (await call("Creating the contact", () => resend.contacts.create({
|
|
231
|
+
email,
|
|
232
|
+
firstName,
|
|
233
|
+
lastName,
|
|
234
|
+
segments: [{ id: list.segmentId }],
|
|
235
|
+
topics
|
|
236
|
+
}))).id };
|
|
237
|
+
if (found.error !== null) throw new ResendRequestError("Looking up the contact", found.error);
|
|
238
|
+
await call("Updating the contact", () => resend.contacts.update({
|
|
239
|
+
email,
|
|
240
|
+
unsubscribed: false,
|
|
241
|
+
firstName,
|
|
242
|
+
lastName
|
|
243
|
+
}));
|
|
244
|
+
await call("Adding the contact to the segment", () => resend.contacts.segments.add({
|
|
245
|
+
email,
|
|
246
|
+
segmentId: list.segmentId
|
|
247
|
+
}));
|
|
248
|
+
if (topics.length > 0) await call("Opting the contact in to the topic", () => resend.contacts.topics.update({
|
|
249
|
+
email,
|
|
250
|
+
topics
|
|
251
|
+
}));
|
|
252
|
+
return { contactId: found.data.id };
|
|
253
|
+
},
|
|
254
|
+
/**
|
|
255
|
+
* Move a contact to a new address, keeping its name, its lists and its
|
|
256
|
+
* topics. Resend can't change a contact's email, so this creates the new
|
|
257
|
+
* contact and removes the old one.
|
|
258
|
+
*/
|
|
259
|
+
changeEmail: async ({ from, to }) => {
|
|
260
|
+
const contact = await call("Looking up the contact", () => resend.contacts.get({ email: from }));
|
|
261
|
+
const segments = await everyPage("Listing the contact segments", (after) => resend.contacts.segments.list({
|
|
262
|
+
email: from,
|
|
263
|
+
limit: 100,
|
|
264
|
+
...after === void 0 ? {} : { after }
|
|
265
|
+
}));
|
|
266
|
+
const topics = await everyPage("Listing the contact topics", (after) => resend.contacts.topics.list({
|
|
267
|
+
email: from,
|
|
268
|
+
limit: 100,
|
|
269
|
+
...after === void 0 ? {} : { after }
|
|
270
|
+
}));
|
|
271
|
+
const created = await call("Creating the contact", () => resend.contacts.create({
|
|
272
|
+
email: to,
|
|
273
|
+
firstName: contact.first_name ?? void 0,
|
|
274
|
+
lastName: contact.last_name ?? void 0,
|
|
275
|
+
unsubscribed: contact.unsubscribed,
|
|
276
|
+
segments: segments.map((segment) => ({ id: segment.id })),
|
|
277
|
+
topics: topics.map((topic) => ({
|
|
278
|
+
id: topic.id,
|
|
279
|
+
subscription: topic.subscription
|
|
280
|
+
}))
|
|
281
|
+
}));
|
|
282
|
+
await call("Removing the old contact", () => resend.contacts.remove({ email: from }));
|
|
283
|
+
return { contactId: created.id };
|
|
284
|
+
},
|
|
285
|
+
/**
|
|
286
|
+
* Take a contact off one list, or off everything.
|
|
287
|
+
*/
|
|
288
|
+
unsubscribe: async ({ email, list: name }) => {
|
|
289
|
+
if (name === void 0) {
|
|
290
|
+
await call("Unsubscribing the contact", () => resend.contacts.update({
|
|
291
|
+
email,
|
|
292
|
+
unsubscribed: true
|
|
293
|
+
}));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const { segmentId, topicId } = listNamed(name);
|
|
297
|
+
if (topicId === void 0) {
|
|
298
|
+
await call("Removing the contact from the segment", () => resend.contacts.segments.remove({
|
|
299
|
+
email,
|
|
300
|
+
segmentId
|
|
301
|
+
}));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
await call("Opting the contact out of the topic", () => resend.contacts.topics.update({
|
|
305
|
+
email,
|
|
306
|
+
topics: [{
|
|
307
|
+
id: topicId,
|
|
308
|
+
subscription: "opt_out"
|
|
309
|
+
}]
|
|
310
|
+
}));
|
|
311
|
+
},
|
|
312
|
+
/**
|
|
313
|
+
* Send a broadcast to a list, now or at `scheduledAt`.
|
|
314
|
+
*/
|
|
315
|
+
sendBroadcast: ({ list: name, from, scheduledAt, ...content }) => {
|
|
316
|
+
const list = listNamed(name);
|
|
317
|
+
return call("Sending the broadcast", () => resend.broadcasts.create({
|
|
318
|
+
...content,
|
|
319
|
+
from: from ?? options.from,
|
|
320
|
+
segmentId: list.segmentId,
|
|
321
|
+
topicId: list.topicId,
|
|
322
|
+
send: true,
|
|
323
|
+
scheduledAt
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/webhooks.ts
|
|
330
|
+
/**
|
|
331
|
+
* The webhook event functions, with `jobs` and `plugins` typed from `Config`.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* export const resendEvents = defineResendEvents<Config>({
|
|
335
|
+
* 'email.bounced': async ({ event, jobs }) => {
|
|
336
|
+
* await jobs.users.markUndeliverable.queue({
|
|
337
|
+
* input: {
|
|
338
|
+
* email: event.data.to[0],
|
|
339
|
+
* },
|
|
340
|
+
* });
|
|
341
|
+
* },
|
|
342
|
+
* });
|
|
343
|
+
*/
|
|
344
|
+
const defineResendEvents = (handlers) => handlers;
|
|
345
|
+
/**
|
|
346
|
+
* Checks each delivery's signature, then runs the function for its event.
|
|
347
|
+
*/
|
|
348
|
+
const webhookRoute = (resend, webhookSecret, on) => (0, kizunajs_plugin.route)({
|
|
349
|
+
method: "POST",
|
|
350
|
+
path: "/webhooks",
|
|
351
|
+
auth: false,
|
|
352
|
+
rawBody: true,
|
|
353
|
+
summary: "Receive Resend webhook events",
|
|
354
|
+
headers: zod.z.object({
|
|
355
|
+
"svix-id": zod.z.string(),
|
|
356
|
+
"svix-timestamp": zod.z.string(),
|
|
357
|
+
"svix-signature": zod.z.string()
|
|
358
|
+
}),
|
|
359
|
+
body: zod.z.unknown(),
|
|
360
|
+
responses: {
|
|
361
|
+
204: zod.z.void(),
|
|
362
|
+
400: kizunajs_schemas.ProblemDetailsSchema
|
|
363
|
+
}
|
|
364
|
+
}).handler(async (args) => {
|
|
365
|
+
const { rawBody, headers, throwError } = args;
|
|
366
|
+
let event;
|
|
367
|
+
try {
|
|
368
|
+
event = resend.webhooks.verify({
|
|
369
|
+
payload: rawBody,
|
|
370
|
+
headers: {
|
|
371
|
+
id: headers["svix-id"],
|
|
372
|
+
timestamp: headers["svix-timestamp"],
|
|
373
|
+
signature: headers["svix-signature"]
|
|
374
|
+
},
|
|
375
|
+
webhookSecret
|
|
376
|
+
});
|
|
377
|
+
} catch {
|
|
378
|
+
return throwError({
|
|
379
|
+
status: 400,
|
|
380
|
+
body: { detail: "The webhook signature doesn't match its body." }
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const handle = on[event.type];
|
|
384
|
+
const { jobs, plugins } = args;
|
|
385
|
+
await handle?.({
|
|
386
|
+
event,
|
|
387
|
+
deliveryId: headers["svix-id"],
|
|
388
|
+
jobs,
|
|
389
|
+
plugins
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
status: 204,
|
|
393
|
+
body: void 0
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/plugin.ts
|
|
398
|
+
/**
|
|
399
|
+
* Send email, run newsletters and broadcasts, and receive webhooks through
|
|
400
|
+
* Resend. Handlers reach it at `plugins.resend`.
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```ts
|
|
404
|
+
* export default defineConfig({
|
|
405
|
+
* routes,
|
|
406
|
+
* plugins: [
|
|
407
|
+
* resendPlugin({
|
|
408
|
+
* apiKey: process.env.RESEND_API_KEY,
|
|
409
|
+
* from: 'Kizuna <hello@example.com>',
|
|
410
|
+
* }),
|
|
411
|
+
* ],
|
|
412
|
+
* });
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
const resendPlugin = (0, kizunajs_plugin.definePlugin)({
|
|
416
|
+
slug: "resend",
|
|
417
|
+
basePath: "/resend",
|
|
418
|
+
options: ResendPluginOptionsSchema,
|
|
419
|
+
setup: ({ options }) => {
|
|
420
|
+
const resend$1 = new resend.Resend(options.apiKey, options.resend);
|
|
421
|
+
const routes = {};
|
|
422
|
+
if (options.webhookSecret !== void 0) routes.webhook = webhookRoute(resend$1, options.webhookSecret, options.on ?? {});
|
|
423
|
+
return {
|
|
424
|
+
exports: resendExports(resend$1, options),
|
|
425
|
+
routes
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
//#endregion
|
|
430
|
+
exports.ResendListSchema = ResendListSchema;
|
|
431
|
+
exports.ResendPluginOptionsSchema = ResendPluginOptionsSchema;
|
|
432
|
+
exports.ResendRequestError = ResendRequestError;
|
|
433
|
+
exports.defineResendEvents = defineResendEvents;
|
|
434
|
+
exports.resendPlugin = resendPlugin;
|