@cosmicdrift/kumiko-samples 0.362.0 → 0.363.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 (252) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +17 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  12. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  13. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  15. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
  18. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  20. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  21. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  22. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  23. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  24. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  25. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  26. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  27. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  28. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  29. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
  33. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  34. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
  35. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  36. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  37. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
  38. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  39. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  40. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  41. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
  42. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  43. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
  44. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  45. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  46. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  47. package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
  48. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  49. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  50. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  51. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  52. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  53. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  54. package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
  55. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  56. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  57. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  58. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  59. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  60. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  61. package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
  62. package/packages/bundled-features/src/delivery/changes.json +48 -0
  63. package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
  64. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  65. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  66. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  67. package/packages/bundled-features/src/delivery/index.ts +17 -2
  68. package/packages/bundled-features/src/delivery/jobs.ts +48 -5
  69. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  70. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  71. package/packages/bundled-features/src/delivery/types.ts +28 -18
  72. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  73. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  74. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  75. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  76. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  78. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  79. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  80. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  82. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  83. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  84. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  85. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  89. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  91. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  92. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  93. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  94. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  95. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  96. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  97. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  98. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  99. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  100. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  101. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  102. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  103. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  104. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  105. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  106. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  107. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  108. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  109. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  110. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  111. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  112. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  113. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  114. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  115. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  116. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  117. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  118. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  119. package/packages/bundled-features/src/shared/index.ts +6 -0
  120. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  121. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  122. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  123. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  124. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  125. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  126. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  127. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  128. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  129. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  130. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  131. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  132. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  133. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  134. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  135. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  136. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  137. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  138. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  140. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  141. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  144. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  145. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  149. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  153. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  158. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  159. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  160. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  162. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  163. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  164. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  165. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  166. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  167. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  168. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  169. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  170. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  171. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  172. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  173. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  174. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  175. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  176. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  177. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  178. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  179. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  180. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  181. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  182. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  183. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  184. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  186. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  188. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  189. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  190. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  191. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  193. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  194. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  195. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  196. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  197. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  198. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  199. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  200. package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
  201. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  202. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  203. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  204. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  205. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  206. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  207. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  208. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  209. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  210. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  211. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  212. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  213. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  214. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  215. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  216. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  217. package/samples/apps/ui-walkthrough/package.json +2 -0
  218. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  219. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  220. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  221. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  222. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  223. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  224. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  225. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  226. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  227. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  228. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  229. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  230. package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
  231. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  232. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  233. package/samples/apps/use-all-bundled/package.json +2 -0
  234. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  235. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  237. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  238. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  239. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  240. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  241. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  242. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  243. package/samples/recipes/delivery-notifications/README.md +26 -25
  244. package/samples/recipes/delivery-notifications/package.json +2 -1
  245. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  246. package/samples/recipes/seed-migration/README.md +1 -1
  247. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  248. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  249. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  250. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  251. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  252. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -1,138 +0,0 @@
