@beignet/provider-mail-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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/provider-mail-resend
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @beignet/provider-mail-resend
2
+
3
+ Resend-backed mail provider for Beignet.
4
+
5
+ The provider installs the app-facing `ctx.ports.mailer` port and exposes
6
+ `ctx.ports.resend.client` only as an escape hatch for Resend-specific features.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ bun add @beignet/provider-mail-resend resend
12
+ ```
13
+
14
+ ## Setup
15
+
16
+ ```typescript
17
+ import { mailResendProvider } from "@beignet/provider-mail-resend";
18
+ import { createServer } from "@beignet/core/server";
19
+
20
+ const server = await createServer({
21
+ ports: basePorts,
22
+ providers: [mailResendProvider],
23
+ createContext: ({ ports }) => ({ ports }),
24
+ routes,
25
+ });
26
+ ```
27
+
28
+ Required environment variables:
29
+
30
+ | Variable | Description |
31
+ | --- | --- |
32
+ | `RESEND_API_KEY` | Resend API key |
33
+ | `RESEND_FROM` | Default sender, e.g. `My App <no-reply@example.com>` |
34
+
35
+ ## Use in application code
36
+
37
+ ```typescript
38
+ await ctx.ports.mailer.send({
39
+ to: "user@example.com",
40
+ subject: "Welcome",
41
+ html: "<h1>Hello</h1>",
42
+ });
43
+ ```
44
+
45
+ The same `MailerPort` works with SMTP, memory fakes, and other adapters:
46
+
47
+ ```typescript
48
+ await ctx.ports.mailer.send({
49
+ from: { email: "support@example.com", name: "Support" },
50
+ to: ["user@example.com", "admin@example.com"],
51
+ replyTo: "support@example.com",
52
+ subject: "Account updated",
53
+ text: "Your account was updated.",
54
+ html: "<p>Your account was updated.</p>",
55
+ });
56
+ ```
57
+
58
+ ## Escape hatch
59
+
60
+ Use the Resend client only when you need a Resend-specific feature not covered
61
+ by `MailerPort`:
62
+
63
+ ```typescript
64
+ await ctx.ports.resend.client.emails.send({
65
+ from: "sender@example.com",
66
+ to: "user@example.com",
67
+ subject: "Invoice",
68
+ html: "<p>Attached.</p>",
69
+ attachments: [
70
+ {
71
+ filename: "invoice.pdf",
72
+ content: pdfBuffer,
73
+ },
74
+ ],
75
+ });
76
+ ```
77
+
78
+ ## Devtools
79
+
80
+ When `ctx.ports.devtools` is installed, this provider records `mail.send`,
81
+ `mail.sent`, and `mail.failed` events under the `mail` watcher.
82
+
83
+ ## Errors
84
+
85
+ Delivery failures throw `MailDeliveryError` from `@beignet/core/mail`.
86
+ Startup configuration problems throw during provider setup.
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @beignet/provider-mail-resend
3
+ *
4
+ * Resend-backed mail provider for Beignet.
5
+ */
6
+ import { type MailAddress, type MailerPort } from "@beignet/core/mail";
7
+ import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
8
+ import { Resend } from "resend";
9
+ import { z } from "zod";
10
+ declare const ResendMailConfigSchema: z.ZodObject<{
11
+ API_KEY: z.ZodString;
12
+ FROM: z.ZodString;
13
+ }, z.core.$strip>;
14
+ export type ResendMailConfig = z.infer<typeof ResendMailConfigSchema>;
15
+ export interface ResendMailEscapeHatch {
16
+ client: Resend;
17
+ }
18
+ export interface ResendMailProviderPorts {
19
+ mailer: MailerPort;
20
+ resend: ResendMailEscapeHatch;
21
+ }
22
+ export interface CreateResendMailerOptions {
23
+ client: Resend;
24
+ from: MailAddress;
25
+ instrumentation?: ProviderInstrumentationTarget;
26
+ }
27
+ export declare function createResendMailer({ client, from, instrumentation: instrumentationTarget, }: CreateResendMailerOptions): MailerPort;
28
+ export declare const mailResendProvider: import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
29
+ API_KEY: z.ZodString;
30
+ FROM: z.ZodString;
31
+ }, z.core.$strip>, {
32
+ mailer: MailerPort;
33
+ resend: {
34
+ client: Resend;
35
+ };
36
+ }>;
37
+ export {};
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGL,KAAK,WAAW,EAEhB,KAAK,UAAU,EAIhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAA2B,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,sBAAsB;;;iBAG1B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,qBAAqB,CAAC;CAC/B;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,6BAA6B,CAAC;CACjD;AAqDD,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,IAAI,EACJ,eAAe,EAAE,qBAAqB,GACvC,EAAE,yBAAyB,GAAG,UAAU,CAsFxC;AAED,eAAO,MAAM,kBAAkB;;;;;;;;EAkC7B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @beignet/provider-mail-resend
3
+ *
4
+ * Resend-backed mail provider for Beignet.
5
+ */
6
+ import { formatMailAddress, formatMailAddressList, MailDeliveryError, normalizeMailMessage, } from "@beignet/core/mail";
7
+ import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
8
+ import { Resend } from "resend";
9
+ import { z } from "zod";
10
+ const ResendMailConfigSchema = z.object({
11
+ API_KEY: z.string().min(1),
12
+ FROM: z.string().min(1),
13
+ });
14
+ function addOptional(target, key, value) {
15
+ if (value !== undefined) {
16
+ Object.assign(target, { [key]: value });
17
+ }
18
+ }
19
+ function createResendPayload(message, defaultFrom) {
20
+ const normalized = normalizeMailMessage(message, { defaultFrom });
21
+ if (!normalized.from) {
22
+ throw new MailDeliveryError({
23
+ provider: "resend",
24
+ message: "Cannot send email without a from address.",
25
+ });
26
+ }
27
+ const payload = {
28
+ from: formatMailAddress(normalized.from),
29
+ to: formatMailAddressList(normalized.to),
30
+ subject: normalized.subject,
31
+ };
32
+ addOptional(payload, "text", normalized.text);
33
+ addOptional(payload, "html", normalized.html);
34
+ addOptional(payload, "cc", normalized.cc ? formatMailAddressList(normalized.cc) : undefined);
35
+ addOptional(payload, "bcc", normalized.bcc ? formatMailAddressList(normalized.bcc) : undefined);
36
+ addOptional(payload, "replyTo", normalized.replyTo ? formatMailAddressList(normalized.replyTo) : undefined);
37
+ addOptional(payload, "headers", normalized.headers);
38
+ return payload;
39
+ }
40
+ export function createResendMailer({ client, from, instrumentation: instrumentationTarget, }) {
41
+ const instrumentation = createProviderInstrumentation(instrumentationTarget, {
42
+ providerName: "mail-resend",
43
+ watcher: "mail",
44
+ });
45
+ return {
46
+ async send(message) {
47
+ const payload = createResendPayload(message, from);
48
+ instrumentation.custom({
49
+ name: "mail.send",
50
+ label: "Mail send",
51
+ summary: `Sending "${message.subject}"`,
52
+ details: {
53
+ provider: "resend",
54
+ subject: message.subject,
55
+ to: payload.to,
56
+ },
57
+ });
58
+ try {
59
+ const result = await client.emails.send(payload);
60
+ if (result.error) {
61
+ instrumentation.custom({
62
+ name: "mail.failed",
63
+ label: "Mail failed",
64
+ summary: `Failed to send "${message.subject}"`,
65
+ details: {
66
+ provider: "resend",
67
+ subject: message.subject,
68
+ to: payload.to,
69
+ error: result.error.message,
70
+ },
71
+ });
72
+ throw new MailDeliveryError({
73
+ provider: "resend",
74
+ message: result.error.message || "Resend failed to send email.",
75
+ cause: result.error,
76
+ });
77
+ }
78
+ instrumentation.custom({
79
+ name: "mail.sent",
80
+ label: "Mail sent",
81
+ summary: `Sent "${message.subject}"`,
82
+ details: {
83
+ provider: "resend",
84
+ subject: message.subject,
85
+ to: payload.to,
86
+ id: result.data?.id,
87
+ },
88
+ });
89
+ return {
90
+ id: result.data?.id,
91
+ provider: "resend",
92
+ };
93
+ }
94
+ catch (error) {
95
+ if (error instanceof MailDeliveryError) {
96
+ throw error;
97
+ }
98
+ instrumentation.custom({
99
+ name: "mail.failed",
100
+ label: "Mail failed",
101
+ summary: `Failed to send "${message.subject}"`,
102
+ details: {
103
+ provider: "resend",
104
+ subject: message.subject,
105
+ to: payload.to,
106
+ error: error instanceof Error ? error.message : String(error),
107
+ },
108
+ });
109
+ throw new MailDeliveryError({
110
+ provider: "resend",
111
+ message: error instanceof Error
112
+ ? error.message
113
+ : "Resend failed to send email.",
114
+ cause: error,
115
+ });
116
+ }
117
+ },
118
+ };
119
+ }
120
+ export const mailResendProvider = createProvider({
121
+ name: "mail-resend",
122
+ config: {
123
+ schema: ResendMailConfigSchema,
124
+ envPrefix: "RESEND_",
125
+ },
126
+ async setup({ config, ports }) {
127
+ if (!config) {
128
+ throw new Error("[mailResendProvider] Missing Resend config. " +
129
+ "Please set RESEND_API_KEY and RESEND_FROM environment variables.");
130
+ }
131
+ const client = new Resend(config.API_KEY);
132
+ const instrumentation = createProviderInstrumentation(ports, {
133
+ providerName: "mail-resend",
134
+ watcher: "mail",
135
+ });
136
+ const mailer = createResendMailer({
137
+ client,
138
+ from: config.FROM,
139
+ instrumentation,
140
+ });
141
+ return {
142
+ ports: {
143
+ mailer,
144
+ resend: { client },
145
+ },
146
+ };
147
+ },
148
+ });
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@beignet/provider-mail-resend",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Resend mail provider for Beignet - adds mailer port using Resend",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/*.test-d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "clean": "rm -rf dist coverage .turbo",
27
+ "test": "bun test",
28
+ "test:coverage": "bun test --coverage",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "beignet",
33
+ "mail",
34
+ "email",
35
+ "provider",
36
+ "resend",
37
+ "ports"
38
+ ],
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/taylorbryant/beignet.git",
43
+ "directory": "packages/provider-mail-resend"
44
+ },
45
+ "author": "Taylor Bryant",
46
+ "homepage": "https://github.com/taylorbryant/beignet#readme",
47
+ "bugs": "https://github.com/taylorbryant/beignet/issues",
48
+ "sideEffects": false,
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "peerDependencies": {
56
+ "resend": "^2.0.0 || ^3.0.0 || ^4.0.0"
57
+ },
58
+ "dependencies": {
59
+ "zod": "^4.0.0",
60
+ "@beignet/core": "*"
61
+ },
62
+ "devDependencies": {
63
+ "@beignet/devtools": "*",
64
+ "@types/bun": "^1.3.13",
65
+ "@types/node": "^20.10.0",
66
+ "resend": "^4.0.1",
67
+ "typescript": "^5.3.0"
68
+ }
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * @beignet/provider-mail-resend
3
+ *
4
+ * Resend-backed mail provider for Beignet.
5
+ */
6
+
7
+ import {
8
+ formatMailAddress,
9
+ formatMailAddressList,
10
+ type MailAddress,
11
+ MailDeliveryError,
12
+ type MailerPort,
13
+ normalizeMailMessage,
14
+ type SendMailOptions,
15
+ type SendMailResult,
16
+ } from "@beignet/core/mail";
17
+ import {
18
+ createProvider,
19
+ createProviderInstrumentation,
20
+ type ProviderInstrumentationTarget,
21
+ } from "@beignet/core/providers";
22
+ import { type CreateEmailOptions, Resend } from "resend";
23
+ import { z } from "zod";
24
+
25
+ const ResendMailConfigSchema = z.object({
26
+ API_KEY: z.string().min(1),
27
+ FROM: z.string().min(1),
28
+ });
29
+
30
+ export type ResendMailConfig = z.infer<typeof ResendMailConfigSchema>;
31
+
32
+ export interface ResendMailEscapeHatch {
33
+ client: Resend;
34
+ }
35
+
36
+ export interface ResendMailProviderPorts {
37
+ mailer: MailerPort;
38
+ resend: ResendMailEscapeHatch;
39
+ }
40
+
41
+ export interface CreateResendMailerOptions {
42
+ client: Resend;
43
+ from: MailAddress;
44
+ instrumentation?: ProviderInstrumentationTarget;
45
+ }
46
+
47
+ function addOptional<T extends Record<string, unknown>, K extends string, V>(
48
+ target: T,
49
+ key: K,
50
+ value: V | undefined,
51
+ ): void {
52
+ if (value !== undefined) {
53
+ Object.assign(target, { [key]: value });
54
+ }
55
+ }
56
+
57
+ function createResendPayload(
58
+ message: SendMailOptions,
59
+ defaultFrom: MailAddress,
60
+ ): CreateEmailOptions {
61
+ const normalized = normalizeMailMessage(message, { defaultFrom });
62
+
63
+ if (!normalized.from) {
64
+ throw new MailDeliveryError({
65
+ provider: "resend",
66
+ message: "Cannot send email without a from address.",
67
+ });
68
+ }
69
+
70
+ const payload = {
71
+ from: formatMailAddress(normalized.from),
72
+ to: formatMailAddressList(normalized.to),
73
+ subject: normalized.subject,
74
+ } satisfies Partial<CreateEmailOptions>;
75
+
76
+ addOptional(payload, "text", normalized.text);
77
+ addOptional(payload, "html", normalized.html);
78
+ addOptional(
79
+ payload,
80
+ "cc",
81
+ normalized.cc ? formatMailAddressList(normalized.cc) : undefined,
82
+ );
83
+ addOptional(
84
+ payload,
85
+ "bcc",
86
+ normalized.bcc ? formatMailAddressList(normalized.bcc) : undefined,
87
+ );
88
+ addOptional(
89
+ payload,
90
+ "replyTo",
91
+ normalized.replyTo ? formatMailAddressList(normalized.replyTo) : undefined,
92
+ );
93
+ addOptional(payload, "headers", normalized.headers);
94
+
95
+ return payload as CreateEmailOptions;
96
+ }
97
+
98
+ export function createResendMailer({
99
+ client,
100
+ from,
101
+ instrumentation: instrumentationTarget,
102
+ }: CreateResendMailerOptions): MailerPort {
103
+ const instrumentation = createProviderInstrumentation(instrumentationTarget, {
104
+ providerName: "mail-resend",
105
+ watcher: "mail",
106
+ });
107
+
108
+ return {
109
+ async send(message): Promise<SendMailResult> {
110
+ const payload = createResendPayload(message, from);
111
+ instrumentation.custom({
112
+ name: "mail.send",
113
+ label: "Mail send",
114
+ summary: `Sending "${message.subject}"`,
115
+ details: {
116
+ provider: "resend",
117
+ subject: message.subject,
118
+ to: payload.to,
119
+ },
120
+ });
121
+
122
+ try {
123
+ const result = await client.emails.send(payload);
124
+
125
+ if (result.error) {
126
+ instrumentation.custom({
127
+ name: "mail.failed",
128
+ label: "Mail failed",
129
+ summary: `Failed to send "${message.subject}"`,
130
+ details: {
131
+ provider: "resend",
132
+ subject: message.subject,
133
+ to: payload.to,
134
+ error: result.error.message,
135
+ },
136
+ });
137
+ throw new MailDeliveryError({
138
+ provider: "resend",
139
+ message: result.error.message || "Resend failed to send email.",
140
+ cause: result.error,
141
+ });
142
+ }
143
+
144
+ instrumentation.custom({
145
+ name: "mail.sent",
146
+ label: "Mail sent",
147
+ summary: `Sent "${message.subject}"`,
148
+ details: {
149
+ provider: "resend",
150
+ subject: message.subject,
151
+ to: payload.to,
152
+ id: result.data?.id,
153
+ },
154
+ });
155
+
156
+ return {
157
+ id: result.data?.id,
158
+ provider: "resend",
159
+ };
160
+ } catch (error) {
161
+ if (error instanceof MailDeliveryError) {
162
+ throw error;
163
+ }
164
+
165
+ instrumentation.custom({
166
+ name: "mail.failed",
167
+ label: "Mail failed",
168
+ summary: `Failed to send "${message.subject}"`,
169
+ details: {
170
+ provider: "resend",
171
+ subject: message.subject,
172
+ to: payload.to,
173
+ error: error instanceof Error ? error.message : String(error),
174
+ },
175
+ });
176
+
177
+ throw new MailDeliveryError({
178
+ provider: "resend",
179
+ message:
180
+ error instanceof Error
181
+ ? error.message
182
+ : "Resend failed to send email.",
183
+ cause: error,
184
+ });
185
+ }
186
+ },
187
+ };
188
+ }
189
+
190
+ export const mailResendProvider = createProvider({
191
+ name: "mail-resend",
192
+
193
+ config: {
194
+ schema: ResendMailConfigSchema,
195
+ envPrefix: "RESEND_",
196
+ },
197
+
198
+ async setup({ config, ports }) {
199
+ if (!config) {
200
+ throw new Error(
201
+ "[mailResendProvider] Missing Resend config. " +
202
+ "Please set RESEND_API_KEY and RESEND_FROM environment variables.",
203
+ );
204
+ }
205
+
206
+ const client = new Resend(config.API_KEY);
207
+ const instrumentation = createProviderInstrumentation(ports, {
208
+ providerName: "mail-resend",
209
+ watcher: "mail",
210
+ });
211
+ const mailer = createResendMailer({
212
+ client,
213
+ from: config.FROM,
214
+ instrumentation,
215
+ });
216
+
217
+ return {
218
+ ports: {
219
+ mailer,
220
+ resend: { client },
221
+ } satisfies ResendMailProviderPorts,
222
+ };
223
+ },
224
+ });