@shware/workflow 1.0.0 → 2.0.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.
Files changed (106) hide show
  1. package/README.md +42 -11
  2. package/dist/client.d.cts +1 -1
  3. package/dist/client.d.mts +1 -1
  4. package/dist/cloudflare/bindings.cjs.map +1 -1
  5. package/dist/cloudflare/bindings.d.cts +1 -1
  6. package/dist/cloudflare/bindings.d.mts +1 -1
  7. package/dist/cloudflare/bindings.mjs.map +1 -1
  8. package/dist/cloudflare/channels.cjs +1 -4
  9. package/dist/cloudflare/channels.cjs.map +1 -1
  10. package/dist/cloudflare/channels.d.cts +3 -3
  11. package/dist/cloudflare/channels.d.mts +3 -3
  12. package/dist/cloudflare/channels.mjs +1 -4
  13. package/dist/cloudflare/channels.mjs.map +1 -1
  14. package/dist/cloudflare/index.d.cts +4 -4
  15. package/dist/cloudflare/index.d.mts +4 -4
  16. package/dist/cloudflare/router.d.cts +1 -1
  17. package/dist/cloudflare/router.d.mts +1 -1
  18. package/dist/cloudflare/runner.d.cts +2 -2
  19. package/dist/cloudflare/runner.d.mts +2 -2
  20. package/dist/cloudflare/senders.cjs +15 -8
  21. package/dist/cloudflare/senders.cjs.map +1 -1
  22. package/dist/cloudflare/senders.d.cts +15 -11
  23. package/dist/cloudflare/senders.d.cts.map +1 -1
  24. package/dist/cloudflare/senders.d.mts +15 -11
  25. package/dist/cloudflare/senders.d.mts.map +1 -1
  26. package/dist/cloudflare/senders.mjs +15 -8
  27. package/dist/cloudflare/senders.mjs.map +1 -1
  28. package/dist/cloudflare/worker.cjs +7 -4
  29. package/dist/cloudflare/worker.cjs.map +1 -1
  30. package/dist/cloudflare/worker.d.cts +2 -2
  31. package/dist/cloudflare/worker.d.cts.map +1 -1
  32. package/dist/cloudflare/worker.d.mts +2 -2
  33. package/dist/cloudflare/worker.d.mts.map +1 -1
  34. package/dist/cloudflare/worker.mjs +8 -5
  35. package/dist/cloudflare/worker.mjs.map +1 -1
  36. package/dist/config.cjs +8 -9
  37. package/dist/config.cjs.map +1 -1
  38. package/dist/config.d.cts +9 -32
  39. package/dist/config.d.cts.map +1 -1
  40. package/dist/config.d.mts +9 -32
  41. package/dist/config.d.mts.map +1 -1
  42. package/dist/config.mjs +9 -9
  43. package/dist/config.mjs.map +1 -1
  44. package/dist/email-context.cjs +29 -0
  45. package/dist/email-context.cjs.map +1 -0
  46. package/dist/email-context.d.cts +11 -0
  47. package/dist/email-context.d.cts.map +1 -0
  48. package/dist/email-context.d.mts +11 -0
  49. package/dist/email-context.d.mts.map +1 -0
  50. package/dist/email-context.mjs +27 -0
  51. package/dist/email-context.mjs.map +1 -0
  52. package/dist/email.cjs +72 -0
  53. package/dist/email.cjs.map +1 -0
  54. package/dist/email.d.cts +63 -0
  55. package/dist/email.d.cts.map +1 -0
  56. package/dist/email.d.mts +63 -0
  57. package/dist/email.d.mts.map +1 -0
  58. package/dist/email.mjs +65 -0
  59. package/dist/email.mjs.map +1 -0
  60. package/dist/engine/index.d.cts +2 -2
  61. package/dist/engine/index.d.mts +2 -2
  62. package/dist/engine/ports.d.cts +7 -2
  63. package/dist/engine/ports.d.cts.map +1 -1
  64. package/dist/engine/ports.d.mts +7 -2
  65. package/dist/engine/ports.d.mts.map +1 -1
  66. package/dist/index.cjs +8 -1
  67. package/dist/index.d.cts +4 -3
  68. package/dist/index.d.mts +4 -3
  69. package/dist/index.mjs +3 -2
  70. package/dist/react-email.cjs +11 -3
  71. package/dist/react-email.cjs.map +1 -1
  72. package/dist/react-email.d.cts +2 -1
  73. package/dist/react-email.d.cts.map +1 -1
  74. package/dist/react-email.d.mts +2 -1
  75. package/dist/react-email.d.mts.map +1 -1
  76. package/dist/react-email.mjs +11 -3
  77. package/dist/react-email.mjs.map +1 -1
  78. package/dist/reporting/classify.cjs.map +1 -1
  79. package/dist/reporting/classify.d.cts +1 -1
  80. package/dist/reporting/classify.d.mts +1 -1
  81. package/dist/reporting/classify.mjs.map +1 -1
  82. package/dist/reporting/observer.cjs +5 -4
  83. package/dist/reporting/observer.cjs.map +1 -1
  84. package/dist/reporting/observer.d.cts +3 -2
  85. package/dist/reporting/observer.d.cts.map +1 -1
  86. package/dist/reporting/observer.d.mts +3 -2
  87. package/dist/reporting/observer.d.mts.map +1 -1
  88. package/dist/reporting/observer.mjs +5 -4
  89. package/dist/reporting/observer.mjs.map +1 -1
  90. package/dist/reporting/tracking.cjs +3 -12
  91. package/dist/reporting/tracking.cjs.map +1 -1
  92. package/dist/reporting/tracking.d.cts +18 -7
  93. package/dist/reporting/tracking.d.cts.map +1 -1
  94. package/dist/reporting/tracking.d.mts +18 -7
  95. package/dist/reporting/tracking.d.mts.map +1 -1
  96. package/dist/reporting/tracking.mjs +3 -12
  97. package/dist/reporting/tracking.mjs.map +1 -1
  98. package/dist/templates.cjs +6 -1
  99. package/dist/templates.cjs.map +1 -1
  100. package/dist/templates.d.cts +21 -2
  101. package/dist/templates.d.cts.map +1 -1
  102. package/dist/templates.d.mts +21 -2
  103. package/dist/templates.d.mts.map +1 -1
  104. package/dist/templates.mjs +6 -1
  105. package/dist/templates.mjs.map +1 -1
  106. package/package.json +11 -1