1
- import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
-
3
- export type MailTone = "neutral" | "info" | "success" | "warning" | "danger";
4
- export type MailStageState = "done" | "current" | "upcoming";
5
-
6
- export type MailBadge = { readonly label: string; readonly tone?: MailTone };
7
- export type MailStage = { readonly label: string; readonly state: MailStageState };
8
- export type MailChip = { readonly label: string; readonly tone?: MailTone };
9
-
10
- // Literal types (as const) keep the colors compile-time known, so they can go into style
11
- // attributes without escaping.
12
- const TONE_COLORS = {
13
- neutral: { bg: "#f3f4f6", fg: "#374151" },
14
- info: { bg: "#dbeafe", fg: "#1e40af" },
15
- success: { bg: "#dcfce7", fg: "#166534" },
16
- warning: { bg: "#fef3c7", fg: "#92400e" },
17
- danger: { bg: "#fee2e2", fg: "#991b1b" },
18
- } as const satisfies Readonly<Record<MailTone, { readonly bg: string; readonly fg: string }>>;
19
-
20
- type ToneColors = (typeof TONE_COLORS)[MailTone];
21
- type PillMargin = "0" | "0 6px 6px 0";
22
-
23
- const STAGE_STATES: readonly MailStageState[] = ["done", "current", "upcoming"];
24
- const STAGE_DONE_COLOR = "#16a34a";
25
- const STAGE_UPCOMING_BG = "#e5e7eb";
26
- const STAGE_UPCOMING_FG = "#9ca3af";
27
-
28
- function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
29
- return typeof value === "object" && value !== null;
30
- }
31
-
32
- function isMailTone(value: unknown): value is MailTone {
33
- return typeof value === "string" && Object.hasOwn(TONE_COLORS, value);
34
- }
35
-
36
- function toneColors(tone: unknown): ToneColors {
37
- return TONE_COLORS[isMailTone(tone) ? tone : "neutral"];
38
- }
39
-
40
- function isStageState(value: unknown): value is MailStageState {
41
- return STAGE_STATES.some((state) => state === value);
42
- }
43
-
44
- // Template data is runtime data (variables of a stored template): entries that do not match are
45
- // skipped instead of failing the whole mail.
46
- function isStage(value: unknown): value is MailStage {
47
- return isRecord(value) && typeof value["label"] === "string" && isStageState(value["state"]);
48
- }
49
-
50
- function isChip(value: unknown): value is MailChip {
51
- return isRecord(value) && typeof value["label"] === "string";
52
- }
53
-
54
- export function parseBadge(value: unknown): MailBadge | undefined {
55
- return isChip(value) && value.label !== "" ? value : undefined;
56
- }
57
-
58
- function validStages(value: unknown): readonly MailStage[] {
59
- return Array.isArray(value) ? value.filter(isStage) : [];
60
- }
61
-
62
- function validChips(value: unknown): readonly MailChip[] {
63
- return Array.isArray(value) ? value.filter(isChip) : [];
64
- }
65
-
66
- function pill(chip: MailChip, margin: PillMargin): string {
67
- const { bg, fg } = toneColors(chip.tone);
68
- return `<span style="display:inline-block;margin:${margin};padding:2px 10px;border-radius:999px;background:${bg};color:${fg};font-size:12px;font-weight:600;line-height:18px">${escapeHtml(chip.label)}</span>`;
69
- }
70
-
71
- export function renderBadge(badge: MailBadge): string {
72
- return `<p style="margin:0 0 12px">${pill(badge, "0")}</p>`;
73
- }
74
-
75
- export function renderChips(chips: unknown): string {
76
- const valid = validChips(chips);
77
- if (valid.length === 0) return "";
78
- return `<p style="margin:0 0 16px">${valid.map((chip) => pill(chip, "0 6px 6px 0")).join("")}</p>`;
79
- }
80
-
81
- function stageMarker(stage: MailStage, position: number, primaryColor: string): string {
82
- const base =
83
- "display:inline-block;width:24px;height:24px;line-height:24px;border-radius:12px;text-align:center;font-size:12px;font-weight:700";
84
- const primary = escapeHtmlAttr(primaryColor);
85
- if (stage.state === "done") {
86
- return `<span style="${base};background:${STAGE_DONE_COLOR};color:#fff">&#10003;</span>`;
87
- }
88
- if (stage.state === "current") {
89
- return `<span style="${base};background:#fff;border:2px solid ${primary};line-height:20px;width:20px;height:20px;color:${primary}">${position}</span>`;
90
- }
91
- return `<span style="${base};background:${STAGE_UPCOMING_BG};color:${STAGE_UPCOMING_FG}">${position}</span>`;
92
- }
93
-
94
- function stageCell(stage: MailStage, position: number, primaryColor: string): string {
95
- const labelColor = stage.state === "upcoming" ? STAGE_UPCOMING_FG : "#111";
96
- const weight = stage.state === "current" ? 700 : 400;
97
- return `<td align="center" valign="top" style="padding:0 4px;font-family:sans-serif">${stageMarker(stage, position, primaryColor)}<div style="margin:4px 0 0;font-size:12px;font-weight:${weight};color:${labelColor}">${escapeHtml(stage.label)}</div></td>`;
98
- }
99
-
100
- function stageConnector(previous: MailStage): string {
101
- const color = previous.state === "done" ? STAGE_DONE_COLOR : STAGE_UPCOMING_BG;
102
- return `<td valign="top" style="padding:11px 0 0"><div style="width:24px;height:2px;line-height:2px;font-size:2px;background:${color}">&nbsp;</div></td>`;
103
- }
104
-
105
- export function renderStages(stages: unknown, primaryColor: string): string {
106
- const valid = validStages(stages);
107
- if (valid.length === 0) return "";
108
- const cellsHtml = valid
109
- .map((stage, index) => {
110
- const previous = valid[index - 1];
111
- const connector = previous ? stageConnector(previous) : "";
112
- return `${connector}${stageCell(stage, index + 1, primaryColor)}`;
113
- })
114
- .join("");
115
- return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 16px"><tr>${cellsHtml}</tr></table>`;
116
- }
117
-
118
- const STAGE_TEXT_MARKER: Readonly<Record<MailStageState, string>> = {
119
- done: "✓",
120
- current: "▶",
121
- upcoming: "○",
122
- };
123
-
124
- export function badgeText(badge: MailBadge): string {
125
- return `[${badge.label}]`;
126
- }
127
-
128
- export function stagesText(stages: unknown): string {
129
- return validStages(stages)
130
- .map((stage) => `${STAGE_TEXT_MARKER[stage.state]} ${stage.label}`)
131
- .join(" → ");
132
- }
133
-
134
- export function chipsText(chips: unknown): string {
135
- return validChips(chips)
136
- .map((chip) => chip.label)
137
- .join(" · ");
138
- }
@@ -1,57 +0,0 @@
1
- import type { RendererContext, RenderRequest } from "../renderer-foundation/index.js";
2
- import {
3
- createTemplateResolverApi,
4
- FALLBACK_LOCALE,
5
- TemplateNotFoundError,
6
- } from "../template-resolver/index.js";
7
-
8
- type NotificationRequest = Extract<RenderRequest, { kind: "notification" }>;
9
-
10
- /** Merge template-resolver content (plain JSON EmailTemplateData) with runtime variables. */
11
- export async function resolveNotificationVariables(
12
- req: NotificationRequest,
13
- ctx: RendererContext,
14
- ): Promise<Readonly<Record<string, unknown>>> {
15
- const variables = req.payload.variables ?? {};
16
- const slug = req.payload.template?.trim();
17
- if (!slug || req.payload.content || !ctx.db || !ctx.registry.features.has("template-resolver")) {
18
- return variables;
19
- }
20
-
21
- try {
22
- const api = createTemplateResolverApi(ctx.db);
23
- const locale = req.payload.locale ?? FALLBACK_LOCALE;
24
- const resolved = await api.resolveTemplate({
25
- tenantId: ctx.tenantId,
26
- slug,
27
- kind: "notification",
28
- locale,
29
- });
30
- if (resolved.contentFormat === "plain") {
31
- const base = parsePlainTemplateContent(resolved.content);
32
- return { ...base, ...variables };
33
- }
34
- // body always wins from the resolved template for rendered formats — a runtime
35
- // "variables.body" override would clobber the actual rendered content.
36
- return { ...variables, body: resolved.content };
37
- } catch (err) {
38
- if (err instanceof TemplateNotFoundError) {
39
- return variables;
40
- }
41
- throw err;
42
- }
43
- }
44
-
45
- function parsePlainTemplateContent(content: string): Record<string, unknown> {
46
- const trimmed = content.trim();
47
- if (!trimmed) return {};
48
- try {
49
- const parsed: unknown = JSON.parse(trimmed);
50
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
51
- return parsed as Record<string, unknown>;
52
- }
53
- } catch {
54
- // ponytail: non-JSON plain content becomes a single body section downstream
55
- }
56
- return { body: content };
57
- }
@@ -1,304 +0,0 @@
1
- import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
- import type { NotificationRenderer } from "../delivery/index.js";
3
- import { renderSafeMarkdown } from "../page-render/index.js";
4
- import {
5
- badgeText,
6
- chipsText,
7
- type MailBadge,
8
- type MailChip,
9
- type MailStage,
10
- parseBadge,
11
- renderBadge,
12
- renderChips,
13
- renderStages,
14
- stagesText,
15
- } from "./mail-blocks.js";
16
-
17
- export type MailBranding = {
18
- /** Shown as text when there is no logo, and as the logo alt text. */
19
- readonly productName?: string;
20
- /** Hex color (#rgb, #rrggbb, #rrggbbaa); used for buttons and the header accent. */
21
- readonly primaryColor?: string;
22
- /** Absolute http(s) URL; anything else is dropped. Mutually exclusive with `logoPath`. */
23
- readonly logoUrl?: string;
24
- /** Absolute path on `baseUrl` (e.g. "/logo.png"); use PNG/JPEG, mail clients block SVG. */
25
- readonly logoPath?: string;
26
- /** Absolute http(s) origin that `logoPath` is resolved against; same base as `auth.mail.baseUrl`. */
27
- readonly baseUrl?: string;
28
- /** Locale tried after the exact and language-part match when a localized field has no entry. */
29
- readonly defaultLocale?: string;
30
- readonly footerText?: LocalizedText;
31
- /** Absolute http(s) URLs only; string URLs that are not are dropped, localized ones fail at boot. */
32
- readonly footerLinks?: readonly {
33
- readonly label: LocalizedText;
34
- readonly url: LocalizedText;
35
- }[];
36
- };
37
-
38
- export type LocalizedText = string | Readonly<Record<string, string>>;
39
-
40
- const DEFAULT_PRIMARY_COLOR = "#2563eb";
41
- const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
42
-
43
- function isHttpUrl(value: string): boolean {
44
- try {
45
- const { protocol } = new URL(value);
46
- return protocol === "http:" || protocol === "https:";
47
- } catch {
48
- return false;
49
- }
50
- }
51
-
52
- function languagePart(locale: string): string {
53
- return locale.split("-")[0] ?? locale;
54
- }
55
-
56
- function pickByLocale(
57
- entries: Readonly<Record<string, string>>,
58
- locale: string,
59
- ): string | undefined {
60
- // Own keys only: a locale like "constructor" must not hit Object.prototype.
61
- const ownEntry = (key: string): string | undefined =>
62
- Object.hasOwn(entries, key) ? entries[key] : undefined;
63
- return ownEntry(locale) ?? ownEntry(languagePart(locale));
64
- }
65
-
66
- function resolveLocalized(
67
- value: LocalizedText | undefined,
68
- locale: string | undefined,
69
- defaultLocale: string | undefined,
70
- ): string | undefined {
71
- if (value === undefined || typeof value === "string") return value;
72
- return (
73
- (locale !== undefined ? pickByLocale(value, locale) : undefined) ??
74
- (defaultLocale !== undefined ? pickByLocale(value, defaultLocale) : undefined) ??
75
- Object.values(value)[0]
76
- );
77
- }
78
-
79
- function assertLocalizedUrlsAreHttp(branding: MailBranding): void {
80
- for (const link of branding.footerLinks ?? []) {
81
- if (typeof link.url === "string") continue;
82
- for (const [locale, url] of Object.entries(link.url)) {
83
- if (!isHttpUrl(url)) {
84
- throw new Error(
85
- `createSimpleRenderer: footerLinks url for locale "${locale}" must be an absolute http(s) URL`,
86
- );
87
- }
88
- }
89
- }
90
- }
91
-
92
- function resolveLogoUrl(branding: MailBranding | undefined): string | undefined {
93
- if (!branding) return undefined;
94
- const { logoUrl, logoPath, baseUrl } = branding;
95
- if (logoPath === undefined) {
96
- return logoUrl && isHttpUrl(logoUrl) ? logoUrl : undefined;
97
- }
98
- if (logoUrl !== undefined) {
99
- throw new Error("createSimpleRenderer: logoPath and logoUrl are mutually exclusive");
100
- }
101
- if (baseUrl === undefined || !isHttpUrl(baseUrl)) {
102
- throw new Error("createSimpleRenderer: logoPath requires an absolute http(s) baseUrl");
103
- }
104
- if (!/^\/(?![/\\])/.test(logoPath) || logoPath.includes("\\")) {
105
- throw new Error('createSimpleRenderer: logoPath must start with a single "/"');
106
- }
107
- const resolved = new URL(logoPath, baseUrl);
108
- if (resolved.origin !== new URL(baseUrl).origin) {
109
- throw new Error("createSimpleRenderer: logoPath must stay on the baseUrl origin");
110
- }
111
- return resolved.href;
112
- }
113
-
114
- function resolvePrimaryColor(branding: MailBranding | undefined): string {
115
- const color = branding?.primaryColor;
116
- return color && HEX_COLOR_PATTERN.test(color) ? color : DEFAULT_PRIMARY_COLOR;
117
- }
118
-
119
- function renderBrandingHeader(
120
- branding: MailBranding | undefined,
121
- primaryColor: string,
122
- logoUrl: string | undefined,
123
- ): string {
124
- if (!branding) return "";
125
- if (!logoUrl && !branding.productName) return "";
126
- const logo = logoUrl
127
- ? `<img src="${escapeHtmlAttr(logoUrl)}" alt="${escapeHtmlAttr(branding.productName ?? "")}" style="display:block;max-height:40px;max-width:200px;border:0" />`
128
- : `<span style="font-size:18px;font-weight:700;color:${escapeHtmlAttr(primaryColor)}">${escapeHtml(branding.productName ?? "")}</span>`;
129
- return `<div style="margin:0 0 24px;padding:0 0 16px;border-bottom:3px solid ${escapeHtmlAttr(primaryColor)}">${logo}</div>`;
130
- }
131
-
132
- type ResolvedBrandingFooter = {
133
- readonly footerText: string | undefined;
134
- readonly links: readonly { readonly label: string; readonly url: string }[];
135
- };
136
-
137
- function resolveBrandingFooter(
138
- branding: MailBranding,
139
- locale: string | undefined,
140
- ): ResolvedBrandingFooter {
141
- const pick = (value: LocalizedText | undefined): string | undefined =>
142
- resolveLocalized(value, locale, branding.defaultLocale);
143
- const links = (branding.footerLinks ?? []).flatMap((link) => {
144
- const url = pick(link.url);
145
- if (url === undefined || !isHttpUrl(url)) return [];
146
- return [{ label: pick(link.label) ?? "", url }];
147
- });
148
- return { footerText: pick(branding.footerText), links };
149
- }
150
-
151
- function renderBrandingFooter(
152
- branding: MailBranding | undefined,
153
- locale: string | undefined,
154
- ): string {
155
- if (!branding) return "";
156
- const { footerText, links: resolvedLinks } = resolveBrandingFooter(branding, locale);
157
- const links = resolvedLinks.map(
158
- (link) =>
159
- `<a href="${escapeHtmlAttr(link.url)}" style="color:#999">${escapeHtml(link.label)}</a>`,
160
- );
161
- const footerPartsHtml = [footerText ? escapeHtml(footerText) : "", links.join(" · ")].filter(
162
- (part) => part !== "",
163
- );
164
- if (footerPartsHtml.length === 0) return "";
165
- const footerHtml = footerPartsHtml.join("<br />");
166
- return `<p style="margin:16px 0 0;color:#999;font-size:12px">${footerHtml}</p>`;
167
- }
168
-
169
- function brandingFooterLines(
170
- branding: MailBranding | undefined,
171
- locale: string | undefined,
172
- ): readonly string[] {
173
- if (!branding) return [];
174
- const { footerText, links } = resolveBrandingFooter(branding, locale);
175
- return [
176
- ...(footerText ? [footerText] : []),
177
- ...links.map((link) => (link.label ? `${link.label}: ${link.url}` : link.url)),
178
- ];
179
- }
180
-
181
- type Section =
182
- | { readonly text: string }
183
- | { readonly heading: string }
184
- | { readonly markdown: string }
185
- | { readonly button: { readonly label: string; readonly url: string } }
186
- | { readonly stages: readonly MailStage[] }
187
- | { readonly chips: readonly MailChip[] };
188
-
189
- type EmailTemplateData = {
190
- // Preferred: structured email data
191
- readonly badge?: MailBadge;
192
- readonly header?: string;
193
- readonly sections?: readonly Section[];
194
- readonly footer?: string;
195
- // Fallback: plain title + body (used when no structured template is defined)
196
- readonly title?: string;
197
- readonly body?: string;
198
- };
199
-
200
- function templateContent(variables: Readonly<Record<string, unknown>>): {
201
- readonly badge: MailBadge | undefined;
202
- readonly header: string | undefined;
203
- readonly sections: readonly Section[] | undefined;
204
- readonly footer: string | undefined;
205
- } {
206
- const data = variables as EmailTemplateData; // @cast-boundary render-helper
207
- // Without structured fields, title + body become the header and a single text section.
208
- return {
209
- badge: parseBadge(data.badge),
210
- header: data.header ?? data.title,
211
- sections: data.sections ?? (data.body ? [{ text: data.body }] : undefined),
212
- footer: data.footer,
213
- };
214
- }
215
-
216
- // Markdown stays as written: its source is already readable plain text.
217
- function sectionText(section: Section): string {
218
- if ("text" in section) return section.text;
219
- if ("heading" in section) return section.heading;
220
- if ("markdown" in section) return section.markdown;
221
- if ("button" in section) return `${section.button.label}: ${section.button.url}`;
222
- if ("stages" in section) return stagesText(section.stages);
223
- if ("chips" in section) return chipsText(section.chips);
224
- return "";
225
- }
226
-
227
- function renderSection(section: Section, primaryColor: string): string {
228
- if ("text" in section) {
229
- return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
230
- }
231
- if ("heading" in section) {
232
- return `<h2 style="margin:24px 0 12px;color:#111;font-size:16px;font-weight:600">${escapeHtml(section.heading)}</h2>`;
233
- }
234
- if ("markdown" in section) {
235
- const markdownHtml = renderSafeMarkdown(section.markdown);
236
- return `<div style="color:#333;font-size:14px;line-height:1.5">${markdownHtml}</div>`;
237
- }
238
- if ("button" in section) {
239
- return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:${escapeHtmlAttr(primaryColor)};color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
240
- }
241
- if ("stages" in section) return renderStages(section.stages, primaryColor);
242
- if ("chips" in section) return renderChips(section.chips);
243
- return "";
244
- }
245
-
246
- // Simple Renderer: turns structured email template data into HTML with inline CSS.
247
- // No external dependencies, no template engine — just string concatenation.
248
- export function createSimpleRenderer(branding?: MailBranding): NotificationRenderer {
249
- const primaryColor = resolvePrimaryColor(branding);
250
- const logoUrl = resolveLogoUrl(branding);
251
- if (branding) assertLocalizedUrlsAreHttp(branding);
252
- return {
253
- name: "simple",
254
-
255
- async render(input) {
256
- const { badge, header, sections, footer } = templateContent(input.variables);
257
-
258
- const parts: string[] = [];
259
- parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
260
- parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
261
- parts.push(renderBrandingHeader(branding, primaryColor, logoUrl));
262
-
263
- if (badge) parts.push(renderBadge(badge));
264
-
265
- if (header) {
266
- parts.push(
267
- `<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`,
268
- );
269
- }
270
-
271
- if (sections) {
272
- for (const section of sections) {
273
- parts.push(renderSection(section, primaryColor));
274
- }
275
- }
276
-
277
- if (footer) {
278
- parts.push(
279
- `<p style="margin:24px 0 0;color:#999;font-size:12px;border-top:1px solid #eee;padding-top:16px">${escapeHtml(footer)}</p>`,
280
- );
281
- }
282
-
283
- parts.push(renderBrandingFooter(branding, input.locale));
284
- parts.push("</div></body></html>");
285
- return parts.join("");
286
- },
287
-
288
- async renderText(input) {
289
- const { badge, header, sections, footer } = templateContent(input.variables);
290
- const brandingFooter = brandingFooterLines(branding, input.locale).join("\n");
291
- return [
292
- badge ? badgeText(badge) : undefined,
293
- header,
294
- ...(sections ?? []).map(sectionText),
295
- footer,
296
- brandingFooter,
297
- ]
298
- .filter((block): block is string => block !== undefined && block !== "")
299
- .join("\n\n");
300
- },
301
- };
302
- }
303
-
304
- export const simpleRenderer: NotificationRenderer = createSimpleRenderer();