@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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 (321) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +23 -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/audit/changes.json +6 -0
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  11. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  12. package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
  13. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  14. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  15. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  16. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  18. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  23. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  24. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
  25. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  26. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  27. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  30. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  32. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  33. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  34. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  35. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  36. package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
  37. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  38. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
  39. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  40. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  51. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  52. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  53. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  55. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
  57. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  58. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  59. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
  60. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  61. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  62. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  63. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  64. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  65. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  66. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  67. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  68. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  69. package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
  70. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  71. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  72. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  73. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  74. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  75. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  76. package/packages/bundled-features/src/config/changes.json +6 -0
  77. package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
  78. package/packages/bundled-features/src/config/resolver.ts +15 -12
  79. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  80. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  81. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  82. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  83. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  84. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  85. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  86. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  87. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  88. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  89. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  90. package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
  91. package/packages/bundled-features/src/delivery/changes.json +66 -0
  92. package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
  93. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  94. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  95. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  96. package/packages/bundled-features/src/delivery/index.ts +19 -2
  97. package/packages/bundled-features/src/delivery/jobs.ts +97 -17
  98. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  99. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  100. package/packages/bundled-features/src/delivery/types.ts +34 -18
  101. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  102. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  103. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  104. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  105. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  106. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  107. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  108. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
  109. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  110. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  111. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  112. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  113. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  114. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  115. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  116. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  118. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  120. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  122. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  123. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  124. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  125. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  126. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  127. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
  128. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  129. package/packages/bundled-features/src/jobs/changes.json +19 -0
  130. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  131. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  132. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  133. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  134. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  135. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  136. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  137. package/packages/bundled-features/src/ledger/changes.json +7 -0
  138. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  139. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  140. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  141. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  142. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  143. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  144. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  145. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  146. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  147. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  148. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  149. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  150. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  151. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  152. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  153. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  154. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  155. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  156. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  157. package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
  158. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  159. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  160. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  161. package/packages/bundled-features/src/secrets/changes.json +6 -0
  162. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  163. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  164. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  165. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  166. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  167. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  168. package/packages/bundled-features/src/shared/changes.json +6 -0
  169. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  170. package/packages/bundled-features/src/shared/index.ts +6 -0
  171. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  172. package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
  173. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  174. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  175. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  176. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  177. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  178. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  179. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  180. package/packages/bundled-features/src/template-resolver/README.md +66 -8
  181. package/packages/bundled-features/src/template-resolver/api.ts +85 -9
  182. package/packages/bundled-features/src/template-resolver/changes.json +186 -0
  183. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
  184. package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
  185. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  186. package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
  187. package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
  188. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  189. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  190. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
  191. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
  192. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  193. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
  194. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
  195. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  196. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  197. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
  198. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
  199. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  200. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
  201. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  202. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  203. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  204. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  205. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  206. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  207. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
  208. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  209. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  210. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
  211. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
  212. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
  213. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
  214. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  215. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
  216. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
  217. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  218. package/packages/bundled-features/src/template-resolver/index.ts +111 -0
  219. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  220. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  221. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  222. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  223. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  224. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  225. package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
  226. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  227. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  228. package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
  229. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  230. package/packages/bundled-features/src/template-resolver/table.ts +27 -1
  231. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  232. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  233. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  234. package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
  235. package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
  236. package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
  237. package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
  238. package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
  239. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  240. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
  241. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
  245. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  246. package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
  247. package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
  248. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  249. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
  250. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  251. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  252. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  253. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  254. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  255. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  256. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  257. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  258. package/packages/bundled-features/src/user/changes.json +6 -0
  259. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  260. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  261. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  262. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  263. package/packages/bundled-features/src/user/seeding.ts +19 -5
  264. package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
  265. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  266. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  267. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  268. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  269. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  270. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  271. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  272. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  273. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  274. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  275. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  276. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  277. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  278. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  279. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  280. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  281. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  282. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  283. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
  284. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  285. package/samples/apps/ui-walkthrough/package.json +11 -0
  286. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  287. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  288. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  289. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
  290. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  291. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  292. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
  293. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
  294. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  295. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  296. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  297. package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
  298. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  299. package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
  300. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  301. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  302. package/samples/apps/use-all-bundled/package.json +2 -0
  303. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  304. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  305. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  306. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  307. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  308. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  309. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  310. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  311. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  312. package/samples/recipes/delivery-notifications/README.md +26 -25
  313. package/samples/recipes/delivery-notifications/package.json +2 -1
  314. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  315. package/samples/recipes/seed-migration/README.md +1 -1
  316. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  317. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  318. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  319. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  320. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  321. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -30,6 +30,12 @@ import {
30
30
  } from "./handlers/signup-request.write.js";