@@ -0,0 +1,29 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let react = require("react");
3
+ //#region src/email-context.ts
4
+ /**
5
+ * `@shware/workflow/email-context` — the email being rendered, for its components: the resolved
6
+ * metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the
7
+ * template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)
8
+ * and the studio's preview both provide it; the props stay the template's own.
9
+ *
10
+ * const { unsubscribeUrl } = useEmailContext();
11
+ * {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}
12
+ *
13
+ * Browser-safe: React only — no react-dom/server, no Worker bindings.
14
+ */
15
+ const EmailContext = (0, react.createContext)(void 0);
16
+ function EmailProvider({ email, children }) {
17
+ return (0, react.createElement)(EmailContext.Provider, { value: email }, children);
18
+ }
19
+ /** The email being rendered; throws outside a render the SDK or the studio provides. */
20
+ function useEmailContext() {
21
+ const email = (0, react.useContext)(EmailContext);
22
+ if (email === void 0) throw new Error("useEmailContext() is called outside an email render: render the template with @shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>");
23
+ return email;
24
+ }
25
+ //#endregion
26
+ exports.EmailProvider = EmailProvider;
27
+ exports.useEmailContext = useEmailContext;
28
+
29
+ //# sourceMappingURL=email-context.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-context.cjs","names":["createContext","createElement","useContext"],"sources":["../src/email-context.ts"],"sourcesContent":["import { type ReactNode, createContext, createElement, useContext } from 'react';\nimport type { ResolvedEmail } from './email';\n\n/**\n * `@shware/workflow/email-context` — the email being rendered, for its components: the resolved\n * metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the\n * template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)\n * and the studio's preview both provide it; the props stay the template's own.\n *\n * const { unsubscribeUrl } = useEmailContext();\n * {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}\n *\n * Browser-safe: React only — no react-dom/server, no Worker bindings.\n */\n\nconst EmailContext = createContext<ResolvedEmail | undefined>(undefined);\n\nexport function EmailProvider({\n email,\n children,\n}: {\n email: ResolvedEmail;\n children?: ReactNode;\n}): ReactNode {\n return createElement(EmailContext.Provider, { value: email }, children);\n}\n\n/** The email being rendered; throws outside a render the SDK or the studio provides. */\nexport function useEmailContext(): ResolvedEmail {\n const email = useContext(EmailContext);\n if (email === undefined) {\n throw new Error(\n 'useEmailContext() is called outside an email render: render the template with ' +\n \"@shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>\"\n );\n }\n return email;\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAM,gBAAA,GAAeA,MAAAA,cAAAA,CAAyC,KAAA,CAAS;AAEvE,SAAgB,cAAc,EAC5B,OACA,YAIY;CACZ,QAAA,GAAOC,MAAAA,cAAAA,CAAc,aAAa,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AACxE;;AAGA,SAAgB,kBAAiC;CAC/C,MAAM,SAAA,GAAQC,MAAAA,WAAAA,CAAW,YAAY;CACrC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,2MAEF;CAEF,OAAO;AACT"}
@@ -0,0 +1,11 @@
1
+ import { ResolvedEmail } from "./email.cjs";
2
+ import { ReactNode } from "react";
3
+ //#region src/email-context.d.ts
4
+ export declare function EmailProvider({ email, children }: {
5
+ email: ResolvedEmail;
6
+ children?: ReactNode;
7
+ }): ReactNode;
8
+ /** The email being rendered; throws outside a render the SDK or the studio provides. */
9
+ export declare function useEmailContext(): ResolvedEmail;
10
+ //#endregion
11
+ //# sourceMappingURL=email-context.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-context.d.cts","names":[],"sources":["../src/email-context.ts"],"mappings":";;;wBAiBgB,gBACd,OACA;EAEA,OAAO;EACP,WAAW;IACT;;wBAKY,mBAAmB"}
@@ -0,0 +1,11 @@
1
+ import { ResolvedEmail } from "./email.mjs";
2
+ import { ReactNode } from "react";
3
+ //#region src/email-context.d.ts
4
+ export declare function EmailProvider({ email, children }: {
5
+ email: ResolvedEmail;
6
+ children?: ReactNode;
7
+ }): ReactNode;
8
+ /** The email being rendered; throws outside a render the SDK or the studio provides. */
9
+ export declare function useEmailContext(): ResolvedEmail;
10
+ //#endregion
11
+ //# sourceMappingURL=email-context.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-context.d.mts","names":[],"sources":["../src/email-context.ts"],"mappings":";;;wBAiBgB,gBACd,OACA;EAEA,OAAO;EACP,WAAW;IACT;;wBAKY,mBAAmB"}
@@ -0,0 +1,27 @@
1
+ import { createContext, createElement, useContext } from "react";
2
+ //#region src/email-context.ts
3
+ /**
4
+ * `@shware/workflow/email-context` — the email being rendered, for its components: the resolved
5
+ * metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the
6
+ * template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)
7
+ * and the studio's preview both provide it; the props stay the template's own.
8
+ *
9
+ * const { unsubscribeUrl } = useEmailContext();
10
+ * {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}
11
+ *
12
+ * Browser-safe: React only — no react-dom/server, no Worker bindings.
13
+ */
14
+ const EmailContext = createContext(void 0);
15
+ function EmailProvider({ email, children }) {
16
+ return createElement(EmailContext.Provider, { value: email }, children);
17
+ }
18
+ /** The email being rendered; throws outside a render the SDK or the studio provides. */
19
+ function useEmailContext() {
20
+ const email = useContext(EmailContext);
21
+ if (email === void 0) throw new Error("useEmailContext() is called outside an email render: render the template with @shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>");
22
+ return email;
23
+ }
24
+ //#endregion
25
+ export { EmailProvider, useEmailContext };
26
+
27
+ //# sourceMappingURL=email-context.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-context.mjs","names":[],"sources":["../src/email-context.ts"],"sourcesContent":["import { type ReactNode, createContext, createElement, useContext } from 'react';\nimport type { ResolvedEmail } from './email';\n\n/**\n * `@shware/workflow/email-context` — the email being rendered, for its components: the resolved\n * metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the\n * template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)\n * and the studio's preview both provide it; the props stay the template's own.\n *\n * const { unsubscribeUrl } = useEmailContext();\n * {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}\n *\n * Browser-safe: React only — no react-dom/server, no Worker bindings.\n */\n\nconst EmailContext = createContext<ResolvedEmail | undefined>(undefined);\n\nexport function EmailProvider({\n email,\n children,\n}: {\n email: ResolvedEmail;\n children?: ReactNode;\n}): ReactNode {\n return createElement(EmailContext.Provider, { value: email }, children);\n}\n\n/** The email being rendered; throws outside a render the SDK or the studio provides. */\nexport function useEmailContext(): ResolvedEmail {\n const email = useContext(EmailContext);\n if (email === undefined) {\n throw new Error(\n 'useEmailContext() is called outside an email render: render the template with ' +\n \"@shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>\"\n );\n }\n return email;\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,eAAe,cAAyC,KAAA,CAAS;AAEvE,SAAgB,cAAc,EAC5B,OACA,YAIY;CACZ,OAAO,cAAc,aAAa,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AACxE;;AAGA,SAAgB,kBAAiC;CAC/C,MAAM,QAAQ,WAAW,YAAY;CACrC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,2MAEF;CAEF,OAAO;AACT"}
package/dist/email.cjs ADDED
@@ -0,0 +1,72 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/email.ts
3
+ /** `Name <email>`, or the bare address. */
4
+ function formatEmailAddress(address) {
5
+ if (typeof address === "string") return address;
6
+ return address.name === void 0 ? address.email : `${address.name} <${address.email}>`;
7
+ }
8
+ /** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
9
+ function resolveEmail(template, config) {
10
+ const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;
11
+ return {
12
+ from: template.from,
13
+ ...template.replyTo === void 0 ? {} : { replyTo: template.replyTo },
14
+ ...unsubscribeUrl === void 0 ? {} : { unsubscribeUrl }
15
+ };
16
+ }
17
+ const ADDRESS = /^[^\s@<>]+@[^\s@<>]+$/;
18
+ /** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
19
+ function assertEmailAddress(address, where) {
20
+ const email = typeof address === "object" && address !== null ? address.email : address;
21
+ const name = typeof address === "object" && address !== null ? address.name : void 0;
22
+ if (typeof email !== "string" || !ADDRESS.test(email)) throw new TypeError(`${where} must be an email address or { email, name? }`);
23
+ if (name !== void 0 && typeof name !== "string") throw new TypeError(`${where}.name must be a string`);
24
+ }
25
+ /** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
26
+ function assertUnsubscribeUrl(url, where) {
27
+ let parsed;
28
+ try {
29
+ parsed = typeof url === "string" ? new URL(url) : void 0;
30
+ } catch {
31
+ parsed = void 0;
32
+ }
33
+ if (parsed === void 0 || parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);
34
+ }
35
+ /** The config's `email` and every registered template's metadata; throws on the first invalid value. */
36
+ function assertEmailConfig(config, templates) {
37
+ if (Object.hasOwn(config, "emails")) throw new TypeError("the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, and the rest moved to `email: { unsubscribeUrl, tracking }`");
38
+ const email = config.email;
39
+ if (email !== void 0) {
40
+ if (email.unsubscribeUrl !== void 0) assertUnsubscribeUrl(email.unsubscribeUrl, "email.unsubscribeUrl");
41
+ const quick = email.tracking?.quickClickMs;
42
+ if (quick !== void 0 && !(typeof quick === "number" && Number.isFinite(quick) && quick >= 0)) throw new TypeError("email.tracking.quickClickMs must be a non-negative number");
43
+ }
44
+ for (const template of templates) assertEmailMetadata(template, `email template '${template.id}'`);
45
+ }
46
+ /** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
47
+ function assertEmailMetadata(template, where) {
48
+ assertEmailAddress(template.from, `${where}: from`);
49
+ if (template.replyTo !== void 0) assertEmailAddress(template.replyTo, `${where}: replyTo`);
50
+ if (template.unsubscribeUrl !== void 0) assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);
51
+ }
52
+ /** The unsubscribe link may carry its own query (a token, a list): the path names it. */
53
+ function isUnsubscribeLink(url, unsubscribeUrl) {
54
+ if (unsubscribeUrl === void 0) return false;
55
+ try {
56
+ const a = new URL(url);
57
+ const b = new URL(unsubscribeUrl);
58
+ return a.origin === b.origin && a.pathname === b.pathname;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ //#endregion
64
+ exports.assertEmailAddress = assertEmailAddress;
65
+ exports.assertEmailConfig = assertEmailConfig;
66
+ exports.assertEmailMetadata = assertEmailMetadata;
67
+ exports.assertUnsubscribeUrl = assertUnsubscribeUrl;
68
+ exports.formatEmailAddress = formatEmailAddress;
69
+ exports.isUnsubscribeLink = isUnsubscribeLink;
70
+ exports.resolveEmail = resolveEmail;
71
+
72
+ //# sourceMappingURL=email.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.cjs","names":[],"sources":["../src/email.ts"],"sourcesContent":["import type { EmailTemplate } from './templates';\n\n/**\n * Email metadata: who a template's email comes from, where replies go, and the unsubscribe link\n * its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should\n * go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's\n * `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,\n * the Worker's preview and the studio all go through it. No React and no I/O here, so a browser\n * can import it.\n */\n\n/** An address as the transports take it: bare, or with a display name. */\nexport type EmailAddress = string | { email: string; name?: string };\n\n/** `Name <email>`, or the bare address. */\nexport function formatEmailAddress(address: EmailAddress): string {\n if (typeof address === 'string') return address;\n return address.name === undefined ? address.email : `${address.name} <${address.email}>`;\n}\n\n/** The config's `email`: what every email template shares. */\nexport interface EmailConfig {\n /**\n * The unsubscribe link of every email template that names none of its own. The layout reads it\n * with `useEmailContext()`; sent, it is tracked like every link, but following it shows a\n * confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.\n */\n unsubscribeUrl?: string;\n /** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */\n tracking?: {\n /**\n * An open or click this many milliseconds after delivery is a weak machine signal — machine\n * only together with another (a second link within seconds); 1000 by default. A person\n * tapping a mail notification can click within a few seconds, so keep it short.\n */\n quickClickMs?: number;\n };\n}\n\n/** One template's email metadata, resolved against the config. */\nexport interface ResolvedEmail {\n from: EmailAddress;\n /** Absent when the template names none: the email goes out without a Reply-To. */\n replyTo?: EmailAddress;\n /** Absent when neither the template nor the config names one: the email has no unsubscribe link. */\n unsubscribeUrl?: string;\n}\n\n/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */\nexport function resolveEmail(\n template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>,\n config?: EmailConfig\n): ResolvedEmail {\n const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;\n return {\n from: template.from,\n ...(template.replyTo === undefined ? {} : { replyTo: template.replyTo }),\n ...(unsubscribeUrl === undefined ? {} : { unsubscribeUrl }),\n };\n}\n\nconst ADDRESS = /^[^\\s@<>]+@[^\\s@<>]+$/;\n\n/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */\nexport function assertEmailAddress(address: unknown, where: string): void {\n const email =\n typeof address === 'object' && address !== null\n ? (address as { email?: unknown }).email\n : address;\n const name =\n typeof address === 'object' && address !== null\n ? (address as { name?: unknown }).name\n : undefined;\n if (typeof email !== 'string' || !ADDRESS.test(email)) {\n throw new TypeError(`${where} must be an email address or { email, name? }`);\n }\n if (name !== undefined && typeof name !== 'string') {\n throw new TypeError(`${where}.name must be a string`);\n }\n}\n\n/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */\nexport function assertUnsubscribeUrl(url: unknown, where: string): void {\n let parsed: URL | undefined;\n try {\n parsed = typeof url === 'string' ? new URL(url) : undefined;\n } catch {\n parsed = undefined;\n }\n if (parsed === undefined || (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')) {\n throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);\n }\n}\n\n/** The config's `email` and every registered template's metadata; throws on the first invalid value. */\nexport function assertEmailConfig(\n config: { email?: EmailConfig },\n templates: Iterable<EmailTemplate>\n): void {\n if (Object.hasOwn(config, 'emails')) {\n throw new TypeError(\n \"the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, \" +\n 'and the rest moved to `email: { unsubscribeUrl, tracking }`'\n );\n }\n const email = config.email;\n if (email !== undefined) {\n if (email.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(email.unsubscribeUrl, 'email.unsubscribeUrl');\n }\n const quick = email.tracking?.quickClickMs;\n if (\n quick !== undefined &&\n !(typeof quick === 'number' && Number.isFinite(quick) && quick >= 0)\n ) {\n throw new TypeError('email.tracking.quickClickMs must be a non-negative number');\n }\n }\n for (const template of templates)\n assertEmailMetadata(template, `email template '${template.id}'`);\n}\n\n/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */\nexport function assertEmailMetadata(\n template: { from?: unknown; replyTo?: unknown; unsubscribeUrl?: unknown },\n where: string\n): void {\n assertEmailAddress(template.from, `${where}: from`);\n if (template.replyTo !== undefined) assertEmailAddress(template.replyTo, `${where}: replyTo`);\n if (template.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);\n }\n}\n\n/** The unsubscribe link may carry its own query (a token, a list): the path names it. */\nexport function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean {\n if (unsubscribeUrl === undefined) return false;\n try {\n const a = new URL(url);\n const b = new URL(unsubscribeUrl);\n return a.origin === b.origin && a.pathname === b.pathname;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;AAeA,SAAgB,mBAAmB,SAA+B;CAChE,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,OAAO,QAAQ,SAAS,KAAA,IAAY,QAAQ,QAAQ,GAAG,QAAQ,KAAK,IAAI,QAAQ,MAAM;AACxF;;AA+BA,SAAgB,aACd,UACA,QACe;CACf,MAAM,iBAAiB,SAAS,kBAAkB,QAAQ;CAC1D,OAAO;EACL,MAAM,SAAS;EACf,GAAI,SAAS,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,SAAS,QAAQ;EACtE,GAAI,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe;CAC3D;AACF;AAEA,MAAM,UAAU;;AAGhB,SAAgB,mBAAmB,SAAkB,OAAqB;CACxE,MAAM,QACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAAgC,QACjC;CACN,MAAM,OACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAA+B,OAChC,KAAA;CACN,IAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,KAAK,KAAK,GAClD,MAAM,IAAI,UAAU,GAAG,MAAM,8CAA8C;CAE7E,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,UACxC,MAAM,IAAI,UAAU,GAAG,MAAM,uBAAuB;AAExD;;AAGA,SAAgB,qBAAqB,KAAc,OAAqB;CACtE,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,QAAQ,WAAW,IAAI,IAAI,GAAG,IAAI,KAAA;CACpD,QAAQ;EACN,SAAS,KAAA;CACX;CACA,IAAI,WAAW,KAAA,KAAc,OAAO,aAAa,YAAY,OAAO,aAAa,SAC/E,MAAM,IAAI,UAAU,GAAG,MAAM,gDAAgD;AAEjF;;AAGA,SAAgB,kBACd,QACA,WACM;CACN,IAAI,OAAO,OAAO,QAAQ,QAAQ,GAChC,MAAM,IAAI,UACR,qJAEF;CAEF,MAAM,QAAQ,OAAO;CACrB,IAAI,UAAU,KAAA,GAAW;EACvB,IAAI,MAAM,mBAAmB,KAAA,GAC3B,qBAAqB,MAAM,gBAAgB,sBAAsB;EAEnE,MAAM,QAAQ,MAAM,UAAU;EAC9B,IACE,UAAU,KAAA,KACV,EAAE,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,SAAS,IAElE,MAAM,IAAI,UAAU,2DAA2D;CAEnF;CACA,KAAK,MAAM,YAAY,WACrB,oBAAoB,UAAU,mBAAmB,SAAS,GAAG,EAAE;AACnE;;AAGA,SAAgB,oBACd,UACA,OACM;CACN,mBAAmB,SAAS,MAAM,GAAG,MAAM,OAAO;CAClD,IAAI,SAAS,YAAY,KAAA,GAAW,mBAAmB,SAAS,SAAS,GAAG,MAAM,UAAU;CAC5F,IAAI,SAAS,mBAAmB,KAAA,GAC9B,qBAAqB,SAAS,gBAAgB,GAAG,MAAM,iBAAiB;AAE5E;;AAGA,SAAgB,kBAAkB,KAAa,gBAA6C;CAC1F,IAAI,mBAAmB,KAAA,GAAW,OAAO;CACzC,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,GAAG;EACrB,MAAM,IAAI,IAAI,IAAI,cAAc;EAChC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE;CACnD,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -0,0 +1,63 @@
1
+ import { EmailTemplate } from "./templates.cjs";
2
+ //#region src/email.d.ts
3
+ /**
4
+ * Email metadata: who a template's email comes from, where replies go, and the unsubscribe link
5
+ * its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should
6
+ * go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's
7
+ * `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,
8
+ * the Worker's preview and the studio all go through it. No React and no I/O here, so a browser
9
+ * can import it.
10
+ */
11
+ /** An address as the transports take it: bare, or with a display name. */
12
+ export type EmailAddress = string | {
13
+ email: string;
14
+ name?: string;
15
+ };
16
+ /** `Name <email>`, or the bare address. */
17
+ export declare function formatEmailAddress(address: EmailAddress): string;
18
+ /** The config's `email`: what every email template shares. */
19
+ export interface EmailConfig {
20
+ /**
21
+ * The unsubscribe link of every email template that names none of its own. The layout reads it
22
+ * with `useEmailContext()`; sent, it is tracked like every link, but following it shows a
23
+ * confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.
24
+ */
25
+ unsubscribeUrl?: string;
26
+ /** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */
27
+ tracking?: {
28
+ /**
29
+ * An open or click this many milliseconds after delivery is a weak machine signal — machine
30
+ * only together with another (a second link within seconds); 1000 by default. A person
31
+ * tapping a mail notification can click within a few seconds, so keep it short.
32
+ */
33
+ quickClickMs?: number;
34
+ };
35
+ }
36
+ /** One template's email metadata, resolved against the config. */
37
+ export interface ResolvedEmail {
38
+ from: EmailAddress;
39
+ /** Absent when the template names none: the email goes out without a Reply-To. */
40
+ replyTo?: EmailAddress;
41
+ /** Absent when neither the template nor the config names one: the email has no unsubscribe link. */
42
+ unsubscribeUrl?: string;
43
+ }
44
+ /** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
45
+ export declare function resolveEmail(template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>, config?: EmailConfig): ResolvedEmail;
46
+ /** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
47
+ export declare function assertEmailAddress(address: unknown, where: string): void;
48
+ /** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
49
+ export declare function assertUnsubscribeUrl(url: unknown, where: string): void;
50
+ /** The config's `email` and every registered template's metadata; throws on the first invalid value. */
51
+ export declare function assertEmailConfig(config: {
52
+ email?: EmailConfig;
53
+ }, templates: Iterable<EmailTemplate>): void;
54
+ /** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
55
+ export declare function assertEmailMetadata(template: {
56
+ from?: unknown;
57
+ replyTo?: unknown;
58
+ unsubscribeUrl?: unknown;
59
+ }, where: string): void;
60
+ /** The unsubscribe link may carry its own query (a token, a list): the path names it. */
61
+ export declare function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean;
62
+ //#endregion
63
+ //# sourceMappingURL=email.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.cts","names":[],"sources":["../src/email.ts"],"mappings":";;;;;;;;;;;YAYY;EAA0B;EAAe;;;wBAGrC,mBAAmB,SAAS;;iBAM3B;;;;;;EAMf;;EAEA;;;;;;IAME;;;;iBAKa;EACf,MAAM;;EAEN,UAAU;;EAEV;;;wBAIc,aACd,UAAU,KAAK,uDACf,SAAS,cACR;;wBAYa,mBAAmB,kBAAkB;;wBAkBrC,qBAAqB,cAAc;;wBAanC,kBACd;EAAU,QAAQ;GAClB,WAAW,SAAS;;wBA0BN,oBACd;EAAY;EAAgB;EAAmB;GAC/C;;wBAUc,kBAAkB,aAAa"}
@@ -0,0 +1,63 @@
1
+ import { EmailTemplate } from "./templates.mjs";
2
+ //#region src/email.d.ts
3
+ /**
4
+ * Email metadata: who a template's email comes from, where replies go, and the unsubscribe link
5
+ * its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should
6
+ * go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's
7
+ * `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,
8
+ * the Worker's preview and the studio all go through it. No React and no I/O here, so a browser
9
+ * can import it.
10
+ */
11
+ /** An address as the transports take it: bare, or with a display name. */
12
+ export type EmailAddress = string | {
13
+ email: string;
14
+ name?: string;
15
+ };
16
+ /** `Name <email>`, or the bare address. */
17
+ export declare function formatEmailAddress(address: EmailAddress): string;
18
+ /** The config's `email`: what every email template shares. */
19
+ export interface EmailConfig {
20
+ /**
21
+ * The unsubscribe link of every email template that names none of its own. The layout reads it
22
+ * with `useEmailContext()`; sent, it is tracked like every link, but following it shows a
23
+ * confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.
24
+ */
25
+ unsubscribeUrl?: string;
26
+ /** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */
27
+ tracking?: {
28
+ /**
29
+ * An open or click this many milliseconds after delivery is a weak machine signal — machine
30
+ * only together with another (a second link within seconds); 1000 by default. A person
31
+ * tapping a mail notification can click within a few seconds, so keep it short.
32
+ */
33
+ quickClickMs?: number;
34
+ };
35
+ }
36
+ /** One template's email metadata, resolved against the config. */
37
+ export interface ResolvedEmail {
38
+ from: EmailAddress;
39
+ /** Absent when the template names none: the email goes out without a Reply-To. */
40
+ replyTo?: EmailAddress;
41
+ /** Absent when neither the template nor the config names one: the email has no unsubscribe link. */
42
+ unsubscribeUrl?: string;
43
+ }
44
+ /** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
45
+ export declare function resolveEmail(template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>, config?: EmailConfig): ResolvedEmail;
46
+ /** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
47
+ export declare function assertEmailAddress(address: unknown, where: string): void;
48
+ /** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
49
+ export declare function assertUnsubscribeUrl(url: unknown, where: string): void;
50
+ /** The config's `email` and every registered template's metadata; throws on the first invalid value. */
51
+ export declare function assertEmailConfig(config: {
52
+ email?: EmailConfig;
53
+ }, templates: Iterable<EmailTemplate>): void;
54
+ /** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
55
+ export declare function assertEmailMetadata(template: {
56
+ from?: unknown;
57
+ replyTo?: unknown;
58
+ unsubscribeUrl?: unknown;
59
+ }, where: string): void;
60
+ /** The unsubscribe link may carry its own query (a token, a list): the path names it. */
61
+ export declare function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean;
62
+ //#endregion
63
+ //# sourceMappingURL=email.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.mts","names":[],"sources":["../src/email.ts"],"mappings":";;;;;;;;;;;YAYY;EAA0B;EAAe;;;wBAGrC,mBAAmB,SAAS;;iBAM3B;;;;;;EAMf;;EAEA;;;;;;IAME;;;;iBAKa;EACf,MAAM;;EAEN,UAAU;;EAEV;;;wBAIc,aACd,UAAU,KAAK,uDACf,SAAS,cACR;;wBAYa,mBAAmB,kBAAkB;;wBAkBrC,qBAAqB,cAAc;;wBAanC,kBACd;EAAU,QAAQ;GAClB,WAAW,SAAS;;wBA0BN,oBACd;EAAY;EAAgB;EAAmB;GAC/C;;wBAUc,kBAAkB,aAAa"}
package/dist/email.mjs ADDED
@@ -0,0 +1,65 @@
1
+ //#region src/email.ts
2
+ /** `Name <email>`, or the bare address. */
3
+ function formatEmailAddress(address) {
4
+ if (typeof address === "string") return address;
5
+ return address.name === void 0 ? address.email : `${address.name} <${address.email}>`;
6
+ }
7
+ /** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
8
+ function resolveEmail(template, config) {
9
+ const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;
10
+ return {
11
+ from: template.from,
12
+ ...template.replyTo === void 0 ? {} : { replyTo: template.replyTo },
13
+ ...unsubscribeUrl === void 0 ? {} : { unsubscribeUrl }
14
+ };
15
+ }
16
+ const ADDRESS = /^[^\s@<>]+@[^\s@<>]+$/;
17
+ /** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
18
+ function assertEmailAddress(address, where) {
19
+ const email = typeof address === "object" && address !== null ? address.email : address;
20
+ const name = typeof address === "object" && address !== null ? address.name : void 0;
21
+ if (typeof email !== "string" || !ADDRESS.test(email)) throw new TypeError(`${where} must be an email address or { email, name? }`);
22
+ if (name !== void 0 && typeof name !== "string") throw new TypeError(`${where}.name must be a string`);
23
+ }
24
+ /** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
25
+ function assertUnsubscribeUrl(url, where) {
26
+ let parsed;
27
+ try {
28
+ parsed = typeof url === "string" ? new URL(url) : void 0;
29
+ } catch {
30
+ parsed = void 0;
31
+ }
32
+ if (parsed === void 0 || parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);
33
+ }
34
+ /** The config's `email` and every registered template's metadata; throws on the first invalid value. */
35
+ function assertEmailConfig(config, templates) {
36
+ if (Object.hasOwn(config, "emails")) throw new TypeError("the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, and the rest moved to `email: { unsubscribeUrl, tracking }`");
37
+ const email = config.email;
38
+ if (email !== void 0) {
39
+ if (email.unsubscribeUrl !== void 0) assertUnsubscribeUrl(email.unsubscribeUrl, "email.unsubscribeUrl");
40
+ const quick = email.tracking?.quickClickMs;
41
+ if (quick !== void 0 && !(typeof quick === "number" && Number.isFinite(quick) && quick >= 0)) throw new TypeError("email.tracking.quickClickMs must be a non-negative number");
42
+ }
43
+ for (const template of templates) assertEmailMetadata(template, `email template '${template.id}'`);
44
+ }
45
+ /** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
46
+ function assertEmailMetadata(template, where) {
47
+ assertEmailAddress(template.from, `${where}: from`);
48
+ if (template.replyTo !== void 0) assertEmailAddress(template.replyTo, `${where}: replyTo`);
49
+ if (template.unsubscribeUrl !== void 0) assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);
50
+ }
51
+ /** The unsubscribe link may carry its own query (a token, a list): the path names it. */
52
+ function isUnsubscribeLink(url, unsubscribeUrl) {
53
+ if (unsubscribeUrl === void 0) return false;
54
+ try {
55
+ const a = new URL(url);
56
+ const b = new URL(unsubscribeUrl);
57
+ return a.origin === b.origin && a.pathname === b.pathname;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ //#endregion
63
+ export { assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail };
64
+
65
+ //# sourceMappingURL=email.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.mjs","names":[],"sources":["../src/email.ts"],"sourcesContent":["import type { EmailTemplate } from './templates';\n\n/**\n * Email metadata: who a template's email comes from, where replies go, and the unsubscribe link\n * its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should\n * go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's\n * `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,\n * the Worker's preview and the studio all go through it. No React and no I/O here, so a browser\n * can import it.\n */\n\n/** An address as the transports take it: bare, or with a display name. */\nexport type EmailAddress = string | { email: string; name?: string };\n\n/** `Name <email>`, or the bare address. */\nexport function formatEmailAddress(address: EmailAddress): string {\n if (typeof address === 'string') return address;\n return address.name === undefined ? address.email : `${address.name} <${address.email}>`;\n}\n\n/** The config's `email`: what every email template shares. */\nexport interface EmailConfig {\n /**\n * The unsubscribe link of every email template that names none of its own. The layout reads it\n * with `useEmailContext()`; sent, it is tracked like every link, but following it shows a\n * confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.\n */\n unsubscribeUrl?: string;\n /** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */\n tracking?: {\n /**\n * An open or click this many milliseconds after delivery is a weak machine signal — machine\n * only together with another (a second link within seconds); 1000 by default. A person\n * tapping a mail notification can click within a few seconds, so keep it short.\n */\n quickClickMs?: number;\n };\n}\n\n/** One template's email metadata, resolved against the config. */\nexport interface ResolvedEmail {\n from: EmailAddress;\n /** Absent when the template names none: the email goes out without a Reply-To. */\n replyTo?: EmailAddress;\n /** Absent when neither the template nor the config names one: the email has no unsubscribe link. */\n unsubscribeUrl?: string;\n}\n\n/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */\nexport function resolveEmail(\n template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>,\n config?: EmailConfig\n): ResolvedEmail {\n const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;\n return {\n from: template.from,\n ...(template.replyTo === undefined ? {} : { replyTo: template.replyTo }),\n ...(unsubscribeUrl === undefined ? {} : { unsubscribeUrl }),\n };\n}\n\nconst ADDRESS = /^[^\\s@<>]+@[^\\s@<>]+$/;\n\n/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */\nexport function assertEmailAddress(address: unknown, where: string): void {\n const email =\n typeof address === 'object' && address !== null\n ? (address as { email?: unknown }).email\n : address;\n const name =\n typeof address === 'object' && address !== null\n ? (address as { name?: unknown }).name\n : undefined;\n if (typeof email !== 'string' || !ADDRESS.test(email)) {\n throw new TypeError(`${where} must be an email address or { email, name? }`);\n }\n if (name !== undefined && typeof name !== 'string') {\n throw new TypeError(`${where}.name must be a string`);\n }\n}\n\n/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */\nexport function assertUnsubscribeUrl(url: unknown, where: string): void {\n let parsed: URL | undefined;\n try {\n parsed = typeof url === 'string' ? new URL(url) : undefined;\n } catch {\n parsed = undefined;\n }\n if (parsed === undefined || (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')) {\n throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);\n }\n}\n\n/** The config's `email` and every registered template's metadata; throws on the first invalid value. */\nexport function assertEmailConfig(\n config: { email?: EmailConfig },\n templates: Iterable<EmailTemplate>\n): void {\n if (Object.hasOwn(config, 'emails')) {\n throw new TypeError(\n \"the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, \" +\n 'and the rest moved to `email: { unsubscribeUrl, tracking }`'\n );\n }\n const email = config.email;\n if (email !== undefined) {\n if (email.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(email.unsubscribeUrl, 'email.unsubscribeUrl');\n }\n const quick = email.tracking?.quickClickMs;\n if (\n quick !== undefined &&\n !(typeof quick === 'number' && Number.isFinite(quick) && quick >= 0)\n ) {\n throw new TypeError('email.tracking.quickClickMs must be a non-negative number');\n }\n }\n for (const template of templates)\n assertEmailMetadata(template, `email template '${template.id}'`);\n}\n\n/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */\nexport function assertEmailMetadata(\n template: { from?: unknown; replyTo?: unknown; unsubscribeUrl?: unknown },\n where: string\n): void {\n assertEmailAddress(template.from, `${where}: from`);\n if (template.replyTo !== undefined) assertEmailAddress(template.replyTo, `${where}: replyTo`);\n if (template.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);\n }\n}\n\n/** The unsubscribe link may carry its own query (a token, a list): the path names it. */\nexport function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean {\n if (unsubscribeUrl === undefined) return false;\n try {\n const a = new URL(url);\n const b = new URL(unsubscribeUrl);\n return a.origin === b.origin && a.pathname === b.pathname;\n } catch {\n return false;\n }\n}\n"],"mappings":";;AAeA,SAAgB,mBAAmB,SAA+B;CAChE,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,OAAO,QAAQ,SAAS,KAAA,IAAY,QAAQ,QAAQ,GAAG,QAAQ,KAAK,IAAI,QAAQ,MAAM;AACxF;;AA+BA,SAAgB,aACd,UACA,QACe;CACf,MAAM,iBAAiB,SAAS,kBAAkB,QAAQ;CAC1D,OAAO;EACL,MAAM,SAAS;EACf,GAAI,SAAS,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,SAAS,QAAQ;EACtE,GAAI,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe;CAC3D;AACF;AAEA,MAAM,UAAU;;AAGhB,SAAgB,mBAAmB,SAAkB,OAAqB;CACxE,MAAM,QACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAAgC,QACjC;CACN,MAAM,OACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAA+B,OAChC,KAAA;CACN,IAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,KAAK,KAAK,GAClD,MAAM,IAAI,UAAU,GAAG,MAAM,8CAA8C;CAE7E,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,UACxC,MAAM,IAAI,UAAU,GAAG,MAAM,uBAAuB;AAExD;;AAGA,SAAgB,qBAAqB,KAAc,OAAqB;CACtE,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,QAAQ,WAAW,IAAI,IAAI,GAAG,IAAI,KAAA;CACpD,QAAQ;EACN,SAAS,KAAA;CACX;CACA,IAAI,WAAW,KAAA,KAAc,OAAO,aAAa,YAAY,OAAO,aAAa,SAC/E,MAAM,IAAI,UAAU,GAAG,MAAM,gDAAgD;AAEjF;;AAGA,SAAgB,kBACd,QACA,WACM;CACN,IAAI,OAAO,OAAO,QAAQ,QAAQ,GAChC,MAAM,IAAI,UACR,qJAEF;CAEF,MAAM,QAAQ,OAAO;CACrB,IAAI,UAAU,KAAA,GAAW;EACvB,IAAI,MAAM,mBAAmB,KAAA,GAC3B,qBAAqB,MAAM,gBAAgB,sBAAsB;EAEnE,MAAM,QAAQ,MAAM,UAAU;EAC9B,IACE,UAAU,KAAA,KACV,EAAE,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,SAAS,IAElE,MAAM,IAAI,UAAU,2DAA2D;CAEnF;CACA,KAAK,MAAM,YAAY,WACrB,oBAAoB,UAAU,mBAAmB,SAAS,GAAG,EAAE;AACnE;;AAGA,SAAgB,oBACd,UACA,OACM;CACN,mBAAmB,SAAS,MAAM,GAAG,MAAM,OAAO;CAClD,IAAI,SAAS,YAAY,KAAA,GAAW,mBAAmB,SAAS,SAAS,GAAG,MAAM,UAAU;CAC5F,IAAI,SAAS,mBAAmB,KAAA,GAC9B,qBAAqB,SAAS,gBAAgB,GAAG,MAAM,iBAAiB;AAE5E;;AAGA,SAAgB,kBAAkB,KAAa,gBAA6C;CAC1F,IAAI,mBAAmB,KAAA,GAAW,OAAO;CACzC,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,GAAG;EACrB,MAAM,IAAI,IAAI,IAAI,cAAc;EAChC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE;CACnD,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -1,8 +1,8 @@
1
- import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.cjs";
1
+ import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, LinkPurpose, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.cjs";
2
2
  import { ActionVersionPolicy, RegisteredAction, RegistryActionInvoker } from "./actions.cjs";
3
3
  import { hashToBucket, murmur3 } from "./bucket.cjs";
4
4
  import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluateCondition, matchesWhere, relevantEvents } from "./condition.cjs";
5
5
  import { runJourney } from "./interpreter.cjs";
6
6
  import { scaleDurations, scaleSegmentWindows } from "./time-scale.cjs";
7
7
  import { nextWindowStart, resolveTimeZone } from "./time-window.cjs";
8
- export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
8
+ export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
@@ -1,8 +1,8 @@
1
- import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.mjs";
1
+ import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, LinkPurpose, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.mjs";
2
2
  import { ActionVersionPolicy, RegisteredAction, RegistryActionInvoker } from "./actions.mjs";
3
3
  import { hashToBucket, murmur3 } from "./bucket.mjs";
4
4
  import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluateCondition, matchesWhere, relevantEvents } from "./condition.mjs";
5
5
  import { runJourney } from "./interpreter.mjs";
6
6
  import { scaleDurations, scaleSegmentWindows } from "./time-scale.mjs";
7
7
  import { nextWindowStart, resolveTimeZone } from "./time-window.mjs";
8
- export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
8
+ export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
@@ -93,14 +93,19 @@ export interface OutboundMessage {
93
93
  /** What the host's observer asked the sender to add for measuring this copy (see `MessageTracking`). */
94
94
  tracking?: MessageTracking | undefined;
95
95
  }
96
+ /** What following a tracked email link does: redirect to it, or confirm an unsubscribe. */
97
+ export type LinkPurpose = 'link' | 'unsubscribe';
96
98
  /**
97
99
  * Measuring a copy, as the built-in senders apply it: an email's links are routed through
98
100
  * `link` and `pixel` is appended as a 1×1 image; a push carries `data` in its payload.
99
101
  * Each is optional, and a sender ignores what its channel has no use for.
100
102
  */
101
103
  export interface MessageTracking {
102
- /** The original `http(s)` URL in, the URL that records the click and redirects out. */
103
- link?: (url: string) => string;
104
+ /**
105
+ * The original `http(s)` URL in, the URL that records the click and redirects out — or, for the
106
+ * email's unsubscribe link, asks the recipient to confirm.
107
+ */
108
+ link?: (url: string, purpose: LinkPurpose) => string;
104
109
  /** Loaded when the email is opened. */
105
110
  pixel?: string;
106
111
  /** Extra string keys in the push payload. */
@@ -1 +1 @@
1
- {"version":3,"file":"ports.d.cts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;;;;;iBAQI;;EAEf,QAAQ;;EAER;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
1
+ {"version":3,"file":"ports.d.cts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;YAID;;;;;;iBAOK;;;;;EAKf,QAAQ,aAAa,SAAS;;EAE9B;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
@@ -93,14 +93,19 @@ export interface OutboundMessage {
93
93
  /** What the host's observer asked the sender to add for measuring this copy (see `MessageTracking`). */
94
94
  tracking?: MessageTracking | undefined;
95
95
  }
96
+ /** What following a tracked email link does: redirect to it, or confirm an unsubscribe. */
97
+ export type LinkPurpose = 'link' | 'unsubscribe';
96
98
  /**
97
99
  * Measuring a copy, as the built-in senders apply it: an email's links are routed through
98
100
  * `link` and `pixel` is appended as a 1×1 image; a push carries `data` in its payload.
99
101
  * Each is optional, and a sender ignores what its channel has no use for.
100
102
  */
101
103
  export interface MessageTracking {
102
- /** The original `http(s)` URL in, the URL that records the click and redirects out. */
103
- link?: (url: string) => string;
104
+ /**
105
+ * The original `http(s)` URL in, the URL that records the click and redirects out — or, for the
106
+ * email's unsubscribe link, asks the recipient to confirm.
107
+ */
108
+ link?: (url: string, purpose: LinkPurpose) => string;
104
109
  /** Loaded when the email is opened. */
105
110
  pixel?: string;
106
111
  /** Extra string keys in the push payload. */
@@ -1 +1 @@
1
- {"version":3,"file":"ports.d.mts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;;;;;iBAQI;;EAEf,QAAQ;;EAER;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
1
+ {"version":3,"file":"ports.d.mts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;YAID;;;;;;iBAOK;;;;;EAKf,QAAQ,aAAa,SAAS;;EAE9B;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ const require_dsl_action = require("./dsl/action.cjs");
4
4
  const require_dsl_refs = require("./dsl/refs.cjs");
5
5
  const require_dsl_template = require("./dsl/template.cjs");
6
6
  const require_dsl_condition = require("./dsl/condition.cjs");
7
+ const require_email = require("./email.cjs");
7
8
  const require_templates = require("./templates.cjs");
8
9
  const require_dsl_flow = require("./dsl/flow.cjs");
9
10
  const require_dsl_trigger = require("./dsl/trigger.cjs");
@@ -42,7 +43,11 @@ exports.WorkflowIR = require_ir.WorkflowIR;
42
43
  exports.action = require_dsl_action.action;
43
44
  exports.and = require_dsl_condition.and;
44
45
  exports.apnsEnvironment = require_config.apnsEnvironment;
46
+ exports.assertEmailAddress = require_email.assertEmailAddress;
47
+ exports.assertEmailConfig = require_email.assertEmailConfig;
48
+ exports.assertEmailMetadata = require_email.assertEmailMetadata;
45
49
  exports.assertSupportedSchema = require_templates.assertSupportedSchema;
50
+ exports.assertUnsubscribeUrl = require_email.assertUnsubscribeUrl;
46
51
  exports.between = require_dsl_condition.between;
47
52
  exports.canonicalJSON = require_hash.canonicalJSON;
48
53
  exports.checkEvent = require_contract.checkEvent;
@@ -57,7 +62,7 @@ exports.eq = require_dsl_condition.eq;
57
62
  exports.event = require_dsl_refs.event;
58
63
  exports.exists = require_dsl_condition.exists;
59
64
  exports.flow = require_dsl_flow.flow;
60
- exports.formatEmailAddress = require_config.formatEmailAddress;
65
+ exports.formatEmailAddress = require_email.formatEmailAddress;
61
66
  exports.fullHash = require_hash.fullHash;
62
67
  exports.gt = require_dsl_condition.gt;
63
68
  exports.gte = require_dsl_condition.gte;
@@ -71,6 +76,7 @@ exports.isPushTemplate = require_templates.isPushTemplate;
71
76
  exports.isSemanticChange = require_plan.isSemanticChange;
72
77
  exports.isSlackTemplate = require_templates.isSlackTemplate;
73
78
  exports.isTemplate = require_templates.isTemplate;
79
+ exports.isUnsubscribeLink = require_email.isUnsubscribeLink;
74
80
  exports.lookupTemplate = require_templates.lookupTemplate;
75
81
  exports.lt = require_dsl_condition.lt;
76
82
  exports.lte = require_dsl_condition.lte;
@@ -90,6 +96,7 @@ exports.registryValidator = require_templates.registryValidator;
90
96
  exports.renderDiscordContent = require_templates.renderDiscordContent;
91
97
  exports.renderPushContent = require_templates.renderPushContent;
92
98
  exports.renderSlackContent = require_templates.renderSlackContent;
99
+ exports.resolveEmail = require_email.resolveEmail;
93
100
  exports.resolveRuntime = require_runtime.resolveRuntime;
94
101
  exports.segment = require_dsl_condition.segment;
95
102
  exports.semanticHash = require_hash.semanticHash;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.cjs";
2
- import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.cjs";
2
+ import { EmailAddress, EmailConfig, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail } from "./email.cjs";
3
+ import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailMetadata, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.cjs";
3
4
  import { ActionContext, ActionHandler, ActionRef, action } from "./dsl/action.cjs";
4
5
  import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/base.cjs";
5
6
  import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./dsl/refs.cjs";
@@ -12,7 +13,7 @@ import { compileBundle } from "./dsl/bundle.cjs";
12
13
  import "./dsl/index.cjs";
13
14
  import { Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DeclaredOutput, EventPayload, ProfilePatch, checkEvent, defineContract, isContract } from "./contract.cjs";
14
15
  import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntime } from "./runtime.cjs";
15
- import { ApnsConfig, ConfigCompileError, DatabaseConfig, EmailAddress, EmailConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.cjs";
16
+ import { ApnsConfig, ConfigCompileError, DatabaseConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.cjs";
16
17
  import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.cjs";
17
18
  import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.cjs";
18
- export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
19
+ export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };