@cosmicdrift/kumiko-bundled-features 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 (307) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/auth-email-password/email-templates.d.ts +4 -26
  8. package/dist/auth-email-password/email-templates.js +9 -101
  9. package/dist/auth-email-password/feature.js +24 -6
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  12. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  13. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  14. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
  16. package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
  17. package/dist/auth-email-password/index.d.ts +2 -2
  18. package/dist/auth-email-password/index.js +1 -1
  19. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  20. package/dist/auth-email-password/magic-link-mail.js +19 -14
  21. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  22. package/dist/auth-email-password/mail-notifications.js +116 -0
  23. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  24. package/dist/auth-email-password/signup-activation.js +2 -3
  25. package/dist/auth-foundation/types.js +2 -8
  26. package/dist/auth-mfa/feature.js +4 -4
  27. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  28. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  29. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  30. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  31. package/dist/billing-foundation/constants.d.ts +3 -3
  32. package/dist/billing-foundation/constants.js +5 -4
  33. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  34. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
  35. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  36. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  37. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  38. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  39. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
  40. package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
  41. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  42. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  43. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
  44. package/dist/billing-foundation/handlers/process-event.write.js +2 -2
  45. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  46. package/dist/billing-foundation/subscription-tier-sync.js +3 -4
  47. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  48. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  49. package/dist/channel-email/email-channel.d.ts +2 -2
  50. package/dist/channel-email/email-channel.js +89 -26
  51. package/dist/channel-email/feature.js +3 -3
  52. package/dist/channel-email/index.d.ts +1 -1
  53. package/dist/channel-email/index.js +1 -1
  54. package/dist/channel-email/pii-guard.js +10 -0
  55. package/dist/channel-email/smtp-transport.js +13 -0
  56. package/dist/channel-email/types.d.ts +8 -0
  57. package/dist/config/handlers/set.write.js +1 -2
  58. package/dist/custom-fields/lib/value-schema.js +10 -11
  59. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  60. package/dist/data-retention/index.d.ts +1 -0
  61. package/dist/data-retention/index.js +1 -0
  62. package/dist/data-retention/run-retention-cleanup.js +6 -6
  63. package/dist/delivery/attachment-ref.d.ts +39 -0
  64. package/dist/delivery/attachment-ref.js +64 -0
  65. package/dist/delivery/channel-context.d.ts +8 -4
  66. package/dist/delivery/channel-context.js +76 -2
  67. package/dist/delivery/constants.d.ts +1 -0
  68. package/dist/delivery/constants.js +1 -0
  69. package/dist/delivery/delivery-service.d.ts +4 -1
  70. package/dist/delivery/delivery-service.js +38 -32
  71. package/dist/delivery/feature.js +1 -1
  72. package/dist/delivery/index.d.ts +5 -2
  73. package/dist/delivery/index.js +4 -1
  74. package/dist/delivery/jobs.js +25 -5
  75. package/dist/delivery/notify-factory.js +4 -1
  76. package/dist/delivery/testing.d.ts +4 -1
  77. package/dist/delivery/types.d.ts +20 -14
  78. package/dist/delivery/unsubscribe.js +4 -1
  79. package/dist/derivatives-sharp/render.js +1 -18
  80. package/dist/document-ingest-foundation/pages.js +4 -4
  81. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  82. package/dist/file-foundation/index.d.ts +1 -0
  83. package/dist/file-foundation/index.js +1 -0
  84. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  85. package/dist/file-foundation/read-file-ref-content.js +49 -0
  86. package/dist/folders/web/folder-manager.js +3 -2
  87. package/dist/form-draft/db/queries/cleanup.js +4 -4
  88. package/dist/form-draft/db/queries/draft-count.js +2 -2
  89. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  90. package/dist/form-draft/release-file-refs.js +3 -2
  91. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  92. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  93. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  94. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  95. package/dist/inbound-mail-foundation/types.js +4 -4
  96. package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
  97. package/dist/inbound-provider-imap/imap-client.js +4 -3
  98. package/dist/ledger/recurring.js +1 -3
  99. package/dist/managed-pages/branding.d.ts +2 -0
  100. package/dist/managed-pages/branding.js +24 -2
  101. package/dist/managed-pages/feature.js +1 -0
  102. package/dist/managed-pages/i18n.js +1 -0
  103. package/dist/managed-pages/screens/branding-screen.js +8 -1
  104. package/dist/page-render/branding.d.ts +1 -0
  105. package/dist/page-render/branding.js +1 -0
  106. package/dist/page-render/markdown.js +1 -1
  107. package/dist/personal-access-tokens/feature.js +1 -1
  108. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  109. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  110. package/dist/renderer-foundation/api.d.ts +13 -13
  111. package/dist/renderer-foundation/api.js +1 -1
  112. package/dist/renderer-foundation/constants.js +2 -4
  113. package/dist/renderer-foundation/feature.js +7 -7
  114. package/dist/renderer-foundation/index.d.ts +2 -1
  115. package/dist/renderer-foundation/index.js +1 -0
  116. package/dist/renderer-foundation/types.d.ts +92 -32
  117. package/dist/renderer-pdf/feature.js +7 -1
  118. package/dist/sessions/db/queries/cleanup.js +2 -2
  119. package/dist/sessions/feature.js +1 -1
  120. package/dist/sessions/handlers/list.query.js +2 -1
  121. package/dist/shared/attachment-file-name.d.ts +3 -0
  122. package/dist/shared/attachment-file-name.js +35 -0
  123. package/dist/shared/index.d.ts +3 -0
  124. package/dist/shared/index.js +3 -0
  125. package/dist/shared/is-tenant-db.js +1 -1
  126. package/dist/shared/mail-template-body.d.ts +4 -0
  127. package/dist/shared/mail-template-body.js +17 -0
  128. package/dist/shared/sharp-input-limits.d.ts +6 -0
  129. package/dist/shared/sharp-input-limits.js +22 -0
  130. package/dist/shared/signup-handover.js +2 -2
  131. package/dist/tags/handlers/assign-tag.write.js +3 -1
  132. package/dist/tags/web/tag-chip.js +2 -0
  133. package/dist/template-resolver/api.d.ts +30 -2
  134. package/dist/template-resolver/api.js +40 -5
  135. package/dist/template-resolver/compile-document-body.d.ts +8 -0
  136. package/dist/template-resolver/compile-document-body.js +163 -0
  137. package/dist/template-resolver/constants.d.ts +13 -4
  138. package/dist/template-resolver/constants.js +30 -10
  139. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  140. package/dist/template-resolver/document-sanitizer.js +201 -0
  141. package/dist/template-resolver/document-schema.d.ts +21 -0
  142. package/dist/template-resolver/document-schema.js +296 -0
  143. package/dist/template-resolver/feature.d.ts +18 -0
  144. package/dist/template-resolver/feature.js +50 -6
  145. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  146. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  147. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  148. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  149. package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
  150. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  151. package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
  152. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  153. package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
  154. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  155. package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
  156. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  157. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  158. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  159. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  160. package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
  161. package/dist/template-resolver/handlers/layout-source.query.js +64 -0
  162. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  163. package/dist/template-resolver/handlers/list.query.js +5 -7
  164. package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
  165. package/dist/template-resolver/handlers/overview.query.js +165 -0
  166. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  167. package/dist/template-resolver/handlers/preview.query.js +24 -0
  168. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  169. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  170. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  171. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  172. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  173. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  174. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  175. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  176. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  177. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  178. package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
  179. package/dist/template-resolver/handlers/send-test.write.js +81 -0
  180. package/dist/template-resolver/handlers/set.write.d.ts +4 -1
  181. package/dist/template-resolver/handlers/set.write.js +25 -3
  182. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  183. package/dist/template-resolver/handlers/shared.js +6 -2
  184. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  185. package/dist/template-resolver/handlers/template-gates.js +131 -0
  186. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  187. package/dist/template-resolver/handlers/template-write.js +109 -0
  188. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
  189. package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
  190. package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
  191. package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
  192. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  193. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  194. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  195. package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
  196. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  197. package/dist/template-resolver/handlers/usage.query.js +15 -0
  198. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  199. package/dist/template-resolver/handlers/validate.query.js +80 -0
  200. package/dist/template-resolver/index.d.ts +18 -1
  201. package/dist/template-resolver/index.js +16 -1
  202. package/dist/template-resolver/layout-regions.d.ts +23 -0
  203. package/dist/template-resolver/layout-regions.js +125 -0
  204. package/dist/template-resolver/linked-resources.d.ts +17 -0
  205. package/dist/template-resolver/linked-resources.js +75 -0
  206. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  207. package/dist/template-resolver/preview-display-html.js +27 -0
  208. package/dist/template-resolver/print-document.d.ts +1 -0
  209. package/dist/template-resolver/print-document.js +55 -0
  210. package/dist/template-resolver/qualified-names.d.ts +30 -0
  211. package/dist/template-resolver/qualified-names.js +30 -0
  212. package/dist/template-resolver/record-mapping.d.ts +2 -0
  213. package/dist/template-resolver/record-mapping.js +21 -0
  214. package/dist/template-resolver/resource-image.d.ts +15 -0
  215. package/dist/template-resolver/resource-image.js +34 -0
  216. package/dist/template-resolver/seeding.d.ts +14 -1
  217. package/dist/template-resolver/seeding.js +105 -15
  218. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  219. package/dist/template-resolver/store-template-resource.js +264 -0
  220. package/dist/template-resolver/table.d.ts +47 -8
  221. package/dist/template-resolver/table.js +11 -0
  222. package/dist/template-resolver/template-bundle.d.ts +36 -0
  223. package/dist/template-resolver/template-bundle.js +60 -0
  224. package/dist/template-resolver/template-diff.d.ts +7 -0
  225. package/dist/template-resolver/template-diff.js +68 -0
  226. package/dist/template-resolver/template-meta.d.ts +60 -0
  227. package/dist/template-resolver/template-meta.js +101 -0
  228. package/dist/template-resolver/template-preview.d.ts +86 -0
  229. package/dist/template-resolver/template-preview.js +374 -0
  230. package/dist/template-resolver/template-resources.d.ts +14 -0
  231. package/dist/template-resolver/template-resources.js +42 -0
  232. package/dist/template-resolver/template-usage.d.ts +12 -0
  233. package/dist/template-resolver/template-usage.js +30 -0
  234. package/dist/template-resolver/template-validation.d.ts +22 -0
  235. package/dist/template-resolver/template-validation.js +172 -0
  236. package/dist/template-resolver/testing.js +5 -5
  237. package/dist/template-resolver/user-content-table.d.ts +14 -8
  238. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  239. package/dist/template-resolver/web/client-plugin.js +9 -6
  240. package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
  241. package/dist/template-resolver/web/document-editor-extensions.js +107 -0
  242. package/dist/template-resolver/web/document-editor.d.ts +3 -0
  243. package/dist/template-resolver/web/document-editor.js +11 -0
  244. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  245. package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
  246. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
  247. package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
  248. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  249. package/dist/template-resolver/web/editor-handle.js +24 -0
  250. package/dist/template-resolver/web/i18n.js +76 -0
  251. package/dist/template-resolver/web/index.d.ts +4 -0
  252. package/dist/template-resolver/web/index.js +4 -0
  253. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  254. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  255. package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
  256. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  257. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  258. package/dist/template-resolver/web/placeholder-text.js +34 -0
  259. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  260. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  261. package/dist/tenant/handlers/team-list.query.js +3 -2
  262. package/dist/tenant/seeding.js +31 -6
  263. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  264. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  265. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  266. package/dist/user/handlers/detail.query.js +2 -1
  267. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  268. package/dist/user-data-rights/email-templates.d.ts +1 -29
  269. package/dist/user-data-rights/email-templates.js +5 -106
  270. package/dist/user-data-rights/feature.js +53 -35
  271. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  272. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  273. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  274. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  275. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  276. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  277. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  278. package/dist/user-data-rights/mail-notifications.js +118 -0
  279. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  280. package/dist/user-profile/web/profile-screen.js +2 -1
  281. package/dist/waitlist/feature.js +10 -0
  282. package/dist/waitlist/handlers/submit.write.js +3 -4
  283. package/dist/waitlist/mail.d.ts +19 -6
  284. package/dist/waitlist/mail.js +50 -14
  285. package/package.json +28 -17
  286. package/src/admin-shell/changes.json +6 -0
  287. package/src/auth-email-password/changes.json +14 -0
  288. package/src/billing-foundation/changes.json +7 -0
  289. package/src/channel-email/changes.json +19 -0
  290. package/src/data-retention/changes.json +6 -0
  291. package/src/delivery/changes.json +48 -0
  292. package/src/managed-pages/changes.json +6 -0
  293. package/src/renderer-foundation/changes.json +39 -0
  294. package/src/template-resolver/changes.json +118 -0
  295. package/src/user-data-rights/changes.json +7 -0
  296. package/src/waitlist/changes.json +7 -0
  297. package/dist/renderer-simple/feature.d.ts +0 -10
  298. package/dist/renderer-simple/feature.js +0 -42
  299. package/dist/renderer-simple/index.d.ts +0 -3
  300. package/dist/renderer-simple/index.js +0 -2
  301. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  302. package/dist/renderer-simple/mail-blocks.js +0 -107
  303. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  304. package/dist/renderer-simple/resolve-variables.js +0 -47
  305. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  306. package/dist/renderer-simple/simple-renderer.js +0 -200
  307. package/src/renderer-simple/changes.json +0 -27
