@resq-systems/email-templates 0.3.0 → 0.4.0

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 CHANGED
@@ -1,3 +1,4 @@
1
+ import { EmailAddress } from "./schemas.js";
1
2
  import { EmailValidationError } from "./mailer.js";
2
3
  import { resqMailer } from "./suite.js";
3
4
  //#region src/contract.d.ts
@@ -17,7 +18,7 @@ type EmailTemplateData<N extends EmailName> = Extract<EmailPayload, {
17
18
  */
18
19
  declare const decodeEmailPayload: (input: unknown) => {
19
20
  readonly name: "incident-alert";
20
- readonly to: string;
21
+ readonly to: EmailAddress;
21
22
  readonly data: {
22
23
  readonly incidentId: string;
23
24
  readonly title: string;
@@ -31,7 +32,7 @@ declare const decodeEmailPayload: (input: unknown) => {
31
32
  readonly unsubscribeUrl?: string;
32
33
  } | {
33
34
  readonly name: "mission-approval";
34
- readonly to: string;
35
+ readonly to: EmailAddress;
35
36
  readonly data: {
36
37
  readonly missionId: string;
37
38
  readonly title: string;
@@ -45,7 +46,7 @@ declare const decodeEmailPayload: (input: unknown) => {
45
46
  readonly unsubscribeUrl?: string;
46
47
  } | {
47
48
  readonly name: "new-device-login";
48
- readonly to: string;
49
+ readonly to: EmailAddress;
49
50
  readonly data: {
50
51
  readonly firstName?: string | undefined;
51
52
  readonly device?: string | undefined;
@@ -58,7 +59,7 @@ declare const decodeEmailPayload: (input: unknown) => {
58
59
  readonly unsubscribeUrl?: string;
59
60
  } | {
60
61
  readonly name: "notification";
61
- readonly to: string;
62
+ readonly to: EmailAddress;
62
63
  readonly data: {
63
64
  readonly title: string;
64
65
  readonly body: string;
@@ -70,7 +71,7 @@ declare const decodeEmailPayload: (input: unknown) => {
70
71
  readonly unsubscribeUrl?: string;
71
72
  } | {
72
73
  readonly name: "org-invitation";
73
- readonly to: string;
74
+ readonly to: EmailAddress;
74
75
  readonly data: {
75
76
  readonly orgName: string;
76
77
  readonly inviterName?: string | undefined;
@@ -82,7 +83,7 @@ declare const decodeEmailPayload: (input: unknown) => {
82
83
  readonly unsubscribeUrl?: string;
83
84
  } | {
84
85
  readonly name: "otp";
85
- readonly to: string;
86
+ readonly to: EmailAddress;
86
87
  readonly data: {
87
88
  readonly code: string;
88
89
  readonly firstName?: string | undefined;
@@ -92,7 +93,7 @@ declare const decodeEmailPayload: (input: unknown) => {
92
93
  readonly unsubscribeUrl?: string;
93
94
  } | {
94
95
  readonly name: "password-changed";
95
- readonly to: string;
96
+ readonly to: EmailAddress;
96
97
  readonly data: {
97
98
  readonly firstName?: string | undefined;
98
99
  readonly changedAt?: string | undefined;
@@ -102,7 +103,7 @@ declare const decodeEmailPayload: (input: unknown) => {
102
103
  readonly unsubscribeUrl?: string;
103
104
  } | {
104
105
  readonly name: "password-reset";
105
- readonly to: string;
106
+ readonly to: EmailAddress;
106
107
  readonly data: {
107
108
  readonly firstName?: string | undefined;
108
109
  readonly resetUrl: string;
@@ -112,7 +113,7 @@ declare const decodeEmailPayload: (input: unknown) => {
112
113
  readonly unsubscribeUrl?: string;
113
114
  } | {
114
115
  readonly name: "welcome";
115
- readonly to: string;
116
+ readonly to: EmailAddress;
116
117
  readonly data: {
117
118
  readonly firstName: string;
118
119
  readonly verifyUrl?: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","names":[],"sources":["../src/contract.ts"],"mappings":";;;;cAqBa,sCAAY;;KAGb,eAAe,kBAAkB,WAAW;;KAG5C,YAAY;;KAGZ,kBAAkB,UAAU,aAAa,QAAQ;EAAgB,MAAM;;;;;;cAMtE,qBAAkB"}
1
+ {"version":3,"file":"contract.d.ts","names":[],"sources":["../src/contract.ts"],"mappings":";;;;;cAqBa,sCAAY;;KAGb,eAAe,kBAAkB,WAAW;;KAG5C,YAAY;;KAGZ,kBAAkB,UAAU,aAAa,QAAQ;EAAgB,MAAM;;;;;;cAMtE,qBAAkB;;eAAlB"}
@@ -1,5 +1,5 @@
1
- import { TailwindConfig } from "../node_modules/@react-email/tailwind/dist/index.js";
2
1
  import { EmailCategory } from "../schemas.js";
2
+ import { TailwindConfig } from "../node_modules/@react-email/tailwind/dist/index.js";
3
3
  import { ReactElement } from "react";
4
4
  //#region src/emails/theme.d.ts
5
5
  interface EmailThemeFonts {
package/lib/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { EmailCategory, HttpUrl, IncidentAlertData, MissionApprovalData, NewDeviceLoginData, NotificationData, OrgInvitationData, OtpData, PasswordChangedData, PasswordResetData, WelcomeData, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData } from "./schemas.js";
1
+ import { EmailAddress, EmailCategory, HttpUrl, IncidentAlertData, MissionApprovalData, NewDeviceLoginData, NotificationData, OrgInvitationData, OtpData, PasswordChangedData, PasswordResetData, WelcomeData, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData } from "./schemas.js";
2
2
  import { EmailMessage, EmailMessageContext, EmailOrgIdentity, EmailTheme, EmailThemeContext, EmailThemeFonts, EmailThemeOverride, buildTailwindConfig, defaultEmailTheme, mergeEmailTheme, resolveEmailTheme, withEmailMessage, withEmailTheme } from "./emails/theme.js";
3
3
  import { EmailRegistryEntry, EmailTemplateDef, EmailValidationError, Mailer, MailerPayload, MailerTemplateData, RenderEmailOptions, RenderedEmail, createMailer, defineEmailTemplate } from "./mailer.js";
4
4
  import { EmailName, EmailPayload, EmailTemplateData, decodeEmailPayload } from "./contract.js";
5
5
  import { registry } from "./registry.js";
6
6
  import { renderEmail } from "./render.js";
7
7
  import { incidentAlertTemplate, missionApprovalTemplate, newDeviceLoginTemplate, notificationTemplate, orgInvitationTemplate, otpTemplate, passwordChangedTemplate, passwordResetTemplate, resqEmailTemplates, welcomeTemplate } from "./templates.js";
8
- export { EmailCategory, EmailMessage, EmailMessageContext, EmailName, EmailOrgIdentity, EmailPayload, type EmailRegistryEntry, EmailTemplateData, type EmailTemplateDef, EmailTheme, EmailThemeContext, EmailThemeFonts, EmailThemeOverride, EmailValidationError, HttpUrl, IncidentAlertData, type Mailer, type MailerPayload, type MailerTemplateData, MissionApprovalData, NewDeviceLoginData, NotificationData, OrgInvitationData, OtpData, PasswordChangedData, PasswordResetData, type RenderEmailOptions, type RenderedEmail, WelcomeData, buildTailwindConfig, createMailer, decodeEmailPayload, defaultEmailTheme, defineEmailTemplate, emailCategory, incidentAlertData, incidentAlertTemplate, mergeEmailTheme, missionApprovalData, missionApprovalTemplate, newDeviceLoginData, newDeviceLoginTemplate, notificationData, notificationTemplate, orgInvitationData, orgInvitationTemplate, otpData, otpTemplate, passwordChangedData, passwordChangedTemplate, passwordResetData, passwordResetTemplate, registry, renderEmail, resolveEmailTheme, resqEmailTemplates, welcomeData, welcomeTemplate, withEmailMessage, withEmailTheme };
8
+ export { EmailAddress, EmailCategory, EmailMessage, EmailMessageContext, EmailName, EmailOrgIdentity, EmailPayload, type EmailRegistryEntry, EmailTemplateData, type EmailTemplateDef, EmailTheme, EmailThemeContext, EmailThemeFonts, EmailThemeOverride, EmailValidationError, HttpUrl, IncidentAlertData, type Mailer, type MailerPayload, type MailerTemplateData, MissionApprovalData, NewDeviceLoginData, NotificationData, OrgInvitationData, OtpData, PasswordChangedData, PasswordResetData, type RenderEmailOptions, type RenderedEmail, WelcomeData, buildTailwindConfig, createMailer, decodeEmailPayload, defaultEmailTheme, defineEmailTemplate, emailCategory, incidentAlertData, incidentAlertTemplate, mergeEmailTheme, missionApprovalData, missionApprovalTemplate, newDeviceLoginData, newDeviceLoginTemplate, notificationData, notificationTemplate, orgInvitationData, orgInvitationTemplate, otpData, otpTemplate, passwordChangedData, passwordChangedTemplate, passwordResetData, passwordResetTemplate, registry, renderEmail, resolveEmailTheme, resqEmailTemplates, welcomeData, welcomeTemplate, withEmailMessage, withEmailTheme };
package/lib/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { EmailMessageContext, EmailThemeContext, buildTailwindConfig, defaultEmailTheme, mergeEmailTheme, resolveEmailTheme, withEmailMessage, withEmailTheme } from "./emails/theme.js";
2
- import { HttpUrl, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData } from "./schemas.js";
2
+ import { EmailAddress, HttpUrl, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData } from "./schemas.js";
3
3
  import { EmailValidationError, createMailer, defineEmailTemplate } from "./mailer.js";
4
4
  import { incidentAlertTemplate, missionApprovalTemplate, newDeviceLoginTemplate, notificationTemplate, orgInvitationTemplate, otpTemplate, passwordChangedTemplate, passwordResetTemplate, resqEmailTemplates, welcomeTemplate } from "./templates.js";
5
5
  import { EmailPayload, decodeEmailPayload } from "./contract.js";
6
6
  import { registry } from "./registry.js";
7
7
  import { renderEmail } from "./render.js";
8
- export { EmailMessageContext, EmailPayload, EmailThemeContext, EmailValidationError, HttpUrl, buildTailwindConfig, createMailer, decodeEmailPayload, defaultEmailTheme, defineEmailTemplate, emailCategory, incidentAlertData, incidentAlertTemplate, mergeEmailTheme, missionApprovalData, missionApprovalTemplate, newDeviceLoginData, newDeviceLoginTemplate, notificationData, notificationTemplate, orgInvitationData, orgInvitationTemplate, otpData, otpTemplate, passwordChangedData, passwordChangedTemplate, passwordResetData, passwordResetTemplate, registry, renderEmail, resolveEmailTheme, resqEmailTemplates, welcomeData, welcomeTemplate, withEmailMessage, withEmailTheme };
8
+ export { EmailAddress, EmailMessageContext, EmailPayload, EmailThemeContext, EmailValidationError, HttpUrl, buildTailwindConfig, createMailer, decodeEmailPayload, defaultEmailTheme, defineEmailTemplate, emailCategory, incidentAlertData, incidentAlertTemplate, mergeEmailTheme, missionApprovalData, missionApprovalTemplate, newDeviceLoginData, newDeviceLoginTemplate, notificationData, notificationTemplate, orgInvitationData, orgInvitationTemplate, otpData, otpTemplate, passwordChangedData, passwordChangedTemplate, passwordResetData, passwordResetTemplate, registry, renderEmail, resolveEmailTheme, resqEmailTemplates, welcomeData, welcomeTemplate, withEmailMessage, withEmailTheme };
package/lib/mailer.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EmailAddress } from "./schemas.js";
1
2
  import { EmailThemeOverride } from "./emails/theme.js";
2
3
  import { Schema } from "effect";
3
4
  import { ReactElement } from "react";
@@ -26,7 +27,7 @@ interface AnyTemplateDef {
26
27
  /** The `{ name, to, data, category?, unsubscribeUrl? }` payload for a single template def. */
27
28
  type PayloadFor<Def> = Def extends EmailTemplateDef<infer Name, infer DataSchema> ? {
28
29
  readonly name: Name;
29
- readonly to: string;
30
+ readonly to: EmailAddress;
30
31
  readonly data: DataSchema["Type"];
31
32
  /** Compliance class for this send; defaults to `transactional`. */
32
33
  readonly category?: "transactional" | "marketing";
@@ -41,7 +42,8 @@ type MailerTemplateData<Defs extends readonly AnyTemplateDef[], Name extends Mai
41
42
  }>["data"];
42
43
  /** The rendered, provider-ready email. */
43
44
  interface RenderedEmail {
44
- to: string;
45
+ /** Validated recipient (branded {@link EmailAddress}), carried from decode. */
46
+ to: EmailAddress;
45
47
  subject: string;
46
48
  html: string;
47
49
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"mailer.d.ts","names":[],"sources":["../src/mailer.tsx"],"mappings":";;;;;UA8BiB,iBAAiB,qBAAqB,mBAAmB,OAAO;WACvE,MAAM;WACN,MAAM;WACN,UAAU,MAAM;WAChB,YAAY,MAAM,uBAAuB;;;iBAInC,0BAA0B,qBAAqB,mBAAmB,OAAO,KACxF,KAAK,iBAAiB,MAAM,cAC1B,iBAAiB,MAAM;;;;;;;UAUhB;WACA;WACA,MAAM,OAAO;WACb,UAAU;WACV,YAAY,gBAAgB;;;KAIjC,WAAW,OACf,YAAY,uBAAuB,YAAY;WAEnC,MAAM;WACN;WACA,MAAM;;WAEN;;WAEA;;;KAKD,cAAc,sBAAsB,oBAAoB,WAAW;;KAGnE,mBACX,sBAAsB,kBACtB,aAAa,cAAc,iBACxB,QAAQ,cAAc;EAAS,MAAM;;;UAGxB;EAChB;EACA;EACA;EACA;;;UAIgB;;EAEhB,QAAQ;;;cAII,6BAA6B;WACvB;;;UAIF;EAChB,UAAU;EACV,SAAS,kBAAkB;;;UAIX,OAChB;WAA2B;WAAuB;WAAqB;;;WAG9D,QAAQ,OAAO;;WAEf,UAAU,OAAO,iBAAiB;;WAElC,gBAAgB;;EAEzB,OAAO,iBAAiB;;EAExB,YAAY,gBAAgB,UAAU,qBAAqB,QAAQ;;;;;;;;iBASpD,mBAAmB,sBAAsB,kBACxD,MAAM,OACJ,OAAO,cAAc"}
1
+ {"version":3,"file":"mailer.d.ts","names":[],"sources":["../src/mailer.tsx"],"mappings":";;;;;;UAoCiB,iBAAiB,qBAAqB,mBAAmB,OAAO;WACvE,MAAM;WACN,MAAM;WACN,UAAU,MAAM;WAChB,YAAY,MAAM,uBAAuB;;;iBAInC,0BAA0B,qBAAqB,mBAAmB,OAAO,KACxF,KAAK,iBAAiB,MAAM,cAC1B,iBAAiB,MAAM;;;;;;;UAUhB;WACA;WACA,MAAM,OAAO;WACb,UAAU;WACV,YAAY,gBAAgB;;;KAIjC,WAAW,OACf,YAAY,uBAAuB,YAAY;WAEnC,MAAM;WACN,IAAI;WACJ,MAAM;;WAEN;;WAEA;;;KAKD,cAAc,sBAAsB,oBAAoB,WAAW;;KAGnE,mBACX,sBAAsB,kBACtB,aAAa,cAAc,iBACxB,QAAQ,cAAc;EAAS,MAAM;;;UAGxB;;EAEhB,IAAI;EACJ;EACA;EACA;;;UAIgB;;EAEhB,QAAQ;;;cAII,6BAA6B;WACvB;;;UAIF;EAChB,UAAU;EACV,SAAS,kBAAkB;;;UAIX,OAChB;WAA2B;WAAuB;WAAqB;;;WAG9D,QAAQ,OAAO;;WAEf,UAAU,OAAO,iBAAiB;;WAElC,gBAAgB;;EAEzB,OAAO,iBAAiB;;EAExB,YAAY,gBAAgB,UAAU,qBAAqB,QAAQ;;;;;;;;iBASpD,mBAAmB,sBAAsB,kBACxD,MAAM,OACJ,OAAO,cAAc"}
package/lib/mailer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { withEmailMessage, withEmailTheme } from "./emails/theme.js";
2
- import { HttpUrl, emailCategory } from "./schemas.js";
2
+ import { EmailAddress, HttpUrl, emailCategory } from "./schemas.js";
3
3
  import { render } from "@react-email/render";
4
4
  import { Cause, Exit, Schema } from "effect";
5
5
  //#region src/mailer.tsx
@@ -18,7 +18,13 @@ import { Cause, Exit, Schema } from "effect";
18
18
  * See the License for the specific language governing permissions and
19
19
  * limitations under the License.
20
20
  */
21
- const Recipient = Schema.String;
21
+ /**
22
+ * Recipient schema for every payload's `to`. Validated (not a bare
23
+ * `Schema.String`) so a malformed or header-injecting address is rejected at
24
+ * the decode boundary and the decoded `to` carries the {@link EmailAddress}
25
+ * brand all the way to the provider.
26
+ */
27
+ const Recipient = EmailAddress;
22
28
  /** Identity helper that infers and preserves a template def's literal types. */
23
29
  function defineEmailTemplate(def) {
24
30
  return def;
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 { HttpUrl, emailCategory } from \"./schemas.js\";\n\nconst Recipient = Schema.String;\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: string;\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\tto: string;\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\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);\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\t// Cast past the `Decoder` services constraint: the union's fields are concrete\n\t// schemas with no decoding services at runtime.\n\tconst decodeExit = Schema.decodeUnknownExit(\n\t\tschema as unknown as Parameters<typeof Schema.decodeUnknownExit>[0],\n\t);\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 as Payload;\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":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,YAAY,OAAO;;AAWzB,SAAgB,oBACf,KACqC;CACrC,OAAO;AACR;;AAqDA,IAAa,uBAAb,cAA0C,MAAM;CAC/C,OAAyB;AAC1B;;;;;;;AA8BA,SAAgB,aACf,MAC8B;CAG9B,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;CAIxC,MAAM,aAAa,OAAO,kBACzB,MACD;CAEA,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\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"}
package/lib/schemas.d.ts CHANGED
@@ -6,6 +6,25 @@ import { Schema } from "effect";
6
6
  * props stay ergonomic. Exported for consumers building their own templates.
7
7
  */
8
8
  declare const HttpUrl: Schema.String;
9
+ /**
10
+ * A single, syntactically-valid recipient email address (branded).
11
+ *
12
+ * The pattern mirrors `@resq-systems/security`'s `EmailSchema` — one `@`, a
13
+ * dotted domain, and a 2+ character TLD, or a Punycode/IDN `xn--…` TLD (e.g.
14
+ * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Because
15
+ * the character classes admit no whitespace or control characters and the check
16
+ * is anchored (`^…$`), it also rejects the CR/LF that underpins SMTP header
17
+ * injection: a `to` smuggling `"…\r\nBcc: attacker@evil"` into a provider that
18
+ * concatenates headers is a type-*and*-runtime error at the boundary, not a
19
+ * silent extra recipient.
20
+ *
21
+ * The {@link EmailAddress} brand marks a string that has cleared this check, so
22
+ * a validated address is not interchangeable with a raw `string` downstream
23
+ * (the `to` field on the decoded mailer payload and the rendered email).
24
+ */
25
+ declare const EmailAddress: Schema.brand<Schema.String, "EmailAddress">;
26
+ /** A recipient address that has passed {@link EmailAddress} validation. */
27
+ type EmailAddress = typeof EmailAddress.Type;
9
28
  /**
10
29
  * Compliance class of an email send. `transactional` skips unsubscribe UI;
11
30
  * `marketing` requires an unsubscribe affordance in the legal footer.
@@ -82,5 +101,5 @@ type NewDeviceLoginData = typeof newDeviceLoginData.Type;
82
101
  type MissionApprovalData = typeof missionApprovalData.Type;
83
102
  type OrgInvitationData = typeof orgInvitationData.Type;
84
103
  //#endregion
85
- export { EmailCategory, HttpUrl, IncidentAlertData, MissionApprovalData, NewDeviceLoginData, NotificationData, OrgInvitationData, OtpData, PasswordChangedData, PasswordResetData, WelcomeData, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData };
104
+ export { EmailAddress, EmailCategory, HttpUrl, IncidentAlertData, MissionApprovalData, NewDeviceLoginData, NotificationData, OrgInvitationData, OtpData, PasswordChangedData, PasswordResetData, WelcomeData, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData };
86
105
  //# sourceMappingURL=schemas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","names":[],"sources":["../src/schemas.ts"],"mappings":";;;;;;;cAuBa,SAAO,OAAA;;;;;cAMP,eAAa,OAAA;KACd,uBAAuB,cAAc;cAEpC,SAAO,OAAA;;;;;cAMP,aAAW,OAAA;;;;cAKX,mBAAiB,OAAA;;;;;cAMjB,kBAAgB,OAAA;;;;;;;cAQhB,mBAAiB,OAAA;;;;;;;;;cAYjB,qBAAmB,OAAA;;;;;cAQnB,oBAAkB,OAAA;;;;;;;;cAalB,qBAAmB,OAAA;;;;;;;;;cAWnB,mBAAiB,OAAA;;;;;;;;KASlB,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"}
1
+ {"version":3,"file":"schemas.d.ts","names":[],"sources":["../src/schemas.ts"],"mappings":";;;;;;;cAuBa,SAAO,OAAA;;;;;;;;;;;;;;;;;cAkBP,cAAY,OAAA,MAAA,OAAA;;KAKb,sBAAsB,aAAa;;;;;cAMlC,eAAa,OAAA;KACd,uBAAuB,cAAc;cAEpC,SAAO,OAAA;;;;;cAMP,aAAW,OAAA;;;;cAKX,mBAAiB,OAAA;;;;;cAMjB,kBAAgB,OAAA;;;;;;;cAQhB,mBAAiB,OAAA;;;;;;;;;cAYjB,qBAAmB,OAAA;;;;;cAQnB,oBAAkB,OAAA;;;;;;;;cAalB,qBAAmB,OAAA;;;;;;;;;cAWnB,mBAAiB,OAAA;;;;;;;;KASlB,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
@@ -22,6 +22,23 @@ import { Schema } from "effect";
22
22
  */
23
23
  const HttpUrl = Schema.String.check(Schema.isPattern(/^https?:\/\/\S+$/i));
24
24
  /**
25
+ * A single, syntactically-valid recipient email address (branded).
26
+ *
27
+ * The pattern mirrors `@resq-systems/security`'s `EmailSchema` — one `@`, a
28
+ * dotted domain, and a 2+ character TLD, or a Punycode/IDN `xn--…` TLD (e.g.
29
+ * `.xn--p1ai` for `.рф`) so internationalized domains are not rejected. Because
30
+ * the character classes admit no whitespace or control characters and the check
31
+ * is anchored (`^…$`), it also rejects the CR/LF that underpins SMTP header
32
+ * injection: a `to` smuggling `"…\r\nBcc: attacker@evil"` into a provider that
33
+ * concatenates headers is a type-*and*-runtime error at the boundary, not a
34
+ * silent extra recipient.
35
+ *
36
+ * The {@link EmailAddress} brand marks a string that has cleared this check, so
37
+ * a validated address is not interchangeable with a raw `string` downstream
38
+ * (the `to` field on the decoded mailer payload and the rendered email).
39
+ */
40
+ const EmailAddress = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.(?:[A-Za-z]{2,}|xn--[A-Za-z0-9-]+)$/)).pipe(Schema.brand("EmailAddress"));
41
+ /**
25
42
  * Compliance class of an email send. `transactional` skips unsubscribe UI;
26
43
  * `marketing` requires an unsubscribe affordance in the legal footer.
27
44
  */
@@ -99,6 +116,6 @@ const orgInvitationData = Schema.Struct({
99
116
  expiresInDays: Schema.optional(Schema.Number)
100
117
  });
101
118
  //#endregion
102
- export { HttpUrl, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData };
119
+ export { EmailAddress, HttpUrl, emailCategory, incidentAlertData, missionApprovalData, newDeviceLoginData, notificationData, orgInvitationData, otpData, passwordChangedData, passwordResetData, welcomeData };
103
120
 
104
121
  //# sourceMappingURL=schemas.js.map
@@ -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 * 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":";;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,UAAUA,OAAE,OAAO,MAAMA,OAAE,UAAU,mBAAmB,CAAC;;;;;AAMtE,MAAa,gBAAgBA,OAAE,SAAS,CAAC,iBAAiB,WAAW,CAAC;AAGtE,MAAa,UAAUA,OAAE,OAAO;CAC/B,MAAMA,OAAE;CACR,WAAWA,OAAE,SAASA,OAAE,MAAM;CAC9B,kBAAkBA,OAAE,SAASA,OAAE,MAAM;AACtC,CAAC;AAED,MAAa,cAAcA,OAAE,OAAO;CACnC,WAAWA,OAAE;CACb,WAAWA,OAAE,SAAS,OAAO;AAC9B,CAAC;AAED,MAAa,oBAAoBA,OAAE,OAAO;CACzC,WAAWA,OAAE,SAASA,OAAE,MAAM;CAC9B,UAAU;CACV,kBAAkBA,OAAE,SAASA,OAAE,MAAM;AACtC,CAAC;AAED,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;AAED,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;AAED,MAAa,sBAAsBA,OAAE,OAAO;CAC3C,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,kBAAkBA,OAAE,SAAS,OAAO;AACrC,CAAC;AAED,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;AAED,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;AAED,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"}
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":";;;;;;;;;;;;;;;;;;;;;;AAuBA,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;AAGtE,MAAa,UAAUA,OAAE,OAAO;CAC/B,MAAMA,OAAE;CACR,WAAWA,OAAE,SAASA,OAAE,MAAM;CAC9B,kBAAkBA,OAAE,SAASA,OAAE,MAAM;AACtC,CAAC;AAED,MAAa,cAAcA,OAAE,OAAO;CACnC,WAAWA,OAAE;CACb,WAAWA,OAAE,SAAS,OAAO;AAC9B,CAAC;AAED,MAAa,oBAAoBA,OAAE,OAAO;CACzC,WAAWA,OAAE,SAASA,OAAE,MAAM;CAC9B,UAAU;CACV,kBAAkBA,OAAE,SAASA,OAAE,MAAM;AACtC,CAAC;AAED,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;AAED,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;AAED,MAAa,sBAAsBA,OAAE,OAAO;CAC3C,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,WAAWA,OAAE,SAASA,OAAE,MAAM;CAE9B,kBAAkBA,OAAE,SAAS,OAAO;AACrC,CAAC;AAED,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;AAED,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;AAED,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/suite.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EmailAddress } from "./schemas.js";
1
2
  import { Mailer } from "./mailer.js";
2
3
  //#region src/suite.d.ts
3
4
  /**
@@ -22,7 +23,7 @@ import { Mailer } from "./mailer.js";
22
23
  */
23
24
  declare const resqMailer: Mailer<{
24
25
  readonly name: "incident-alert";
25
- readonly to: string;
26
+ readonly to: EmailAddress;
26
27
  readonly data: {
27
28
  readonly incidentId: string;
28
29
  readonly title: string;
@@ -36,7 +37,7 @@ declare const resqMailer: Mailer<{
36
37
  readonly unsubscribeUrl?: string;
37
38
  } | {
38
39
  readonly name: "mission-approval";
39
- readonly to: string;
40
+ readonly to: EmailAddress;
40
41
  readonly data: {
41
42
  readonly missionId: string;
42
43
  readonly title: string;
@@ -50,7 +51,7 @@ declare const resqMailer: Mailer<{
50
51
  readonly unsubscribeUrl?: string;
51
52
  } | {
52
53
  readonly name: "new-device-login";
53
- readonly to: string;
54
+ readonly to: EmailAddress;
54
55
  readonly data: {
55
56
  readonly firstName?: string | undefined;
56
57
  readonly device?: string | undefined;
@@ -63,7 +64,7 @@ declare const resqMailer: Mailer<{
63
64
  readonly unsubscribeUrl?: string;
64
65
  } | {
65
66
  readonly name: "notification";
66
- readonly to: string;
67
+ readonly to: EmailAddress;
67
68
  readonly data: {
68
69
  readonly title: string;
69
70
  readonly body: string;
@@ -75,7 +76,7 @@ declare const resqMailer: Mailer<{
75
76
  readonly unsubscribeUrl?: string;
76
77
  } | {
77
78
  readonly name: "org-invitation";
78
- readonly to: string;
79
+ readonly to: EmailAddress;
79
80
  readonly data: {
80
81
  readonly orgName: string;
81
82
  readonly inviterName?: string | undefined;
@@ -87,7 +88,7 @@ declare const resqMailer: Mailer<{
87
88
  readonly unsubscribeUrl?: string;
88
89
  } | {
89
90
  readonly name: "otp";
90
- readonly to: string;
91
+ readonly to: EmailAddress;
91
92
  readonly data: {
92
93
  readonly code: string;
93
94
  readonly firstName?: string | undefined;
@@ -97,7 +98,7 @@ declare const resqMailer: Mailer<{
97
98
  readonly unsubscribeUrl?: string;
98
99
  } | {
99
100
  readonly name: "password-changed";
100
- readonly to: string;
101
+ readonly to: EmailAddress;
101
102
  readonly data: {
102
103
  readonly firstName?: string | undefined;
103
104
  readonly changedAt?: string | undefined;
@@ -107,7 +108,7 @@ declare const resqMailer: Mailer<{
107
108
  readonly unsubscribeUrl?: string;
108
109
  } | {
109
110
  readonly name: "password-reset";
110
- readonly to: string;
111
+ readonly to: EmailAddress;
111
112
  readonly data: {
112
113
  readonly firstName?: string | undefined;
113
114
  readonly resetUrl: string;
@@ -117,7 +118,7 @@ declare const resqMailer: Mailer<{
117
118
  readonly unsubscribeUrl?: string;
118
119
  } | {
119
120
  readonly name: "welcome";
120
- readonly to: string;
121
+ readonly to: EmailAddress;
121
122
  readonly data: {
122
123
  readonly firstName: string;
123
124
  readonly verifyUrl?: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"suite.d.ts","names":[],"sources":["../src/suite.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;cAwBa,YAAA"}
1
+ {"version":3,"file":"suite.d.ts","names":[],"sources":["../src/suite.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;cAwBa;;eAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resq-systems/email-templates",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Type-safe transactional email templates: an Effect Schema contract, React Email components, headless render-to-html/text, and an optional Resend sender",
5
5
  "license": "Apache-2.0",
6
6
  "author": {