@resq-systems/email-templates 0.6.0 → 0.6.2
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/lib/contract.d.ts +56 -43
- package/lib/contract.d.ts.map +1 -1
- package/lib/contract.js +12 -1
- package/lib/contract.js.map +1 -1
- package/lib/emails/incident-alert.d.ts +1 -0
- package/lib/emails/incident-alert.d.ts.map +1 -1
- package/lib/emails/incident-alert.js.map +1 -1
- package/lib/emails/index.d.ts +1 -1
- package/lib/emails/mission-approval.d.ts +1 -0
- package/lib/emails/mission-approval.d.ts.map +1 -1
- package/lib/emails/mission-approval.js.map +1 -1
- package/lib/emails/new-device-login.d.ts +1 -0
- package/lib/emails/new-device-login.d.ts.map +1 -1
- package/lib/emails/new-device-login.js.map +1 -1
- package/lib/emails/notification.d.ts +1 -0
- package/lib/emails/notification.d.ts.map +1 -1
- package/lib/emails/notification.js.map +1 -1
- package/lib/emails/org-invitation.d.ts +1 -0
- package/lib/emails/org-invitation.d.ts.map +1 -1
- package/lib/emails/org-invitation.js.map +1 -1
- package/lib/emails/otp.d.ts +1 -0
- package/lib/emails/otp.d.ts.map +1 -1
- package/lib/emails/otp.js.map +1 -1
- package/lib/emails/password-changed.d.ts +1 -0
- package/lib/emails/password-changed.d.ts.map +1 -1
- package/lib/emails/password-changed.js.map +1 -1
- package/lib/emails/password-reset.d.ts +1 -0
- package/lib/emails/password-reset.d.ts.map +1 -1
- package/lib/emails/password-reset.js.map +1 -1
- package/lib/emails/primitives.d.ts.map +1 -1
- package/lib/emails/primitives.js +7 -0
- package/lib/emails/primitives.js.map +1 -1
- package/lib/emails/theme.d.ts +86 -8
- package/lib/emails/theme.d.ts.map +1 -1
- package/lib/emails/theme.js +51 -3
- package/lib/emails/theme.js.map +1 -1
- package/lib/emails/tokens.d.ts +2 -1
- package/lib/emails/tokens.d.ts.map +1 -1
- package/lib/emails/tokens.js +7 -0
- package/lib/emails/tokens.js.map +1 -1
- package/lib/emails/welcome.d.ts +1 -0
- package/lib/emails/welcome.d.ts.map +1 -1
- package/lib/emails/welcome.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/mailer.d.ts +118 -11
- package/lib/mailer.d.ts.map +1 -1
- package/lib/mailer.js +48 -2
- package/lib/mailer.js.map +1 -1
- package/lib/node_modules/@react-email/tailwind/dist/index.d.ts +1 -1
- package/lib/node_modules/{tailwindcss → @react-email/tailwind/node_modules/tailwindcss}/dist/colors.d.ts +1 -1
- package/lib/node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/colors.d.ts.map +1 -0
- package/lib/node_modules/{tailwindcss → @react-email/tailwind/node_modules/tailwindcss}/dist/lib.d.ts +1 -1
- package/lib/node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/lib.d.ts.map +1 -0
- package/lib/node_modules/{tailwindcss → @react-email/tailwind/node_modules/tailwindcss}/dist/resolve-config-QUZ9b-Gn.d.ts +1 -1
- package/lib/node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.ts.map +1 -0
- package/lib/node_modules/{tailwindcss → @react-email/tailwind/node_modules/tailwindcss}/dist/types-DWdTiksJ.d.ts +1 -1
- package/lib/node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/types-DWdTiksJ.d.ts.map +1 -0
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +6 -0
- package/lib/registry.js.map +1 -1
- package/lib/render.d.ts +9 -2
- package/lib/render.d.ts.map +1 -1
- package/lib/render.js +16 -2
- package/lib/render.js.map +1 -1
- package/lib/schemas.d.ts +9 -0
- package/lib/schemas.d.ts.map +1 -1
- package/lib/schemas.js +16 -0
- package/lib/schemas.js.map +1 -1
- package/lib/send/index.d.ts +1 -1
- package/lib/send/resend-sender.d.ts +11 -0
- package/lib/send/resend-sender.d.ts.map +1 -1
- package/lib/send/resend-sender.js +17 -0
- package/lib/send/resend-sender.js.map +1 -1
- package/lib/send/send-email.d.ts +21 -0
- package/lib/send/send-email.d.ts.map +1 -1
- package/lib/send/send-email.js +26 -0
- package/lib/send/send-email.js.map +1 -1
- package/lib/send/sender.d.ts +26 -2
- package/lib/send/sender.d.ts.map +1 -1
- package/lib/suite.d.ts.map +1 -1
- package/lib/suite.js +7 -0
- package/lib/suite.js.map +1 -1
- package/lib/templates.d.ts +9 -0
- package/lib/templates.d.ts.map +1 -1
- package/lib/templates.js +16 -0
- package/lib/templates.js.map +1 -1
- package/package.json +7 -7
- package/lib/node_modules/tailwindcss/dist/colors.d.ts.map +0 -1
- package/lib/node_modules/tailwindcss/dist/lib.d.ts.map +0 -1
- package/lib/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.ts.map +0 -1
- package/lib/node_modules/tailwindcss/dist/types-DWdTiksJ.d.ts.map +0 -1
package/lib/mailer.js
CHANGED
|
@@ -19,17 +19,43 @@ import { Cause, Exit, Schema } from "effect";
|
|
|
19
19
|
* limitations under the License.
|
|
20
20
|
*/
|
|
21
21
|
/**
|
|
22
|
+
* @fileoverview Mailer factory — composes template definitions into a typed,
|
|
23
|
+
* discriminated `{ name, to, data }` contract with a boundary decoder, a registry,
|
|
24
|
+
* and a headless renderer.
|
|
25
|
+
*
|
|
26
|
+
* @module @resq-systems/email-templates/mailer
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
22
29
|
* Recipient schema for every payload's `to`. Validated (not a bare
|
|
23
30
|
* `Schema.String`) so a malformed or header-injecting address is rejected at
|
|
24
31
|
* the decode boundary and the decoded `to` carries the {@link EmailAddress}
|
|
25
32
|
* brand all the way to the provider.
|
|
26
33
|
*/
|
|
27
34
|
const Recipient = EmailAddress;
|
|
28
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Identity helper that infers and preserves a template def's literal types.
|
|
37
|
+
*
|
|
38
|
+
* Pure — returns `def` by reference, unchanged. It exists only so the `const` type
|
|
39
|
+
* parameters capture the literal `name` and the schema's `Type` at the call site;
|
|
40
|
+
* a bare object literal would widen `name` to `string` and lose the discriminant.
|
|
41
|
+
*
|
|
42
|
+
* @template Name - The literal template name; preserved via the `const` modifier.
|
|
43
|
+
* @template DataSchema - The template's `data` schema.
|
|
44
|
+
* @param def - The template definition to brand with its inferred literal types.
|
|
45
|
+
* @returns The same `def` object, typed with its narrowed literals.
|
|
46
|
+
*/
|
|
29
47
|
function defineEmailTemplate(def) {
|
|
30
48
|
return def;
|
|
31
49
|
}
|
|
32
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when an untrusted payload fails schema validation at the decode boundary
|
|
52
|
+
* ({@link Mailer.decode}, and transitively {@link Mailer.renderEmail}).
|
|
53
|
+
*
|
|
54
|
+
* The `message` is the squashed Effect `Cause` from the failed decode; `name` is
|
|
55
|
+
* the stable literal `"EmailValidationError"`, so a caller can tell a bad payload
|
|
56
|
+
* apart from other failures by `name` without relying on `instanceof` across
|
|
57
|
+
* module/realm boundaries.
|
|
58
|
+
*/
|
|
33
59
|
var EmailValidationError = class extends Error {
|
|
34
60
|
name = "EmailValidationError";
|
|
35
61
|
};
|
|
@@ -38,6 +64,26 @@ var EmailValidationError = class extends Error {
|
|
|
38
64
|
* `{ name, to, data }` contract, a boundary decoder, a registry, and a headless
|
|
39
65
|
* renderer. Spread the built-in `resqEmailTemplates` and add your own — each
|
|
40
66
|
* template's `data` is validated by its Effect Schema.
|
|
67
|
+
*
|
|
68
|
+
* Pure: builds the schema union and registry eagerly and holds no mutable state;
|
|
69
|
+
* the returned `decode`/`renderEmail` are the only fallible surfaces. `defs`
|
|
70
|
+
* should have unique `name`s — a duplicate makes the later def win in the registry
|
|
71
|
+
* while both remain in the schema union (see {@link EmailTemplateDef}).
|
|
72
|
+
*
|
|
73
|
+
* @template Defs - The `as const` tuple of template defs to compose.
|
|
74
|
+
* @param defs - The template definitions; pass `[...resqEmailTemplates, myDef]` to extend the built-ins.
|
|
75
|
+
* @returns A {@link Mailer} whose `decode` throws (and `renderEmail` rejects with)
|
|
76
|
+
* {@link EmailValidationError} on invalid input.
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const mailer = createMailer(resqEmailTemplates);
|
|
80
|
+
* const { subject } = await mailer.renderEmail({
|
|
81
|
+
* name: "otp",
|
|
82
|
+
* to: "user@example.com",
|
|
83
|
+
* data: { code: "123456" },
|
|
84
|
+
* });
|
|
85
|
+
* subject; // → "Your ResQ Systems verification code: 123456"
|
|
86
|
+
* ```
|
|
41
87
|
*/
|
|
42
88
|
function createMailer(defs) {
|
|
43
89
|
const schema = Schema.Union(defs.map((def) => Schema.Struct({
|
package/lib/mailer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mailer.js","names":[],"sources":["../src/mailer.tsx"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { render } from \"@react-email/render\";\nimport { Cause, Exit, Schema } from \"effect\";\nimport type { ReactElement } from \"react\";\nimport {\n\ttype EmailMessage,\n\ttype EmailThemeOverride,\n\twithEmailMessage,\n\twithEmailTheme,\n} from \"./emails/theme.js\";\nimport { EmailAddress, HttpUrl, emailCategory } from \"./schemas.js\";\n\n/**\n * Recipient schema for every payload's `to`. Validated (not a bare\n * `Schema.String`) so a malformed or header-injecting address is rejected at\n * the decode boundary and the decoded `to` carries the {@link EmailAddress}\n * brand all the way to the provider.\n */\nconst Recipient = EmailAddress;\n\n/** A template definition: its name, `data` schema, subject line, and component. */\nexport interface EmailTemplateDef<Name extends string, DataSchema extends Schema.Top> {\n\treadonly name: Name;\n\treadonly data: DataSchema;\n\treadonly subject: (data: DataSchema[\"Type\"]) => string;\n\treadonly Component: (data: DataSchema[\"Type\"]) => ReactElement;\n}\n\n/** Identity helper that infers and preserves a template def's literal types. */\nexport function defineEmailTemplate<const Name extends string, DataSchema extends Schema.Top>(\n\tdef: EmailTemplateDef<Name, DataSchema>,\n): EmailTemplateDef<Name, DataSchema> {\n\treturn def;\n}\n\n/**\n * The common supertype for a heterogeneous list of template defs. The `never`\n * parameters make every concrete `EmailTemplateDef<Name, DataSchema>` assignable\n * here (function parameters are contravariant), so defs with different `data`\n * types can live in one array.\n */\ninterface AnyTemplateDef {\n\treadonly name: string;\n\treadonly data: Schema.Top;\n\treadonly subject: (data: never) => string;\n\treadonly Component: (data: never) => ReactElement;\n}\n\n/** The `{ name, to, data, category?, unsubscribeUrl? }` payload for a single template def. */\ntype PayloadFor<Def> =\n\tDef extends EmailTemplateDef<infer Name, infer DataSchema>\n\t\t? {\n\t\t\t\treadonly name: Name;\n\t\t\t\treadonly to: EmailAddress;\n\t\t\t\treadonly data: DataSchema[\"Type\"];\n\t\t\t\t/** Compliance class for this send; defaults to `transactional`. */\n\t\t\t\treadonly category?: \"transactional\" | \"marketing\";\n\t\t\t\t/** Unsubscribe/preferences URL, surfaced in the legal footer for `marketing`. */\n\t\t\t\treadonly unsubscribeUrl?: string;\n\t\t\t}\n\t\t: never;\n\n/** The discriminated payload union for a tuple of template defs. */\nexport type MailerPayload<Defs extends readonly AnyTemplateDef[]> = PayloadFor<Defs[number]>;\n\n/** The `data` type for a given template name within a set of defs. */\nexport type MailerTemplateData<\n\tDefs extends readonly AnyTemplateDef[],\n\tName extends MailerPayload<Defs>[\"name\"],\n> = Extract<MailerPayload<Defs>, { name: Name }>[\"data\"];\n\n/** The rendered, provider-ready email. */\nexport interface RenderedEmail {\n\t/** Validated recipient (branded {@link EmailAddress}), carried from decode. */\n\tto: EmailAddress;\n\tsubject: string;\n\thtml: string;\n\ttext: string;\n}\n\n/** Options for a mailer's `renderEmail`. */\nexport interface RenderEmailOptions {\n\t/** Rebrand this render by overriding theme colors/fonts (see `EmailThemeOverride`). */\n\ttheme?: EmailThemeOverride;\n}\n\n/** Thrown when an untrusted payload fails schema validation at the boundary. */\nexport class EmailValidationError extends Error {\n\toverride readonly name = \"EmailValidationError\";\n}\n\n/** A registry entry: the subject builder and component renderer for one template. */\nexport interface EmailRegistryEntry {\n\tsubject: (data: unknown) => string;\n\trender: (data: unknown) => ReactElement;\n}\n\n/** A composed set of templates: contract schema, decoder, registry, and renderer. */\nexport interface Mailer<\n\tPayload extends { readonly name: string; readonly to: string; readonly data: unknown },\n> {\n\t/** The Effect Schema union describing every `{ name, to, data }` payload. */\n\treadonly schema: Schema.Top;\n\t/** name → { subject, render } for every template. */\n\treadonly registry: Record<Payload[\"name\"], EmailRegistryEntry>;\n\t/** Every registered template name. */\n\treadonly names: readonly Payload[\"name\"][];\n\t/** Validate an untrusted payload (throws {@link EmailValidationError}). */\n\tdecode(input: unknown): Payload;\n\t/** Validate then render to `{ to, subject, html, text }` (headless, pipeline-safe). */\n\trenderEmail(input: unknown, options?: RenderEmailOptions): Promise<RenderedEmail>;\n}\n\n/**\n * Compose template definitions into a typed mailer: a discriminated\n * `{ name, to, data }` contract, a boundary decoder, a registry, and a headless\n * renderer. Spread the built-in `resqEmailTemplates` and add your own — each\n * template's `data` is validated by its Effect Schema.\n */\nexport function createMailer<const Defs extends readonly AnyTemplateDef[]>(\n\tdefs: Defs,\n): Mailer<MailerPayload<Defs>> {\n\ttype Payload = MailerPayload<Defs>;\n\n\t// `Schema.Union(defs.map(...))` maps the def tuple to an array, widening each\n\t// struct's literal `name` and collapsing the discriminant, so TS can't prove the\n\t// union's decoded type is the `Payload` union. Assert it as a services-free\n\t// `Codec<Payload, unknown>` (every field schema decodes without services — cf.\n\t// `@resq-systems/http`'s `SyncSchema`); `decode` returns the narrowed value.\n\tconst schema = Schema.Union(\n\t\tdefs.map((def) =>\n\t\t\tSchema.Struct({\n\t\t\t\tname: Schema.Literal(def.name),\n\t\t\t\tto: Recipient,\n\t\t\t\tdata: def.data,\n\t\t\t\tcategory: Schema.optional(emailCategory),\n\t\t\t\tunsubscribeUrl: Schema.optional(HttpUrl),\n\t\t\t}),\n\t\t),\n\t) as unknown as Schema.Codec<Payload, unknown, never>;\n\n\t// Entries are stored with `unknown` params; the def's data type is enforced at\n\t// the call boundary by `decode`, so these casts are safe.\n\tconst registry: Record<string, EmailRegistryEntry> = Object.fromEntries(\n\t\tdefs.map((def) => [\n\t\t\tdef.name,\n\t\t\t{\n\t\t\t\tsubject: def.subject as EmailRegistryEntry[\"subject\"],\n\t\t\t\trender: def.Component as EmailRegistryEntry[\"render\"],\n\t\t\t},\n\t\t]),\n\t);\n\n\tconst names = defs.map((def) => def.name);\n\n\tconst decodeExit = Schema.decodeUnknownExit(schema);\n\n\tfunction decode(input: unknown): Payload {\n\t\tconst result = decodeExit(input);\n\t\tif (Exit.isFailure(result)) {\n\t\t\tconst squashed = Cause.squash(result.cause);\n\t\t\tthrow new EmailValidationError(\n\t\t\t\tsquashed instanceof Error ? squashed.message : String(squashed),\n\t\t\t);\n\t\t}\n\t\treturn result.value;\n\t}\n\n\tasync function renderEmail(input: unknown, options?: RenderEmailOptions): Promise<RenderedEmail> {\n\t\tconst payload = decode(input);\n\t\tconst entry = registry[payload.name as string];\n\t\tconst message: EmailMessage = {\n\t\t\tcategory: payload.category ?? \"transactional\",\n\t\t\tunsubscribeUrl: payload.unsubscribeUrl,\n\t\t};\n\t\tconst element = withEmailMessage(\n\t\t\twithEmailTheme(entry.render(payload.data), options?.theme),\n\t\t\tmessage,\n\t\t);\n\t\tconst [html, text] = await Promise.all([render(element), render(element, { plainText: true })]);\n\t\treturn { to: payload.to, subject: entry.subject(payload.data), html, text };\n\t}\n\n\treturn {\n\t\tschema,\n\t\tregistry: registry as Record<Payload[\"name\"], EmailRegistryEntry>,\n\t\tnames: names as readonly Payload[\"name\"][],\n\t\tdecode,\n\t\trenderEmail,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,YAAY;;AAWlB,SAAgB,oBACf,KACqC;CACrC,OAAO;AACR;;AAsDA,IAAa,uBAAb,cAA0C,MAAM;CAC/C,OAAyB;AAC1B;;;;;;;AA8BA,SAAgB,aACf,MAC8B;CAQ9B,MAAM,SAAS,OAAO,MACrB,KAAK,KAAK,QACT,OAAO,OAAO;EACb,MAAM,OAAO,QAAQ,IAAI,IAAI;EAC7B,IAAI;EACJ,MAAM,IAAI;EACV,UAAU,OAAO,SAAS,aAAa;EACvC,gBAAgB,OAAO,SAAS,OAAO;CACxC,CAAC,CACF,CACD;CAIA,MAAM,WAA+C,OAAO,YAC3D,KAAK,KAAK,QAAQ,CACjB,IAAI,MACJ;EACC,SAAS,IAAI;EACb,QAAQ,IAAI;CACb,CACD,CAAC,CACF;CAEA,MAAM,QAAQ,KAAK,KAAK,QAAQ,IAAI,IAAI;CAExC,MAAM,aAAa,OAAO,kBAAkB,MAAM;CAElD,SAAS,OAAO,OAAyB;EACxC,MAAM,SAAS,WAAW,KAAK;EAC/B,IAAI,KAAK,UAAU,MAAM,GAAG;GAC3B,MAAM,WAAW,MAAM,OAAO,OAAO,KAAK;GAC1C,MAAM,IAAI,qBACT,oBAAoB,QAAQ,SAAS,UAAU,OAAO,QAAQ,CAC/D;EACD;EACA,OAAO,OAAO;CACf;CAEA,eAAe,YAAY,OAAgB,SAAsD;EAChG,MAAM,UAAU,OAAO,KAAK;EAC5B,MAAM,QAAQ,SAAS,QAAQ;EAC/B,MAAM,UAAwB;GAC7B,UAAU,QAAQ,YAAY;GAC9B,gBAAgB,QAAQ;EACzB;EACA,MAAM,UAAU,iBACf,eAAe,MAAM,OAAO,QAAQ,IAAI,GAAG,SAAS,KAAK,GACzD,OACD;EACA,MAAM,CAAC,MAAM,QAAQ,MAAM,QAAQ,IAAI,CAAC,OAAO,OAAO,GAAG,OAAO,SAAS,EAAE,WAAW,KAAK,CAAC,CAAC,CAAC;EAC9F,OAAO;GAAE,IAAI,QAAQ;GAAI,SAAS,MAAM,QAAQ,QAAQ,IAAI;GAAG;GAAM;EAAK;CAC3E;CAEA,OAAO;EACN;EACU;EACH;EACP;EACA;CACD;AACD"}
|
|
1
|
+
{"version":3,"file":"mailer.js","names":[],"sources":["../src/mailer.tsx"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Mailer factory — composes template definitions into a typed,\n * discriminated `{ name, to, data }` contract with a boundary decoder, a registry,\n * and a headless renderer.\n *\n * @module @resq-systems/email-templates/mailer\n */\n\nimport { render } from \"@react-email/render\";\nimport { Cause, Exit, Schema } from \"effect\";\nimport type { ReactElement } from \"react\";\nimport {\n\ttype EmailMessage,\n\ttype EmailThemeOverride,\n\twithEmailMessage,\n\twithEmailTheme,\n} from \"./emails/theme.js\";\nimport { EmailAddress, HttpUrl, emailCategory } from \"./schemas.js\";\n\n//#region Constants\n\n/**\n * Recipient schema for every payload's `to`. Validated (not a bare\n * `Schema.String`) so a malformed or header-injecting address is rejected at\n * the decode boundary and the decoded `to` carries the {@link EmailAddress}\n * brand all the way to the provider.\n */\nconst Recipient = EmailAddress;\n\n//#endregion\n\n//#region Types\n\n/**\n * A template definition: its discriminant {@link EmailTemplateDef.name}, `data`\n * schema, subject builder, and React component.\n *\n * `name` must be unique across the defs handed to {@link createMailer} — it is the\n * payload union's discriminant, and a later def with a duplicate name silently\n * overwrites the earlier one in the registry (last write wins in\n * `Object.fromEntries`). `subject` and `Component` are only ever invoked with data\n * that has already cleared `data`'s schema at the decode boundary, so they may\n * treat every field as valid and should stay pure.\n *\n * @template Name - The literal template name (e.g. `\"otp\"`); the payload discriminant.\n * @template DataSchema - The Effect Schema whose decoded `Type` is this template's `data`.\n */\nexport interface EmailTemplateDef<Name extends string, DataSchema extends Schema.Top> {\n\t/** Unique template name; the payload union's discriminant. */\n\treadonly name: Name;\n\t/** Effect Schema that validates this template's `data` at the decode boundary. */\n\treadonly data: DataSchema;\n\t/** Builds the subject line from already-validated `data`. Should be pure. */\n\treadonly subject: (data: DataSchema[\"Type\"]) => string;\n\t/** Renders the email body from already-validated `data`. */\n\treadonly Component: (data: DataSchema[\"Type\"]) => ReactElement;\n}\n\n/**\n * Identity helper that infers and preserves a template def's literal types.\n *\n * Pure — returns `def` by reference, unchanged. It exists only so the `const` type\n * parameters capture the literal `name` and the schema's `Type` at the call site;\n * a bare object literal would widen `name` to `string` and lose the discriminant.\n *\n * @template Name - The literal template name; preserved via the `const` modifier.\n * @template DataSchema - The template's `data` schema.\n * @param def - The template definition to brand with its inferred literal types.\n * @returns The same `def` object, typed with its narrowed literals.\n */\nexport function defineEmailTemplate<const Name extends string, DataSchema extends Schema.Top>(\n\tdef: EmailTemplateDef<Name, DataSchema>,\n): EmailTemplateDef<Name, DataSchema> {\n\treturn def;\n}\n\n/**\n * The common supertype for a heterogeneous list of template defs. The `never`\n * parameters make every concrete `EmailTemplateDef<Name, DataSchema>` assignable\n * here (function parameters are contravariant), so defs with different `data`\n * types can live in one array.\n */\ninterface AnyTemplateDef {\n\treadonly name: string;\n\treadonly data: Schema.Top;\n\treadonly subject: (data: never) => string;\n\treadonly Component: (data: never) => ReactElement;\n}\n\n/** The `{ name, to, data, category?, unsubscribeUrl? }` payload for a single template def. */\ntype PayloadFor<Def> =\n\tDef extends EmailTemplateDef<infer Name, infer DataSchema>\n\t\t? {\n\t\t\t\treadonly name: Name;\n\t\t\t\treadonly to: EmailAddress;\n\t\t\t\treadonly data: DataSchema[\"Type\"];\n\t\t\t\t/** Compliance class for this send; defaults to `transactional`. */\n\t\t\t\treadonly category?: \"transactional\" | \"marketing\";\n\t\t\t\t/** Unsubscribe/preferences URL, surfaced in the legal footer for `marketing`. */\n\t\t\t\treadonly unsubscribeUrl?: string;\n\t\t\t}\n\t\t: never;\n\n/**\n * The discriminated payload union for a tuple of template defs — one\n * `{ name, to, data, category?, unsubscribeUrl? }` variant per def, discriminated\n * by the literal `name` field. Narrow a value with `payload.name` to recover the\n * matching `data` type.\n *\n * @template Defs - The `as const` tuple of template defs the union is built over.\n */\nexport type MailerPayload<Defs extends readonly AnyTemplateDef[]> = PayloadFor<Defs[number]>;\n\n/**\n * The `data` type for a given template name within a set of defs — the `data`\n * field of the {@link MailerPayload} variant whose discriminant equals `Name`.\n *\n * @template Defs - The tuple of template defs the payload union is built over.\n * @template Name - The literal `name` selecting a single variant's `data` shape.\n */\nexport type MailerTemplateData<\n\tDefs extends readonly AnyTemplateDef[],\n\tName extends MailerPayload<Defs>[\"name\"],\n> = Extract<MailerPayload<Defs>, { name: Name }>[\"data\"];\n\n/**\n * The rendered, provider-ready email — the resolved output of\n * {@link Mailer.renderEmail}. `html` and `text` are two renderings of the *same*\n * message, so a provider may attach both as a multipart alternative.\n */\nexport interface RenderedEmail {\n\t/** Validated recipient (branded {@link EmailAddress}), carried through from decode. */\n\tto: EmailAddress;\n\t/** The subject line produced by the template's `subject` builder. */\n\tsubject: string;\n\t/** The complete standalone HTML document for the email body. */\n\thtml: string;\n\t/** The plain-text alternative rendering of the same body, for text-only clients. */\n\ttext: string;\n}\n\n/** Options for a mailer's `renderEmail`. */\nexport interface RenderEmailOptions {\n\t/** Rebrand this render by overriding theme colors/fonts (see `EmailThemeOverride`). */\n\ttheme?: EmailThemeOverride;\n}\n\n/**\n * Thrown when an untrusted payload fails schema validation at the decode boundary\n * ({@link Mailer.decode}, and transitively {@link Mailer.renderEmail}).\n *\n * The `message` is the squashed Effect `Cause` from the failed decode; `name` is\n * the stable literal `\"EmailValidationError\"`, so a caller can tell a bad payload\n * apart from other failures by `name` without relying on `instanceof` across\n * module/realm boundaries.\n */\nexport class EmailValidationError extends Error {\n\toverride readonly name = \"EmailValidationError\";\n}\n\n/**\n * A registry entry: the subject builder and component renderer for one template.\n *\n * Both functions take `unknown` because the registry is keyed by name and has\n * erased each def's `data` type. They must only be called with data that has\n * already passed that template's schema (as {@link Mailer.renderEmail} does after\n * {@link Mailer.decode}); calling them with unvalidated data is unsound.\n */\nexport interface EmailRegistryEntry {\n\t/** Builds the subject from validated `data` (typed `unknown` after name-erasure). */\n\tsubject: (data: unknown) => string;\n\t/** Renders the body element from validated `data` (typed `unknown` after name-erasure). */\n\trender: (data: unknown) => ReactElement;\n}\n\n/** A composed set of templates: contract schema, decoder, registry, and renderer. */\nexport interface Mailer<\n\tPayload extends { readonly name: string; readonly to: string; readonly data: unknown },\n> {\n\t/** The Effect Schema union describing every `{ name, to, data }` payload. */\n\treadonly schema: Schema.Top;\n\t/** name → { subject, render } for every template. */\n\treadonly registry: Record<Payload[\"name\"], EmailRegistryEntry>;\n\t/** Every registered template name, in def order. */\n\treadonly names: readonly Payload[\"name\"][];\n\t/**\n\t * Validate an untrusted payload against the contract union and return the\n\t * narrowed {@link Payload}.\n\t *\n\t * @param input - Untrusted `{ name, to, data }` value from the boundary.\n\t * @returns The validated, branded payload.\n\t * @throws {EmailValidationError} If `input` matches no template variant — bad\n\t * `name`, a malformed/header-injecting `to`, or `data` failing its schema.\n\t */\n\tdecode(input: unknown): Payload;\n\t/**\n\t * Validate then render a payload to `{ to, subject, html, text }`.\n\t *\n\t * Validates via {@link decode} first, then renders headlessly through\n\t * `@react-email/render` — no browser, DOM, network, or clock — so it is safe in\n\t * queue workers and cron jobs. Pure and stateless: concurrent calls against one\n\t * mailer do not interfere, and there is no ordering guarantee between them. Does\n\t * not honour an `AbortSignal`.\n\t *\n\t * @param input - Untrusted payload to validate and render.\n\t * @param options - Optional per-render theme override.\n\t * @returns A promise resolving to the rendered email.\n\t * @throws {EmailValidationError} As a rejected promise, when `input` fails\n\t * validation (surfaced from {@link decode}).\n\t */\n\trenderEmail(input: unknown, options?: RenderEmailOptions): Promise<RenderedEmail>;\n}\n\n//#endregion\n\n//#region Public API\n\n/**\n * Compose template definitions into a typed mailer: a discriminated\n * `{ name, to, data }` contract, a boundary decoder, a registry, and a headless\n * renderer. Spread the built-in `resqEmailTemplates` and add your own — each\n * template's `data` is validated by its Effect Schema.\n *\n * Pure: builds the schema union and registry eagerly and holds no mutable state;\n * the returned `decode`/`renderEmail` are the only fallible surfaces. `defs`\n * should have unique `name`s — a duplicate makes the later def win in the registry\n * while both remain in the schema union (see {@link EmailTemplateDef}).\n *\n * @template Defs - The `as const` tuple of template defs to compose.\n * @param defs - The template definitions; pass `[...resqEmailTemplates, myDef]` to extend the built-ins.\n * @returns A {@link Mailer} whose `decode` throws (and `renderEmail` rejects with)\n * {@link EmailValidationError} on invalid input.\n * @example\n * ```ts\n * const mailer = createMailer(resqEmailTemplates);\n * const { subject } = await mailer.renderEmail({\n * name: \"otp\",\n * to: \"user@example.com\",\n * data: { code: \"123456\" },\n * });\n * subject; // → \"Your ResQ Systems verification code: 123456\"\n * ```\n */\nexport function createMailer<const Defs extends readonly AnyTemplateDef[]>(\n\tdefs: Defs,\n): Mailer<MailerPayload<Defs>> {\n\ttype Payload = MailerPayload<Defs>;\n\n\t// `Schema.Union(defs.map(...))` maps the def tuple to an array, widening each\n\t// struct's literal `name` and collapsing the discriminant, so TS can't prove the\n\t// union's decoded type is the `Payload` union. Assert it as a services-free\n\t// `Codec<Payload, unknown>` (every field schema decodes without services — cf.\n\t// `@resq-systems/http`'s `SyncSchema`); `decode` returns the narrowed value.\n\tconst schema = Schema.Union(\n\t\tdefs.map((def) =>\n\t\t\tSchema.Struct({\n\t\t\t\tname: Schema.Literal(def.name),\n\t\t\t\tto: Recipient,\n\t\t\t\tdata: def.data,\n\t\t\t\tcategory: Schema.optional(emailCategory),\n\t\t\t\tunsubscribeUrl: Schema.optional(HttpUrl),\n\t\t\t}),\n\t\t),\n\t) as unknown as Schema.Codec<Payload, unknown, never>;\n\n\t// Entries are stored with `unknown` params; the def's data type is enforced at\n\t// the call boundary by `decode`, so these casts are safe.\n\tconst registry: Record<string, EmailRegistryEntry> = Object.fromEntries(\n\t\tdefs.map((def) => [\n\t\t\tdef.name,\n\t\t\t{\n\t\t\t\tsubject: def.subject as EmailRegistryEntry[\"subject\"],\n\t\t\t\trender: def.Component as EmailRegistryEntry[\"render\"],\n\t\t\t},\n\t\t]),\n\t);\n\n\tconst names = defs.map((def) => def.name);\n\n\tconst decodeExit = Schema.decodeUnknownExit(schema);\n\n\tfunction decode(input: unknown): Payload {\n\t\tconst result = decodeExit(input);\n\t\tif (Exit.isFailure(result)) {\n\t\t\tconst squashed = Cause.squash(result.cause);\n\t\t\tthrow new EmailValidationError(\n\t\t\t\tsquashed instanceof Error ? squashed.message : String(squashed),\n\t\t\t);\n\t\t}\n\t\treturn result.value;\n\t}\n\n\tasync function renderEmail(input: unknown, options?: RenderEmailOptions): Promise<RenderedEmail> {\n\t\tconst payload = decode(input);\n\t\tconst entry = registry[payload.name as string];\n\t\tconst message: EmailMessage = {\n\t\t\tcategory: payload.category ?? \"transactional\",\n\t\t\tunsubscribeUrl: payload.unsubscribeUrl,\n\t\t};\n\t\tconst element = withEmailMessage(\n\t\t\twithEmailTheme(entry.render(payload.data), options?.theme),\n\t\t\tmessage,\n\t\t);\n\t\tconst [html, text] = await Promise.all([render(element), render(element, { plainText: true })]);\n\t\treturn { to: payload.to, subject: entry.subject(payload.data), html, text };\n\t}\n\n\treturn {\n\t\tschema,\n\t\tregistry: registry as Record<Payload[\"name\"], EmailRegistryEntry>,\n\t\tnames: names as readonly Payload[\"name\"][],\n\t\tdecode,\n\t\trenderEmail,\n\t};\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,YAAY;;;;;;;;;;;;;AA2ClB,SAAgB,oBACf,KACqC;CACrC,OAAO;AACR;;;;;;;;;;AAkFA,IAAa,uBAAb,cAA0C,MAAM;CAC/C,OAAyB;AAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqFA,SAAgB,aACf,MAC8B;CAQ9B,MAAM,SAAS,OAAO,MACrB,KAAK,KAAK,QACT,OAAO,OAAO;EACb,MAAM,OAAO,QAAQ,IAAI,IAAI;EAC7B,IAAI;EACJ,MAAM,IAAI;EACV,UAAU,OAAO,SAAS,aAAa;EACvC,gBAAgB,OAAO,SAAS,OAAO;CACxC,CAAC,CACF,CACD;CAIA,MAAM,WAA+C,OAAO,YAC3D,KAAK,KAAK,QAAQ,CACjB,IAAI,MACJ;EACC,SAAS,IAAI;EACb,QAAQ,IAAI;CACb,CACD,CAAC,CACF;CAEA,MAAM,QAAQ,KAAK,KAAK,QAAQ,IAAI,IAAI;CAExC,MAAM,aAAa,OAAO,kBAAkB,MAAM;CAElD,SAAS,OAAO,OAAyB;EACxC,MAAM,SAAS,WAAW,KAAK;EAC/B,IAAI,KAAK,UAAU,MAAM,GAAG;GAC3B,MAAM,WAAW,MAAM,OAAO,OAAO,KAAK;GAC1C,MAAM,IAAI,qBACT,oBAAoB,QAAQ,SAAS,UAAU,OAAO,QAAQ,CAC/D;EACD;EACA,OAAO,OAAO;CACf;CAEA,eAAe,YAAY,OAAgB,SAAsD;EAChG,MAAM,UAAU,OAAO,KAAK;EAC5B,MAAM,QAAQ,SAAS,QAAQ;EAC/B,MAAM,UAAwB;GAC7B,UAAU,QAAQ,YAAY;GAC9B,gBAAgB,QAAQ;EACzB;EACA,MAAM,UAAU,iBACf,eAAe,MAAM,OAAO,QAAQ,IAAI,GAAG,SAAS,KAAK,GACzD,OACD;EACA,MAAM,CAAC,MAAM,QAAQ,MAAM,QAAQ,IAAI,CAAC,OAAO,OAAO,GAAG,OAAO,SAAS,EAAE,WAAW,KAAK,CAAC,CAAC,CAAC;EAC9F,OAAO;GAAE,IAAI,QAAQ;GAAI,SAAS,MAAM,QAAQ,QAAQ,IAAI;GAAG;GAAM;EAAK;CAC3E;CAEA,OAAO;EACN;EACU;EACH;EACP;EACA;CACD;AACD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"colors.d.ts","names":[],"sources":["../../../../../../../../../node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/colors.d.mts"],"x_google_ignoreList":[0],"mappings":";cAAc;EACV;EACA;EACA;EACA;EACA;EACA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { U as UserConfig } from "./types-DWdTiksJ.js";
|
|
2
|
-
//#region ../../node_modules/tailwindcss/dist/lib.d.mts
|
|
2
|
+
//#region ../../node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/lib.d.mts
|
|
3
3
|
interface Config extends UserConfig {}
|
|
4
4
|
//#endregion
|
|
5
5
|
export type { Config };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","names":[],"sources":["../../../../../../../../../node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/lib.d.mts"],"x_google_ignoreList":[0],"mappings":";;UA4TU,eAAe"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import _default from "./colors.js";
|
|
2
|
-
//#region ../../node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts
|
|
2
|
+
//#region ../../node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts
|
|
3
3
|
type NamedUtilityValue = {
|
|
4
4
|
kind: 'named';
|
|
5
5
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-config-QUZ9b-Gn.d.ts","names":[],"sources":["../../../../../../../../../node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts"],"x_google_ignoreList":[0],"mappings":";;KA4BK;EACD;;;;;;;;;;EAUA;;;;;;;EAOA;;KA0IC;EACD,QAAQ,iBAAiB;EACzB,eAAe"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { N as NamedUtilityValue, P as PluginUtils } from "./resolve-config-QUZ9b-Gn.js";
|
|
2
|
-
//#region ../../node_modules/tailwindcss/dist/types-DWdTiksJ.d.mts
|
|
2
|
+
//#region ../../node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/types-DWdTiksJ.d.mts
|
|
3
3
|
/**
|
|
4
4
|
* The source code for one or more nodes in the AST
|
|
5
5
|
*
|
package/lib/node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/types-DWdTiksJ.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-DWdTiksJ.d.ts","names":[],"sources":["../../../../../../../../../node_modules/@react-email/tailwind/node_modules/tailwindcss/dist/types-DWdTiksJ.d.mts"],"x_google_ignoreList":[0],"mappings":";;;;;;;UAOU;;;;;;EAMN;;;;EAIA;;;;;;;;;KASC,kBAAkB,QAAQ,QAAQ,eAAe;KAGjD,YAAY,KAAK;KACjB;EACD,SAAS;EACT,SAAS;;EAET;EACA,MAAM;;KAEL,kBAAkB;GAClB,UAAU,IAAI;EACf;;KAEC,SAAS,WAAW,mBAAmB;KACvC;EACD,QAAQ,MAAM;EACd,WAAW,cAAc,6BAA6B;EACtD,aAAa,YAAY,cAAc,KAAK,OAAO,YAAY;IAC3D;2BACqB;IACrB,SAAS,eAAe;IACxB,MAAM;MACF,OAAO;MACP;OACD;MACC,OAAO;MACP;;;EAGR,aAAa,WAAW,eAAe,UAAU,aAAa,eAAe,UAAU,cAAc;EACrG,eAAe,WAAW,gBAAgB,eAAe;IACrD;QACE,UAAU,YAAY,UAAU;IAClC;IACA;IACA,QAAQ;MACJ,kBAAkB,OAAO;;IAE7B,mBAAmB;;EAEvB,cAAc,WAAW,eAAe,WAAW,eAAe,YAAY;EAC9E,gBAAgB,WAAW,gBAAgB,eAAe;IACtD;QACE,UAAU,UAAU;IACtB;IACA;IACA,QAAQ;MACJ,kBAAkB,OAAO;;IAE7B,mBAAmB;;EAEvB,MAAM,cAAc;EACpB,OAAO,eAAe;EACtB,OAAO;;KAEN;GACA,kCAAkC,UAAU;;KAG5C,aAAa,KAAK,MAAM,OAAO,gBAAgB;KAC/C,aAAa,aAAa;KAC1B,cAAc,eAAe;EAC9B,SAAS,eAAe;;KAEvB;EACD;EACA;;KAEC;UACK;EACN,UAAU;EACV,QAAQ;EACR,UAAU;;UAEJ;EACN,UAAU;IACN;IACA,OAAO;;;UAGL;EACN,WAAW;;UAEL;EACN;;UAEM;EACN;;UAEM;EACN;;UAEM;EACN,iBAAiB;;UAEX;EACN,uBAAuB"}
|
package/lib/registry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","names":[],"sources":["../src/registry.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","names":[],"sources":["../src/registry.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;cA6Ba,UAAQ,mKAAR"}
|
package/lib/registry.js
CHANGED
|
@@ -16,6 +16,12 @@ import { resqMailer } from "./suite.js";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
/**
|
|
19
|
+
* @fileoverview Name-to-renderer registry for the built-in templates, derived from
|
|
20
|
+
* the default mailer so it always matches the contract.
|
|
21
|
+
*
|
|
22
|
+
* @module @resq-systems/email-templates/registry
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
19
25
|
* Maps each built-in template `name` to its `{ subject, render }`. Derived from
|
|
20
26
|
* the default mailer, so it always matches the contract.
|
|
21
27
|
*/
|
package/lib/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","names":[],"sources":["../src/registry.tsx"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resqMailer } from \"./suite.js\";\n\n/**\n * Maps each built-in template `name` to its `{ subject, render }`. Derived from\n * the default mailer, so it always matches the contract.\n */\nexport const registry = resqMailer.registry;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"registry.js","names":[],"sources":["../src/registry.tsx"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Name-to-renderer registry for the built-in templates, derived from\n * the default mailer so it always matches the contract.\n *\n * @module @resq-systems/email-templates/registry\n */\n\nimport { resqMailer } from \"./suite.js\";\n\n/**\n * Maps each built-in template `name` to its `{ subject, render }`. Derived from\n * the default mailer, so it always matches the contract.\n */\nexport const registry = resqMailer.registry;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,WAAW,WAAW"}
|
package/lib/render.d.ts
CHANGED
|
@@ -3,9 +3,16 @@ import { RenderEmailOptions, RenderedEmail } from "./mailer.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Validate an untrusted payload and render it to `{ to, subject, html, text }`.
|
|
5
5
|
*
|
|
6
|
-
* Runs headlessly (no browser/DOM
|
|
7
|
-
* queue workers, cron jobs, and other
|
|
6
|
+
* Runs headlessly (no browser/DOM, no network, no clock) via
|
|
7
|
+
* `@react-email/render`, so it is safe from queue workers, cron jobs, and other
|
|
8
|
+
* pipeline contexts. Pure and stateless — concurrent calls are safe and there is
|
|
9
|
+
* no ordering guarantee; it does not honour an `AbortSignal`. Pass `{ theme }` to
|
|
8
10
|
* rebrand a single render.
|
|
11
|
+
*
|
|
12
|
+
* @param input - Untrusted `{ name, to, data }` payload to validate and render.
|
|
13
|
+
* @param options - Optional per-render theme override.
|
|
14
|
+
* @returns A promise resolving to the rendered `{ to, subject, html, text }`.
|
|
15
|
+
* @throws {EmailValidationError} As a rejected promise, when `input` fails validation.
|
|
9
16
|
*/
|
|
10
17
|
declare const renderEmail: (input: unknown, options?: RenderEmailOptions) => Promise<RenderedEmail>;
|
|
11
18
|
//#endregion
|
package/lib/render.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","names":[],"sources":["../src/render.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"render.d.ts","names":[],"sources":["../src/render.ts"],"mappings":";;;;;;;;;;;;;;;;cA0Ca,cAAW,gBAAA,UAAX,uBAAW,QAAA"}
|
package/lib/render.js
CHANGED
|
@@ -16,11 +16,25 @@ import { resqMailer } from "./suite.js";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
/**
|
|
19
|
+
* @fileoverview Headless render entry — validates a payload and renders it to
|
|
20
|
+
* `{ to, subject, html, text }` without a browser, so it is safe from queue workers
|
|
21
|
+
* and cron jobs.
|
|
22
|
+
*
|
|
23
|
+
* @module @resq-systems/email-templates/render
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
19
26
|
* Validate an untrusted payload and render it to `{ to, subject, html, text }`.
|
|
20
27
|
*
|
|
21
|
-
* Runs headlessly (no browser/DOM
|
|
22
|
-
* queue workers, cron jobs, and other
|
|
28
|
+
* Runs headlessly (no browser/DOM, no network, no clock) via
|
|
29
|
+
* `@react-email/render`, so it is safe from queue workers, cron jobs, and other
|
|
30
|
+
* pipeline contexts. Pure and stateless — concurrent calls are safe and there is
|
|
31
|
+
* no ordering guarantee; it does not honour an `AbortSignal`. Pass `{ theme }` to
|
|
23
32
|
* rebrand a single render.
|
|
33
|
+
*
|
|
34
|
+
* @param input - Untrusted `{ name, to, data }` payload to validate and render.
|
|
35
|
+
* @param options - Optional per-render theme override.
|
|
36
|
+
* @returns A promise resolving to the rendered `{ to, subject, html, text }`.
|
|
37
|
+
* @throws {EmailValidationError} As a rejected promise, when `input` fails validation.
|
|
24
38
|
*/
|
|
25
39
|
const renderEmail = resqMailer.renderEmail;
|
|
26
40
|
//#endregion
|
package/lib/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","names":[],"sources":["../src/render.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resqMailer } from \"./suite.js\";\n\nexport type { RenderEmailOptions, RenderedEmail } from \"./mailer.js\";\n\n/**\n * Validate an untrusted payload and render it to `{ to, subject, html, text }`.\n *\n * Runs headlessly (no browser/DOM) via `@react-email/render`, so it is safe from
|
|
1
|
+
{"version":3,"file":"render.js","names":[],"sources":["../src/render.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Headless render entry — validates a payload and renders it to\n * `{ to, subject, html, text }` without a browser, so it is safe from queue workers\n * and cron jobs.\n *\n * @module @resq-systems/email-templates/render\n */\n\nimport { resqMailer } from \"./suite.js\";\n\nexport type { RenderEmailOptions, RenderedEmail } from \"./mailer.js\";\n\n/**\n * Validate an untrusted payload and render it to `{ to, subject, html, text }`.\n *\n * Runs headlessly (no browser/DOM, no network, no clock) via\n * `@react-email/render`, so it is safe from queue workers, cron jobs, and other\n * pipeline contexts. Pure and stateless — concurrent calls are safe and there is\n * no ordering guarantee; it does not honour an `AbortSignal`. Pass `{ theme }` to\n * rebrand a single render.\n *\n * @param input - Untrusted `{ name, to, data }` payload to validate and render.\n * @param options - Optional per-render theme override.\n * @returns A promise resolving to the rendered `{ to, subject, html, text }`.\n * @throws {EmailValidationError} As a rejected promise, when `input` fails validation.\n */\nexport const renderEmail = resqMailer.renderEmail;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAa,cAAc,WAAW"}
|
package/lib/schemas.d.ts
CHANGED
|
@@ -31,20 +31,24 @@ type EmailAddress = typeof EmailAddress.Type;
|
|
|
31
31
|
*/
|
|
32
32
|
declare const emailCategory: Schema.Literals<readonly ["transactional", "marketing"]>;
|
|
33
33
|
type EmailCategory = typeof emailCategory.Type;
|
|
34
|
+
/** `data` schema for the one-time-code (OTP) email. */
|
|
34
35
|
declare const otpData: Schema.Struct<{
|
|
35
36
|
readonly code: Schema.NonEmptyString;
|
|
36
37
|
readonly firstName: Schema.optional<Schema.String>;
|
|
37
38
|
readonly expiresInMinutes: Schema.optional<Schema.Number>;
|
|
38
39
|
}>;
|
|
40
|
+
/** `data` schema for the account welcome / onboarding email. */
|
|
39
41
|
declare const welcomeData: Schema.Struct<{
|
|
40
42
|
readonly firstName: Schema.NonEmptyString;
|
|
41
43
|
readonly verifyUrl: Schema.optional<Schema.String>;
|
|
42
44
|
}>;
|
|
45
|
+
/** `data` schema for the password-reset email. */
|
|
43
46
|
declare const passwordResetData: Schema.Struct<{
|
|
44
47
|
readonly firstName: Schema.optional<Schema.String>;
|
|
45
48
|
readonly resetUrl: Schema.String;
|
|
46
49
|
readonly expiresInMinutes: Schema.optional<Schema.Number>;
|
|
47
50
|
}>;
|
|
51
|
+
/** `data` schema for the generic notification / alert email. */
|
|
48
52
|
declare const notificationData: Schema.Struct<{
|
|
49
53
|
readonly title: Schema.NonEmptyString;
|
|
50
54
|
readonly body: Schema.NonEmptyString;
|
|
@@ -52,6 +56,7 @@ declare const notificationData: Schema.Struct<{
|
|
|
52
56
|
readonly actionUrl: Schema.optional<Schema.String>;
|
|
53
57
|
readonly actionLabel: Schema.optional<Schema.String>;
|
|
54
58
|
}>;
|
|
59
|
+
/** `data` schema for the incident / dispatch alert email. */
|
|
55
60
|
declare const incidentAlertData: Schema.Struct<{
|
|
56
61
|
readonly incidentId: Schema.NonEmptyString;
|
|
57
62
|
readonly title: Schema.NonEmptyString;
|
|
@@ -61,11 +66,13 @@ declare const incidentAlertData: Schema.Struct<{
|
|
|
61
66
|
readonly detectedAt: Schema.optional<Schema.String>;
|
|
62
67
|
readonly dashboardUrl: Schema.String;
|
|
63
68
|
}>;
|
|
69
|
+
/** `data` schema for the password-changed security notice. */
|
|
64
70
|
declare const passwordChangedData: Schema.Struct<{
|
|
65
71
|
readonly firstName: Schema.optional<Schema.String>;
|
|
66
72
|
readonly changedAt: Schema.optional<Schema.String>;
|
|
67
73
|
readonly secureAccountUrl: Schema.optional<Schema.String>;
|
|
68
74
|
}>;
|
|
75
|
+
/** `data` schema for the new-device sign-in security alert. */
|
|
69
76
|
declare const newDeviceLoginData: Schema.Struct<{
|
|
70
77
|
readonly firstName: Schema.optional<Schema.String>;
|
|
71
78
|
readonly device: Schema.optional<Schema.String>;
|
|
@@ -74,6 +81,7 @@ declare const newDeviceLoginData: Schema.Struct<{
|
|
|
74
81
|
readonly at: Schema.optional<Schema.String>;
|
|
75
82
|
readonly secureAccountUrl: Schema.optional<Schema.String>;
|
|
76
83
|
}>;
|
|
84
|
+
/** `data` schema for the mission-approval sign-off request. */
|
|
77
85
|
declare const missionApprovalData: Schema.Struct<{
|
|
78
86
|
readonly missionId: Schema.NonEmptyString;
|
|
79
87
|
readonly title: Schema.NonEmptyString;
|
|
@@ -83,6 +91,7 @@ declare const missionApprovalData: Schema.Struct<{
|
|
|
83
91
|
readonly approveUrl: Schema.String;
|
|
84
92
|
readonly expiresInMinutes: Schema.optional<Schema.Number>;
|
|
85
93
|
}>;
|
|
94
|
+
/** `data` schema for the organization / team invitation email. */
|
|
86
95
|
declare const orgInvitationData: Schema.Struct<{
|
|
87
96
|
readonly orgName: Schema.NonEmptyString;
|
|
88
97
|
readonly inviterName: Schema.optional<Schema.String>;
|
package/lib/schemas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.d.ts","names":[],"sources":["../src/schemas.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","names":[],"sources":["../src/schemas.ts"],"mappings":";;;;;;;cAiCa,SAAO,OAAA;;;;;;;;;;;;;;;;;cAkBP,cAAY,OAAA,MAAA,OAAA;;KAKb,sBAAsB,aAAa;;;;;cAMlC,eAAa,OAAA;KACd,uBAAuB,cAAc;;cAOpC,SAAO,OAAA;;;;;;cAOP,aAAW,OAAA;;;;;cAMX,mBAAiB,OAAA;;;;;;cAOjB,kBAAgB,OAAA;;;;;;;;cAShB,mBAAiB,OAAA;;;;;;;;;;cAajB,qBAAmB,OAAA;;;;;;cASnB,oBAAkB,OAAA;;;;;;;;;cAclB,qBAAmB,OAAA;;;;;;;;;;cAYnB,mBAAiB,OAAA;;;;;;;;KAalB,iBAAiB,QAAQ;KACzB,qBAAqB,YAAY;KACjC,2BAA2B,kBAAkB;KAC7C,0BAA0B,iBAAiB;KAC3C,2BAA2B,kBAAkB;KAC7C,6BAA6B,oBAAoB;KACjD,4BAA4B,mBAAmB;KAC/C,6BAA6B,oBAAoB;KACjD,2BAA2B,kBAAkB"}
|
package/lib/schemas.js
CHANGED
|
@@ -16,6 +16,13 @@ import { Schema } from "effect";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
/**
|
|
19
|
+
* @fileoverview Effect Schema building blocks for email payloads — validated URL and
|
|
20
|
+
* recipient primitives, the send-category literal, and each built-in template's
|
|
21
|
+
* `data` struct with its inferred type.
|
|
22
|
+
*
|
|
23
|
+
* @module @resq-systems/email-templates/schemas
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
19
26
|
* A non-empty, absolute http(s) URL. Validated with a pattern check (blocks
|
|
20
27
|
* `javascript:`/relative hrefs) but decoded as a plain `string`, so template
|
|
21
28
|
* props stay ergonomic. Exported for consumers building their own templates.
|
|
@@ -43,20 +50,24 @@ const EmailAddress = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9._%+-]+@[A
|
|
|
43
50
|
* `marketing` requires an unsubscribe affordance in the legal footer.
|
|
44
51
|
*/
|
|
45
52
|
const emailCategory = Schema.Literals(["transactional", "marketing"]);
|
|
53
|
+
/** `data` schema for the one-time-code (OTP) email. */
|
|
46
54
|
const otpData = Schema.Struct({
|
|
47
55
|
code: Schema.NonEmptyString,
|
|
48
56
|
firstName: Schema.optional(Schema.String),
|
|
49
57
|
expiresInMinutes: Schema.optional(Schema.Number)
|
|
50
58
|
});
|
|
59
|
+
/** `data` schema for the account welcome / onboarding email. */
|
|
51
60
|
const welcomeData = Schema.Struct({
|
|
52
61
|
firstName: Schema.NonEmptyString,
|
|
53
62
|
verifyUrl: Schema.optional(HttpUrl)
|
|
54
63
|
});
|
|
64
|
+
/** `data` schema for the password-reset email. */
|
|
55
65
|
const passwordResetData = Schema.Struct({
|
|
56
66
|
firstName: Schema.optional(Schema.String),
|
|
57
67
|
resetUrl: HttpUrl,
|
|
58
68
|
expiresInMinutes: Schema.optional(Schema.Number)
|
|
59
69
|
});
|
|
70
|
+
/** `data` schema for the generic notification / alert email. */
|
|
60
71
|
const notificationData = Schema.Struct({
|
|
61
72
|
title: Schema.NonEmptyString,
|
|
62
73
|
body: Schema.NonEmptyString,
|
|
@@ -69,6 +80,7 @@ const notificationData = Schema.Struct({
|
|
|
69
80
|
actionUrl: Schema.optional(HttpUrl),
|
|
70
81
|
actionLabel: Schema.optional(Schema.String)
|
|
71
82
|
});
|
|
83
|
+
/** `data` schema for the incident / dispatch alert email. */
|
|
72
84
|
const incidentAlertData = Schema.Struct({
|
|
73
85
|
incidentId: Schema.NonEmptyString,
|
|
74
86
|
title: Schema.NonEmptyString,
|
|
@@ -82,11 +94,13 @@ const incidentAlertData = Schema.Struct({
|
|
|
82
94
|
detectedAt: Schema.optional(Schema.String),
|
|
83
95
|
dashboardUrl: HttpUrl
|
|
84
96
|
});
|
|
97
|
+
/** `data` schema for the password-changed security notice. */
|
|
85
98
|
const passwordChangedData = Schema.Struct({
|
|
86
99
|
firstName: Schema.optional(Schema.String),
|
|
87
100
|
changedAt: Schema.optional(Schema.String),
|
|
88
101
|
secureAccountUrl: Schema.optional(HttpUrl)
|
|
89
102
|
});
|
|
103
|
+
/** `data` schema for the new-device sign-in security alert. */
|
|
90
104
|
const newDeviceLoginData = Schema.Struct({
|
|
91
105
|
firstName: Schema.optional(Schema.String),
|
|
92
106
|
device: Schema.optional(Schema.String),
|
|
@@ -95,6 +109,7 @@ const newDeviceLoginData = Schema.Struct({
|
|
|
95
109
|
at: Schema.optional(Schema.String),
|
|
96
110
|
secureAccountUrl: Schema.optional(HttpUrl)
|
|
97
111
|
});
|
|
112
|
+
/** `data` schema for the mission-approval sign-off request. */
|
|
98
113
|
const missionApprovalData = Schema.Struct({
|
|
99
114
|
missionId: Schema.NonEmptyString,
|
|
100
115
|
title: Schema.NonEmptyString,
|
|
@@ -108,6 +123,7 @@ const missionApprovalData = Schema.Struct({
|
|
|
108
123
|
approveUrl: HttpUrl,
|
|
109
124
|
expiresInMinutes: Schema.optional(Schema.Number)
|
|
110
125
|
});
|
|
126
|
+
/** `data` schema for the organization / team invitation email. */
|
|
111
127
|
const orgInvitationData = Schema.Struct({
|
|
112
128
|
orgName: Schema.NonEmptyString,
|
|
113
129
|
inviterName: Schema.optional(Schema.String),
|
package/lib/schemas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.js","names":["S"],"sources":["../src/schemas.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Schema as S } from \"effect\";\n\n/**\n * A non-empty, absolute http(s) URL. Validated with a pattern check (blocks\n * `javascript:`/relative hrefs) but decoded as a plain `string`, so template\n * props stay ergonomic. Exported for consumers building their own templates.\n */\nexport const HttpUrl = S.String.check(S.isPattern(/^https?:\\/\\/\\S+$/i));\n\n/**\n * A single, syntactically-valid recipient email address (branded).\n *\n * The pattern mirrors `@resq-systems/security`'s `EmailSchema` — one `@`, a\n * dotted domain, and a 2+ character TLD, or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Because\n * the character classes admit no whitespace or control characters and the check\n * is anchored (`^…$`), it also rejects the CR/LF that underpins SMTP header\n * injection: a `to` smuggling `\"…\\r\\nBcc: attacker@evil\"` into a provider that\n * concatenates headers is a type-*and*-runtime error at the boundary, not a\n * silent extra recipient.\n *\n * The {@link EmailAddress} brand marks a string that has cleared this check, so\n * a validated address is not interchangeable with a raw `string` downstream\n * (the `to` field on the decoded mailer payload and the rendered email).\n */\nexport const EmailAddress = S.String.check(\n\tS.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/),\n).pipe(S.brand(\"EmailAddress\"));\n\n/** A recipient address that has passed {@link EmailAddress} validation. */\nexport type EmailAddress = typeof EmailAddress.Type;\n\n/**\n * Compliance class of an email send. `transactional` skips unsubscribe UI;\n * `marketing` requires an unsubscribe affordance in the legal footer.\n */\nexport const emailCategory = S.Literals([\"transactional\", \"marketing\"]);\nexport type EmailCategory = typeof emailCategory.Type;\n\nexport const otpData = S.Struct({\n\tcode: S.NonEmptyString,\n\tfirstName: S.optional(S.String),\n\texpiresInMinutes: S.optional(S.Number),\n});\n\nexport const welcomeData = S.Struct({\n\tfirstName: S.NonEmptyString,\n\tverifyUrl: S.optional(HttpUrl),\n});\n\nexport const passwordResetData = S.Struct({\n\tfirstName: S.optional(S.String),\n\tresetUrl: HttpUrl,\n\texpiresInMinutes: S.optional(S.Number),\n});\n\nexport const notificationData = S.Struct({\n\ttitle: S.NonEmptyString,\n\tbody: S.NonEmptyString,\n\tseverity: S.optional(S.Literals([\"info\", \"success\", \"warning\", \"error\"])),\n\tactionUrl: S.optional(HttpUrl),\n\tactionLabel: S.optional(S.String),\n});\n\nexport const incidentAlertData = S.Struct({\n\tincidentId: S.NonEmptyString,\n\ttitle: S.NonEmptyString,\n\tseverity: S.Literals([\"info\", \"warning\", \"critical\"]),\n\tsummary: S.NonEmptyString,\n\tlocation: S.optional(S.String),\n\t// Preformatted timestamp string (caller formats/localizes) to avoid\n\t// timezone/format ambiguity inside the email.\n\tdetectedAt: S.optional(S.String),\n\tdashboardUrl: HttpUrl,\n});\n\nexport const passwordChangedData = S.Struct({\n\tfirstName: S.optional(S.String),\n\t// Preformatted timestamp string (caller formats/localizes).\n\tchangedAt: S.optional(S.String),\n\t// Where to secure the account if the change was not authorized.\n\tsecureAccountUrl: S.optional(HttpUrl),\n});\n\nexport const newDeviceLoginData = S.Struct({\n\tfirstName: S.optional(S.String),\n\t// Human-readable client, e.g. \"Chrome on macOS\".\n\tdevice: S.optional(S.String),\n\t// Human-readable place, e.g. \"Newark, DE, USA\".\n\tlocation: S.optional(S.String),\n\tipAddress: S.optional(S.String),\n\t// Preformatted timestamp string (caller formats/localizes).\n\tat: S.optional(S.String),\n\t// Where to review activity / secure the account if the sign-in was not the recipient.\n\tsecureAccountUrl: S.optional(HttpUrl),\n});\n\nexport const missionApprovalData = S.Struct({\n\tmissionId: S.NonEmptyString,\n\ttitle: S.NonEmptyString,\n\tsummary: S.optional(S.String),\n\trequestedBy: S.optional(S.String),\n\tseverity: S.optional(S.Literals([\"info\", \"warning\", \"critical\"])),\n\t// Approver sign-off link (e.g. the HCE mission-approval route).\n\tapproveUrl: HttpUrl,\n\texpiresInMinutes: S.optional(S.Number),\n});\n\nexport const orgInvitationData = S.Struct({\n\torgName: S.NonEmptyString,\n\tinviterName: S.optional(S.String),\n\torgRole: S.optional(S.String),\n\tacceptUrl: HttpUrl,\n\texpiresInDays: S.optional(S.Number),\n});\n\n/** Inferred `data` types for the built-in templates (used for component props). */\nexport type OtpData = typeof otpData.Type;\nexport type WelcomeData = typeof welcomeData.Type;\nexport type PasswordResetData = typeof passwordResetData.Type;\nexport type NotificationData = typeof notificationData.Type;\nexport type IncidentAlertData = typeof incidentAlertData.Type;\nexport type PasswordChangedData = typeof passwordChangedData.Type;\nexport type NewDeviceLoginData = typeof newDeviceLoginData.Type;\nexport type MissionApprovalData = typeof missionApprovalData.Type;\nexport type OrgInvitationData = typeof orgInvitationData.Type;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"schemas.js","names":["S"],"sources":["../src/schemas.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Effect Schema building blocks for email payloads — validated URL and\n * recipient primitives, the send-category literal, and each built-in template's\n * `data` struct with its inferred type.\n *\n * @module @resq-systems/email-templates/schemas\n */\n\nimport { Schema as S } from \"effect\";\n\n//#region Validation primitives\n\n/**\n * A non-empty, absolute http(s) URL. Validated with a pattern check (blocks\n * `javascript:`/relative hrefs) but decoded as a plain `string`, so template\n * props stay ergonomic. Exported for consumers building their own templates.\n */\nexport const HttpUrl = S.String.check(S.isPattern(/^https?:\\/\\/\\S+$/i));\n\n/**\n * A single, syntactically-valid recipient email address (branded).\n *\n * The pattern mirrors `@resq-systems/security`'s `EmailSchema` — one `@`, a\n * dotted domain, and a 2+ character TLD, or a Punycode/IDN `xn--…` TLD (e.g.\n * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Because\n * the character classes admit no whitespace or control characters and the check\n * is anchored (`^…$`), it also rejects the CR/LF that underpins SMTP header\n * injection: a `to` smuggling `\"…\\r\\nBcc: attacker@evil\"` into a provider that\n * concatenates headers is a type-*and*-runtime error at the boundary, not a\n * silent extra recipient.\n *\n * The {@link EmailAddress} brand marks a string that has cleared this check, so\n * a validated address is not interchangeable with a raw `string` downstream\n * (the `to` field on the decoded mailer payload and the rendered email).\n */\nexport const EmailAddress = S.String.check(\n\tS.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/),\n).pipe(S.brand(\"EmailAddress\"));\n\n/** A recipient address that has passed {@link EmailAddress} validation. */\nexport type EmailAddress = typeof EmailAddress.Type;\n\n/**\n * Compliance class of an email send. `transactional` skips unsubscribe UI;\n * `marketing` requires an unsubscribe affordance in the legal footer.\n */\nexport const emailCategory = S.Literals([\"transactional\", \"marketing\"]);\nexport type EmailCategory = typeof emailCategory.Type;\n\n//#endregion\n\n//#region Schemas\n\n/** `data` schema for the one-time-code (OTP) email. */\nexport const otpData = S.Struct({\n\tcode: S.NonEmptyString,\n\tfirstName: S.optional(S.String),\n\texpiresInMinutes: S.optional(S.Number),\n});\n\n/** `data` schema for the account welcome / onboarding email. */\nexport const welcomeData = S.Struct({\n\tfirstName: S.NonEmptyString,\n\tverifyUrl: S.optional(HttpUrl),\n});\n\n/** `data` schema for the password-reset email. */\nexport const passwordResetData = S.Struct({\n\tfirstName: S.optional(S.String),\n\tresetUrl: HttpUrl,\n\texpiresInMinutes: S.optional(S.Number),\n});\n\n/** `data` schema for the generic notification / alert email. */\nexport const notificationData = S.Struct({\n\ttitle: S.NonEmptyString,\n\tbody: S.NonEmptyString,\n\tseverity: S.optional(S.Literals([\"info\", \"success\", \"warning\", \"error\"])),\n\tactionUrl: S.optional(HttpUrl),\n\tactionLabel: S.optional(S.String),\n});\n\n/** `data` schema for the incident / dispatch alert email. */\nexport const incidentAlertData = S.Struct({\n\tincidentId: S.NonEmptyString,\n\ttitle: S.NonEmptyString,\n\tseverity: S.Literals([\"info\", \"warning\", \"critical\"]),\n\tsummary: S.NonEmptyString,\n\tlocation: S.optional(S.String),\n\t// Preformatted timestamp string (caller formats/localizes) to avoid\n\t// timezone/format ambiguity inside the email.\n\tdetectedAt: S.optional(S.String),\n\tdashboardUrl: HttpUrl,\n});\n\n/** `data` schema for the password-changed security notice. */\nexport const passwordChangedData = S.Struct({\n\tfirstName: S.optional(S.String),\n\t// Preformatted timestamp string (caller formats/localizes).\n\tchangedAt: S.optional(S.String),\n\t// Where to secure the account if the change was not authorized.\n\tsecureAccountUrl: S.optional(HttpUrl),\n});\n\n/** `data` schema for the new-device sign-in security alert. */\nexport const newDeviceLoginData = S.Struct({\n\tfirstName: S.optional(S.String),\n\t// Human-readable client, e.g. \"Chrome on macOS\".\n\tdevice: S.optional(S.String),\n\t// Human-readable place, e.g. \"Newark, DE, USA\".\n\tlocation: S.optional(S.String),\n\tipAddress: S.optional(S.String),\n\t// Preformatted timestamp string (caller formats/localizes).\n\tat: S.optional(S.String),\n\t// Where to review activity / secure the account if the sign-in was not the recipient.\n\tsecureAccountUrl: S.optional(HttpUrl),\n});\n\n/** `data` schema for the mission-approval sign-off request. */\nexport const missionApprovalData = S.Struct({\n\tmissionId: S.NonEmptyString,\n\ttitle: S.NonEmptyString,\n\tsummary: S.optional(S.String),\n\trequestedBy: S.optional(S.String),\n\tseverity: S.optional(S.Literals([\"info\", \"warning\", \"critical\"])),\n\t// Approver sign-off link (e.g. the HCE mission-approval route).\n\tapproveUrl: HttpUrl,\n\texpiresInMinutes: S.optional(S.Number),\n});\n\n/** `data` schema for the organization / team invitation email. */\nexport const orgInvitationData = S.Struct({\n\torgName: S.NonEmptyString,\n\tinviterName: S.optional(S.String),\n\torgRole: S.optional(S.String),\n\tacceptUrl: HttpUrl,\n\texpiresInDays: S.optional(S.Number),\n});\n\n//#endregion\n\n//#region Types\n\n/** Inferred `data` types for the built-in templates (used for component props). */\nexport type OtpData = typeof otpData.Type;\nexport type WelcomeData = typeof welcomeData.Type;\nexport type PasswordResetData = typeof passwordResetData.Type;\nexport type NotificationData = typeof notificationData.Type;\nexport type IncidentAlertData = typeof incidentAlertData.Type;\nexport type PasswordChangedData = typeof passwordChangedData.Type;\nexport type NewDeviceLoginData = typeof newDeviceLoginData.Type;\nexport type MissionApprovalData = typeof missionApprovalData.Type;\nexport type OrgInvitationData = typeof orgInvitationData.Type;\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAa,UAAUA,OAAE,OAAO,MAAMA,OAAE,UAAU,mBAAmB,CAAC;;;;;;;;;;;;;;;;;AAkBtE,MAAa,eAAeA,OAAE,OAAO,MACpCA,OAAE,UAAU,wEAAwE,CACrF,CAAC,CAAC,KAAKA,OAAE,MAAM,cAAc,CAAC;;;;;AAS9B,MAAa,gBAAgBA,OAAE,SAAS,CAAC,iBAAiB,WAAW,CAAC;;AAQtE,MAAa,UAAUA,OAAE,OAAO;CAC/B,MAAMA,OAAE;CACR,WAAWA,OAAE,SAASA,OAAE,MAAM;CAC9B,kBAAkBA,OAAE,SAASA,OAAE,MAAM;AACtC,CAAC;;AAGD,MAAa,cAAcA,OAAE,OAAO;CACnC,WAAWA,OAAE;CACb,WAAWA,OAAE,SAAS,OAAO;AAC9B,CAAC;;AAGD,MAAa,oBAAoBA,OAAE,OAAO;CACzC,WAAWA,OAAE,SAASA,OAAE,MAAM;CAC9B,UAAU;CACV,kBAAkBA,OAAE,SAASA,OAAE,MAAM;AACtC,CAAC;;AAGD,MAAa,mBAAmBA,OAAE,OAAO;CACxC,OAAOA,OAAE;CACT,MAAMA,OAAE;CACR,UAAUA,OAAE,SAASA,OAAE,SAAS;EAAC;EAAQ;EAAW;EAAW;CAAO,CAAC,CAAC;CACxE,WAAWA,OAAE,SAAS,OAAO;CAC7B,aAAaA,OAAE,SAASA,OAAE,MAAM;AACjC,CAAC;;AAGD,MAAa,oBAAoBA,OAAE,OAAO;CACzC,YAAYA,OAAE;CACd,OAAOA,OAAE;CACT,UAAUA,OAAE,SAAS;EAAC;EAAQ;EAAW;CAAU,CAAC;CACpD,SAASA,OAAE;CACX,UAAUA,OAAE,SAASA,OAAE,MAAM;CAG7B,YAAYA,OAAE,SAASA,OAAE,MAAM;CAC/B,cAAc;AACf,CAAC;;AAGD,MAAa,sBAAsBA,OAAE,OAAO;CAC3C,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,kBAAkBA,OAAE,SAAS,OAAO;AACrC,CAAC;;AAGD,MAAa,qBAAqBA,OAAE,OAAO;CAC1C,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,QAAQA,OAAE,SAASA,OAAE,MAAM;CAE3B,UAAUA,OAAE,SAASA,OAAE,MAAM;CAC7B,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,IAAIA,OAAE,SAASA,OAAE,MAAM;CAEvB,kBAAkBA,OAAE,SAAS,OAAO;AACrC,CAAC;;AAGD,MAAa,sBAAsBA,OAAE,OAAO;CAC3C,WAAWA,OAAE;CACb,OAAOA,OAAE;CACT,SAASA,OAAE,SAASA,OAAE,MAAM;CAC5B,aAAaA,OAAE,SAASA,OAAE,MAAM;CAChC,UAAUA,OAAE,SAASA,OAAE,SAAS;EAAC;EAAQ;EAAW;CAAU,CAAC,CAAC;CAEhE,YAAY;CACZ,kBAAkBA,OAAE,SAASA,OAAE,MAAM;AACtC,CAAC;;AAGD,MAAa,oBAAoBA,OAAE,OAAO;CACzC,SAASA,OAAE;CACX,aAAaA,OAAE,SAASA,OAAE,MAAM;CAChC,SAASA,OAAE,SAASA,OAAE,MAAM;CAC5B,WAAW;CACX,eAAeA,OAAE,SAASA,OAAE,MAAM;AACnC,CAAC"}
|
package/lib/send/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { EmailSender, SendEmailInput, SendResult } from "./sender.js";
|
|
2
2
|
import { createResendSender } from "./resend-sender.js";
|
|
3
3
|
import { SendEmailOptions, sendEmail } from "./send-email.js";
|
|
4
|
-
export { EmailSender, SendEmailInput, SendEmailOptions, SendResult, createResendSender, sendEmail };
|
|
4
|
+
export { type EmailSender, type SendEmailInput, type SendEmailOptions, type SendResult, createResendSender, sendEmail };
|
|
@@ -7,6 +7,17 @@ import { EmailSender } from "./sender.js";
|
|
|
7
7
|
* The key is read from the explicit argument or the `RESEND_API_KEY` env var and
|
|
8
8
|
* validated up front (fail fast). Resend returns `{ data, error }` for API-level
|
|
9
9
|
* failures rather than throwing, so we branch on `error` instead of try/catch.
|
|
10
|
+
*
|
|
11
|
+
* Reads `process.env.RESEND_API_KEY` when no key is passed, and constructs a
|
|
12
|
+
* Resend HTTP client. The returned `send` upholds the never-throws contract of
|
|
13
|
+
* {@link EmailSender.send}: API errors, an empty response, and transport-level
|
|
14
|
+
* throws (fetch rejection, DNS, aborted request) all become `{ ok: false, error }`
|
|
15
|
+
* with a distinguishing `error.name` (the Resend error name, `"unknown_error"`,
|
|
16
|
+
* or `"transport_error"`). It does not honour an `AbortSignal`.
|
|
17
|
+
*
|
|
18
|
+
* @param apiKey - Resend API key; defaults to `process.env.RESEND_API_KEY`.
|
|
19
|
+
* @returns An {@link EmailSender} that delivers through Resend.
|
|
20
|
+
* @throws {Error} If no API key is available from the argument or the environment.
|
|
10
21
|
*/
|
|
11
22
|
declare function createResendSender(apiKey?: string | undefined): EmailSender;
|
|
12
23
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resend-sender.d.ts","names":[],"sources":["../../src/send/resend-sender.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"resend-sender.d.ts","names":[],"sources":["../../src/send/resend-sender.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBA6CgB,mBACf,8BACE"}
|
|
@@ -16,12 +16,29 @@ import { Resend } from "resend";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
/**
|
|
19
|
+
* @fileoverview Resend adapter implementing the `EmailSender` port — server-only,
|
|
20
|
+
* normalizes API and transport failures into a `SendResult` instead of throwing.
|
|
21
|
+
*
|
|
22
|
+
* @module @resq-systems/email-templates/send/resend-sender
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
19
25
|
* Create an {@link EmailSender} backed by Resend.
|
|
20
26
|
*
|
|
21
27
|
* Server-only — never import this into a browser bundle or the API key leaks.
|
|
22
28
|
* The key is read from the explicit argument or the `RESEND_API_KEY` env var and
|
|
23
29
|
* validated up front (fail fast). Resend returns `{ data, error }` for API-level
|
|
24
30
|
* failures rather than throwing, so we branch on `error` instead of try/catch.
|
|
31
|
+
*
|
|
32
|
+
* Reads `process.env.RESEND_API_KEY` when no key is passed, and constructs a
|
|
33
|
+
* Resend HTTP client. The returned `send` upholds the never-throws contract of
|
|
34
|
+
* {@link EmailSender.send}: API errors, an empty response, and transport-level
|
|
35
|
+
* throws (fetch rejection, DNS, aborted request) all become `{ ok: false, error }`
|
|
36
|
+
* with a distinguishing `error.name` (the Resend error name, `"unknown_error"`,
|
|
37
|
+
* or `"transport_error"`). It does not honour an `AbortSignal`.
|
|
38
|
+
*
|
|
39
|
+
* @param apiKey - Resend API key; defaults to `process.env.RESEND_API_KEY`.
|
|
40
|
+
* @returns An {@link EmailSender} that delivers through Resend.
|
|
41
|
+
* @throws {Error} If no API key is available from the argument or the environment.
|
|
25
42
|
*/
|
|
26
43
|
function createResendSender(apiKey = process.env.RESEND_API_KEY) {
|
|
27
44
|
if (!apiKey) throw new Error("RESEND_API_KEY is required to create a Resend sender");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resend-sender.js","names":[],"sources":["../../src/send/resend-sender.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Resend } from \"resend\";\nimport type { EmailSender, SendEmailInput, SendResult } from \"./sender.js\";\n\n/**\n * Create an {@link EmailSender} backed by Resend.\n *\n * Server-only — never import this into a browser bundle or the API key leaks.\n * The key is read from the explicit argument or the `RESEND_API_KEY` env var and\n * validated up front (fail fast). Resend returns `{ data, error }` for API-level\n * failures rather than throwing, so we branch on `error` instead of try/catch.\n */\nexport function createResendSender(\n\tapiKey: string | undefined = process.env.RESEND_API_KEY,\n): EmailSender {\n\tif (!apiKey) {\n\t\tthrow new Error(\"RESEND_API_KEY is required to create a Resend sender\");\n\t}\n\n\tconst resend = new Resend(apiKey);\n\n\treturn {\n\t\tasync send(input: SendEmailInput): Promise<SendResult> {\n\t\t\t// `to`/`html`/`text` are always populated on the pipeline path (see\n\t\t\t// renderEmail); the localized cast keeps Resend's html|text|react union\n\t\t\t// happy without leaking `any`.\n\t\t\tconst payload = {\n\t\t\t\tfrom: input.from,\n\t\t\t\tto: input.to,\n\t\t\t\tsubject: input.subject,\n\t\t\t\thtml: input.html,\n\t\t\t\ttext: input.text,\n\t\t\t\treplyTo: input.replyTo,\n\t\t\t\tcc: input.cc,\n\t\t\t\tbcc: input.bcc,\n\t\t\t\theaders: input.headers,\n\t\t\t} as Parameters<typeof resend.emails.send>[0];\n\n\t\t\tconst options = input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : undefined;\n\n\t\t\ttry {\n\t\t\t\tconst { data, error } = await resend.emails.send(payload, options);\n\n\t\t\t\tif (error) {\n\t\t\t\t\treturn { ok: false, error: { name: error.name, message: error.message } };\n\t\t\t\t}\n\n\t\t\t\tif (!data) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\terror: { name: \"unknown_error\", message: \"Resend returned no data and no error\" },\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\treturn { ok: true, id: data.id };\n\t\t\t} catch (err) {\n\t\t\t\t// The SDK returns { data, error } for API errors, but can still THROW\n\t\t\t\t// on transport failures (fetch rejection, DNS, aborted request), so the\n\t\t\t\t// whole call is wrapped to honor the never-throws contract of send().\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tname: \"transport_error\",\n\t\t\t\t\t\tmessage: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t};\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"resend-sender.js","names":[],"sources":["../../src/send/resend-sender.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Resend adapter implementing the `EmailSender` port — server-only,\n * normalizes API and transport failures into a `SendResult` instead of throwing.\n *\n * @module @resq-systems/email-templates/send/resend-sender\n */\n\nimport { Resend } from \"resend\";\nimport type { EmailSender, SendEmailInput, SendResult } from \"./sender.js\";\n\n/**\n * Create an {@link EmailSender} backed by Resend.\n *\n * Server-only — never import this into a browser bundle or the API key leaks.\n * The key is read from the explicit argument or the `RESEND_API_KEY` env var and\n * validated up front (fail fast). Resend returns `{ data, error }` for API-level\n * failures rather than throwing, so we branch on `error` instead of try/catch.\n *\n * Reads `process.env.RESEND_API_KEY` when no key is passed, and constructs a\n * Resend HTTP client. The returned `send` upholds the never-throws contract of\n * {@link EmailSender.send}: API errors, an empty response, and transport-level\n * throws (fetch rejection, DNS, aborted request) all become `{ ok: false, error }`\n * with a distinguishing `error.name` (the Resend error name, `\"unknown_error\"`,\n * or `\"transport_error\"`). It does not honour an `AbortSignal`.\n *\n * @param apiKey - Resend API key; defaults to `process.env.RESEND_API_KEY`.\n * @returns An {@link EmailSender} that delivers through Resend.\n * @throws {Error} If no API key is available from the argument or the environment.\n */\nexport function createResendSender(\n\tapiKey: string | undefined = process.env.RESEND_API_KEY,\n): EmailSender {\n\tif (!apiKey) {\n\t\tthrow new Error(\"RESEND_API_KEY is required to create a Resend sender\");\n\t}\n\n\tconst resend = new Resend(apiKey);\n\n\treturn {\n\t\tasync send(input: SendEmailInput): Promise<SendResult> {\n\t\t\t// `to`/`html`/`text` are always populated on the pipeline path (see\n\t\t\t// renderEmail); the localized cast keeps Resend's html|text|react union\n\t\t\t// happy without leaking `any`.\n\t\t\tconst payload = {\n\t\t\t\tfrom: input.from,\n\t\t\t\tto: input.to,\n\t\t\t\tsubject: input.subject,\n\t\t\t\thtml: input.html,\n\t\t\t\ttext: input.text,\n\t\t\t\treplyTo: input.replyTo,\n\t\t\t\tcc: input.cc,\n\t\t\t\tbcc: input.bcc,\n\t\t\t\theaders: input.headers,\n\t\t\t} as Parameters<typeof resend.emails.send>[0];\n\n\t\t\tconst options = input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : undefined;\n\n\t\t\ttry {\n\t\t\t\tconst { data, error } = await resend.emails.send(payload, options);\n\n\t\t\t\tif (error) {\n\t\t\t\t\treturn { ok: false, error: { name: error.name, message: error.message } };\n\t\t\t\t}\n\n\t\t\t\tif (!data) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\terror: { name: \"unknown_error\", message: \"Resend returned no data and no error\" },\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\treturn { ok: true, id: data.id };\n\t\t\t} catch (err) {\n\t\t\t\t// The SDK returns { data, error } for API errors, but can still THROW\n\t\t\t\t// on transport failures (fetch rejection, DNS, aborted request), so the\n\t\t\t\t// whole call is wrapped to honor the never-throws contract of send().\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tname: \"transport_error\",\n\t\t\t\t\t\tmessage: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,mBACf,SAA6B,QAAQ,IAAI,gBAC3B;CACd,IAAI,CAAC,QACJ,MAAM,IAAI,MAAM,sDAAsD;CAGvE,MAAM,SAAS,IAAI,OAAO,MAAM;CAEhC,OAAO,EACN,MAAM,KAAK,OAA4C;EAItD,MAAM,UAAU;GACf,MAAM,MAAM;GACZ,IAAI,MAAM;GACV,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,IAAI,MAAM;GACV,KAAK,MAAM;GACX,SAAS,MAAM;EAChB;EAEA,MAAM,UAAU,MAAM,iBAAiB,EAAE,gBAAgB,MAAM,eAAe,IAAI,KAAA;EAElF,IAAI;GACH,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,OAAO,KAAK,SAAS,OAAO;GAEjE,IAAI,OACH,OAAO;IAAE,IAAI;IAAO,OAAO;KAAE,MAAM,MAAM;KAAM,SAAS,MAAM;IAAQ;GAAE;GAGzE,IAAI,CAAC,MACJ,OAAO;IACN,IAAI;IACJ,OAAO;KAAE,MAAM;KAAiB,SAAS;IAAuC;GACjF;GAGD,OAAO;IAAE,IAAI;IAAM,IAAI,KAAK;GAAG;EAChC,SAAS,KAAK;GAIb,OAAO;IACN,IAAI;IACJ,OAAO;KACN,MAAM;KACN,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;IACzD;GACD;EACD;CACD,EACD;AACD"}
|
package/lib/send/send-email.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { EmailSender, SendResult } from "./sender.js";
|
|
2
2
|
//#region src/send/send-email.d.ts
|
|
3
|
+
/** Delivery options for {@link sendEmail} — everything the render step can't supply. */
|
|
3
4
|
interface SendEmailOptions {
|
|
4
5
|
/** Verified sender address, e.g. "ResQ Systems <updates@send.resq.software>". */
|
|
5
6
|
from: string;
|
|
7
|
+
/** Reply-To address(es); absent means the provider default applies. */
|
|
6
8
|
replyTo?: string | string[];
|
|
7
9
|
/** Stable key so Resend de-dupes identical sends for 24h. */
|
|
8
10
|
idempotencyKey?: string;
|
|
@@ -12,6 +14,25 @@ interface SendEmailOptions {
|
|
|
12
14
|
/**
|
|
13
15
|
* Render a validated payload and hand it to a sender in one call. Convenience
|
|
14
16
|
* wiring for the common pipeline case: `sendEmail(sender, payload, { from })`.
|
|
17
|
+
*
|
|
18
|
+
* Never throws or rejects: every failure — an invalid payload, a render error, or
|
|
19
|
+
* a throwing/failing sender — is normalized into a `{ ok: false, error }`
|
|
20
|
+
* {@link SendResult}. Distinguish failures by `error.name`: `"EmailValidationError"`
|
|
21
|
+
* (payload failed schema validation), `"render_error"` (any other render failure),
|
|
22
|
+
* `"sender_error"` (the sender threw instead of returning a result), or whatever
|
|
23
|
+
* `name` the sender itself reports on a normal `{ ok: false }` return. The
|
|
24
|
+
* `idempotencyKey` is passed through to the sender and only takes effect insofar
|
|
25
|
+
* as the sender honours it.
|
|
26
|
+
*
|
|
27
|
+
* @param sender - The transport port that performs delivery.
|
|
28
|
+
* @param payload - Untrusted `{ name, to, data }` payload to validate and render.
|
|
29
|
+
* @param options - Delivery options; `from` is required.
|
|
30
|
+
* @returns A promise resolving to the send outcome — never a rejection.
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const result = await sendEmail(sender, badPayload, { from: "ResQ <ops@resq.software>" });
|
|
34
|
+
* result.ok; // → false
|
|
35
|
+
* ```
|
|
15
36
|
*/
|
|
16
37
|
declare function sendEmail(sender: EmailSender, payload: unknown, options: SendEmailOptions): Promise<SendResult>;
|
|
17
38
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-email.d.ts","names":[],"sources":["../../src/send/send-email.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"send-email.d.ts","names":[],"sources":["../../src/send/send-email.ts"],"mappings":";;;UA6BiB;;EAEhB;;EAEA;;EAEA;;EAEA,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BW,UACrB,QAAQ,aACR,kBACA,SAAS,mBACP,QAAQ"}
|