31
31
  import { createSwitchTenantMfaGateHandler } from "./handlers/switch-tenant-mfa-gate.write.js";
32
32
  import { createVerifyEmailHandler } from "./handlers/verify-email.write.js";
33
+ import {
34
+ AUTH_MAIL_VARIABLES,
35
+ authMailLocalizedTemplate,
36
+ INVITE_MAIL_VARIABLES,
37
+ sendsMagicLinkMail,
38
+ } from "./mail-notifications.js";
33
39
 
34
40
  /**
35
41
  * Env-vars contract for the `auth-email-password` feature.
@@ -199,17 +205,32 @@ export function createAuthEmailPasswordFeature(
199
205
  // All five magic-link flows (reset/verify/signup/invite/unlock) dispatch
200
206
  // via ctx.notify → delivery must be mounted. Fail closed at boot instead
201
207
  // of silently dropping the mail.
202
- if (
203
- opts.passwordReset ||
204
- opts.emailVerification ||
205
- opts.signup ||
206
- opts.invite ||
207
- opts.accountUnlock
208
- ) {
209
- r.requires("delivery");
210
- }
208
+ if (sendsMagicLinkMail(opts)) r.requires("delivery");
211
209
  r.envSchema(authEmailPasswordEnvSchema);
212
210
 
211
+ // Declared unconditionally: apps also issue the activation link from their
212
+ // own flows (issueSignupActivation) without enabling the signup option.
213
+ r.notification("password-reset", {
214
+ variables: AUTH_MAIL_VARIABLES,
215
+ localizedTemplate: authMailLocalizedTemplate("passwordReset"),
216
+ });
217
+ r.notification("account-unlock", {
218
+ variables: AUTH_MAIL_VARIABLES,
219
+ localizedTemplate: authMailLocalizedTemplate("accountUnlock"),
220
+ });
221
+ r.notification("email-verification", {
222
+ variables: AUTH_MAIL_VARIABLES,
223
+ localizedTemplate: authMailLocalizedTemplate("emailVerification"),
224
+ });
225
+ r.notification("signup-activation", {
226
+ variables: AUTH_MAIL_VARIABLES,
227
+ localizedTemplate: authMailLocalizedTemplate("signupActivation"),
228
+ });
229
+ r.notification("invite", {
230
+ variables: INVITE_MAIL_VARIABLES,
231
+ localizedTemplate: authMailLocalizedTemplate("invite"),
232
+ });
233
+
213
234
  const handlers = {
214
235
  login: r.writeHandler(
215
236
  createLoginHandler({
@@ -1,5 +1,4 @@
1
1
  import {
2
- access,
3
2
  createSystemUser,
4
3
  defineWriteHandler,
5
4
  SYSTEM_TENANT_ID,
@@ -20,7 +19,12 @@ export const changePasswordWrite = defineWriteHandler({
20
19
  oldPassword: z.string().min(1),
21
20
  newPassword: passwordSchema,
22
21
  }),
23
- access: { roles: access.authenticated },
22
+ access: {
23
+ openToAll: {
24
+ reason:
25
+ "Self-service: acts only on event.user.id and re-checks the caller's current password; invited members hold only an app role, so a fixed role list would lock them out.",
26
+ },
27
+ },
24
28
  escapeHatch: {
25
29
  grants: ["systemIdentity"],
26
30
  reason:
@@ -55,12 +55,10 @@ import { GLOBAL_ROLE_OPTIONS } from "../../user/constants.js";
55
55
  import { userTable } from "../../user/schema/user.js";
56
56
  import { AUTH_INVITE_DEFAULT_TTL_MINUTES } from "../constants.js";
57
57
  import type { AuthMailLocale } from "../email-templates.js";
58
- import { renderInviteEmail } from "../email-templates.js";
59
58
  import { inviteAlreadyAccepted } from "../errors.js";
60
59
  import { invalidateExistingInviteToken, storeInviteToken } from "../invite-token-store.js";
61
60
  import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.js";
62
-
63
- const INVITE_NOTIFICATION_TYPE = "auth-email-password:invite";
61
+ import { AuthMailNotificationTypes } from "../mail-notifications.js";
64
62
 
65
63
  type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
66
64
 
@@ -247,8 +245,8 @@ async function issueInvitation(
247
245
  ctx.notify,
248
246
  {
249
247
  handlerName: "invite-create",
250
- notificationType: INVITE_NOTIFICATION_TYPE,
251
- renderContent: (renderArgs) => renderInviteEmail({ ...renderArgs, role: request.role }),
248
+ notificationType: AuthMailNotificationTypes.invite,
249
+ fallbackAppName: "Workspace",
252
250
  },
253
251
  {
254
252
  email,
@@ -257,6 +255,7 @@ async function issueInvitation(
257
255
  expiresAt: expiresAt.toString(),
258
256
  issuedAt: issuedAt.toString(),
259
257
  timeZone: ctx.tz.user,
258
+ role: request.role,
260
259
  ...(opts.appName !== undefined && { appName: opts.appName }),
261
260
  locale,
262
261
  },
@@ -1,4 +1,4 @@
1
- import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
3
 
4
4
  // Logout — JWT is stateless, so server-side we only return OK. A future
@@ -7,7 +7,12 @@ import * as z from "zod";
7
7
  export const logoutWrite = defineWriteHandler({
8
8
  name: "logout",
9
9
  schema: z.object({}),
10
- access: { roles: access.authenticated },
10
+ access: {
11
+ openToAll: {
12
+ reason:
13
+ "Self-service: touches no data, any signed-in member must be able to sign out regardless of which role they hold.",
14
+ },
15
+ },
11
16
  description:
12
17
  "Acknowledges a sign-out for the authenticated caller so the client can discard its token; the JWT is stateless, so no server-side session state changes here.",
13
18
  perform: stepsPipeline(({ r }) => [
@@ -1,5 +1,5 @@
1
1
  import { AUTH_UNLOCK_DEFAULT_TTL_MINUTES, AuthErrors } from "../constants.js";
2
- import { renderUnlockAccountEmail } from "../email-templates.js";
2
+ import { AuthMailNotificationTypes } from "../mail-notifications.js";
3
3
  import { signUnlockToken } from "../unlock-token.js";
4
4
  import {
5
5
  createTokenRequestHandler,
@@ -7,8 +7,6 @@ import {
7
7
  type TokenRequestOptions,
8
8
  } from "./token-request-handler.js";
9
9
 
10
- const UNLOCK_NOTIFICATION_TYPE = "auth-email-password:account-unlock";
11
-
12
10
  export type RequestAccountUnlockOptions = TokenRequestOptions;
13
11
 
14
12
  // Public shape re-exported for callers that build custom routes on top of
@@ -32,8 +30,7 @@ export function createRequestAccountUnlockHandler(opts: RequestAccountUnlockOpti
32
30
  // locked right now. Confirming just clears the (possibly already
33
31
  // empty) lockout state — harmless no-op if they weren't locked.
34
32
  extraSilentSkip: () => false,
35
- notificationType: UNLOCK_NOTIFICATION_TYPE,
36
- renderContent: renderUnlockAccountEmail,
33
+ notificationType: AuthMailNotificationTypes.accountUnlock,
37
34
  },
38
35
  opts,
39
36
  );
@@ -1,5 +1,5 @@
1
1
  import { AUTH_VERIFY_DEFAULT_TTL_MINUTES, AuthErrors } from "../constants.js";
2
- import { renderVerifyEmail } from "../email-templates.js";
2
+ import { AuthMailNotificationTypes } from "../mail-notifications.js";
3
3
  import { signVerificationToken } from "../verification-token.js";
4
4
  import {
5
5
  createTokenRequestHandler,
@@ -7,8 +7,6 @@ import {
7
7
  type TokenRequestOptions,
8
8
  } from "./token-request-handler.js";
9
9
 
10
- const VERIFY_NOTIFICATION_TYPE = "auth-email-password:email-verification";
11
-
12
10
  export type RequestEmailVerificationOptions = TokenRequestOptions;
13
11
 
14
12
  export type RequestVerificationData = TokenRequestData<"verification-requested">;
@@ -29,8 +27,7 @@ export function createRequestEmailVerificationHandler(opts: RequestEmailVerifica
29
27
  // unknown/deleted to keep the enumeration surface symmetric — the
30
28
  // caller sees the same 200 regardless of whether a token was minted.
31
29
  extraSilentSkip: (user) => user.emailVerified === true,
32
- notificationType: VERIFY_NOTIFICATION_TYPE,
33
- renderContent: renderVerifyEmail,
30
+ notificationType: AuthMailNotificationTypes.emailVerification,
34
31
  },
35
32
  opts,
36
33
  );
@@ -1,5 +1,5 @@
1
1
  import { AUTH_RESET_DEFAULT_TTL_MINUTES, AuthErrors } from "../constants.js";
2
- import { renderResetPasswordEmail } from "../email-templates.js";
2
+ import { AuthMailNotificationTypes } from "../mail-notifications.js";
3
3
  import { signResetToken } from "../reset-token.js";
4
4
  import {
5
5
  createTokenRequestHandler,
@@ -7,8 +7,6 @@ import {
7
7
  type TokenRequestOptions,
8
8
  } from "./token-request-handler.js";
9
9
 
10
- const RESET_NOTIFICATION_TYPE = "auth-email-password:password-reset";
11
-
12
10
  export type RequestPasswordResetOptions = TokenRequestOptions;
13
11
 
14
12
  // Public shape re-exported for callers that build custom routes on top of
@@ -31,8 +29,7 @@ export function createRequestPasswordResetHandler(opts: RequestPasswordResetOpti
31
29
  // non-deleted user can initiate a reset regardless of verification
32
30
  // state. The sessions feature handles the post-change revocation.
33
31
  extraSilentSkip: () => false,
34
- notificationType: RESET_NOTIFICATION_TYPE,
35
- renderContent: renderResetPasswordEmail,
32
+ notificationType: AuthMailNotificationTypes.passwordReset,
36
33
  },
37
34
  opts,
38
35
  );
@@ -36,6 +36,7 @@ import {
36
36
  } from "@cosmicdrift/kumiko-framework/errors";
37
37
  import { generateUniqueName } from "@cosmicdrift/kumiko-framework/random";
38
38
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
39
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
39
40
  import * as z from "zod";
40
41
  import {
41
42
  findSignupHandoverProvider,
@@ -100,10 +101,8 @@ const SIGNUP_CONFIRM_PROVISION_REASON =
100
101
  // match; this reads the same field the framework itself uses.
101
102
  function isBenignClaimRejection(error: WriteErrorInfo): boolean {
102
103
  return (
103
- typeof error.details === "object" &&
104
- error.details !== null &&
105
- (error.details as Record<string, unknown>)["reason"] ===
106
- SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE
104
+ isPlainObject(error.details) &&
105
+ error.details["reason"] === SIGNUP_HANDOVER_BENIGN_CLAIM_REJECTION_CODE
107
106
  );
108
107
  }
109
108
 
@@ -25,6 +25,7 @@ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
25
25
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
26
26
  import * as z from "zod";
27
27
  import { findSignupHandoverProvider, type SignupHandoverBinding } from "../../shared/index.js";
28
+ import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.js";
28
29
  import type { AuthMailLocale } from "../email-templates.js";
29
30
  import { resolveHandlerMailLocale } from "../magic-link-mail.js";
30
31
  import { AUTH_SELF_REGISTRATION_FEATURE } from "../self-registration-toggle.js";
@@ -79,7 +80,7 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
79
80
  schema: SignupRequestSchema,
80
81
  access: { roles: ["anonymous"] },
81
82
  rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
82
- additionalRateLimits: [{ per: { payloadField: "email" }, limit: 3, windowSeconds: 86400 }],
83
+ additionalRateLimits: AUTH_EMAIL_PAYLOAD_RATE_LIMITS,
83
84
  description:
84
85
  "Starts magic-link self-registration by mailing a fresh activation link to an address and invalidating any link still outstanding for it; the answer looks the same whether or not the address is already registered.",
85
86
  escapeHatch: { reason: HANDOVER_GRANT_VERIFY_REASON, grants: ["unsafeRaw"] },
@@ -19,11 +19,8 @@ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
19
19
  import * as z from "zod";
20
20
  import { UserQueries } from "../../user/index.js";
21
21
  import { type AuthUserRow, parseAuthUserRow } from "../auth-user-row.js";
22
- import type {
23
- AuthMailContent,
24
- AuthMailLocale,
25
- RenderTokenContentArgs,
26
- } from "../email-templates.js";
22
+ import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.js";
23
+ import type { AuthMailLocale } from "../email-templates.js";
27
24
  import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.js";
28
25
 
29
26
  const RequestTokenSchema = z.object({
@@ -67,13 +64,9 @@ export type TokenRequestSpec<TName extends string, TSuccessKind extends string>
67
64
  // soft-deleted". Verification skips when emailVerified is already true;
68
65
  // password-reset has no extra condition (returns false).
69
66
  readonly extraSilentSkip: (user: AuthUserRow) => boolean;
70
- // Notification type dispatched via ctx.notify on success. No r.notification
71
- // is declared (the recipient is an anonymous email, not a userId) — the
72
- // route:{email} path delivers directly and buildMessage falls back to data.
67
+ // Trigger-less r.notification dispatched via ctx.notify on success; the
68
+ // recipient is an anonymous email, so route:{email} delivers directly.
73
69
  readonly notificationType: string;
74
- // Builds the structured mail body from the magic-link + expiry. Per-flow
75
- // (renderResetPasswordEmail / renderVerifyEmail).
76
- readonly renderContent: (args: RenderTokenContentArgs) => AuthMailContent;
77
70
  };
78
71
 
79
72
  export type TokenRequestOptions = {
@@ -107,7 +100,7 @@ export function createTokenRequestHandler<TName extends string, TSuccessKind ext
107
100
  schema: RequestTokenSchema,
108
101
  access: { roles: ["anonymous"] },
109
102
  rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
110
- additionalRateLimits: [{ per: { payloadField: "email" }, limit: 5, windowSeconds: 86400 }],
103
+ additionalRateLimits: AUTH_EMAIL_PAYLOAD_RATE_LIMITS,
111
104
  escapeHatch: {
112
105
  grants: ["systemIdentity"],
113
106
  reason:
@@ -154,7 +147,6 @@ export function createTokenRequestHandler<TName extends string, TSuccessKind ext
154
147
  {
155
148
  handlerName: spec.handlerName,
156
149
  notificationType: spec.notificationType,
157
- renderContent: spec.renderContent,
158
150
  },
159
151
  {
160
152
  // Row value is ciphertext with an active KMS; the byte-exact
@@ -21,23 +21,7 @@ export type {
21
21
  } from "./bootstrap.js";
22
22
  export { BootstrapPlanError, BootstrapWriteError, bootstrapTenants } from "./bootstrap.js";
23
23
  export { AUTH_EMAIL_PASSWORD_FEATURE, AuthErrors, AuthHandlers, AuthQueries } from "./constants.js";
24
- // Renderers for the auth mails. All four magic-link flows (reset, verify,
25
- // signup-activation, invite) emit structured AuthMailContent through delivery
26
- // (ctx.notify).
27
- export type {
28
- AuthMailContent,
29
- AuthMailLocale,
30
- AuthMailSection,
31
- RenderInviteEmailArgs,
32
- RenderTokenContentArgs,
33
- } from "./email-templates.js";
34
- export {
35
- renderActivationEmail,
36
- renderInviteEmail,
37
- renderResetPasswordEmail,
38
- renderUnlockAccountEmail,
39
- renderVerifyEmail,
40
- } from "./email-templates.js";
24
+ export type { AuthMailLocale } from "./email-templates.js";
41
25
  export type {
42
26
  AccountLockoutOptions,
43
27
  AccountUnlockOptions,
@@ -48,6 +32,7 @@ export type {
48
32
  SignupOptions,
49
33
  } from "./feature.js";
50
34
  export { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "./feature.js";
35
+ export { AuthMailNotificationTypes } from "./mail-notifications.js";
51
36
  export {
52
37
  AUTH_SELF_REGISTRATION_FEATURE,
53
38
  createAuthSelfRegistrationToggleFeature,
@@ -1,24 +1,24 @@
1
1
  // Shared dispatch for the magic-link mails (password-reset, email-verification,
2
- // signup-activation). All three follow the same delivery shape: append the
3
- // token to the app page, render structured content, and notify the recipient
4
- // email directly (route:{email} skips preferences — the recipient may not have
5
- // a user account yet — and critical priority keeps a security mail
6
- // non-unsubscribable). No r.notification is declared; buildMessage falls back
7
- // to the structured `data` for the renderer.
2
+ // account-unlock, signup-activation, invite). All follow the same delivery
3
+ // shape: append the token to the app page and notify the recipient email
4
+ // directly (route:{email} skips preferences — the recipient may not have a user
5
+ // account yet — and critical priority keeps a security mail non-unsubscribable).
6
+ // Each mail type is a trigger-less r.notification whose stored template renders
7
+ // from the variables passed here.
8
8
 
9
9
  import { requestContext } from "@cosmicdrift/kumiko-framework/api";
10
10
  import type { NotifyFn } from "@cosmicdrift/kumiko-framework/engine";
11
11
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
12
- import { resolveMailLocale } from "@cosmicdrift/kumiko-framework/i18n";
13
- import type { AuthMailContent, AuthMailLocale, RenderTokenContentArgs } from "./email-templates.js";
12
+ import { mailT, resolveMailLocale } from "@cosmicdrift/kumiko-framework/i18n";
13
+ import { type AuthMailLocale, tokenMailExpiry } from "./email-templates.js";
14
+ import { authMailTemplateLocale } from "./mail-notifications.js";
14
15
 
15
- // Per-flow constants: which notification type to dispatch and how to render the
16
- // body. renderContent is the flow's template (renderResetPasswordEmail / … /
17
- // renderActivationEmail), all unified on RenderTokenContentArgs → AuthMailContent.
16
+ // Per-flow constants: which notification type to dispatch. fallbackAppName
17
+ // replaces the translated default when the flow has no app name of its own.
18
18
  export type MagicLinkMailSpec = {
19
19
  readonly handlerName: string;
20
20
  readonly notificationType: string;
21
- readonly renderContent: (args: RenderTokenContentArgs) => AuthMailContent;
21
+ readonly fallbackAppName?: string;
22
22
  };
23
23
 
24
24
  // Per-request values: the recipient + the app page that receives the token, plus
@@ -36,6 +36,8 @@ export type MagicLinkMailParams = {
36
36
  readonly locale?: AuthMailLocale;
37
37
  readonly issuedAt?: string;
38
38
  readonly timeZone?: string;
39
+ // Invite mails name the role the recipient is invited as.
40
+ readonly role?: string;
39
41
  };
40
42
 
41
43
  function resolveAppUrl(appUrl: string | ((locale: string) => string), locale: string): string {
@@ -75,18 +77,24 @@ export async function dispatchMagicLinkMail(
75
77
  // mailT already root-falls-back; appUrl must use the same negotiated tag
76
78
  // or language-in-path apps 404 on e.g. /de-AT/activate.
77
79
  const urlLocale = resolveMailLocale(locale);
78
- const content = spec.renderContent({
79
- url: appendToken(resolveAppUrl(params.appUrl, urlLocale), params.token),
80
- expiresAt: params.expiresAt,
81
- ...(params.locale !== undefined && { locale: params.locale }),
82
- ...(params.appName !== undefined && { appName: params.appName }),
83
- ...(params.issuedAt !== undefined && { issuedAt: params.issuedAt }),
84
- ...(params.timeZone !== undefined && { timeZone: params.timeZone }),
85
- });
80
+ const { validFor, validUntil } = tokenMailExpiry(
81
+ {
82
+ expiresAt: params.expiresAt,
83
+ ...(params.issuedAt !== undefined && { issuedAt: params.issuedAt }),
84
+ ...(params.timeZone !== undefined && { timeZone: params.timeZone }),
85
+ },
86
+ locale,
87
+ );
86
88
  await notify(spec.notificationType, {
87
89
  route: { email: params.email },
88
- data: content,
89
- locale,
90
+ data: {
91
+ appName: params.appName ?? spec.fallbackAppName ?? mailT(locale, "auth.mail.appNameDefault"),
92
+ actionUrl: appendToken(resolveAppUrl(params.appUrl, urlLocale), params.token),
93
+ validFor,
94
+ validUntil,
95
+ ...(params.role !== undefined && { role: params.role }),
96
+ },
97
+ locale: authMailTemplateLocale(spec.notificationType, locale),
90
98
  priority: "critical",
91
99
  });
92
100
  }
@@ -0,0 +1,158 @@
1
+ import {
2
+ type NotificationLocalizedTemplate,
3
+ QnTypes,
4
+ qn,
5
+ type TemplateVariables,
6
+ templateVariable,
7
+ } from "@cosmicdrift/kumiko-framework/engine";
8
+ import {
9
+ hasMailTranslationKey,
10
+ listMailLocales,
11
+ mailT,
12
+ resolveMailLocale,
13
+ } from "@cosmicdrift/kumiko-framework/i18n";
14
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
15
+ import { AUTH_EMAIL_PASSWORD_FEATURE } from "./constants.js";
16
+
17
+ export const AUTH_MAIL_KINDS = {
18
+ passwordReset: { name: "password-reset", translationKey: "reset" },
19
+ accountUnlock: { name: "account-unlock", translationKey: "unlock" },
20
+ emailVerification: { name: "email-verification", translationKey: "verify" },
21
+ signupActivation: { name: "signup-activation", translationKey: "activation" },
22
+ invite: { name: "invite", translationKey: "invite" },
23
+ } as const;
24
+
25
+ export type AuthMailKind = keyof typeof AUTH_MAIL_KINDS;
26
+
27
+ type MagicLinkFlowOptions = Partial<
28
+ Record<"passwordReset" | "emailVerification" | "signup" | "invite" | "accountUnlock", unknown>
29
+ >;
30
+
31
+ export function sendsMagicLinkMail(flows: MagicLinkFlowOptions): boolean {
32
+ return Boolean(
33
+ flows.passwordReset ||
34
+ flows.emailVerification ||
35
+ flows.signup ||
36
+ flows.invite ||
37
+ flows.accountUnlock,
38
+ );
39
+ }
40
+
41
+ function authMailType(kind: AuthMailKind): string {
42
+ return qn(AUTH_EMAIL_PASSWORD_FEATURE, QnTypes.notify, AUTH_MAIL_KINDS[kind].name);
43
+ }
44
+
45
+ export const AuthMailNotificationTypes: Readonly<Record<AuthMailKind, string>> = {
46
+ passwordReset: authMailType("passwordReset"),
47
+ accountUnlock: authMailType("accountUnlock"),
48
+ emailVerification: authMailType("emailVerification"),
49
+ signupActivation: authMailType("signupActivation"),
50
+ invite: authMailType("invite"),
51
+ };
52
+
53
+ function subjectKey(kind: AuthMailKind): string {
54
+ return `auth.mail.${AUTH_MAIL_KINDS[kind].translationKey}.subject`;
55
+ }
56
+
57
+ const ALL_AUTH_MAIL_KINDS: readonly AuthMailKind[] = [
58
+ "passwordReset",
59
+ "accountUnlock",
60
+ "emailVerification",
61
+ "signupActivation",
62
+ "invite",
63
+ ];
64
+
65
+ const SUBJECT_KEY_BY_TYPE: ReadonlyMap<string, string> = new Map(
66
+ ALL_AUTH_MAIL_KINDS.map((kind) => [AuthMailNotificationTypes[kind], subjectKey(kind)]),
67
+ );
68
+
69
+ // A registered locale whose bundle lacks the subject key has no template (see
70
+ // authMailLocalizedTemplate); the renderer has no locale fallback, so the
71
+ // sender must pick a locale that has one or the mail would not go out at all.
72
+ export function authMailTemplateLocale(notificationType: string, requestedLocale: string): string {
73
+ const locale = resolveMailLocale(requestedLocale);
74
+ const key = SUBJECT_KEY_BY_TYPE.get(notificationType);
75
+ return key !== undefined && hasMailTranslationKey(locale, key) ? locale : "en";
76
+ }
77
+
78
+ export const AUTH_MAIL_VARIABLES: TemplateVariables = {
79
+ appName: templateVariable.text({ required: true }),
80
+ actionUrl: templateVariable.link({ required: true }),
81
+ validFor: templateVariable.text({ required: true }),
82
+ validUntil: templateVariable.text({ required: true }),
83
+ };
84
+
85
+ export const INVITE_MAIL_VARIABLES: TemplateVariables = {
86
+ ...AUTH_MAIL_VARIABLES,
87
+ role: templateVariable.text({ required: true }),
88
+ };
89
+
90
+ // Translation placeholder -> template variable. The markers survive HTML
91
+ // escaping, so only the translated text is escaped, never the placeholders.
92
+ const PLACEHOLDER_VARIABLES: Readonly<Record<string, string>> = {
93
+ app: "appName",
94
+ duration: "validFor",
95
+ when: "validUntil",
96
+ role: "role",
97
+ };
98
+ const MARKER_OPEN = "\uE000";
99
+ const MARKER_CLOSE = "\uE001";
100
+ const MARKER_PATTERN = new RegExp(`${MARKER_OPEN}(\\w+)${MARKER_CLOSE}`, "g");
101
+
102
+ function markers(): Readonly<Record<string, string>> {
103
+ return Object.fromEntries(
104
+ Object.keys(PLACEHOLDER_VARIABLES).map((name) => [
105
+ name,
106
+ `${MARKER_OPEN}${name}${MARKER_CLOSE}`,
107
+ ]),
108
+ );
109
+ }
110
+
111
+ function translatedHtml(locale: string, key: string): string {
112
+ return escapeHtml(mailT(locale, key, markers())).replace(MARKER_PATTERN, (_, name: string) => {
113
+ const variable = PLACEHOLDER_VARIABLES[name];
114
+ return variable === undefined
115
+ ? ""
116
+ : `<span data-variable="${escapeHtmlAttr(variable)}"></span>`;
117
+ });
118
+ }
119
+
120
+ function mailBody(locale: string, translationKey: string): string {
121
+ const html = (part: string) => translatedHtml(locale, `auth.mail.${translationKey}.${part}`);
122
+ return [
123
+ `<p>${html("greeting")}</p>`,
124
+ `<p>${html("intro")}</p>`,
125
+ `<p><a data-button href="{{actionUrl}}">${html("button")}</a></p>`,
126
+ `<p>${html("expiry")}</p>`,
127
+ `<p>${html("ignore")}</p>`,
128
+ ].join("");
129
+ }
130
+
131
+ // Evaluated when the registry is built, after every locale feature has
132
+ // registered its mail translations. English always ships; another locale only
133
+ // gets a template when it has its own subject, so a missing translation never
134
+ // silently becomes an English mail in that locale's slot.
135
+ export function authMailLocalizedTemplate(kind: AuthMailKind): () => NotificationLocalizedTemplate {
136
+ const { translationKey } = AUTH_MAIL_KINDS[kind];
137
+ const kindSubjectKey = subjectKey(kind);
138
+ return () => {
139
+ const locales = [
140
+ "en",
141
+ ...listMailLocales().filter(
142
+ (locale) => locale !== "en" && hasMailTranslationKey(locale, kindSubjectKey),
143
+ ),
144
+ ];
145
+ return Object.fromEntries(
146
+ locales.map((locale) => [
147
+ locale,
148
+ {
149
+ body: mailBody(locale, translationKey),
150
+ meta: {
151
+ subject: mailT(locale, kindSubjectKey, { app: "{{appName}}" }),
152
+ category: "transactional" as const,
153
+ },
154
+ },
155
+ ]),
156
+ );
157
+ };
158
+ }
@@ -9,15 +9,15 @@ import type { NotifyFn } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
10
  import type { Redis } from "ioredis";
11
11
  import { AUTH_SIGNUP_DEFAULT_TTL_MINUTES } from "./constants.js";
12
- import { renderActivationEmail } from "./email-templates.js";
13
12
  import { dispatchMagicLinkMail } from "./magic-link-mail.js";
13
+ import { AuthMailNotificationTypes } from "./mail-notifications.js";
14
14
  import {
15
15
  invalidateExistingSignupToken,
16
16
  normalizeEmail,
17
17
  storeSignupToken,
18
18
  } from "./signup-token-store.js";
19
19
 
20
- export const SIGNUP_ACTIVATION_NOTIFICATION_TYPE = "auth-email-password:signup-activation";
20
+ export const SIGNUP_ACTIVATION_NOTIFICATION_TYPE = AuthMailNotificationTypes.signupActivation;
21
21
 
22
22
  export type IssueSignupActivationArgs = {
23
23
  readonly redis: Redis;
@@ -65,7 +65,6 @@ export async function issueSignupActivation(
65
65
  {
66
66
  handlerName: "signup-request",
67
67
  notificationType: SIGNUP_ACTIVATION_NOTIFICATION_TYPE,
68
- renderContent: renderActivationEmail,
69
68
  },
70
69
  {
71
70
  email,
@@ -10,7 +10,7 @@
10
10
  // Screen rein konfigurieren, ohne den Gate selbst ersetzen zu müssen.
11
11
 
12
12
  import { useLocale } from "@cosmicdrift/kumiko-renderer";
13
- import { type ComponentType, type ReactNode, useEffect, useState } from "react";
13
+ import { type ComponentType, type ReactNode, useEffect, useMemo, useState } from "react";
14
14
  import {
15
15
  assertNavigableUrl,
16
16
  buildLoginRedirectUrl,
@@ -204,6 +204,21 @@ export function resolveLoginRedirect(
204
204
  };
205
205
  }
206
206
 
207
+ // A throwing loginUrl builder must not take the whole app tree down in render;
208
+ // null falls through to the built-in login screen.
209
+ function resolveLoginRedirectOrNull(
210
+ loginUrl: string | LoginUrlBuilder,
211
+ locale: string,
212
+ location: Pick<Location, "origin" | "pathname" | "search" | "hash">,
213
+ ): LoginRedirect | null {
214
+ try {
215
+ return resolveLoginRedirect(loginUrl, locale, location);
216
+ } catch (error) {
217
+ console.error("Auth gate: loginUrl could not be resolved, showing the built-in login", error);
218
+ return null;
219
+ }
220
+ }
221
+
207
222
  export function makeAuthGate(opts: AuthGateOptions = {}): ComponentType<{
208
223
  children: ReactNode;
209
224
  }> {
@@ -213,10 +228,14 @@ export function makeAuthGate(opts: AuthGateOptions = {}): ComponentType<{
213
228
  function AuthGate({ children }: { readonly children: ReactNode }): ReactNode {
214
229
  const { status } = useSession();
215
230
  const locale = useLocale().locale();
216
- const loginRedirect =
217
- loginUrl !== undefined && status === "unauthenticated"
218
- ? resolveLoginRedirect(loginUrl, locale, window.location)
219
- : null;
231
+ const { origin, pathname, search, hash } = window.location;
232
+ const loginRedirect = useMemo(
233
+ () =>
234
+ loginUrl !== undefined && status === "unauthenticated"
235
+ ? resolveLoginRedirectOrNull(loginUrl, locale, { origin, pathname, search, hash })
236
+ : null,
237
+ [status, locale, origin, pathname, search, hash],
238
+ );
220
239
  const redirectUrl = loginRedirect?.kind === "redirect" ? loginRedirect.url : null;
221
240
  // kumiko-lint-ignore no-raw-hooks Phase-3 conversion tracked in #2312
222
241
  useEffect(() => {