@absolutejs/dispatch-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/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @absolutejs/dispatch-resend
2
+
3
+ Resend-backed `EmailAdapter` for
4
+ [@absolutejs/dispatch](https://github.com/absolutejs/dispatch).
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ bun add @absolutejs/dispatch @absolutejs/dispatch-resend resend
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```ts
15
+ import { Resend } from 'resend';
16
+ import { createDispatcher } from '@absolutejs/dispatch';
17
+ import { createResendAdapter } from '@absolutejs/dispatch-resend';
18
+
19
+ const resend = new Resend(process.env.RESEND_KEY!);
20
+
21
+ const dispatcher = createDispatcher({
22
+ email: createResendAdapter({
23
+ client: resend,
24
+ defaultFrom: 'no-reply@acme.io',
25
+ }),
26
+ });
27
+
28
+ const result = await dispatcher.email({
29
+ to: 'alice@example.com',
30
+ subject: 'Welcome to Acme',
31
+ text: 'Click here to verify: ...',
32
+ // String metadata becomes Resend tags by default:
33
+ metadata: { campaign: 'welcome-v2', priority: 'high' },
34
+ });
35
+
36
+ console.log(result.id); // Resend's message id
37
+ ```
38
+
39
+ ## API
40
+
41
+ ```ts
42
+ createResendAdapter({
43
+ client, // Required — your `new Resend(apiKey)`
44
+ defaultFrom?, // Required if your messages don't set `from`
45
+ tagsFromMetadata?, // Customize metadata → Resend tags mapping
46
+ })
47
+ ```
48
+
49
+ ### `tagsFromMetadata`
50
+
51
+ By default the adapter maps every **string-valued** entry in
52
+ `message.metadata` to a Resend tag. Non-string values (numbers,
53
+ booleans, objects) are filtered out — Resend requires `string` for
54
+ both `name` and `value`.
55
+
56
+ Override to customize:
57
+
58
+ ```ts
59
+ createResendAdapter({
60
+ client: resend,
61
+ tagsFromMetadata: (metadata) => [
62
+ // Always include the tenant
63
+ ...(typeof metadata.tenant === 'string'
64
+ ? [{ name: 'tenant', value: metadata.tenant }]
65
+ : []),
66
+ // Drop debug-only entries
67
+ ],
68
+ });
69
+ ```
70
+
71
+ ## Error mapping
72
+
73
+ Resend's `{ data, error }` response shape becomes:
74
+
75
+ - `error` set → adapter throws (`@absolutejs/dispatch` records the
76
+ exception on the `dispatch.email.send` span, bumps `failed`
77
+ counter, emits `dispatch.email.failed` audit event).
78
+ - `data.id` present → `DispatchResult.id` = the Resend message id.
79
+ - `data.id` missing → `DispatchResult.id` is `undefined`,
80
+ `result.provider` still `'resend'`.
81
+
82
+ ## License
83
+
84
+ [Apache 2.0](../LICENSE). Tier B substrate-adjacent under the
85
+ AbsoluteJS licensing policy — rides `@absolutejs/dispatch` (BSL Tier
86
+ A) and `resend` (MIT).
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @absolutejs/dispatch-resend — Resend-backed `EmailAdapter` for
3
+ * `@absolutejs/dispatch`.
4
+ *
5
+ * Takes the user's Resend client (so they control the API key, the
6
+ * fetch impl, the timeout config — everything the Resend SDK accepts
7
+ * directly). Maps `EmailMessage` to Resend's request shape and the
8
+ * response's `id` to `DispatchResult.id`.
9
+ *
10
+ * Resend errors propagate as Promise rejections — `@absolutejs/dispatch`
11
+ * captures them on the `dispatch.email.send` span, bumps the failed
12
+ * counter, emits the `dispatch.email.failed` audit event, and re-throws.
13
+ */
14
+ import type { EmailAdapter } from '@absolutejs/dispatch';
15
+ /**
16
+ * Minimal subset of Resend's `Resend` client we use. Declaring it
17
+ * locally keeps `resend` a true peer dep — its types aren't required
18
+ * at compile time, only its tag-template-style API at runtime.
19
+ */
20
+ export type ResendClientLike = {
21
+ emails: {
22
+ send: (params: {
23
+ from: string;
24
+ to: string | string[];
25
+ subject: string;
26
+ text?: string;
27
+ html?: string;
28
+ reply_to?: string | string[];
29
+ cc?: string | string[];
30
+ bcc?: string | string[];
31
+ headers?: Record<string, string>;
32
+ tags?: Array<{
33
+ name: string;
34
+ value: string;
35
+ }>;
36
+ }) => Promise<{
37
+ data?: {
38
+ id?: string;
39
+ } | null;
40
+ error?: unknown;
41
+ }>;
42
+ };
43
+ };
44
+ export type CreateResendAdapterOptions = {
45
+ /** The Resend client (`new Resend(process.env.RESEND_KEY)`). */
46
+ client: ResendClientLike;
47
+ /**
48
+ * Default `from` address. Resend REQUIRES `from`; if neither the
49
+ * message nor this default is set, the adapter throws.
50
+ */
51
+ defaultFrom?: string;
52
+ /**
53
+ * Map `EmailMessage.metadata` to Resend's `tags` array. The default
54
+ * passes through string-valued entries (Resend tags must be strings).
55
+ * Override to filter / transform / drop based on your conventions.
56
+ */
57
+ tagsFromMetadata?: (metadata: Record<string, unknown>) => Array<{
58
+ name: string;
59
+ value: string;
60
+ }> | undefined;
61
+ };
62
+ export declare const createResendAdapter: (options: CreateResendAdapterOptions) => EmailAdapter;
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,EAAgB,MAAM,sBAAsB,CAAC;AAEvE;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE;QACP,IAAI,EAAE,CAAC,MAAM,EAAE;YACd,IAAI,EAAE,MAAM,CAAC;YACb,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;YACtB,OAAO,EAAE,MAAM,CAAC;YAChB,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;YAC7B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;YACvB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;YACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACjC,IAAI,CAAC,EAAE,KAAK,CAAC;gBAAE,IAAI,EAAE,MAAM,CAAC;gBAAC,KAAK,EAAE,MAAM,CAAA;aAAE,CAAC,CAAC;SAC9C,KAAK,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,EAAE,CAAC,EAAE,MAAM,CAAA;aAAE,GAAG,IAAI,CAAC;YAAC,KAAK,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KAClE,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACxC,gEAAgE;IAChE,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,SAAS,CAAC;CACxD,CAAC;AAsBF,eAAO,MAAM,mBAAmB,GAC/B,SAAS,0BAA0B,KACjC,YA4DF,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ // @bun
2
+ // src/index.ts
3
+ var defaultTagsFromMetadata = (metadata) => {
4
+ const tags = [];
5
+ for (const [name, value] of Object.entries(metadata)) {
6
+ if (typeof value === "string") {
7
+ tags.push({ name, value });
8
+ }
9
+ }
10
+ return tags.length > 0 ? tags : undefined;
11
+ };
12
+ var arrayOrUndefined = (value) => {
13
+ if (value === undefined)
14
+ return;
15
+ if (typeof value === "string")
16
+ return value;
17
+ return [...value];
18
+ };
19
+ var createResendAdapter = (options) => {
20
+ const { client } = options;
21
+ const tagsFromMetadata = options.tagsFromMetadata ?? defaultTagsFromMetadata;
22
+ return {
23
+ name: "resend",
24
+ send: async (message) => {
25
+ const from = message.from ?? options.defaultFrom;
26
+ if (from === undefined || from.length === 0) {
27
+ throw new Error("[dispatch-resend] no `from` address \u2014 Resend requires one. " + "Pass `message.from` per send, or `createResendAdapter({ defaultFrom })`.");
28
+ }
29
+ const tags = message.metadata !== undefined ? tagsFromMetadata(message.metadata) : undefined;
30
+ const response = await client.emails.send({
31
+ from,
32
+ to: typeof message.to === "string" ? message.to : [...message.to],
33
+ subject: message.subject,
34
+ ...message.text !== undefined ? { text: message.text } : {},
35
+ ...message.html !== undefined ? { html: message.html } : {},
36
+ ...message.replyTo !== undefined ? { reply_to: message.replyTo } : {},
37
+ ...arrayOrUndefined(message.cc) !== undefined ? { cc: arrayOrUndefined(message.cc) } : {},
38
+ ...arrayOrUndefined(message.bcc) !== undefined ? { bcc: arrayOrUndefined(message.bcc) } : {},
39
+ ...message.headers !== undefined ? { headers: message.headers } : {},
40
+ ...tags !== undefined ? { tags } : {}
41
+ });
42
+ if (response.error !== undefined && response.error !== null) {
43
+ throw new Error(typeof response.error === "object" && response.error !== null && "message" in response.error ? String(response.error.message) : `[dispatch-resend] Resend error: ${JSON.stringify(response.error)}`);
44
+ }
45
+ return {
46
+ at: Date.now(),
47
+ ...response.data?.id !== undefined ? { id: response.data.id } : {},
48
+ provider: "resend"
49
+ };
50
+ }
51
+ };
52
+ };
53
+ export {
54
+ createResendAdapter
55
+ };
56
+
57
+ //# debugId=00F1909FAADE107B64756E2164756E21
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @absolutejs/dispatch-resend — Resend-backed `EmailAdapter` for\n * `@absolutejs/dispatch`.\n *\n * Takes the user's Resend client (so they control the API key, the\n * fetch impl, the timeout config — everything the Resend SDK accepts\n * directly). Maps `EmailMessage` to Resend's request shape and the\n * response's `id` to `DispatchResult.id`.\n *\n * Resend errors propagate as Promise rejections — `@absolutejs/dispatch`\n * captures them on the `dispatch.email.send` span, bumps the failed\n * counter, emits the `dispatch.email.failed` audit event, and re-throws.\n */\nimport type { EmailAdapter, EmailMessage } from '@absolutejs/dispatch';\n\n/**\n * Minimal subset of Resend's `Resend` client we use. Declaring it\n * locally keeps `resend` a true peer dep — its types aren't required\n * at compile time, only its tag-template-style API at runtime.\n */\nexport type ResendClientLike = {\n\temails: {\n\t\tsend: (params: {\n\t\t\tfrom: string;\n\t\t\tto: string | string[];\n\t\t\tsubject: string;\n\t\t\ttext?: string;\n\t\t\thtml?: string;\n\t\t\treply_to?: string | string[];\n\t\t\tcc?: string | string[];\n\t\t\tbcc?: string | string[];\n\t\t\theaders?: Record<string, string>;\n\t\t\ttags?: Array<{ name: string; value: string }>;\n\t\t}) => Promise<{ data?: { id?: string } | null; error?: unknown }>;\n\t};\n};\n\nexport type CreateResendAdapterOptions = {\n\t/** The Resend client (`new Resend(process.env.RESEND_KEY)`). */\n\tclient: ResendClientLike;\n\t/**\n\t * Default `from` address. Resend REQUIRES `from`; if neither the\n\t * message nor this default is set, the adapter throws.\n\t */\n\tdefaultFrom?: string;\n\t/**\n\t * Map `EmailMessage.metadata` to Resend's `tags` array. The default\n\t * passes through string-valued entries (Resend tags must be strings).\n\t * Override to filter / transform / drop based on your conventions.\n\t */\n\ttagsFromMetadata?: (\n\t\tmetadata: Record<string, unknown>\n\t) => Array<{ name: string; value: string }> | undefined;\n};\n\nconst defaultTagsFromMetadata = (\n\tmetadata: Record<string, unknown>\n): Array<{ name: string; value: string }> | undefined => {\n\tconst tags: Array<{ name: string; value: string }> = [];\n\tfor (const [name, value] of Object.entries(metadata)) {\n\t\tif (typeof value === 'string') {\n\t\t\ttags.push({ name, value });\n\t\t}\n\t}\n\treturn tags.length > 0 ? tags : undefined;\n};\n\nconst arrayOrUndefined = (\n\tvalue: string | ReadonlyArray<string> | undefined\n): string | string[] | undefined => {\n\tif (value === undefined) return undefined;\n\tif (typeof value === 'string') return value;\n\treturn [...value];\n};\n\nexport const createResendAdapter = (\n\toptions: CreateResendAdapterOptions\n): EmailAdapter => {\n\tconst { client } = options;\n\tconst tagsFromMetadata =\n\t\toptions.tagsFromMetadata ?? defaultTagsFromMetadata;\n\n\treturn {\n\t\tname: 'resend',\n\t\tsend: async (message: EmailMessage) => {\n\t\t\tconst from = message.from ?? options.defaultFrom;\n\t\t\tif (from === undefined || from.length === 0) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'[dispatch-resend] no `from` address — Resend requires one. ' +\n\t\t\t\t\t\t'Pass `message.from` per send, or `createResendAdapter({ defaultFrom })`.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst tags =\n\t\t\t\tmessage.metadata !== undefined\n\t\t\t\t\t? tagsFromMetadata(message.metadata)\n\t\t\t\t\t: undefined;\n\t\t\tconst response = await client.emails.send({\n\t\t\t\tfrom,\n\t\t\t\tto:\n\t\t\t\t\ttypeof message.to === 'string' ? message.to : [...message.to],\n\t\t\t\tsubject: message.subject,\n\t\t\t\t...(message.text !== undefined ? { text: message.text } : {}),\n\t\t\t\t...(message.html !== undefined ? { html: message.html } : {}),\n\t\t\t\t...(message.replyTo !== undefined\n\t\t\t\t\t? { reply_to: message.replyTo }\n\t\t\t\t\t: {}),\n\t\t\t\t...(arrayOrUndefined(message.cc) !== undefined\n\t\t\t\t\t? { cc: arrayOrUndefined(message.cc) }\n\t\t\t\t\t: {}),\n\t\t\t\t...(arrayOrUndefined(message.bcc) !== undefined\n\t\t\t\t\t? { bcc: arrayOrUndefined(message.bcc) }\n\t\t\t\t\t: {}),\n\t\t\t\t...(message.headers !== undefined\n\t\t\t\t\t? { headers: message.headers }\n\t\t\t\t\t: {}),\n\t\t\t\t...(tags !== undefined ? { tags } : {})\n\t\t\t});\n\t\t\tif (response.error !== undefined && response.error !== null) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\ttypeof response.error === 'object' &&\n\t\t\t\t\tresponse.error !== null &&\n\t\t\t\t\t'message' in response.error\n\t\t\t\t\t\t? String(\n\t\t\t\t\t\t\t\t(response.error as { message: unknown }).message\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: `[dispatch-resend] Resend error: ${JSON.stringify(response.error)}`\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tat: Date.now(),\n\t\t\t\t...(response.data?.id !== undefined\n\t\t\t\t\t? { id: response.data.id }\n\t\t\t\t\t: {}),\n\t\t\t\tprovider: 'resend'\n\t\t\t};\n\t\t}\n\t};\n};\n"
6
+ ],
7
+ "mappings": ";;AAuDA,IAAM,0BAA0B,CAC/B,aACwD;AAAA,EACxD,MAAM,OAA+C,CAAC;AAAA,EACtD,YAAY,MAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG;AAAA,IACrD,IAAI,OAAO,UAAU,UAAU;AAAA,MAC9B,KAAK,KAAK,EAAE,MAAM,MAAM,CAAC;AAAA,IAC1B;AAAA,EACD;AAAA,EACA,OAAO,KAAK,SAAS,IAAI,OAAO;AAAA;AAGjC,IAAM,mBAAmB,CACxB,UACmC;AAAA,EACnC,IAAI,UAAU;AAAA,IAAW;AAAA,EACzB,IAAI,OAAO,UAAU;AAAA,IAAU,OAAO;AAAA,EACtC,OAAO,CAAC,GAAG,KAAK;AAAA;AAGV,IAAM,sBAAsB,CAClC,YACkB;AAAA,EAClB,QAAQ,WAAW;AAAA,EACnB,MAAM,mBACL,QAAQ,oBAAoB;AAAA,EAE7B,OAAO;AAAA,IACN,MAAM;AAAA,IACN,MAAM,OAAO,YAA0B;AAAA,MACtC,MAAM,OAAO,QAAQ,QAAQ,QAAQ;AAAA,MACrC,IAAI,SAAS,aAAa,KAAK,WAAW,GAAG;AAAA,QAC5C,MAAM,IAAI,MACT,qEACC,0EACF;AAAA,MACD;AAAA,MACA,MAAM,OACL,QAAQ,aAAa,YAClB,iBAAiB,QAAQ,QAAQ,IACjC;AAAA,MACJ,MAAM,WAAW,MAAM,OAAO,OAAO,KAAK;AAAA,QACzC;AAAA,QACA,IACC,OAAO,QAAQ,OAAO,WAAW,QAAQ,KAAK,CAAC,GAAG,QAAQ,EAAE;AAAA,QAC7D,SAAS,QAAQ;AAAA,WACb,QAAQ,SAAS,YAAY,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,WACvD,QAAQ,SAAS,YAAY,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,WACvD,QAAQ,YAAY,YACrB,EAAE,UAAU,QAAQ,QAAQ,IAC5B,CAAC;AAAA,WACA,iBAAiB,QAAQ,EAAE,MAAM,YAClC,EAAE,IAAI,iBAAiB,QAAQ,EAAE,EAAE,IACnC,CAAC;AAAA,WACA,iBAAiB,QAAQ,GAAG,MAAM,YACnC,EAAE,KAAK,iBAAiB,QAAQ,GAAG,EAAE,IACrC,CAAC;AAAA,WACA,QAAQ,YAAY,YACrB,EAAE,SAAS,QAAQ,QAAQ,IAC3B,CAAC;AAAA,WACA,SAAS,YAAY,EAAE,KAAK,IAAI,CAAC;AAAA,MACtC,CAAC;AAAA,MACD,IAAI,SAAS,UAAU,aAAa,SAAS,UAAU,MAAM;AAAA,QAC5D,MAAM,IAAI,MACT,OAAO,SAAS,UAAU,YAC1B,SAAS,UAAU,QACnB,aAAa,SAAS,QACnB,OACC,SAAS,MAA+B,OAC1C,IACC,mCAAmC,KAAK,UAAU,SAAS,KAAK,GACpE;AAAA,MACD;AAAA,MACA,OAAO;AAAA,QACN,IAAI,KAAK,IAAI;AAAA,WACT,SAAS,MAAM,OAAO,YACvB,EAAE,IAAI,SAAS,KAAK,GAAG,IACvB,CAAC;AAAA,QACJ,UAAU;AAAA,MACX;AAAA;AAAA,EAEF;AAAA;",
8
+ "debugId": "00F1909FAADE107B64756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@absolutejs/dispatch-resend",
3
+ "version": "0.0.1",
4
+ "description": "Resend-backed EmailAdapter for @absolutejs/dispatch. Takes your Resend client; emits standard DispatchResult with the Resend message id.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/absolutejs/dispatch-adapters.git",
8
+ "directory": "resend"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "type": "module",
14
+ "license": "Apache-2.0",
15
+ "author": "Alex Kahn",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "dispatch",
21
+ "email",
22
+ "resend",
23
+ "absolutejs",
24
+ "transactional"
25
+ ],
26
+ "scripts": {
27
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun --external @absolutejs/dispatch --external resend && tsc --project tsconfig.build.json",
28
+ "test": "bun test",
29
+ "typecheck": "tsc --noEmit",
30
+ "format": "prettier --write \"./**/*.{ts,json,md}\"",
31
+ "release": "bun run format && bun run build && bun publish"
32
+ },
33
+ "peerDependencies": {
34
+ "@absolutejs/dispatch": ">= 0.0.1",
35
+ "resend": ">= 4.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@absolutejs/dispatch": "^0.0.1",
39
+ "@types/bun": "1.3.14",
40
+ "prettier": "3.5.3",
41
+ "resend": "^4.5.0",
42
+ "typescript": "5.8.3"
43
+ },
44
+ "exports": {
45
+ ".": {
46
+ "types": "./dist/index.d.ts",
47
+ "import": "./dist/index.js",
48
+ "default": "./dist/index.js"
49
+ }
50
+ },
51
+ "files": [
52
+ "dist",
53
+ "README.md"
54
+ ]
55
+ }