@shware/workflow 0.4.0 → 1.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 (90) hide show
  1. package/README.md +48 -38
  2. package/dist/cloudflare/channels.cjs +12 -14
  3. package/dist/cloudflare/channels.cjs.map +1 -1
  4. package/dist/cloudflare/channels.d.cts.map +1 -1
  5. package/dist/cloudflare/channels.d.mts.map +1 -1
  6. package/dist/cloudflare/channels.mjs +13 -15
  7. package/dist/cloudflare/channels.mjs.map +1 -1
  8. package/dist/config.cjs +61 -36
  9. package/dist/config.cjs.map +1 -1
  10. package/dist/config.d.cts +19 -15
  11. package/dist/config.d.cts.map +1 -1
  12. package/dist/config.d.mts +19 -15
  13. package/dist/config.d.mts.map +1 -1
  14. package/dist/config.mjs +61 -36
  15. package/dist/config.mjs.map +1 -1
  16. package/dist/dsl/action.cjs +1 -1
  17. package/dist/dsl/action.cjs.map +1 -1
  18. package/dist/dsl/action.d.cts +1 -1
  19. package/dist/dsl/action.d.mts +1 -1
  20. package/dist/dsl/action.mjs +1 -1
  21. package/dist/dsl/action.mjs.map +1 -1
  22. package/dist/dsl/bundle.cjs +3 -2
  23. package/dist/dsl/bundle.cjs.map +1 -1
  24. package/dist/dsl/bundle.d.cts +3 -2
  25. package/dist/dsl/bundle.d.cts.map +1 -1
  26. package/dist/dsl/bundle.d.mts +3 -2
  27. package/dist/dsl/bundle.d.mts.map +1 -1
  28. package/dist/dsl/bundle.mjs +3 -2
  29. package/dist/dsl/bundle.mjs.map +1 -1
  30. package/dist/dsl/condition.cjs +1 -1
  31. package/dist/dsl/condition.cjs.map +1 -1
  32. package/dist/dsl/condition.d.cts +1 -1
  33. package/dist/dsl/condition.d.mts +1 -1
  34. package/dist/dsl/condition.mjs +1 -1
  35. package/dist/dsl/condition.mjs.map +1 -1
  36. package/dist/dsl/flow.cjs +45 -17
  37. package/dist/dsl/flow.cjs.map +1 -1
  38. package/dist/dsl/flow.d.cts +30 -17
  39. package/dist/dsl/flow.d.cts.map +1 -1
  40. package/dist/dsl/flow.d.mts +30 -17
  41. package/dist/dsl/flow.d.mts.map +1 -1
  42. package/dist/dsl/flow.mjs +45 -18
  43. package/dist/dsl/flow.mjs.map +1 -1
  44. package/dist/dsl/index.cjs +0 -1
  45. package/dist/dsl/index.d.cts +2 -2
  46. package/dist/dsl/index.d.mts +2 -2
  47. package/dist/dsl/index.mjs +2 -2
  48. package/dist/dsl/template.cjs +2 -47
  49. package/dist/dsl/template.cjs.map +1 -1
  50. package/dist/dsl/template.d.cts +4 -54
  51. package/dist/dsl/template.d.cts.map +1 -1
  52. package/dist/dsl/template.d.mts +4 -54
  53. package/dist/dsl/template.d.mts.map +1 -1
  54. package/dist/dsl/template.mjs +3 -47
  55. package/dist/dsl/template.mjs.map +1 -1
  56. package/dist/dsl/workflow.cjs +1 -1
  57. package/dist/dsl/workflow.cjs.map +1 -1
  58. package/dist/dsl/workflow.d.cts +1 -1
  59. package/dist/dsl/workflow.d.mts +1 -1
  60. package/dist/dsl/workflow.mjs +1 -1
  61. package/dist/dsl/workflow.mjs.map +1 -1
  62. package/dist/index.cjs +5 -2
  63. package/dist/index.d.cts +3 -3
  64. package/dist/index.d.mts +3 -3
  65. package/dist/index.mjs +3 -3
  66. package/dist/push.cjs +3 -5
  67. package/dist/push.cjs.map +1 -1
  68. package/dist/push.d.cts +2 -2
  69. package/dist/push.d.cts.map +1 -1
  70. package/dist/push.d.mts +2 -2
  71. package/dist/push.d.mts.map +1 -1
  72. package/dist/push.mjs +4 -6
  73. package/dist/push.mjs.map +1 -1
  74. package/dist/react-email.cjs +5 -5
  75. package/dist/react-email.cjs.map +1 -1
  76. package/dist/react-email.d.cts +1 -1
  77. package/dist/react-email.d.cts.map +1 -1
  78. package/dist/react-email.d.mts +1 -1
  79. package/dist/react-email.d.mts.map +1 -1
  80. package/dist/react-email.mjs +6 -6
  81. package/dist/react-email.mjs.map +1 -1
  82. package/dist/templates.cjs +84 -29
  83. package/dist/templates.cjs.map +1 -1
  84. package/dist/templates.d.cts +49 -11
  85. package/dist/templates.d.cts.map +1 -1
  86. package/dist/templates.d.mts +49 -11
  87. package/dist/templates.d.mts.map +1 -1
  88. package/dist/templates.mjs +81 -30
  89. package/dist/templates.mjs.map +1 -1
  90. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"react-email.cjs","names":["parseTemplateProps","createElement","renderToReadableStream"],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type { EmailRenderer } from './cloudflare';\nimport { type EmailTemplate, parseTemplateProps } from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry maps keys to `emailTemplate({...})` definitions. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element, so hooks and context work. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(emails: Record<string, EmailTemplate>): EmailRenderer {\n return async (key, props) => {\n if (!Object.hasOwn(emails, key)) throw new Error(`unknown email template: ${key}`);\n const template = emails[key];\n const parsed = parseTemplateProps(template, key, props);\n const element = createElement(template.component as ComponentType<object>, parsed);\n return { subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,UACJ;AAEF,SAAgB,iBAAiB,QAAsD;CACrF,OAAO,OAAO,KAAK,UAAU;EAC3B,IAAI,CAAC,OAAO,OAAO,QAAQ,GAAG,GAAG,MAAM,IAAI,MAAM,2BAA2B,KAAK;EACjF,MAAM,WAAW,OAAO;EACxB,MAAM,SAASA,kBAAAA,mBAAmB,UAAU,KAAK,KAAK;EACtD,MAAM,WAAA,GAAUC,MAAAA,cAAAA,CAAc,SAAS,WAAoC,MAAM;EACjF,OAAO;GAAE,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CACpF;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,OAAA,GAAMC,iBAAAA,uBAAAA,CAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
1
+ {"version":3,"file":"react-email.cjs","names":["indexTemplates","lookupTemplate","parseTemplateProps","createElement","renderToReadableStream"],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type { EmailRenderer } from './cloudflare';\nimport {\n type EmailTemplate,\n indexTemplates,\n lookupTemplate,\n parseTemplateProps,\n} from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry holds `emailTemplate({...})` definitions, looked up by their `id`. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element, so hooks and context work. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>): EmailRenderer {\n const index = indexTemplates('email', emails);\n return async (id, props) => {\n const template = lookupTemplate(index, 'email', id);\n const parsed = parseTemplateProps(template, props);\n const element = createElement(template.component as ComponentType<object>, parsed);\n return { subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,UACJ;AAEF,SAAgB,iBAAiB,QAAgE;CAC/F,MAAM,QAAQA,kBAAAA,eAAe,SAAS,MAAM;CAC5C,OAAO,OAAO,IAAI,UAAU;EAC1B,MAAM,WAAWC,kBAAAA,eAAe,OAAO,SAAS,EAAE;EAClD,MAAM,SAASC,kBAAAA,mBAAmB,UAAU,KAAK;EACjD,MAAM,WAAA,GAAUC,MAAAA,cAAAA,CAAc,SAAS,WAAoC,MAAM;EACjF,OAAO;GAAE,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CACpF;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,OAAA,GAAMC,iBAAAA,uBAAAA,CAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
@@ -2,6 +2,6 @@ import { EmailTemplate } from "./templates.cjs";
2
2
  import { EmailRenderer } from "./cloudflare/senders.cjs";
3
3
  import "./cloudflare/index.cjs";
4
4
  //#region src/react-email.d.ts
5
- export declare function registryRenderer(emails: Record<string, EmailTemplate>): EmailRenderer;
5
+ export declare function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>): EmailRenderer;
6
6
  //#endregion
7
7
  //# sourceMappingURL=react-email.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react-email.d.cts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;;wBAwBgB,iBAAiB,QAAQ,eAAe,iBAAiB"}
1
+ {"version":3,"file":"react-email.d.cts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;;wBA6BgB,iBAAiB,QAAQ,SAAS,eAAe,kBAAkB"}
@@ -2,6 +2,6 @@ import { EmailTemplate } from "./templates.mjs";
2
2
  import { EmailRenderer } from "./cloudflare/senders.mjs";
3
3
  import "./cloudflare/index.mjs";
4
4
  //#region src/react-email.d.ts
5
- export declare function registryRenderer(emails: Record<string, EmailTemplate>): EmailRenderer;
5
+ export declare function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>): EmailRenderer;
6
6
  //#endregion
7
7
  //# sourceMappingURL=react-email.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react-email.d.mts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;;wBAwBgB,iBAAiB,QAAQ,eAAe,iBAAiB"}
1
+ {"version":3,"file":"react-email.d.mts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;;wBA6BgB,iBAAiB,QAAQ,SAAS,eAAe,kBAAkB"}
@@ -1,4 +1,4 @@
1
- import { parseTemplateProps } from "./templates.mjs";
1
+ import { indexTemplates, lookupTemplate, parseTemplateProps } from "./templates.mjs";
2
2
  import { createElement } from "react";
3
3
  import { renderToReadableStream } from "react-dom/server";
4
4
  //#region src/react-email.ts
@@ -7,7 +7,7 @@ import { renderToReadableStream } from "react-dom/server";
7
7
  * registry (`react-dom` is an optional peer dependency; import this subpath
8
8
  * only when you use it).
9
9
  *
10
- * The registry maps keys to `emailTemplate({...})` definitions. Props are
10
+ * The registry holds `emailTemplate({...})` definitions, looked up by their `id`. Props are
11
11
  * parsed against the template's schema before the component renders — a
12
12
  * failure throws TemplateValidationError and nothing renders — and the
13
13
  * component renders as an element, so hooks and context work. Rendering
@@ -19,10 +19,10 @@ import { renderToReadableStream } from "react-dom/server";
19
19
  */
20
20
  const DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
21
21
  function registryRenderer(emails) {
22
- return async (key, props) => {
23
- if (!Object.hasOwn(emails, key)) throw new Error(`unknown email template: ${key}`);
24
- const template = emails[key];
25
- const parsed = parseTemplateProps(template, key, props);
22
+ const index = indexTemplates("email", emails);
23
+ return async (id, props) => {
24
+ const template = lookupTemplate(index, "email", id);
25
+ const parsed = parseTemplateProps(template, props);
26
26
  const element = createElement(template.component, parsed);
27
27
  return {
28
28
  subject: template.subject,
@@ -1 +1 @@
1
- {"version":3,"file":"react-email.mjs","names":[],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type { EmailRenderer } from './cloudflare';\nimport { type EmailTemplate, parseTemplateProps } from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry maps keys to `emailTemplate({...})` definitions. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element, so hooks and context work. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(emails: Record<string, EmailTemplate>): EmailRenderer {\n return async (key, props) => {\n if (!Object.hasOwn(emails, key)) throw new Error(`unknown email template: ${key}`);\n const template = emails[key];\n const parsed = parseTemplateProps(template, key, props);\n const element = createElement(template.component as ComponentType<object>, parsed);\n return { subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,MAAM,UACJ;AAEF,SAAgB,iBAAiB,QAAsD;CACrF,OAAO,OAAO,KAAK,UAAU;EAC3B,IAAI,CAAC,OAAO,OAAO,QAAQ,GAAG,GAAG,MAAM,IAAI,MAAM,2BAA2B,KAAK;EACjF,MAAM,WAAW,OAAO;EACxB,MAAM,SAAS,mBAAmB,UAAU,KAAK,KAAK;EACtD,MAAM,UAAU,cAAc,SAAS,WAAoC,MAAM;EACjF,OAAO;GAAE,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CACpF;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,MAAM,uBAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
1
+ {"version":3,"file":"react-email.mjs","names":[],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type { EmailRenderer } from './cloudflare';\nimport {\n type EmailTemplate,\n indexTemplates,\n lookupTemplate,\n parseTemplateProps,\n} from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry holds `emailTemplate({...})` definitions, looked up by their `id`. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element, so hooks and context work. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>): EmailRenderer {\n const index = indexTemplates('email', emails);\n return async (id, props) => {\n const template = lookupTemplate(index, 'email', id);\n const parsed = parseTemplateProps(template, props);\n const element = createElement(template.component as ComponentType<object>, parsed);\n return { subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA0BA,MAAM,UACJ;AAEF,SAAgB,iBAAiB,QAAgE;CAC/F,MAAM,QAAQ,eAAe,SAAS,MAAM;CAC5C,OAAO,OAAO,IAAI,UAAU;EAC1B,MAAM,WAAW,eAAe,OAAO,SAAS,EAAE;EAClD,MAAM,SAAS,mBAAmB,UAAU,KAAK;EACjD,MAAM,UAAU,cAAc,SAAS,WAAoC,MAAM;EACjF,OAAO;GAAE,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CACpF;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,MAAM,uBAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
@@ -5,6 +5,7 @@ zod_mini = require_runtime.__toESM(zod_mini, 1);
5
5
  //#region src/templates.ts
6
6
  /** Declare a Discord webhook template. Throws when `props` is unsupported or `preview` does not parse. */
7
7
  function discordTemplate(definition) {
8
+ assertTemplateId(definition.id, "discordTemplate");
8
9
  assertSupportedSchema(definition.props, definition.title);
9
10
  parsePreview(definition.props, definition.preview, "discord", definition.title);
10
11
  return {
@@ -15,6 +16,7 @@ function discordTemplate(definition) {
15
16
  }
16
17
  /** Declare a Slack webhook template. Throws when `props` is unsupported or `preview` does not parse. */
17
18
  function slackTemplate(definition) {
19
+ assertTemplateId(definition.id, "slackTemplate");
18
20
  assertSupportedSchema(definition.props, definition.title);
19
21
  parsePreview(definition.props, definition.preview, "slack", definition.title);
20
22
  return {
@@ -25,6 +27,7 @@ function slackTemplate(definition) {
25
27
  }
26
28
  /** Declare an email template. Throws when `props` uses an unsupported schema or `preview` does not parse. */
27
29
  function emailTemplate(definition) {
30
+ assertTemplateId(definition.id, "emailTemplate");
28
31
  assertSupportedSchema(definition.props, definition.title);
29
32
  parsePreview(definition.props, definition.preview, "email", definition.title);
30
33
  return {
@@ -35,6 +38,7 @@ function emailTemplate(definition) {
35
38
  }
36
39
  /** Declare a push template. Throws when `props` uses an unsupported schema or `preview` does not parse. */
37
40
  function pushTemplate(definition) {
41
+ assertTemplateId(definition.id, "pushTemplate");
38
42
  assertSupportedSchema(definition.props, definition.title);
39
43
  parsePreview(definition.props, definition.preview, "push", definition.title);
40
44
  return {
@@ -43,6 +47,12 @@ function pushTemplate(definition) {
43
47
  ...definition
44
48
  };
45
49
  }
50
+ function assertTemplateId(id, factory) {
51
+ if (!isTemplateId(id)) throw new TypeError(`${factory}(): 'id' must be a non-empty string`);
52
+ }
53
+ function isTemplateId(id) {
54
+ return typeof id === "string" && id.trim() !== "";
55
+ }
46
56
  function isEmailTemplate(value) {
47
57
  return isMarked(value, "email-template");
48
58
  }
@@ -64,6 +74,44 @@ const CHANNEL_OF = {
64
74
  function isMarked(value, kind) {
65
75
  return typeof value === "object" && value !== null && value.kind === kind && value.version === 1;
66
76
  }
77
+ /** Any template definition, whatever its channel. */
78
+ function isTemplate(value) {
79
+ return typeof value === "object" && value !== null && Object.hasOwn(CHANNEL_OF, value.kind) && value.version === 1;
80
+ }
81
+ const IS_TEMPLATE = {
82
+ email: isEmailTemplate,
83
+ push: isPushTemplate,
84
+ discord: isDiscordTemplate,
85
+ slack: isSlackTemplate
86
+ };
87
+ /**
88
+ * A channel's registry by template id — how everything that turns an id back into a definition
89
+ * (the compiler, the validator, the renderers, the senders, the studio) looks it up. Throws when
90
+ * an entry is not a template of the channel, has no id, or shares its id with another entry: an id
91
+ * is registered once per channel, so two registrations (even of the same object) are an error,
92
+ * not a silent last-wins. Insertion order is the registry's.
93
+ */
94
+ function indexTemplates(channel, registry) {
95
+ const index = /* @__PURE__ */ new Map();
96
+ const paths = /* @__PURE__ */ new Map();
97
+ for (const [name, entry] of Object.entries(registry ?? {})) {
98
+ const path = `templates.${channel}.${name}`;
99
+ if (!IS_TEMPLATE[channel](entry)) throw new TypeError(`${path} is not a ${channel} template`);
100
+ const id = entry.id;
101
+ if (!isTemplateId(id)) throw new TypeError(`${path} has no id`);
102
+ const previous = paths.get(id);
103
+ if (previous !== void 0) throw new TypeError(`${channel} template id '${id}' is registered twice: ${previous} and ${path}`);
104
+ paths.set(id, path);
105
+ index.set(id, entry);
106
+ }
107
+ return index;
108
+ }
109
+ /** A registered template by id; throws naming the channel and id when there is none. */
110
+ function lookupTemplate(index, channel, id) {
111
+ const template = index.get(id);
112
+ if (template === void 0) throw new Error(`${channel} template '${id}' is not registered`);
113
+ return template;
114
+ }
67
115
  /**
68
116
  * A template refused its input (`phase: 'props'` — the component/content was
69
117
  * never called) or its content function returned something that is not a
@@ -94,10 +142,10 @@ function issuesOf(error) {
94
142
  * Parse message props against a template's schema and return the parsed value
95
143
  * — the only value anything downstream may use. Throws TemplateValidationError.
96
144
  */
97
- function parseTemplateProps(template, key, props) {
145
+ function parseTemplateProps(template, props) {
98
146
  const channel = CHANNEL_OF[template.kind];
99
147
  const result = zod_mini.safeParse(template.props, props);
100
- if (!result.success) throw new TemplateValidationError(channel, key, "props", issuesOf(result.error));
148
+ if (!result.success) throw new TemplateValidationError(channel, template.id, "props", issuesOf(result.error));
101
149
  return result.data;
102
150
  }
103
151
  const pushContentSchema = zod_mini.strictObject({
@@ -107,11 +155,11 @@ const pushContentSchema = zod_mini.strictObject({
107
155
  url: zod_mini.optional(zod_mini.string())
108
156
  });
109
157
  /** Run a push template's content over parsed props and check the notification it returns. */
110
- function renderPushContent(template, key, props) {
111
- const parsed = parseTemplateProps(template, key, props);
158
+ function renderPushContent(template, props) {
159
+ const parsed = parseTemplateProps(template, props);
112
160
  const output = template.content(parsed);
113
161
  const result = zod_mini.safeParse(pushContentSchema, output);
114
- if (!result.success) throw new TemplateValidationError("push", key, "content", issuesOf(result.error));
162
+ if (!result.success) throw new TemplateValidationError("push", template.id, "content", issuesOf(result.error));
115
163
  return result.data;
116
164
  }
117
165
  /**
@@ -128,21 +176,24 @@ const slackContentSchema = zod_mini.looseObject({
128
176
  blocks: zod_mini.optional(zod_mini.array(zod_mini.looseObject({ type: zod_mini.string() })))
129
177
  });
130
178
  /** Run a Discord template's content over parsed props and check the message it returns. */
131
- function renderDiscordContent(template, key, props) {
132
- const output = template.content(parseTemplateProps(template, key, props));
179
+ function renderDiscordContent(template, props) {
180
+ const output = template.content(parseTemplateProps(template, props));
133
181
  const result = zod_mini.safeParse(discordContentSchema, output);
134
182
  const empty = result.success && !result.data.content && (result.data.embeds?.length ?? 0) === 0 && output.poll === void 0 && (output.components?.length ?? 0) === 0;
135
- if (!result.success || empty) throw new TemplateValidationError("discord", key, "content", result.success ? [{
136
- path: "",
137
- message: "a Discord message needs content, an embed, a poll or components"
138
- }] : issuesOf(result.error));
183
+ if (!result.success || empty) {
184
+ const issues = result.success ? [{
185
+ path: "",
186
+ message: "a Discord message needs content, an embed, a poll or components"
187
+ }] : issuesOf(result.error);
188
+ throw new TemplateValidationError("discord", template.id, "content", issues);
189
+ }
139
190
  return output;
140
191
  }
141
192
  /** Run a Slack template's content over parsed props and check the message it returns. */
142
- function renderSlackContent(template, key, props) {
143
- const output = template.content(parseTemplateProps(template, key, props));
193
+ function renderSlackContent(template, props) {
194
+ const output = template.content(parseTemplateProps(template, props));
144
195
  const result = zod_mini.safeParse(slackContentSchema, output);
145
- if (!result.success) throw new TemplateValidationError("slack", key, "content", issuesOf(result.error));
196
+ if (!result.success) throw new TemplateValidationError("slack", template.id, "content", issuesOf(result.error));
146
197
  return output;
147
198
  }
148
199
  function parsePreview(schema, preview, channel, title) {
@@ -202,40 +253,44 @@ function rejectCustomChecks(def, where, fail) {
202
253
  * stops the send before any device; Discord and Slack content likewise. Channels without a
203
254
  * registry here (sms, …) pass through to their app-supplied senders.
204
255
  */
205
- function registryValidator(registries) {
256
+ function registryValidator(registry) {
257
+ const email = indexTemplates("email", registry.email);
258
+ const push = indexTemplates("push", registry.push);
259
+ const discord = indexTemplates("discord", registry.discord);
260
+ const slack = indexTemplates("slack", registry.slack);
206
261
  return { validate({ channel, template, props }) {
207
- if (channel === "email") return parseTemplateProps(lookup(registries.email, "email", template), template, props);
262
+ if (channel === "email") return parseTemplateProps(lookupTemplate(email, "email", template), props);
208
263
  if (channel === "push") {
209
- const entry = lookup(registries.push, "push", template);
210
- renderPushContent(entry, template, props);
211
- return parseTemplateProps(entry, template, props);
264
+ const entry = lookupTemplate(push, "push", template);
265
+ renderPushContent(entry, props);
266
+ return parseTemplateProps(entry, props);
212
267
  }
213
268
  if (channel === "discord") {
214
- const entry = lookup(registries.discord, "discord", template);
215
- renderDiscordContent(entry, template, props);
216
- return parseTemplateProps(entry, template, props);
269
+ const entry = lookupTemplate(discord, "discord", template);
270
+ renderDiscordContent(entry, props);
271
+ return parseTemplateProps(entry, props);
217
272
  }
218
273
  if (channel === "slack") {
219
- const entry = lookup(registries.slack, "slack", template);
220
- renderSlackContent(entry, template, props);
221
- return parseTemplateProps(entry, template, props);
274
+ const entry = lookupTemplate(slack, "slack", template);
275
+ renderSlackContent(entry, props);
276
+ return parseTemplateProps(entry, props);
222
277
  }
223
278
  return props;
224
279
  } };
225
280
  }
226
- function lookup(registry, channel, key) {
227
- if (registry === void 0 || !Object.hasOwn(registry, key)) throw new Error(`${channel} template '${key}' is not registered`);
228
- return registry[key];
229
- }
230
281
  //#endregion
282
+ exports.CHANNEL_OF = CHANNEL_OF;
231
283
  exports.TemplateValidationError = TemplateValidationError;
232
284
  exports.assertSupportedSchema = assertSupportedSchema;
233
285
  exports.discordTemplate = discordTemplate;
234
286
  exports.emailTemplate = emailTemplate;
287
+ exports.indexTemplates = indexTemplates;
235
288
  exports.isDiscordTemplate = isDiscordTemplate;
236
289
  exports.isEmailTemplate = isEmailTemplate;
237
290
  exports.isPushTemplate = isPushTemplate;
238
291
  exports.isSlackTemplate = isSlackTemplate;
292
+ exports.isTemplate = isTemplate;
293
+ exports.lookupTemplate = lookupTemplate;
239
294
  exports.parseTemplateProps = parseTemplateProps;
240
295
  exports.pushTemplate = pushTemplate;
241
296
  exports.registryValidator = registryValidator;
@@ -1 +1 @@
1
- {"version":3,"file":"templates.cjs","names":["z"],"sources":["../src/templates.ts"],"sourcesContent":["import type { Block, KnownBlock } from '@slack/types';\nimport type { RESTPostAPIWebhookWithTokenJSONBody } from 'discord-api-types/v10';\nimport * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport type { ScalarIR } from './ir';\n\n/**\n * Template definitions — `emailTemplate`, `pushTemplate`, `discordTemplate` and `slackTemplate`. One\n * object per template, and its Zod `props` schema is the only source of the\n * props type: the DSL's `.email()/.push()` arguments, the `preview`, and the\n * `component`/`content` parameter are all derived from it, and every render\n * parses the props against it first (types alone check nothing at run time).\n *\n * Kept free of React and of any renderer: the definitions are plain data plus\n * a function, so the DSL, the Worker and the studio can all import them; the\n * React rendering lives in `@shware/workflow/react-email`.\n *\n * Supported schemas (v1): a strict object (`z.strictObject`) whose fields are\n * string / number / boolean — optionally as enums or literals, optionally\n * `.optional()`, with the built-in checks (min, email, …). That is exactly\n * what IR message props can carry, and it keeps input and output identical, so\n * producer, studio and sender never disagree about a transformed value.\n * Anything else (nested objects, arrays, null, default/transform/coerce,\n * custom refinements) is rejected when the template is declared.\n */\n\n/** A strict Zod object schema of scalar fields — the accepted shape of `props`. */\nexport type PropsSchema = core.$ZodObject;\n\n/** What a push template's `content` returns: the notification itself. */\nexport interface PushContent {\n title: string;\n body: string;\n /** Rich image URL (iOS attachment / Android BigPicture). */\n image?: string;\n /** Where a tap leads: the page a browser push opens (the site's root when absent). */\n url?: string;\n}\n\n/** A Discord webhook message (`discord-api-types`): content, embeds, username, allowed mentions… */\nexport type DiscordContent = RESTPostAPIWebhookWithTokenJSONBody;\n\n/** A Slack incoming-webhook message: the fallback `text` and, usually, Block Kit `blocks`. */\nexport interface SlackContent {\n /** Shown in notifications and wherever blocks cannot be; required even with blocks. */\n text: string;\n blocks?: (KnownBlock | Block)[];\n username?: string;\n icon_emoji?: string;\n icon_url?: string;\n mrkdwn?: boolean;\n unfurl_links?: boolean;\n unfurl_media?: boolean;\n}\n\n/** The channels a template can be declared for. */\nexport type TemplateChannel = 'email' | 'push' | 'discord' | 'slack';\n\ninterface TemplateBase<S extends PropsSchema> {\n /** Display name in the studio (not the email subject, not the notification title). */\n readonly title: string;\n readonly description?: string | undefined;\n readonly props: S;\n /** Sample props for the studio; parsed against `props` when the template is declared. */\n readonly preview: core.input<S>;\n}\n\nexport interface EmailTemplate<S extends PropsSchema = PropsSchema> extends TemplateBase<S> {\n readonly kind: 'email-template';\n readonly version: 1;\n readonly subject: string;\n /** The email body: a component over the parsed props (rendered by `@shware/workflow/react-email`). */\n readonly component: (props: Readonly<core.output<S>>) => unknown;\n}\n\nexport interface PushTemplate<S extends PropsSchema = PropsSchema> extends TemplateBase<S> {\n readonly kind: 'push-template';\n readonly version: 1;\n /** The notification, from the parsed props. Synchronous and pure; its result is checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => PushContent;\n}\n\n/**\n * A message posted to a team channel through an incoming webhook (Discord, Slack). The webhook\n * URL is a secret, so it lives in the Worker's environment and the template reads it from there\n * at send time: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`, with `Env` the host's\n * zod-validated environment (`journeyWorker({ env: schema })`). The studio never calls it.\n */\ninterface WebhookTemplateBase<S extends PropsSchema, Env> extends TemplateBase<S> {\n readonly version: 1;\n // A method, so a template typed by the host's Env still fits a registry of any Env\n webhook(env: Env): string | undefined;\n}\n\nexport interface DiscordTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'discord-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => DiscordContent;\n}\n\nexport interface SlackTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'slack-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => SlackContent;\n}\n\n/** Strips a Promise return out of the accepted callback types: async content is rejected at the type level. */\ntype Sync<T> = T extends Promise<unknown> ? never : T;\n\nexport interface EmailTemplateDefinition<S extends PropsSchema> {\n title: string;\n description?: string;\n props: S;\n subject: string;\n preview: NoInfer<core.input<S>>;\n component: (props: NoInfer<Readonly<core.output<S>>>) => unknown;\n}\n\nexport interface PushTemplateDefinition<S extends PropsSchema, R> {\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<R> & PushContent;\n}\n\nexport interface WebhookTemplateDefinition<S extends PropsSchema, Env, C> {\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n /** The webhook URL, read from the Worker's environment when a message is sent. */\n webhook: (env: Env) => string | undefined;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<C>;\n}\n\n/** Declare a Discord webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function discordTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, DiscordContent>\n): DiscordTemplate<S, Env> {\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'discord', definition.title);\n return { kind: 'discord-template', version: 1, ...definition };\n}\n\n/** Declare a Slack webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function slackTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, SlackContent>\n): SlackTemplate<S, Env> {\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'slack', definition.title);\n return { kind: 'slack-template', version: 1, ...definition };\n}\n\n/** Declare an email template. Throws when `props` uses an unsupported schema or `preview` does not parse. */\nexport function emailTemplate<S extends PropsSchema>(\n definition: EmailTemplateDefinition<S>\n): EmailTemplate<S> {\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'email', definition.title);\n return { kind: 'email-template', version: 1, ...definition };\n}\n\n/** Declare a push template. Throws when `props` uses an unsupported schema or `preview` does not parse. */\nexport function pushTemplate<S extends PropsSchema, R extends PushContent>(\n definition: PushTemplateDefinition<S, R>\n): PushTemplate<S> {\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'push', definition.title);\n return { kind: 'push-template', version: 1, ...definition } as unknown as PushTemplate<S>;\n}\n\nexport function isEmailTemplate(value: unknown): value is EmailTemplate {\n return isMarked(value, 'email-template');\n}\n\nexport function isPushTemplate(value: unknown): value is PushTemplate {\n return isMarked(value, 'push-template');\n}\n\nexport function isDiscordTemplate(value: unknown): value is DiscordTemplate {\n return isMarked(value, 'discord-template');\n}\n\nexport function isSlackTemplate(value: unknown): value is SlackTemplate {\n return isMarked(value, 'slack-template');\n}\n\n/** Any declared template. */\nexport type AnyTemplate = EmailTemplate | PushTemplate | DiscordTemplate | SlackTemplate;\n\nconst CHANNEL_OF: Record<AnyTemplate['kind'], TemplateChannel> = {\n 'email-template': 'email',\n 'push-template': 'push',\n 'discord-template': 'discord',\n 'slack-template': 'slack',\n};\n\nfunction isMarked(value: unknown, kind: string): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === kind &&\n (value as { version?: unknown }).version === 1\n );\n}\n\n/* ------------------------------- validation -------------------------------- */\n\n/** One failed field, with the dotted path the studio and the logs show. */\nexport interface TemplateIssue {\n path: string;\n message: string;\n}\n\n/**\n * A template refused its input (`phase: 'props'` — the component/content was\n * never called) or its content function returned something that is not a\n * notification (`phase: 'content'` — called, but nothing is sent). Never a\n * provider failure: a sender must not read it as a dead device token.\n */\nexport class TemplateValidationError extends Error {\n override readonly name = 'TemplateValidationError';\n constructor(\n readonly channel: TemplateChannel,\n readonly template: string,\n readonly phase: 'props' | 'content',\n readonly issues: TemplateIssue[]\n ) {\n super(\n `${channel} template '${template}' ${phase === 'props' ? 'rejected its props' : 'returned invalid content'}: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\nfunction issuesOf(error: core.$ZodError): TemplateIssue[] {\n return error.issues.map((issue) => ({\n path: issue.path.map(String).join('.'),\n message: issue.message,\n }));\n}\n\n/**\n * Parse message props against a template's schema and return the parsed value\n * — the only value anything downstream may use. Throws TemplateValidationError.\n */\nexport function parseTemplateProps<S extends PropsSchema>(\n template: EmailTemplate<S> | PushTemplate<S> | DiscordTemplate<S> | SlackTemplate<S>,\n key: string,\n props: Record<string, unknown>\n): core.output<S> {\n const channel = CHANNEL_OF[template.kind];\n const result = z.safeParse(template.props, props);\n if (!result.success) {\n throw new TemplateValidationError(channel, key, 'props', issuesOf(result.error));\n }\n return result.data;\n}\n\nconst pushContentSchema = z.strictObject({\n title: z.string(),\n body: z.string(),\n image: z.optional(z.string()),\n url: z.optional(z.string()),\n});\n\n/** Run a push template's content over parsed props and check the notification it returns. */\nexport function renderPushContent(\n template: PushTemplate,\n key: string,\n props: Record<string, unknown>\n): PushContent {\n const parsed = parseTemplateProps(template, key, props);\n const output: unknown = template.content(parsed);\n // A Promise (async content smuggled past the types) is an object without title/body: refused here\n const result = z.safeParse(pushContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('push', key, 'content', issuesOf(result.error));\n }\n return result.data;\n}\n\n/**\n * What a Discord webhook accepts at all: something to show (content, an embed, a poll or\n * components) and content within Discord's 2000 characters. The rest of the body is Discord's to\n * judge; its types come from `discord-api-types`.\n */\nconst discordContentSchema = z.looseObject({\n content: z.optional(z.string().check(z.maxLength(2000))),\n embeds: z.optional(z.array(z.looseObject({}))),\n});\n\nconst slackContentSchema = z.looseObject({\n text: z.string().check(z.minLength(1)),\n blocks: z.optional(z.array(z.looseObject({ type: z.string() }))),\n});\n\n/** Run a Discord template's content over parsed props and check the message it returns. */\nexport function renderDiscordContent(\n template: DiscordTemplate,\n key: string,\n props: Record<string, unknown>\n): DiscordContent {\n const output: unknown = template.content(parseTemplateProps(template, key, props));\n const result = z.safeParse(discordContentSchema, output);\n const empty =\n result.success &&\n !result.data.content &&\n (result.data.embeds?.length ?? 0) === 0 &&\n (output as { poll?: unknown; components?: unknown[] }).poll === undefined &&\n ((output as { components?: unknown[] }).components?.length ?? 0) === 0;\n if (!result.success || empty) {\n const issues = result.success\n ? [{ path: '', message: 'a Discord message needs content, an embed, a poll or components' }]\n : issuesOf(result.error);\n throw new TemplateValidationError('discord', key, 'content', issues);\n }\n return output as DiscordContent;\n}\n\n/** Run a Slack template's content over parsed props and check the message it returns. */\nexport function renderSlackContent(\n template: SlackTemplate,\n key: string,\n props: Record<string, unknown>\n): SlackContent {\n const output: unknown = template.content(parseTemplateProps(template, key, props));\n const result = z.safeParse(slackContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('slack', key, 'content', issuesOf(result.error));\n }\n return output as SlackContent;\n}\n\nfunction parsePreview(\n schema: PropsSchema,\n preview: unknown,\n channel: TemplateChannel,\n title: string\n) {\n const result = z.safeParse(schema, preview);\n if (!result.success) {\n throw new TemplateValidationError(\n channel,\n title,\n 'props',\n issuesOf(result.error).map((i) => Object.assign(i, { message: `preview: ${i.message}` }))\n );\n }\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\nconst SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'enum']);\n\n/** Throws when `schema` is outside the v1 subset (see the header). */\nexport function assertSupportedSchema(schema: unknown, where: string): void {\n const fail = (reason: string): never => {\n throw new TypeError(`template '${where}': unsupported props schema — ${reason}`);\n };\n const def = (schema as core.$ZodType | undefined)?._zod?.def;\n if (def?.type !== 'object') fail('props must be a z.strictObject({...})');\n const object = def as core.$ZodObjectDef;\n if (object.catchall?._zod.def.type !== 'never') {\n fail(\n 'props must be strict (z.strictObject): unknown fields would be dropped or passed through'\n );\n }\n rejectCustomChecks(object, where, fail);\n for (const [field, value] of Object.entries(object.shape)) {\n let fieldDef = value._zod.def;\n if (fieldDef.type === 'optional') {\n // The wrapper carries checks of its own (`.optional().overwrite(...)`): refuse them too\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n fieldDef = (fieldDef as core.$ZodOptionalDef).innerType._zod.def;\n }\n if (fieldDef.type === 'literal') {\n const values = (fieldDef as core.$ZodLiteralDef<core.util.Literal>).values;\n if (!values.every((v) => ['string', 'number', 'boolean'].includes(typeof v))) {\n fail(`field '${field}' is a literal of a non-scalar value`);\n }\n } else if (!SCALAR_TYPES.has(fieldDef.type)) {\n fail(\n `field '${field}' is ${fieldDef.type}; only string, number, boolean, enum and literal fields (optionally .optional()) are supported`\n );\n }\n if ((fieldDef as { coerce?: boolean }).coerce === true) {\n fail(`field '${field}' coerces its input (z.coerce), which would change what the props mean`);\n }\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n }\n}\n\n/**\n * Checks the subset refuses: `custom` runs arbitrary code (`.refine()`), and\n * `overwrite` changes the value (`.overwrite()`, `.trim()`, `.toLowerCase()`…)\n * — the props are parsed more than once on the way to a send (the validator,\n * then the renderer), so a value-changing check would apply twice and the\n * email would differ from the preview.\n */\nfunction rejectCustomChecks(def: core.$ZodTypeDef, where: string, fail: (reason: string) => never) {\n for (const check of def.checks ?? []) {\n if (check._zod.def.check === 'custom') {\n fail(`'${where}' uses a custom refinement, which the template subset does not support`);\n }\n if (check._zod.def.check === 'overwrite') {\n fail(\n `'${where}' changes the value (.overwrite, .trim, .toLowerCase, …), which the template subset does not support`\n );\n }\n }\n}\n\n/** The scalar props a template accepts, as the IR resolves them before parsing. */\nexport type ResolvedProps = Record<string, ScalarIR | undefined>;\n\n/**\n * The engine's MessageValidator over the template registries: the message's\n * props are parsed by its template (the parsed value is what gets sent); a\n * push's content is also run once and checked, so an invalid notification\n * stops the send before any device; Discord and Slack content likewise. Channels without a\n * registry here (sms, …) pass through to their app-supplied senders.\n */\nexport function registryValidator(registries: {\n email?: Record<string, EmailTemplate> | undefined;\n push?: Record<string, PushTemplate> | undefined;\n discord?: Readonly<Record<string, DiscordTemplate<PropsSchema, never>>> | undefined;\n slack?: Readonly<Record<string, SlackTemplate<PropsSchema, never>>> | undefined;\n}): {\n validate(message: { channel: string; template: string; props: ResolvedProps }): ResolvedProps;\n} {\n return {\n validate({ channel, template, props }) {\n if (channel === 'email') {\n return parseTemplateProps(\n lookup(registries.email, 'email', template),\n template,\n props\n ) as ResolvedProps;\n }\n if (channel === 'push') {\n const entry = lookup(registries.push, 'push', template);\n renderPushContent(entry, template, props);\n return parseTemplateProps(entry, template, props) as ResolvedProps;\n }\n if (channel === 'discord') {\n const entry = lookup(registries.discord, 'discord', template) as DiscordTemplate;\n renderDiscordContent(entry, template, props);\n return parseTemplateProps(entry, template, props) as ResolvedProps;\n }\n if (channel === 'slack') {\n const entry = lookup(registries.slack, 'slack', template) as SlackTemplate;\n renderSlackContent(entry, template, props);\n return parseTemplateProps(entry, template, props) as ResolvedProps;\n }\n return props;\n },\n };\n}\n\nfunction lookup<T>(registry: Record<string, T> | undefined, channel: string, key: string): T {\n if (registry === undefined || !Object.hasOwn(registry, key)) {\n throw new Error(`${channel} template '${key}' is not registered`);\n }\n return registry[key];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"templates.cjs","names":["z"],"sources":["../src/templates.ts"],"sourcesContent":["import type { Block, KnownBlock } from '@slack/types';\nimport type { RESTPostAPIWebhookWithTokenJSONBody } from 'discord-api-types/v10';\nimport * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport type { ScalarIR } from './ir';\n\n/**\n * Template definitions — `emailTemplate`, `pushTemplate`, `discordTemplate` and `slackTemplate`. One\n * object per template, identified by its own `id` (unique per channel): workflows take the\n * definition itself (`.email(welcome, props)`), the config registers it, and the IR, the manifest,\n * the senders and the studio all find it by that id. Its Zod `props` schema is the only source of the\n * props type: the DSL's `.email()/.push()` arguments, the `preview`, and the\n * `component`/`content` parameter are all derived from it, and every render\n * parses the props against it first (types alone check nothing at run time).\n *\n * Kept free of React and of any renderer: the definitions are plain data plus\n * a function, so the DSL, the Worker and the studio can all import them; the\n * React rendering lives in `@shware/workflow/react-email`.\n *\n * Supported schemas (v1): a strict object (`z.strictObject`) whose fields are\n * string / number / boolean — optionally as enums or literals, optionally\n * `.optional()`, with the built-in checks (min, email, …). That is exactly\n * what IR message props can carry, and it keeps input and output identical, so\n * producer, studio and sender never disagree about a transformed value.\n * Anything else (nested objects, arrays, null, default/transform/coerce,\n * custom refinements) is rejected when the template is declared.\n */\n\n/** A strict Zod object schema of scalar fields — the accepted shape of `props`. */\nexport type PropsSchema = core.$ZodObject;\n\n/** What a push template's `content` returns: the notification itself. */\nexport interface PushContent {\n title: string;\n body: string;\n /** Rich image URL (iOS attachment / Android BigPicture). */\n image?: string;\n /** Where a tap leads: the page a browser push opens (the site's root when absent). */\n url?: string;\n}\n\n/** A Discord webhook message (`discord-api-types`): content, embeds, username, allowed mentions… */\nexport type DiscordContent = RESTPostAPIWebhookWithTokenJSONBody;\n\n/** A Slack incoming-webhook message: the fallback `text` and, usually, Block Kit `blocks`. */\nexport interface SlackContent {\n /** Shown in notifications and wherever blocks cannot be; required even with blocks. */\n text: string;\n blocks?: (KnownBlock | Block)[];\n username?: string;\n icon_emoji?: string;\n icon_url?: string;\n mrkdwn?: boolean;\n unfurl_links?: boolean;\n unfurl_media?: boolean;\n}\n\n/** The channels a template can be declared for. */\nexport type TemplateChannel = 'email' | 'push' | 'discord' | 'slack';\n\ninterface TemplateBase<S extends PropsSchema> {\n /**\n * Wire identity, unique per channel: message nodes, the template manifest, every send and the\n * studio's preview links name the template by it. Not something to rename for readability —\n * `title` is; the property a registry holds it under is only a JavaScript name.\n */\n readonly id: string;\n /** Display name in the studio (not the email subject, not the notification title). */\n readonly title: string;\n readonly description?: string | undefined;\n readonly props: S;\n /** Sample props for the studio; parsed against `props` when the template is declared. */\n readonly preview: core.input<S>;\n}\n\nexport interface EmailTemplate<S extends PropsSchema = PropsSchema> extends TemplateBase<S> {\n readonly kind: 'email-template';\n readonly version: 1;\n readonly subject: string;\n /** The email body: a component over the parsed props (rendered by `@shware/workflow/react-email`). */\n readonly component: (props: Readonly<core.output<S>>) => unknown;\n}\n\nexport interface PushTemplate<S extends PropsSchema = PropsSchema> extends TemplateBase<S> {\n readonly kind: 'push-template';\n readonly version: 1;\n /** The notification, from the parsed props. Synchronous and pure; its result is checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => PushContent;\n}\n\n/**\n * A message posted to a team channel through an incoming webhook (Discord, Slack). The webhook\n * URL is a secret, so it lives in the Worker's environment and the template reads it from there\n * at send time: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`, with `Env` the host's\n * zod-validated environment (`journeyWorker({ env: schema })`). The studio never calls it.\n */\ninterface WebhookTemplateBase<S extends PropsSchema, Env> extends TemplateBase<S> {\n readonly version: 1;\n // A method, so a template typed by the host's Env still fits a registry of any Env\n webhook(env: Env): string | undefined;\n}\n\nexport interface DiscordTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'discord-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => DiscordContent;\n}\n\nexport interface SlackTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'slack-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => SlackContent;\n}\n\n/** Strips a Promise return out of the accepted callback types: async content is rejected at the type level. */\ntype Sync<T> = T extends Promise<unknown> ? never : T;\n\nexport interface EmailTemplateDefinition<S extends PropsSchema> {\n id: string;\n title: string;\n description?: string;\n props: S;\n subject: string;\n preview: NoInfer<core.input<S>>;\n component: (props: NoInfer<Readonly<core.output<S>>>) => unknown;\n}\n\nexport interface PushTemplateDefinition<S extends PropsSchema, R> {\n id: string;\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<R> & PushContent;\n}\n\nexport interface WebhookTemplateDefinition<S extends PropsSchema, Env, C> {\n id: string;\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n /** The webhook URL, read from the Worker's environment when a message is sent. */\n webhook: (env: Env) => string | undefined;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<C>;\n}\n\n/** Declare a Discord webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function discordTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, DiscordContent>\n): DiscordTemplate<S, Env> {\n assertTemplateId(definition.id, 'discordTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'discord', definition.title);\n return { kind: 'discord-template', version: 1, ...definition };\n}\n\n/** Declare a Slack webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function slackTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, SlackContent>\n): SlackTemplate<S, Env> {\n assertTemplateId(definition.id, 'slackTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'slack', definition.title);\n return { kind: 'slack-template', version: 1, ...definition };\n}\n\n/** Declare an email template. Throws when `props` uses an unsupported schema or `preview` does not parse. */\nexport function emailTemplate<S extends PropsSchema>(\n definition: EmailTemplateDefinition<S>\n): EmailTemplate<S> {\n assertTemplateId(definition.id, 'emailTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'email', definition.title);\n return { kind: 'email-template', version: 1, ...definition };\n}\n\n/** Declare a push template. Throws when `props` uses an unsupported schema or `preview` does not parse. */\nexport function pushTemplate<S extends PropsSchema, R extends PushContent>(\n definition: PushTemplateDefinition<S, R>\n): PushTemplate<S> {\n assertTemplateId(definition.id, 'pushTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'push', definition.title);\n return { kind: 'push-template', version: 1, ...definition } as unknown as PushTemplate<S>;\n}\n\nfunction assertTemplateId(id: unknown, factory: string): void {\n if (!isTemplateId(id)) {\n throw new TypeError(`${factory}(): 'id' must be a non-empty string`);\n }\n}\n\nfunction isTemplateId(id: unknown): id is string {\n return typeof id === 'string' && id.trim() !== '';\n}\n\nexport function isEmailTemplate(value: unknown): value is EmailTemplate {\n return isMarked(value, 'email-template');\n}\n\nexport function isPushTemplate(value: unknown): value is PushTemplate {\n return isMarked(value, 'push-template');\n}\n\nexport function isDiscordTemplate(value: unknown): value is DiscordTemplate {\n return isMarked(value, 'discord-template');\n}\n\nexport function isSlackTemplate(value: unknown): value is SlackTemplate {\n return isMarked(value, 'slack-template');\n}\n\n/** Any declared template. */\nexport type AnyTemplate = EmailTemplate | PushTemplate | DiscordTemplate | SlackTemplate;\n\nexport const CHANNEL_OF: Record<AnyTemplate['kind'], TemplateChannel> = {\n 'email-template': 'email',\n 'push-template': 'push',\n 'discord-template': 'discord',\n 'slack-template': 'slack',\n};\n\nfunction isMarked(value: unknown, kind: string): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === kind &&\n (value as { version?: unknown }).version === 1\n );\n}\n\n/** Any template definition, whatever its channel. */\nexport function isTemplate(value: unknown): value is AnyTemplate {\n return (\n typeof value === 'object' &&\n value !== null &&\n Object.hasOwn(CHANNEL_OF, (value as { kind?: unknown }).kind as string) &&\n (value as { version?: unknown }).version === 1\n );\n}\n\n/* --------------------------------- registry -------------------------------- */\n\n/**\n * The template registry: per channel, the definitions the project registers. Each channel is a\n * plain object whose property names are JavaScript names only — a template is identified by its\n * own `id` (`indexTemplates`).\n */\nexport interface TemplateRegistry {\n readonly email?: Readonly<Record<string, EmailTemplate>>;\n readonly push?: Readonly<Record<string, PushTemplate>>;\n /** Discord webhook templates, each typed by the host's Env its `webhook` reads. */\n readonly discord?: Readonly<Record<string, DiscordTemplate<PropsSchema, never>>>;\n readonly slack?: Readonly<Record<string, SlackTemplate<PropsSchema, never>>>;\n}\n\n/** The template a channel's registry holds. */\nexport interface ChannelTemplate {\n email: EmailTemplate;\n push: PushTemplate;\n discord: DiscordTemplate;\n slack: SlackTemplate;\n}\n\nconst IS_TEMPLATE: Record<TemplateChannel, (value: unknown) => boolean> = {\n email: isEmailTemplate,\n push: isPushTemplate,\n discord: isDiscordTemplate,\n slack: isSlackTemplate,\n};\n\n/**\n * A channel's registry by template id — how everything that turns an id back into a definition\n * (the compiler, the validator, the renderers, the senders, the studio) looks it up. Throws when\n * an entry is not a template of the channel, has no id, or shares its id with another entry: an id\n * is registered once per channel, so two registrations (even of the same object) are an error,\n * not a silent last-wins. Insertion order is the registry's.\n */\nexport function indexTemplates<C extends TemplateChannel>(\n channel: C,\n registry: Readonly<Record<string, unknown>> | undefined\n): ReadonlyMap<string, ChannelTemplate[C]> {\n const index = new Map<string, ChannelTemplate[C]>();\n const paths = new Map<string, string>();\n for (const [name, entry] of Object.entries(registry ?? {})) {\n const path = `templates.${channel}.${name}`;\n if (!IS_TEMPLATE[channel](entry)) {\n throw new TypeError(`${path} is not a ${channel} template`);\n }\n const id = (entry as AnyTemplate).id;\n if (!isTemplateId(id)) throw new TypeError(`${path} has no id`);\n const previous = paths.get(id);\n if (previous !== undefined) {\n throw new TypeError(\n `${channel} template id '${id}' is registered twice: ${previous} and ${path}`\n );\n }\n paths.set(id, path);\n index.set(id, entry as ChannelTemplate[C]);\n }\n return index;\n}\n\n/** A registered template by id; throws naming the channel and id when there is none. */\nexport function lookupTemplate<T>(index: ReadonlyMap<string, T>, channel: string, id: string): T {\n const template = index.get(id);\n if (template === undefined) throw new Error(`${channel} template '${id}' is not registered`);\n return template;\n}\n\n/* ------------------------------- validation -------------------------------- */\n\n/** One failed field, with the dotted path the studio and the logs show. */\nexport interface TemplateIssue {\n path: string;\n message: string;\n}\n\n/**\n * A template refused its input (`phase: 'props'` — the component/content was\n * never called) or its content function returned something that is not a\n * notification (`phase: 'content'` — called, but nothing is sent). Never a\n * provider failure: a sender must not read it as a dead device token.\n */\nexport class TemplateValidationError extends Error {\n override readonly name = 'TemplateValidationError';\n constructor(\n readonly channel: TemplateChannel,\n readonly template: string,\n readonly phase: 'props' | 'content',\n readonly issues: TemplateIssue[]\n ) {\n super(\n `${channel} template '${template}' ${phase === 'props' ? 'rejected its props' : 'returned invalid content'}: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\nfunction issuesOf(error: core.$ZodError): TemplateIssue[] {\n return error.issues.map((issue) => ({\n path: issue.path.map(String).join('.'),\n message: issue.message,\n }));\n}\n\n/**\n * Parse message props against a template's schema and return the parsed value\n * — the only value anything downstream may use. Throws TemplateValidationError.\n */\nexport function parseTemplateProps<S extends PropsSchema>(\n template: EmailTemplate<S> | PushTemplate<S> | DiscordTemplate<S> | SlackTemplate<S>,\n props: Record<string, unknown>\n): core.output<S> {\n const channel = CHANNEL_OF[template.kind];\n const result = z.safeParse(template.props, props);\n if (!result.success) {\n throw new TemplateValidationError(channel, template.id, 'props', issuesOf(result.error));\n }\n return result.data;\n}\n\nconst pushContentSchema = z.strictObject({\n title: z.string(),\n body: z.string(),\n image: z.optional(z.string()),\n url: z.optional(z.string()),\n});\n\n/** Run a push template's content over parsed props and check the notification it returns. */\nexport function renderPushContent(\n template: PushTemplate,\n props: Record<string, unknown>\n): PushContent {\n const parsed = parseTemplateProps(template, props);\n const output: unknown = template.content(parsed);\n // A Promise (async content smuggled past the types) is an object without title/body: refused here\n const result = z.safeParse(pushContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('push', template.id, 'content', issuesOf(result.error));\n }\n return result.data;\n}\n\n/**\n * What a Discord webhook accepts at all: something to show (content, an embed, a poll or\n * components) and content within Discord's 2000 characters. The rest of the body is Discord's to\n * judge; its types come from `discord-api-types`.\n */\nconst discordContentSchema = z.looseObject({\n content: z.optional(z.string().check(z.maxLength(2000))),\n embeds: z.optional(z.array(z.looseObject({}))),\n});\n\nconst slackContentSchema = z.looseObject({\n text: z.string().check(z.minLength(1)),\n blocks: z.optional(z.array(z.looseObject({ type: z.string() }))),\n});\n\n/** Run a Discord template's content over parsed props and check the message it returns. */\nexport function renderDiscordContent(\n template: DiscordTemplate,\n props: Record<string, unknown>\n): DiscordContent {\n const output: unknown = template.content(parseTemplateProps(template, props));\n const result = z.safeParse(discordContentSchema, output);\n const empty =\n result.success &&\n !result.data.content &&\n (result.data.embeds?.length ?? 0) === 0 &&\n (output as { poll?: unknown; components?: unknown[] }).poll === undefined &&\n ((output as { components?: unknown[] }).components?.length ?? 0) === 0;\n if (!result.success || empty) {\n const issues = result.success\n ? [{ path: '', message: 'a Discord message needs content, an embed, a poll or components' }]\n : issuesOf(result.error);\n throw new TemplateValidationError('discord', template.id, 'content', issues);\n }\n return output as DiscordContent;\n}\n\n/** Run a Slack template's content over parsed props and check the message it returns. */\nexport function renderSlackContent(\n template: SlackTemplate,\n props: Record<string, unknown>\n): SlackContent {\n const output: unknown = template.content(parseTemplateProps(template, props));\n const result = z.safeParse(slackContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('slack', template.id, 'content', issuesOf(result.error));\n }\n return output as SlackContent;\n}\n\nfunction parsePreview(\n schema: PropsSchema,\n preview: unknown,\n channel: TemplateChannel,\n title: string\n) {\n const result = z.safeParse(schema, preview);\n if (!result.success) {\n throw new TemplateValidationError(\n channel,\n title,\n 'props',\n issuesOf(result.error).map((i) => Object.assign(i, { message: `preview: ${i.message}` }))\n );\n }\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\nconst SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'enum']);\n\n/** Throws when `schema` is outside the v1 subset (see the header). */\nexport function assertSupportedSchema(schema: unknown, where: string): void {\n const fail = (reason: string): never => {\n throw new TypeError(`template '${where}': unsupported props schema — ${reason}`);\n };\n const def = (schema as core.$ZodType | undefined)?._zod?.def;\n if (def?.type !== 'object') fail('props must be a z.strictObject({...})');\n const object = def as core.$ZodObjectDef;\n if (object.catchall?._zod.def.type !== 'never') {\n fail(\n 'props must be strict (z.strictObject): unknown fields would be dropped or passed through'\n );\n }\n rejectCustomChecks(object, where, fail);\n for (const [field, value] of Object.entries(object.shape)) {\n let fieldDef = value._zod.def;\n if (fieldDef.type === 'optional') {\n // The wrapper carries checks of its own (`.optional().overwrite(...)`): refuse them too\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n fieldDef = (fieldDef as core.$ZodOptionalDef).innerType._zod.def;\n }\n if (fieldDef.type === 'literal') {\n const values = (fieldDef as core.$ZodLiteralDef<core.util.Literal>).values;\n if (!values.every((v) => ['string', 'number', 'boolean'].includes(typeof v))) {\n fail(`field '${field}' is a literal of a non-scalar value`);\n }\n } else if (!SCALAR_TYPES.has(fieldDef.type)) {\n fail(\n `field '${field}' is ${fieldDef.type}; only string, number, boolean, enum and literal fields (optionally .optional()) are supported`\n );\n }\n if ((fieldDef as { coerce?: boolean }).coerce === true) {\n fail(`field '${field}' coerces its input (z.coerce), which would change what the props mean`);\n }\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n }\n}\n\n/**\n * Checks the subset refuses: `custom` runs arbitrary code (`.refine()`), and\n * `overwrite` changes the value (`.overwrite()`, `.trim()`, `.toLowerCase()`…)\n * — the props are parsed more than once on the way to a send (the validator,\n * then the renderer), so a value-changing check would apply twice and the\n * email would differ from the preview.\n */\nfunction rejectCustomChecks(def: core.$ZodTypeDef, where: string, fail: (reason: string) => never) {\n for (const check of def.checks ?? []) {\n if (check._zod.def.check === 'custom') {\n fail(`'${where}' uses a custom refinement, which the template subset does not support`);\n }\n if (check._zod.def.check === 'overwrite') {\n fail(\n `'${where}' changes the value (.overwrite, .trim, .toLowerCase, …), which the template subset does not support`\n );\n }\n }\n}\n\n/** The scalar props a template accepts, as the IR resolves them before parsing. */\nexport type ResolvedProps = Record<string, ScalarIR | undefined>;\n\n/**\n * The engine's MessageValidator over the template registries: the message's\n * props are parsed by its template (the parsed value is what gets sent); a\n * push's content is also run once and checked, so an invalid notification\n * stops the send before any device; Discord and Slack content likewise. Channels without a\n * registry here (sms, …) pass through to their app-supplied senders.\n */\nexport function registryValidator(registry: TemplateRegistry): {\n validate(message: { channel: string; template: string; props: ResolvedProps }): ResolvedProps;\n} {\n const email = indexTemplates('email', registry.email);\n const push = indexTemplates('push', registry.push);\n const discord = indexTemplates('discord', registry.discord);\n const slack = indexTemplates('slack', registry.slack);\n return {\n validate({ channel, template, props }) {\n if (channel === 'email') {\n const entry = lookupTemplate(email, 'email', template);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'push') {\n const entry = lookupTemplate(push, 'push', template);\n renderPushContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'discord') {\n const entry = lookupTemplate(discord, 'discord', template);\n renderDiscordContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'slack') {\n const entry = lookupTemplate(slack, 'slack', template);\n renderSlackContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n return props;\n },\n };\n}\n"],"mappings":"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"}
@@ -5,7 +5,9 @@ import * as core from "zod/v4/core";
5
5
  //#region src/templates.d.ts
6
6
  /**
7
7
  * Template definitions — `emailTemplate`, `pushTemplate`, `discordTemplate` and `slackTemplate`. One
8
- * object per template, and its Zod `props` schema is the only source of the
8
+ * object per template, identified by its own `id` (unique per channel): workflows take the
9
+ * definition itself (`.email(welcome, props)`), the config registers it, and the IR, the manifest,
10
+ * the senders and the studio all find it by that id. Its Zod `props` schema is the only source of the
9
11
  * props type: the DSL's `.email()/.push()` arguments, the `preview`, and the
10
12
  * `component`/`content` parameter are all derived from it, and every render
11
13
  * parses the props against it first (types alone check nothing at run time).
@@ -50,6 +52,12 @@ export interface SlackContent {
50
52
  /** The channels a template can be declared for. */
51
53
  export type TemplateChannel = 'email' | 'push' | 'discord' | 'slack';
52
54
  interface TemplateBase<S extends PropsSchema> {
55
+ /**
56
+ * Wire identity, unique per channel: message nodes, the template manifest, every send and the
57
+ * studio's preview links name the template by it. Not something to rename for readability —
58
+ * `title` is; the property a registry holds it under is only a JavaScript name.
59
+ */
60
+ readonly id: string;
53
61
  /** Display name in the studio (not the email subject, not the notification title). */
54
62
  readonly title: string;
55
63
  readonly description?: string | undefined;
@@ -93,6 +101,7 @@ export interface SlackTemplate<S extends PropsSchema = PropsSchema, Env = Record
93
101
  /** Strips a Promise return out of the accepted callback types: async content is rejected at the type level. */
94
102
  type Sync<T> = T extends Promise<unknown> ? never : T;
95
103
  export interface EmailTemplateDefinition<S extends PropsSchema> {
104
+ id: string;
96
105
  title: string;
97
106
  description?: string;
98
107
  props: S;
@@ -101,6 +110,7 @@ export interface EmailTemplateDefinition<S extends PropsSchema> {
101
110
  component: (props: NoInfer<Readonly<core.output<S>>>) => unknown;
102
111
  }
103
112
  export interface PushTemplateDefinition<S extends PropsSchema, R> {
113
+ id: string;
104
114
  title: string;
105
115
  description?: string;
106
116
  props: S;
@@ -108,6 +118,7 @@ export interface PushTemplateDefinition<S extends PropsSchema, R> {
108
118
  content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<R> & PushContent;
109
119
  }
110
120
  export interface WebhookTemplateDefinition<S extends PropsSchema, Env, C> {
121
+ id: string;
111
122
  title: string;
112
123
  description?: string;
113
124
  props: S;
@@ -130,6 +141,38 @@ export declare function isDiscordTemplate(value: unknown): value is DiscordTempl
130
141
  export declare function isSlackTemplate(value: unknown): value is SlackTemplate;
131
142
  /** Any declared template. */
132
143
  export type AnyTemplate = EmailTemplate | PushTemplate | DiscordTemplate | SlackTemplate;
144
+ export declare const CHANNEL_OF: Record<AnyTemplate['kind'], TemplateChannel>;
145
+ /** Any template definition, whatever its channel. */
146
+ export declare function isTemplate(value: unknown): value is AnyTemplate;
147
+ /**
148
+ * The template registry: per channel, the definitions the project registers. Each channel is a
149
+ * plain object whose property names are JavaScript names only — a template is identified by its
150
+ * own `id` (`indexTemplates`).
151
+ */
152
+ export interface TemplateRegistry {
153
+ readonly email?: Readonly<Record<string, EmailTemplate>>;
154
+ readonly push?: Readonly<Record<string, PushTemplate>>;
155
+ /** Discord webhook templates, each typed by the host's Env its `webhook` reads. */
156
+ readonly discord?: Readonly<Record<string, DiscordTemplate<PropsSchema, never>>>;
157
+ readonly slack?: Readonly<Record<string, SlackTemplate<PropsSchema, never>>>;
158
+ }
159
+ /** The template a channel's registry holds. */
160
+ export interface ChannelTemplate {
161
+ email: EmailTemplate;
162
+ push: PushTemplate;
163
+ discord: DiscordTemplate;
164
+ slack: SlackTemplate;
165
+ }
166
+ /**
167
+ * A channel's registry by template id — how everything that turns an id back into a definition
168
+ * (the compiler, the validator, the renderers, the senders, the studio) looks it up. Throws when
169
+ * an entry is not a template of the channel, has no id, or shares its id with another entry: an id
170
+ * is registered once per channel, so two registrations (even of the same object) are an error,
171
+ * not a silent last-wins. Insertion order is the registry's.
172
+ */
173
+ export declare function indexTemplates<C extends TemplateChannel>(channel: C, registry: Readonly<Record<string, unknown>> | undefined): ReadonlyMap<string, ChannelTemplate[C]>;
174
+ /** A registered template by id; throws naming the channel and id when there is none. */
175
+ export declare function lookupTemplate<T>(index: ReadonlyMap<string, T>, channel: string, id: string): T;
133
176
  /** One failed field, with the dotted path the studio and the logs show. */
134
177
  export interface TemplateIssue {
135
178
  path: string;
@@ -153,13 +196,13 @@ export declare class TemplateValidationError extends Error {
153
196
  * Parse message props against a template's schema and return the parsed value
154
197
  * — the only value anything downstream may use. Throws TemplateValidationError.
155
198
  */
156
- export declare function parseTemplateProps<S extends PropsSchema>(template: EmailTemplate<S> | PushTemplate<S> | DiscordTemplate<S> | SlackTemplate<S>, key: string, props: Record<string, unknown>): core.output<S>;
199
+ export declare function parseTemplateProps<S extends PropsSchema>(template: EmailTemplate<S> | PushTemplate<S> | DiscordTemplate<S> | SlackTemplate<S>, props: Record<string, unknown>): core.output<S>;
157
200
  /** Run a push template's content over parsed props and check the notification it returns. */
158
- export declare function renderPushContent(template: PushTemplate, key: string, props: Record<string, unknown>): PushContent;
201
+ export declare function renderPushContent(template: PushTemplate, props: Record<string, unknown>): PushContent;
159
202
  /** Run a Discord template's content over parsed props and check the message it returns. */
160
- export declare function renderDiscordContent(template: DiscordTemplate, key: string, props: Record<string, unknown>): DiscordContent;
203
+ export declare function renderDiscordContent(template: DiscordTemplate, props: Record<string, unknown>): DiscordContent;
161
204
  /** Run a Slack template's content over parsed props and check the message it returns. */
162
- export declare function renderSlackContent(template: SlackTemplate, key: string, props: Record<string, unknown>): SlackContent;
205
+ export declare function renderSlackContent(template: SlackTemplate, props: Record<string, unknown>): SlackContent;
163
206
  /** Throws when `schema` is outside the v1 subset (see the header). */
164
207
  export declare function assertSupportedSchema(schema: unknown, where: string): void;
165
208
  /** The scalar props a template accepts, as the IR resolves them before parsing. */
@@ -171,12 +214,7 @@ export type ResolvedProps = Record<string, ScalarIR | undefined>;
171
214
  * stops the send before any device; Discord and Slack content likewise. Channels without a
172
215
  * registry here (sms, …) pass through to their app-supplied senders.
173
216
  */
174
- export declare function registryValidator(registries: {
175
- email?: Record<string, EmailTemplate> | undefined;
176
- push?: Record<string, PushTemplate> | undefined;
177
- discord?: Readonly<Record<string, DiscordTemplate<PropsSchema, never>>> | undefined;
178
- slack?: Readonly<Record<string, SlackTemplate<PropsSchema, never>>> | undefined;
179
- }): {
217
+ export declare function registryValidator(registry: TemplateRegistry): {
180
218
  validate(message: {
181
219
  channel: string;
182
220
  template: string;
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.cts","names":[],"sources":["../src/templates.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;YA2BY,cAAc,KAAK;;iBAGd;EACf;EACA;;EAEA;;EAEA;;;YAIU,iBAAiB;;iBAGZ;;EAEf;EACA,UAAU,aAAa;EACvB;EACA;EACA;EACA;EACA;EACA;;;YAIU;UAEF,aAAa,UAAU;;WAEtB;WACA;WACA,OAAO;;WAEP,SAAS,KAAK,MAAM;;iBAGd,cAAc,UAAU,cAAc,qBAAqB,aAAa;WAC9E;WACA;WACA;;WAEA,YAAY,OAAO,SAAS,KAAK,OAAO;;iBAGlC,aAAa,UAAU,cAAc,qBAAqB,aAAa;WAC7E;WACA;;WAEA,UAAU,OAAO,SAAS,KAAK,OAAO,QAAQ;;;;;;;;UAS/C,oBAAoB,UAAU,aAAa,aAAa,aAAa;WACpE;EAET,QAAQ,KAAK;;iBAGE,gBACf,UAAU,cAAc,aACxB,MAAM,iCACE,oBAAoB,GAAG;WACtB;;WAEA,UAAU,OAAO,SAAS,KAAK,OAAO,QAAQ;;iBAGxC,cACf,UAAU,cAAc,aACxB,MAAM,iCACE,oBAAoB,GAAG;WACtB;;WAEA,UAAU,OAAO,SAAS,KAAK,OAAO,QAAQ;;;KAIpD,KAAK,KAAK,UAAU,2BAA2B;iBAEnC,wBAAwB,UAAU;EACjD;EACA;EACA,OAAO;EACP;EACA,SAAS,QAAQ,KAAK,MAAM;EAC5B,YAAY,OAAO,QAAQ,SAAS,KAAK,OAAO;;iBAGjC,uBAAuB,UAAU,aAAa;EAC7D;EACA;EACA,OAAO;EACP,SAAS,QAAQ,KAAK,MAAM;EAC5B,UAAU,OAAO,QAAQ,SAAS,KAAK,OAAO,SAAS,KAAK,KAAK;;iBAGlD,0BAA0B,UAAU,aAAa,KAAK;EACrE;EACA;EACA,OAAO;EACP,SAAS,QAAQ,KAAK,MAAM;;EAE5B,UAAU,KAAK;EACf,UAAU,OAAO,QAAQ,SAAS,KAAK,OAAO,SAAS,KAAK;;;wBAI9C,gBAAgB,UAAU,aAAa,MAAM,yBAC3D,YAAY,0BAA0B,GAAG,KAAK,kBAC7C,gBAAgB,GAAG;;wBAON,cAAc,UAAU,aAAa,MAAM,yBACzD,YAAY,0BAA0B,GAAG,KAAK,gBAC7C,cAAc,GAAG;;wBAOJ,cAAc,UAAU,aACtC,YAAY,wBAAwB,KACnC,cAAc;;wBAOD,aAAa,UAAU,aAAa,UAAU,aAC5D,YAAY,uBAAuB,GAAG,KACrC,aAAa;wBAMA,gBAAgB,iBAAiB,SAAS;wBAI1C,eAAe,iBAAiB,SAAS;wBAIzC,kBAAkB,iBAAiB,SAAS;wBAI5C,gBAAgB,iBAAiB,SAAS;;YAK9C,cAAc,gBAAgB,eAAe,kBAAkB;;iBAqB1D;EACf;EACA;;;;;;;;qBASW,gCAAgC;WAGhC,SAAS;WACT;WACA;WACA,QAAQ;WALD;EAClB,YACW,SAAS,iBACT,kBACA,4BACA,QAAQ;;;;;;wBAqBL,mBAAmB,UAAU,aAC3C,UAAU,cAAc,KAAK,aAAa,KAAK,gBAAgB,KAAK,cAAc,IAClF,aACA,OAAO,0BACN,KAAK,OAAO;;wBAiBC,kBACd,UAAU,cACV,aACA,OAAO,0BACN;;wBA2Ba,qBACd,UAAU,iBACV,aACA,OAAO,0BACN;;wBAmBa,mBACd,UAAU,eACV,aACA,OAAO,0BACN;;wBA+Ba,sBAAsB,iBAAiB;;YA0D3C,gBAAgB,eAAe;;;;;;;;wBAS3B,kBAAkB;EAChC,QAAQ,eAAe;EACvB,OAAO,eAAe;EACtB,UAAU,SAAS,eAAe,gBAAgB;EAClD,QAAQ,SAAS,eAAe,cAAc;;EAE9C,SAAS;IAAW;IAAiB;IAAkB,OAAO;MAAkB"}
1
+ {"version":3,"file":"templates.d.cts","names":[],"sources":["../src/templates.ts"],"mappings":"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"}