@@ -0,0 +1,116 @@
1
+ import { QnTypes, qn, templateVariable, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { hasMailTranslationKey, listMailLocales, mailT, resolveMailLocale, } from "@cosmicdrift/kumiko-framework/i18n";
3
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
4
+ import { AUTH_EMAIL_PASSWORD_FEATURE } from "./constants.js";
5
+ export const AUTH_MAIL_KINDS = {
6
+ passwordReset: { name: "password-reset", translationKey: "reset" },
7
+ accountUnlock: { name: "account-unlock", translationKey: "unlock" },
8
+ emailVerification: { name: "email-verification", translationKey: "verify" },
9
+ signupActivation: { name: "signup-activation", translationKey: "activation" },
10
+ invite: { name: "invite", translationKey: "invite" },
11
+ };
12
+ export function sendsMagicLinkMail(flows) {
13
+ return Boolean(flows.passwordReset ||
14
+ flows.emailVerification ||
15
+ flows.signup ||
16
+ flows.invite ||
17
+ flows.accountUnlock);
18
+ }
19
+ function authMailType(kind) {
20
+ return qn(AUTH_EMAIL_PASSWORD_FEATURE, QnTypes.notify, AUTH_MAIL_KINDS[kind].name);
21
+ }
22
+ export const AuthMailNotificationTypes = {
23
+ passwordReset: authMailType("passwordReset"),
24
+ accountUnlock: authMailType("accountUnlock"),
25
+ emailVerification: authMailType("emailVerification"),
26
+ signupActivation: authMailType("signupActivation"),
27
+ invite: authMailType("invite"),
28
+ };
29
+ function subjectKey(kind) {
30
+ return `auth.mail.${AUTH_MAIL_KINDS[kind].translationKey}.subject`;
31
+ }
32
+ const ALL_AUTH_MAIL_KINDS = [
33
+ "passwordReset",
34
+ "accountUnlock",
35
+ "emailVerification",
36
+ "signupActivation",
37
+ "invite",
38
+ ];
39
+ const SUBJECT_KEY_BY_TYPE = new Map(ALL_AUTH_MAIL_KINDS.map((kind) => [AuthMailNotificationTypes[kind], subjectKey(kind)]));
40
+ // A registered locale whose bundle lacks the subject key has no template (see
41
+ // authMailLocalizedTemplate); the renderer has no locale fallback, so the
42
+ // sender must pick a locale that has one or the mail would not go out at all.
43
+ export function authMailTemplateLocale(notificationType, requestedLocale) {
44
+ const locale = resolveMailLocale(requestedLocale);
45
+ const key = SUBJECT_KEY_BY_TYPE.get(notificationType);
46
+ return key !== undefined && hasMailTranslationKey(locale, key) ? locale : "en";
47
+ }
48
+ export const AUTH_MAIL_VARIABLES = {
49
+ appName: templateVariable.text({ required: true }),
50
+ actionUrl: templateVariable.link({ required: true }),
51
+ validFor: templateVariable.text({ required: true }),
52
+ validUntil: templateVariable.text({ required: true }),
53
+ };
54
+ export const INVITE_MAIL_VARIABLES = {
55
+ ...AUTH_MAIL_VARIABLES,
56
+ role: templateVariable.text({ required: true }),
57
+ };
58
+ // Translation placeholder -> template variable. The markers survive HTML
59
+ // escaping, so only the translated text is escaped, never the placeholders.
60
+ const PLACEHOLDER_VARIABLES = {
61
+ app: "appName",
62
+ duration: "validFor",
63
+ when: "validUntil",
64
+ role: "role",
65
+ };
66
+ const MARKER_OPEN = "\uE000";
67
+ const MARKER_CLOSE = "\uE001";
68
+ const MARKER_PATTERN = new RegExp(`${MARKER_OPEN}(\\w+)${MARKER_CLOSE}`, "g");
69
+ function markers() {
70
+ return Object.fromEntries(Object.keys(PLACEHOLDER_VARIABLES).map((name) => [
71
+ name,
72
+ `${MARKER_OPEN}${name}${MARKER_CLOSE}`,
73
+ ]));
74
+ }
75
+ function translatedHtml(locale, key) {
76
+ return escapeHtml(mailT(locale, key, markers())).replace(MARKER_PATTERN, (_, name) => {
77
+ const variable = PLACEHOLDER_VARIABLES[name];
78
+ return variable === undefined
79
+ ? ""
80
+ : `<span data-variable="${escapeHtmlAttr(variable)}"></span>`;
81
+ });
82
+ }
83
+ function mailBody(locale, translationKey) {
84
+ const html = (part) => translatedHtml(locale, `auth.mail.${translationKey}.${part}`);
85
+ return [
86
+ `<p>${html("greeting")}</p>`,
87
+ `<p>${html("intro")}</p>`,
88
+ `<p><a data-button href="{{actionUrl}}">${html("button")}</a></p>`,
89
+ `<p>${html("expiry")}</p>`,
90
+ `<p>${html("ignore")}</p>`,
91
+ ].join("");
92
+ }
93
+ // Evaluated when the registry is built, after every locale feature has
94
+ // registered its mail translations. English always ships; another locale only
95
+ // gets a template when it has its own subject, so a missing translation never
96
+ // silently becomes an English mail in that locale's slot.
97
+ export function authMailLocalizedTemplate(kind) {
98
+ const { translationKey } = AUTH_MAIL_KINDS[kind];
99
+ const kindSubjectKey = subjectKey(kind);
100
+ return () => {
101
+ const locales = [
102
+ "en",
103
+ ...listMailLocales().filter((locale) => locale !== "en" && hasMailTranslationKey(locale, kindSubjectKey)),
104
+ ];
105
+ return Object.fromEntries(locales.map((locale) => [
106
+ locale,
107
+ {
108
+ body: mailBody(locale, translationKey),
109
+ meta: {
110
+ subject: mailT(locale, kindSubjectKey, { app: "{{appName}}" }),
111
+ category: "transactional",
112
+ },
113
+ },
114
+ ]));
115
+ };
116
+ }
@@ -1,6 +1,6 @@
1
1
  import type { NotifyFn } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import type { Redis } from "ioredis";
3
- export declare const SIGNUP_ACTIVATION_NOTIFICATION_TYPE = "auth-email-password:signup-activation";
3
+ export declare const SIGNUP_ACTIVATION_NOTIFICATION_TYPE: string;
4
4
  export type IssueSignupActivationArgs = {
5
5
  readonly redis: Redis;
6
6
  readonly notify: NotifyFn | undefined;
@@ -6,10 +6,10 @@
6
6
  import { generateToken } from "@cosmicdrift/kumiko-framework/api";
7
7
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
8
8
  import { AUTH_SIGNUP_DEFAULT_TTL_MINUTES } from "./constants.js";
9
- import { renderActivationEmail } from "./email-templates.js";
10
9
  import { dispatchMagicLinkMail } from "./magic-link-mail.js";
10
+ import { AuthMailNotificationTypes } from "./mail-notifications.js";
11
11
  import { invalidateExistingSignupToken, normalizeEmail, storeSignupToken, } from "./signup-token-store.js";
12
- export const SIGNUP_ACTIVATION_NOTIFICATION_TYPE = "auth-email-password:signup-activation";
12
+ export const SIGNUP_ACTIVATION_NOTIFICATION_TYPE = AuthMailNotificationTypes.signupActivation;
13
13
  export async function issueSignupActivation(args) {
14
14
  const ttlSeconds = (args.tokenTtlMinutes ?? AUTH_SIGNUP_DEFAULT_TTL_MINUTES) * 60;
15
15
  // At most one live signup token per email.
@@ -26,7 +26,6 @@ export async function issueSignupActivation(args) {
26
26
  await dispatchMagicLinkMail(args.notify, {
27
27
  handlerName: "signup-request",
28
28
  notificationType: SIGNUP_ACTIVATION_NOTIFICATION_TYPE,
29
- renderContent: renderActivationEmail,
30
29
  }, {
31
30
  email,
32
31
  appUrl: args.appUrl,
@@ -47,10 +47,7 @@ export function tokenShapeKey(shape) {
47
47
  // exactly one implementation must be registered (boot-fails on 0 or ≥2).
48
48
  export const EXT_SESSION_STORE = "sessionStore";
49
49
  export function isSessionStoreProvider(o) {
50
- return (typeof o === "object" &&
51
- o !== null &&
52
- "build" in o &&
53
- typeof o.build === "function");
50
+ return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
54
51
  }
55
52
  // Tenant-resolution extension points (#1373). Both are single-provider and
56
53
  // optional — zero registered means today's single-tenant / header-cookie
@@ -69,8 +66,5 @@ export function isTenantResolverProvider(o) {
69
66
  isTenantResolverTrust(o.trust));
70
67
  }
71
68
  export function isTenantExistenceProvider(o) {
72
- return (typeof o === "object" &&
73
- o !== null &&
74
- "build" in o &&
75
- typeof o.build === "function");
69
+ return typeof o === "object" && o !== null && "build" in o && typeof o.build === "function";
76
70
  }
@@ -25,7 +25,7 @@ export function bindMfaRevokeAllOtherSessionsFromFeature(feature) {
25
25
  if (exports && typeof exports === "object" && "bindRevokeAllOtherSessions" in exports) {
26
26
  const { bindRevokeAllOtherSessions } = exports;
27
27
  if (typeof bindRevokeAllOtherSessions === "function") {
28
- // @cast-boundary exports-walk — feature.exports is untyped by design
28
+ // @cast-boundary feature-exports — feature.exports is untyped by design
29
29
  return bindRevokeAllOtherSessions;
30
30
  }
31
31
  }
@@ -38,7 +38,7 @@ export function mfaStatusCheckerFromFeature(feature) {
38
38
  if (exports && typeof exports === "object" && "checkMfaStatus" in exports) {
39
39
  const { checkMfaStatus } = exports;
40
40
  if (typeof checkMfaStatus === "function") {
41
- // @cast-boundary exports-walk — feature.exports is untyped by design
41
+ // @cast-boundary feature-exports — feature.exports is untyped by design
42
42
  return checkMfaStatus;
43
43
  }
44
44
  }
@@ -53,7 +53,7 @@ export function mfaVerifierFromFeature(feature) {
53
53
  if (exports && typeof exports === "object" && "verifyMfaCode" in exports) {
54
54
  const { verifyMfaCode } = exports;
55
55
  if (typeof verifyMfaCode === "function") {
56
- // @cast-boundary exports-walk — feature.exports is untyped by design
56
+ // @cast-boundary feature-exports — feature.exports is untyped by design
57
57
  return verifyMfaCode;
58
58
  }
59
59
  }
@@ -68,7 +68,7 @@ export function bindRevokeAllPatTokensFromFeature(feature) {
68
68
  if (exports && typeof exports === "object" && "bindRevokeAllPatTokens" in exports) {
69
69
  const { bindRevokeAllPatTokens } = exports;
70
70
  if (typeof bindRevokeAllPatTokens === "function") {
71
- // @cast-boundary exports-walk — feature.exports is untyped by design
71
+ // @cast-boundary feature-exports — feature.exports is untyped by design
72
72
  return bindRevokeAllPatTokens;
73
73
  }
74
74
  }
@@ -1,5 +1,6 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
3
4
  function hmacSign(input, secret) {
4
5
  return createHmac("sha256", secret).update(input).digest("base64url");
5
6
  }
@@ -8,13 +9,12 @@ function hmacSign(input, secret) {
8
9
  // replayed as a challenge token), since both share the same secret and payload shape.
9
10
  export function createHmacTokenCodec(domain, isPayload) {
10
11
  function splitEncodedBody(value) {
11
- if (typeof value !== "object" || value === null)
12
+ if (!isPlainObject(value))
12
13
  return null;
13
- const v = value;
14
- const expiresAtMs = v["expiresAtMs"];
14
+ const expiresAtMs = value["expiresAtMs"];
15
15
  if (typeof expiresAtMs !== "number")
16
16
  return null;
17
- const { expiresAtMs: _drop, ...rest } = v;
17
+ const { expiresAtMs: _drop, ...rest } = value;
18
18
  return isPayload(rest) ? { payload: rest, expiresAtMs } : null;
19
19
  }
20
20
  return {
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { createHmacTokenCodec } from "./hmac-token-codec.js";
3
4
  function isPayload(value) {
4
- if (typeof value !== "object" || value === null)
5
+ if (!isPlainObject(value))
5
6
  return false;
6
- const v = value;
7
- return typeof v["userId"] === "string" && typeof v["tenantId"] === "string";
7
+ return typeof value["userId"] === "string" && typeof value["tenantId"] === "string";
8
8
  }
9
9
  // Domain-separated from mfa-preauth-setup (see hmac-token-codec). Single-use is
10
10
  // enforced by the consumer via burnToken (purpose "mfa-challenge"); this codec
@@ -1,10 +1,10 @@
1
1
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { createHmacTokenCodec } from "./hmac-token-codec.js";
3
4
  function isPayload(value) {
4
- if (typeof value !== "object" || value === null)
5
+ if (!isPlainObject(value))
5
6
  return false;
6
- const v = value;
7
- return typeof v["userId"] === "string" && typeof v["tenantId"] === "string";
7
+ return typeof value["userId"] === "string" && typeof value["tenantId"] === "string";
8
8
  }
9
9
  // Domain "mfa-preauth-setup" must never equal the challenge domain: a setup
10
10
  // token has to fail verification as a challenge token and vice versa. The
@@ -11,6 +11,7 @@
11
11
  // generated secret material, not just re-derive it from a lookup.
12
12
  import { createHmac, timingSafeEqual } from "node:crypto";
13
13
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
14
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
14
15
  function sign(input, secret) {
15
16
  return createHmac("sha256", secret).update(input).digest("base64url");
16
17
  }
@@ -22,15 +23,14 @@ export function signMfaSetupToken(payload, ttlMinutes, secret, now = Temporal.No
22
23
  return { token: `${bodyB64}.${signature}`, expiresAt };
23
24
  }
24
25
  function isEncodedBody(value) {
25
- if (typeof value !== "object" || value === null)
26
+ if (!isPlainObject(value))
26
27
  return false;
27
- const v = value;
28
- return (typeof v["userId"] === "string" &&
29
- typeof v["totpSecretBase32"] === "string" &&
30
- Array.isArray(v["recoveryCodeHashes"]) &&
31
- v["recoveryCodeHashes"].every((h) => typeof h === "string") &&
32
- typeof v["expiresAtMs"] === "number" &&
33
- (v["tenantId"] === undefined || typeof v["tenantId"] === "string"));
28
+ return (typeof value["userId"] === "string" &&
29
+ typeof value["totpSecretBase32"] === "string" &&
30
+ Array.isArray(value["recoveryCodeHashes"]) &&
31
+ value["recoveryCodeHashes"].every((h) => typeof h === "string") &&
32
+ typeof value["expiresAtMs"] === "number" &&
33
+ (value["tenantId"] === undefined || typeof value["tenantId"] === "string"));
34
34
  }
35
35
  export function verifyMfaSetupToken(token, secret, now = Temporal.Now.instant()) {
36
36
  const parts = token.split(".");
@@ -3,11 +3,11 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
3
  export declare const BILLING_FOUNDATION_FEATURE: "billing-foundation";
4
4
  export declare const SUBSCRIPTION_PROVIDER_EXTENSION: "subscriptionProvider";
5
5
  /** delivery notification type of the § 312f contract confirmation mail. */
6
- export declare const CONTRACT_CONFIRMATION_NOTIFICATION_TYPE: "billing-foundation:contract-confirmation";
6
+ export declare const CONTRACT_CONFIRMATION_NOTIFICATION_TYPE: "billing-foundation:notify:contract-confirmation";
7
7
  /** delivery notification types of the § 312k termination receipt and the
8
8
  * operator notice. */
9
- export declare const CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE: "billing-foundation:contract-termination-receipt";
10
- export declare const CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE: "billing-foundation:contract-termination-operator-notice";
9
+ export declare const CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE: "billing-foundation:notify:contract-termination-receipt";
10
+ export declare const CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE: "billing-foundation:notify:contract-termination-operator-notice";
11
11
  export declare const SubscriptionFoundationHandlers: {
12
12
  /** Programmatic entry-point für den webhook-handler. Receives the
13
13
  * parsed SubscriptionEvent (vom Plugin schon verifiziert) + macht
@@ -1,17 +1,18 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  // @runtime client
3
3
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
4
5
  // Feature name
5
6
  export const BILLING_FOUNDATION_FEATURE = "billing-foundation";
6
7
  // Extension-point name fuer Provider-Plugins (subscription-stripe,
7
8
  // subscription-mollie, ...).
8
9
  export const SUBSCRIPTION_PROVIDER_EXTENSION = "subscriptionProvider";
9
10
  /** delivery notification type of the § 312f contract confirmation mail. */
10
- export const CONTRACT_CONFIRMATION_NOTIFICATION_TYPE = "billing-foundation:contract-confirmation";
11
+ export const CONTRACT_CONFIRMATION_NOTIFICATION_TYPE = "billing-foundation:notify:contract-confirmation";
11
12
  /** delivery notification types of the § 312k termination receipt and the
12
13
  * operator notice. */
13
- export const CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE = "billing-foundation:contract-termination-receipt";
14
- export const CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE = "billing-foundation:contract-termination-operator-notice";
14
+ export const CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE = "billing-foundation:notify:contract-termination-receipt";
15
+ export const CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE = "billing-foundation:notify:contract-termination-operator-notice";
15
16
  // Qualified write handler names (QN format: scope:type:name).
16
17
  export const SubscriptionFoundationHandlers = {
17
18
  /** Programmatic entry-point für den webhook-handler. Receives the
@@ -195,5 +196,5 @@ export const DEFAULT_PURCHASE_ROLES = ["TenantAdmin", "SystemAdmin"];
195
196
  // `<plugin-name>:config:price-to-tier`-Key.
196
197
  export const TERMINATION_SCOPES = ["tenant-host", "platform"];
197
198
  export function isTerminationScope(value) {
198
- return TERMINATION_SCOPES.includes(value);
199
+ return isOneOf(TERMINATION_SCOPES, value);
199
200
  }
@@ -1,18 +1,37 @@
1
1
  import type { CheckoutConsentRecordedPayload } from "../events.js";
2
- export type ContractConfirmationSection = {
2
+ import { type ConsentLocale } from "./consent-text.js";
3
+ type ConfirmationLabels = {
4
+ readonly subject: string;
5
+ readonly intro: string;
6
+ readonly plan: string;
7
+ readonly item: string;
8
+ readonly oneOffPayment: string;
9
+ readonly price: string;
10
+ readonly oneOff: string;
11
+ readonly contractStart: string;
12
+ readonly currentPeriodEnd: string;
13
+ readonly consentHeading: string;
14
+ readonly consentGivenAt: string;
15
+ readonly termsHeading: string;
16
+ readonly contact: string;
17
+ readonly intervals: Readonly<Record<string, {
18
+ one: string;
19
+ many: string;
20
+ }>>;
21
+ readonly every: string;
22
+ };
23
+ export declare const CONFIRMATION_LABELS: Readonly<Record<ConsentLocale, ConfirmationLabels>>;
24
+ type TextRow = {
3
25
  readonly text: string;
4
- } | {
5
- readonly heading: string;
6
- } | {
7
- readonly markdown: string;
8
26
  };
9
- export type ContractConfirmationContent = {
10
- readonly subject: string;
11
- readonly header: string;
12
- readonly sections: readonly ContractConfirmationSection[];
13
- readonly footer: string;
27
+ /** Variables of the contract confirmation template. */
28
+ export type ContractConfirmationVariables = {
29
+ readonly contractLines: readonly TextRow[];
30
+ readonly consentGivenAt: string;
31
+ readonly termsParagraphs: readonly TextRow[];
32
+ readonly operatorEmail: string;
14
33
  };
15
- export type RenderContractConfirmationArgs = {
34
+ export type ContractConfirmationArgs = {
16
35
  readonly consent: CheckoutConsentRecordedPayload;
17
36
  readonly timeZone: string;
18
37
  /** Translated plan name; falls back to the raw tier. */
@@ -26,6 +45,5 @@ export type RenderContractConfirmationArgs = {
26
45
  readonly operatorEmail: string;
27
46
  readonly termsContent: string;
28
47
  };
29
- /** Structured mail content (header/sections/footer) for the email channel's
30
- * renderer; the renderer owns HTML escaping, this only builds plain text. */
31
- export declare function renderContractConfirmation(args: RenderContractConfirmationArgs): ContractConfirmationContent;
48
+ export declare function contractConfirmationVariables(args: ContractConfirmationArgs): ContractConfirmationVariables;
49
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
- import { CONSENT_TEXTS, resolveConsentLocale } from "./consent-text.js";
3
- const LABELS = {
2
+ import { resolveConsentLocale } from "./consent-text.js";
3
+ export const CONFIRMATION_LABELS = {
4
4
  de: {
5
5
  subject: "Vertragsbestätigung",
6
6
  intro: "Vielen Dank für deine Bestellung. Hiermit bestätigen wir den Vertragsschluss mit folgendem Inhalt.",
@@ -47,7 +47,7 @@ const LABELS = {
47
47
  },
48
48
  };
49
49
  function formatPrice(consent, locale) {
50
- const labels = LABELS[locale];
50
+ const labels = CONFIRMATION_LABELS[locale];
51
51
  if (consent.unitAmount === null || consent.currency === null)
52
52
  return "-";
53
53
  let amount;
@@ -89,40 +89,35 @@ function formatDate(iso, locale, timeZone) {
89
89
  timeZoneName: "short",
90
90
  }).format(epochMilliseconds);
91
91
  }
92
- /** Structured mail content (header/sections/footer) for the email channel's
93
- * renderer; the renderer owns HTML escaping, this only builds plain text. */
94
- export function renderContractConfirmation(args) {
92
+ // The mail template only knows plain text variables, so the Markdown terms
93
+ // become one row per paragraph; heading markers are dropped.
94
+ function termsParagraphsOf(markdown) {
95
+ return markdown
96
+ .split(/\n\s*\n/)
97
+ .map((paragraph) => paragraph.replace(/^#{1,6}\s+/gm, "").trim())
98
+ .filter((paragraph) => paragraph.length > 0)
99
+ .map((text) => ({ text }));
100
+ }
101
+ export function contractConfirmationVariables(args) {
95
102
  const locale = resolveConsentLocale(args.consent.locale);
96
- const labels = LABELS[locale];
97
- const texts = CONSENT_TEXTS[locale];
98
- const { consent } = args;
99
- const { timeZone } = args;
103
+ const labels = CONFIRMATION_LABELS[locale];
104
+ const { consent, timeZone } = args;
100
105
  const subjectLine = consent.mode === "payment"
101
106
  ? `${labels.item}: ${args.itemLabel ?? labels.oneOffPayment}`
102
107
  : `${labels.plan}: ${args.tierLabel ?? consent.tier ?? labels.oneOffPayment}`;
103
- const contractLines = [
104
- subjectLine,
105
- `${labels.price}: ${formatPrice(consent, locale)}`,
106
- `${labels.contractStart}: ${formatDate(args.contractStartIso, locale, timeZone)}`,
107
- ...(args.currentPeriodEndIso !== undefined
108
- ? [`${labels.currentPeriodEnd}: ${formatDate(args.currentPeriodEndIso, locale, timeZone)}`]
109
- : []),
110
- ];
111
108
  const vatNote = args.vatNote[locale] ?? args.vatNote[resolveConsentLocale()];
112
109
  return {
113
- subject: labels.subject,
114
- header: labels.subject,
115
- sections: [
116
- { text: labels.intro },
117
- ...contractLines.map((text) => ({ text })),
118
- ...(vatNote !== undefined ? [{ text: vatNote }] : []),
119
- { heading: labels.consentHeading },
120
- { text: texts.earlyPerformance },
121
- { text: texts.withdrawalLoss },
122
- { text: `${labels.consentGivenAt}: ${formatDate(args.consentGivenAtIso, locale, timeZone)}` },
123
- { heading: labels.termsHeading },
124
- { markdown: args.termsContent },
125
- ],
126
- footer: `${labels.contact} ${args.operatorEmail}`,
110
+ contractLines: [
111
+ subjectLine,
112
+ `${labels.price}: ${formatPrice(consent, locale)}`,
113
+ `${labels.contractStart}: ${formatDate(args.contractStartIso, locale, timeZone)}`,
114
+ ...(args.currentPeriodEndIso !== undefined
115
+ ? [`${labels.currentPeriodEnd}: ${formatDate(args.currentPeriodEndIso, locale, timeZone)}`]
116
+ : []),
117
+ ...(vatNote !== undefined ? [vatNote] : []),
118
+ ].map((text) => ({ text })),
119
+ consentGivenAt: formatDate(args.consentGivenAtIso, locale, timeZone),
120
+ termsParagraphs: termsParagraphsOf(args.termsContent),
121
+ operatorEmail: args.operatorEmail,
127
122
  };
128
123
  }
@@ -11,7 +11,7 @@ import { UserQueries } from "../../user/index.js";
11
11
  import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
12
12
  import { CONTRACT_CONFIRMATION_NOTIFICATION_TYPE } from "../constants.js";
13
13
  import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, CONTRACT_CONFIRMATION_ISSUED_EVENT_QN, checkoutConsentRecordedPayloadSchema, contractConfirmationIssuedPayloadSchema, PAYMENT_AGGREGATE_TYPE, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
14
- import { renderContractConfirmation } from "./confirmation-mail.js";
14
+ import { contractConfirmationVariables } from "./confirmation-mail.js";
15
15
  import { resolveConsentLocale } from "./consent-text.js";
16
16
  export const issueContractConfirmationSchema = z
17
17
  .object({
@@ -128,7 +128,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
128
128
  ? translatedOrUndefined(i18n.t(consent.itemLabelKey, locale, consent.itemLabelParams), consent.itemLabelKey)
129
129
  : undefined;
130
130
  const issuedAtIso = options.now().toString();
131
- const content = renderContractConfirmation({
131
+ const variables = contractConfirmationVariables({
132
132
  consent,
133
133
  timeZone: pickTimeZone(recipient.data.timezone, ctx.tz.tenant),
134
134
  ...(tierLabel !== undefined && { tierLabel }),
@@ -159,7 +159,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
159
159
  await stream.appendOne({ type: CONTRACT_CONFIRMATION_ISSUED_EVENT_QN, payload: issued });
160
160
  await ctx.notify(CONTRACT_CONFIRMATION_NOTIFICATION_TYPE, {
161
161
  route: { email: recipient.data.email },
162
- data: content,
162
+ data: variables,
163
163
  locale,
164
164
  priority: "critical",
165
165
  });
@@ -0,0 +1,8 @@
1
+ import { type FeatureRegistrar, type NotificationLocalizedTemplate, type TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare const TERMINATION_RECEIPT_VARIABLES: TemplateVariables;
3
+ export declare const OPERATOR_NOTICE_VARIABLES: TemplateVariables;
4
+ export declare const CONTRACT_CONFIRMATION_VARIABLES: TemplateVariables;
5
+ export declare function terminationReceiptTemplate(): NotificationLocalizedTemplate;
6
+ export declare function operatorNoticeTemplate(): NotificationLocalizedTemplate;
7
+ export declare function contractConfirmationTemplate(): NotificationLocalizedTemplate;
8
+ export declare function registerConsumerProtectionMails(r: FeatureRegistrar): void;
@@ -0,0 +1,95 @@
1
+ import { templateVariable, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { mailHeading, mailParagraph, mailRepeatedRows } from "../../shared/index.js";
3
+ import { CONFIRMATION_LABELS } from "./confirmation-mail.js";
4
+ import { CONSENT_LOCALES, CONSENT_TEXTS } from "./consent-text.js";
5
+ import { TERMINATION_TEXTS } from "./termination-texts.js";
6
+ // Slot syntax for the translated texts: `{name}` becomes a template variable.
7
+ const OPERATOR_EMAIL_SLOT = "{operatorEmail}";
8
+ const textRows = () => templateVariable.list({ text: templateVariable.text({ required: true }) }, { required: true });
9
+ export const TERMINATION_RECEIPT_VARIABLES = {
10
+ subject: templateVariable.text({ required: true }),
11
+ receivedAt: templateVariable.text({ required: true }),
12
+ requestId: templateVariable.text({ required: true }),
13
+ details: textRows(),
14
+ operatorEmail: templateVariable.text({ required: true }),
15
+ };
16
+ export const OPERATOR_NOTICE_VARIABLES = {
17
+ subject: templateVariable.text({ required: true }),
18
+ lines: textRows(),
19
+ operatorEmail: templateVariable.text({ required: true }),
20
+ };
21
+ export const CONTRACT_CONFIRMATION_VARIABLES = {
22
+ contractLines: textRows(),
23
+ consentGivenAt: templateVariable.text({ required: true }),
24
+ termsParagraphs: textRows(),
25
+ operatorEmail: templateVariable.text({ required: true }),
26
+ };
27
+ function perLocale(build) {
28
+ return Object.fromEntries(CONSENT_LOCALES.map((locale) => {
29
+ const { body, subject } = build(locale);
30
+ return [locale, { body, meta: { subject, category: "transactional" } }];
31
+ }));
32
+ }
33
+ export function terminationReceiptTemplate() {
34
+ return perLocale((locale) => {
35
+ const texts = TERMINATION_TEXTS[locale];
36
+ return {
37
+ subject: "{{subject}}",
38
+ body: [
39
+ mailParagraph(texts.receiptGreeting),
40
+ mailParagraph(texts.receiptIntro),
41
+ mailParagraph(`${texts.receivedAt}: {receivedAt}`),
42
+ mailParagraph(`${texts.requestId}: {requestId}`),
43
+ mailRepeatedRows("details", "text"),
44
+ mailParagraph(texts.receiptNextSteps),
45
+ mailParagraph(texts.receiptRevoke(OPERATOR_EMAIL_SLOT)),
46
+ mailParagraph(`${texts.contact} {operatorEmail}`),
47
+ ].join(""),
48
+ };
49
+ });
50
+ }
51
+ export function operatorNoticeTemplate() {
52
+ return perLocale((locale) => ({
53
+ subject: "{{subject}}",
54
+ body: [
55
+ mailParagraph(TERMINATION_TEXTS[locale].operatorIntro),
56
+ mailRepeatedRows("lines", "text"),
57
+ mailParagraph("{operatorEmail}"),
58
+ ].join(""),
59
+ }));
60
+ }
61
+ export function contractConfirmationTemplate() {
62
+ return perLocale((locale) => {
63
+ const labels = CONFIRMATION_LABELS[locale];
64
+ const consent = CONSENT_TEXTS[locale];
65
+ return {
66
+ subject: labels.subject,
67
+ body: [
68
+ mailParagraph(labels.intro),
69
+ mailRepeatedRows("contractLines", "text"),
70
+ mailHeading(labels.consentHeading),
71
+ mailParagraph(consent.earlyPerformance),
72
+ mailParagraph(consent.withdrawalLoss),
73
+ mailParagraph(`${labels.consentGivenAt}: {consentGivenAt}`),
74
+ mailHeading(labels.termsHeading),
75
+ mailRepeatedRows("termsParagraphs", "text"),
76
+ mailParagraph(`${labels.contact} {operatorEmail}`),
77
+ ].join(""),
78
+ };
79
+ });
80
+ }
81
+ // The registered notification names are the tail of the QN types in constants.ts.
82
+ export function registerConsumerProtectionMails(r) {
83
+ r.notification("contract-confirmation", {
84
+ variables: CONTRACT_CONFIRMATION_VARIABLES,
85
+ localizedTemplate: contractConfirmationTemplate,
86
+ });
87
+ r.notification("contract-termination-receipt", {
88
+ variables: TERMINATION_RECEIPT_VARIABLES,
89
+ localizedTemplate: terminationReceiptTemplate,
90
+ });
91
+ r.notification("contract-termination-operator-notice", {
92
+ variables: OPERATOR_NOTICE_VARIABLES,
93
+ localizedTemplate: operatorNoticeTemplate,
94
+ });
95
+ }
@@ -2,6 +2,7 @@ import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js"
2
2
  import { SubscriptionFoundationHandlers, SubscriptionStatuses } from "../constants.js";
3
3
  import { CHECKOUT_CONSENT_RECORDED_EVENT_SHORT, CONTRACT_CONFIRMATION_ISSUED_EVENT_SHORT, checkoutConsentRecordedPayloadSchema, contractConfirmationIssuedPayloadSchema, INVOICE_PAID_EVENT_QN, PAYMENT_RECEIVED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_QN, } from "../events.js";
4
4
  import { createIssueContractConfirmationHandler } from "./issue-confirmation.js";
5
+ import { registerConsumerProtectionMails } from "./mail-notifications.js";
5
6
  import { registerContractTermination } from "./register-termination.js";
6
7
  const CONFIRMATION_TRIGGERS = [
7
8
  {
@@ -22,6 +23,7 @@ function isConfirmableSubscriptionStatus(status) {
22
23
  }
23
24
  export function registerConsumerProtection(r, options, consumerProtection) {
24
25
  r.requires("template-resolver", "delivery", "user", "tenant");
26
+ registerConsumerProtectionMails(r);
25
27
  registerContractTermination(r, options, consumerProtection);
26
28
  r.defineEvent(CHECKOUT_CONSENT_RECORDED_EVENT_SHORT, checkoutConsentRecordedPayloadSchema, {
27
29
  piiFields: "none",