@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
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Tier cache: stale single reload cannot overwrite a newer full resync",
6
+ "detail": "A single-tenant reload that started before a full resync and resolved after it could restore an outdated tier. The resync now advances the per-tenant ticket for deleted and adopted tenants too, and assignment changes are published remote-only."
7
+ },
2
8
  {
3
9
  "version": "0.354.0",
4
10
  "type": "fix",
@@ -338,7 +338,8 @@ export function createTierEngineFeature<
338
338
  };
339
339
  const publishAssignmentChange = (tenantId: TenantId): void => {
340
340
  const message: TierAssignmentSyncMessage = { tenantId };
341
- busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message);
341
+ // The writer already updated its own cache; only other pods need to re-read.
342
+ busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message, { remoteOnly: true });
342
343
  };
343
344
 
344
345
  // Trial: zeit-abgeleitet aus tenant.inserted_at (≈ Signup), live am
@@ -533,10 +534,17 @@ export function createTierEngineFeature<
533
534
  const isNewerThanSnapshot = (tenantId: TenantId): boolean =>
534
535
  (latestTicket.get(tenantId) ?? 0) > startedAtTicket;
535
536
  for (const tenantId of [...cache.keys()]) {
536
- if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) cache.delete(tenantId);
537
+ if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) {
538
+ cache.delete(tenantId);
539
+ latestTicket.set(tenantId, startedAtTicket);
540
+ }
537
541
  }
538
542
  for (const [tenantId, features] of loaded) {
539
- if (!isNewerThanSnapshot(tenantId)) cache.set(tenantId, features);
543
+ if (!isNewerThanSnapshot(tenantId)) {
544
+ cache.set(tenantId, features);
545
+ // Makes a single reload that started before this snapshot discard its older result.
546
+ latestTicket.set(tenantId, startedAtTicket);
547
+ }
540
548
  }
541
549
  };
542
550
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "resolveUserDisplayNames no longer swallows decrypt errors",
6
+ "detail": "A misconfigured or unavailable KMS made the delivery log silently show addresses or ids instead of names. Decrypt errors now propagate like in the member directory; users that are missing or have no display name are still left out."
7
+ },
2
8
  {
3
9
  "version": "0.361.0",
4
10
  "type": "improvement",
@@ -37,12 +37,12 @@ export type UserStreamBackfillResult = {
37
37
  export async function backfillUserStreamTenants(
38
38
  db: DbConnection,
39
39
  ): Promise<UserStreamBackfillResult> {
40
- const candidates = (await unsafeReadRetrying(
40
+ const candidates = await unsafeReadRetrying<{ aggregate_id: string }>(
41
41
  db,
42
42
  `SELECT DISTINCT "aggregate_id" FROM "kumiko_events"
43
43
  WHERE "aggregate_type" = 'user' AND "tenant_id" <> $1::uuid`,
44
44
  [SYSTEM_TENANT_ID],
45
- )) as ReadonlyArray<{ aggregate_id: string }>;
45
+ );
46
46
 
47
47
  let aggregatesMigrated = 0;
48
48
  let eventsMigrated = 0;
@@ -67,13 +67,13 @@ async function migrateAggregate(db: DbConnection, aggregateId: string): Promise<
67
67
  const raw = asRawClient(tx);
68
68
  // Global id order = commit-adjacent replay order; merges a split stream
69
69
  // (legacy tenant + SYSTEM) into one consistent version sequence.
70
- const events = (await raw.unsafe(
70
+ const events = await raw.unsafe<{ id: bigint | string }>(
71
71
  `SELECT "id" FROM "kumiko_events"
72
72
  WHERE "aggregate_type" = 'user' AND "aggregate_id" = $1::uuid
73
73
  ORDER BY "id" ASC
74
74
  FOR UPDATE`,
75
75
  [aggregateId],
76
- )) as ReadonlyArray<{ id: bigint | string }>;
76
+ );
77
77
 
78
78
  // Phase 1: negative interim versions — real versions are always >= 1, so
79
79
  // nothing can collide with events_aggregate_version_uq mid-migration.
@@ -3,6 +3,7 @@ import {
3
3
  defineEntityDetailHandler,
4
4
  type HandlerContext,
5
5
  } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  import { userEntity } from "../schema/user.js";
7
8
  import { attachTenantLabels, dbForList } from "./list.query.js";
8
9
 
@@ -24,12 +25,9 @@ export const detailQuery = {
24
25
  ctx: HandlerContext,
25
26
  ) => {
26
27
  const result = await baseDetail.handler?.(query, ctx);
27
- if (result === null || typeof result !== "object") return result;
28
+ if (!isPlainObject(result)) return result;
28
29
  if (!ctx.registry.features.has("tenant")) return result;
29
- const [enriched] = await attachTenantLabels(
30
- [result as Record<string, unknown>],
31
- dbForList(ctx),
32
- );
30
+ const [enriched] = await attachTenantLabels([result], dbForList(ctx));
33
31
  return enriched ?? result;
34
32
  },
35
33
  };
@@ -6,9 +6,10 @@ import { userTable } from "./schema/user.js";
6
6
  // Shares the KMS adapter's small dedicated pool, same as member-directory.
7
7
  const KMS_POOL_CONCURRENCY = 4;
8
8
 
9
- // One batched lookup for a page of user ids. Users that are missing, have no
10
- // display name, or whose name cannot be decrypted (e.g. crypto-shredded) are
11
- // left out, so callers fall back to the raw id instead of failing the page.
9
+ // One batched lookup for a page of user ids. Users that are missing or have no
10
+ // display name are left out, so callers fall back to the raw id. Decrypt errors
11
+ // propagate (a misconfigured or unavailable KMS fails loudly, like member-directory);
12
+ // crypto-shredding is handled by the KMS layer.
12
13
  export async function resolveUserDisplayNames(
13
14
  db: TenantDb,
14
15
  userIds: readonly string[],
@@ -18,18 +19,10 @@ export async function resolveUserDisplayNames(
18
19
  const users = await selectMany(db, userTable, { id: [...userIds] });
19
20
  const named = users.filter((user) => typeof user.displayName === "string");
20
21
  await mapWithConcurrency(named, KMS_POOL_CONCURRENCY, async (user) => {
21
- try {
22
- names.set(
23
- String(user.id),
24
- await decryptStoredPii(
25
- String(user.displayName),
26
- "displayName",
27
- "user:resolve-display-names",
28
- ),
29
- );
30
- } catch {
31
- // Unreadable name: the caller's fallback applies.
32
- }
22
+ names.set(
23
+ String(user.id),
24
+ await decryptStoredPii(String(user.displayName), "displayName", "user:resolve-display-names"),
25
+ );
33
26
  });
34
27
  return names;
35
28
  }
@@ -8,8 +8,8 @@ export const rolesInputSchema = z.union([
8
8
  z.string().transform((raw, ctx) => {
9
9
  try {
10
10
  const parsed: unknown = JSON.parse(raw);
11
- if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) {
12
- return parsed as string[];
11
+ if (Array.isArray(parsed) && parsed.every((v): v is string => typeof v === "string")) {
12
+ return parsed;
13
13
  }
14
14
  } catch {
15
15
  /* fall through */
@@ -11,6 +11,7 @@
11
11
  // das Event UND schreibt die Projection-Zeile in einer TX.
12
12
 
13
13
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { computeBlindIndex, configuredBlindIndexKey } from "@cosmicdrift/kumiko-framework/crypto";
14
15
  import {
15
16
  createEventStoreExecutor,
16
17
  createTenantDb,
@@ -139,12 +140,25 @@ export async function rewriteSeededUserEmail(
139
140
  if (typeof id !== "string") {
140
141
  throw new Error("rewriteSeededUserEmail: existing user row has no string id");
141
142
  }
143
+ const version = existingRow["version"];
144
+ if (typeof version !== "number") {
145
+ throw new Error("rewriteSeededUserEmail: existing user row has no numeric version");
146
+ }
147
+ // Every executor.update appends a user:updated event; skip when the stored
148
+ // blind index already matches so repeated boots do not grow the stream.
149
+ // Queried by the bidx column itself: fetchOne does not return it, and an
150
+ // `email` condition would also match a plaintext row with a blank index.
151
+ const blindIndexKey = configuredBlindIndexKey();
152
+ if (blindIndexKey !== undefined) {
153
+ const indexed = await fetchOne(db, userTable, {
154
+ id,
155
+ emailBidx: computeBlindIndex(blindIndexKey, email),
156
+ });
157
+ // skip: the stored blind index already matches, a rewrite would only append a no-op event.
158
+ if (indexed) return;
159
+ }
142
160
  const tdb = createTenantDb(db, by.tenantId, "system");
143
- const result = await userExecutor.update(
144
- { id, version: existingRow["version"] as number, changes: { email } },
145
- by,
146
- tdb,
147
- );
161
+ const result = await userExecutor.update({ id, version, changes: { email } }, by, tdb);
148
162
  // version_conflict: a concurrent write already changed the row — fine for a seed helper.
149
163
  // unique_violation: a duplicate row already holds the blind index; the repair
150
164
  // has to wait for a manual duplicate cleanup.
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Regression tests prove that forget also shreds the previous email in user.updated events",
6
+ "detail": "No runtime change. Integration tests with a configured subject KMS show that after an email change followed by forget, the old and new email in user.updated changes and previous decrypt only to the erased sentinel and the subject key is gone. Without a configured KMS, event payloads stay plaintext by design."
7
+ },
8
+ {
9
+ "version": "0.363.0",
10
+ "type": "breaking",
11
+ "title": "GDPR default mails are r.notification declarations, render*Email is gone",
12
+ "detail": "export-ready, export-failed, deletion-requested and deletion-executed are declared as trigger-less notifications (user-data-rights:notify:<name>) with variables and localized templates built from the GDPR translations, so a tenant can override the text. The default export-ready, export-failed and deletion-requested mails send through ctx.notify to the user (export mails with immediate so the plain magic-link token never lands in a job payload). deletion-executed renders the seeded template through the mail renderer and sends over the tenant mail transport without a delivery log, because the user is already gone. App-provided send*Email callbacks still win. The defaults now need delivery with channel-email; a boot warning is logged once when mail-foundation and a mail transport are mounted without them. New exports are GdprMailNotificationTypes, gdprMailLocalizedTemplate, gdprMailTemplateLocale, registerGdprMails and formatGdprMailTimestamp.",
13
+ "migration": "renderExportReadyEmail, renderExportFailedEmail, renderDeletionRequestedEmail, renderDeletionExecutedEmail and RenderedEmail are removed from user-data-rights/email-templates; the translations stay. An app that mounts it without includeBundled needs template-resolver, renderer-foundation and @cosmicdrift/kumiko-renderer-mail-html at boot (composeRendererStack() from dev-server mounts all of them), plus a delivery with the channel-email feature; apps without the template tables need a schema migration (`kumiko schema generate`). Delivery preferences that referenced the mails use the new user-data-rights:notify:<name> types."
14
+ },
2
15
  {
3
16
  "version": "0.362.0",
4
17
  "type": "fix",
@@ -1,10 +1,10 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  import { userTable } from "../../../user/index.js";
5
5
 
6
6
  export async function selectUsersDueForForgetCleanup(
7
- db: DbConnection,
7
+ db: DbRunner,
8
8
  status: string,
9
9
  gracePeriodEndCutoff: Temporal.Instant | string,
10
10
  ): Promise<readonly { id: string }[]> {
@@ -1,10 +1,10 @@
1
- // Default HTML renderers for GDPR notification mails. English ships here;
2
- // other languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
1
+ // Translations for the GDPR notification mails (the templates in
2
+ // mail-notifications.ts are built from them). English ships here; other
3
+ // languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
3
4
  // localeEs() call registerMailTranslations). Apps that want their own
4
5
  // branding pass send*Email callbacks and these defaults do not run.
5
6
 
6
- import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
7
- import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
7
+ import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
8
8
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
9
9
 
10
10
  export type GdprMailLocale = string;
@@ -41,151 +41,7 @@ export const GDPR_MAIL_EN: Readonly<Record<string, string>> = {
41
41
 
42
42
  registerMailTranslations("en", GDPR_MAIL_EN);
43
43
 
44
- export type RenderedEmail = {
45
- readonly subject: string;
46
- readonly html: string;
47
- readonly text: string;
48
- };
49
-
50
- export type RenderExportReadyEmailArgs = {
51
- readonly downloadUrl: string;
52
- readonly expiresAt: string;
53
- readonly locale?: GdprMailLocale;
54
- readonly appName?: string;
55
- };
56
-
57
- export type RenderExportFailedEmailArgs = {
58
- readonly locale?: GdprMailLocale;
59
- readonly appName?: string;
60
- };
61
-
62
- export type RenderDeletionRequestedEmailArgs = {
63
- readonly gracePeriodEnd: string;
64
- readonly locale?: GdprMailLocale;
65
- readonly appName?: string;
66
- };
67
-
68
- export type RenderDeletionExecutedEmailArgs = {
69
- readonly executedAt: string;
70
- readonly locale?: GdprMailLocale;
71
- readonly appName?: string;
72
- };
73
-
74
- function t(locale: string, key: string, params?: Readonly<Record<string, string>>): string {
75
- return mailT(locale, key, params);
76
- }
77
-
78
- function appNameFor(args: { locale?: GdprMailLocale; appName?: string }): string {
79
- const locale = args.locale ?? "en";
80
- return args.appName ?? t(locale, "gdpr.mail.appNameDefault");
81
- }
82
-
83
- export function renderExportReadyEmail(args: RenderExportReadyEmailArgs): RenderedEmail {
84
- const locale = args.locale ?? "en";
85
- const app = appNameFor(args);
86
- const subject = t(locale, "gdpr.mail.exportReady.subject", { app });
87
- const body = `
88
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
89
- <p style="margin: 0 0 24px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportReady.intro", { app }))}</p>
90
- <p style="margin: 0 0 24px;">${renderButton({ url: args.downloadUrl, label: t(locale, "gdpr.mail.exportReady.button") })}</p>
91
- <p style="margin: 0 0 8px; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }))}</p>
92
- ${renderFallbackUrl({ url: args.downloadUrl, label: t(locale, "gdpr.mail.fallbackUrl") })}`;
93
- const text = plainTextBody([
94
- t(locale, "gdpr.mail.greeting"),
95
- t(locale, "gdpr.mail.exportReady.intro", { app }),
96
- `${t(locale, "gdpr.mail.exportReady.button")}: ${args.downloadUrl}`,
97
- t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }),
98
- ]);
99
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
100
- }
101
-
102
- export function renderExportFailedEmail(args: RenderExportFailedEmailArgs): RenderedEmail {
103
- const locale = args.locale ?? "en";
104
- const app = appNameFor(args);
105
- const subject = t(locale, "gdpr.mail.exportFailed.subject", { app });
106
- const body = `
107
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
108
- <p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportFailed.intro", { app }))}</p>`;
109
- const text = plainTextBody([
110
- t(locale, "gdpr.mail.greeting"),
111
- t(locale, "gdpr.mail.exportFailed.intro", { app }),
112
- ]);
113
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
114
- }
115
-
116
- export function renderDeletionRequestedEmail(
117
- args: RenderDeletionRequestedEmailArgs,
118
- ): RenderedEmail {
119
- const locale = args.locale ?? "en";
120
- const app = appNameFor(args);
121
- const when = formatTimestamp(args.gracePeriodEnd);
122
- const subject = t(locale, "gdpr.mail.deletionRequested.subject", { app });
123
- const body = `
124
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
125
- <p style="margin: 0 0 16px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.intro", { app, when }))}</p>
126
- <p style="margin: 0; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.cancel"))}</p>`;
127
- const text = plainTextBody([
128
- t(locale, "gdpr.mail.greeting"),
129
- t(locale, "gdpr.mail.deletionRequested.intro", { app, when }),
130
- t(locale, "gdpr.mail.deletionRequested.cancel"),
131
- ]);
132
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
133
- }
134
-
135
- export function renderDeletionExecutedEmail(args: RenderDeletionExecutedEmailArgs): RenderedEmail {
136
- const locale = args.locale ?? "en";
137
- const app = appNameFor(args);
138
- const when = formatTimestamp(args.executedAt);
139
- const subject = t(locale, "gdpr.mail.deletionExecuted.subject", { app });
140
- const body = `
141
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
142
- <p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }))}</p>`;
143
- const text = plainTextBody([
144
- t(locale, "gdpr.mail.greeting"),
145
- t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }),
146
- ]);
147
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
148
- }
149
-
150
- function plainTextBody(paragraphs: readonly string[]): string {
151
- return paragraphs.join("\n\n");
152
- }
153
-
154
- function wrapCell(bodyHtml: string): string {
155
- return `<tr><td>${bodyHtml}</td></tr>`;
156
- }
157
-
158
- function renderShell(args: { title: string; bodyHtml: string; locale: GdprMailLocale }): string {
159
- return `<!DOCTYPE html>
160
- <html lang="${escapeHtmlAttr(args.locale)}">
161
- <head>
162
- <meta charset="utf-8" />
163
- <meta name="viewport" content="width=device-width, initial-scale=1" />
164
- <title>${escapeHtml(args.title)}</title>
165
- </head>
166
- <body style="margin: 0; padding: 0; background: #f7f7f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a;">
167
- <table width="100%" cellpadding="0" cellspacing="0" style="padding: 24px 0;">
168
- <tr>
169
- <td align="center">
170
- <table width="560" cellpadding="0" cellspacing="0" style="max-width: 560px; background: #ffffff; border-radius: 8px; padding: 32px;">
171
- ${args.bodyHtml}
172
- </table>
173
- </td>
174
- </tr>
175
- </table>
176
- </body>
177
- </html>`;
178
- }
179
-
180
- function renderButton(args: { url: string; label: string }): string {
181
- return `<a href="${escapeHtmlAttr(args.url)}" style="display: inline-block; background: #1a1a1a; color: #ffffff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500;">${escapeHtml(args.label)}</a>`;
182
- }
183
-
184
- function renderFallbackUrl(args: { url: string; label: string }): string {
185
- return `<p style="margin: 24px 0 0; font-size: 12px; color: #666;">${escapeHtml(args.label)}<br /><a href="${escapeHtmlAttr(args.url)}" style="color: #1a1a1a; word-break: break-all;">${escapeHtml(args.url)}</a></p>`;
186
- }
187
-
188
- function formatTimestamp(iso: string): string {
44
+ export function formatGdprMailTimestamp(iso: string): string {
189
45
  try {
190
46
  const z = Temporal.Instant.from(iso).toZonedDateTimeISO("UTC");
191
47
  return `${z.year}-${pad2(z.month)}-${pad2(z.day)} ${pad2(z.hour)}:${pad2(z.minute)} UTC`;
@@ -2,16 +2,21 @@
2
2
  import { UNKNOWN_CLIENT_IP } from "@cosmicdrift/kumiko-framework/api";
3
3
  import {
4
4
  createSystemConfig,
5
+ createSystemUser,
5
6
  defineFeature,
6
7
  EXT_USER_DATA,
7
8
  type FeatureDefinition,
8
9
  i18nKey,
9
10
  SYSTEM_USER_ID,
11
+ type TenantId,
10
12
  } from "@cosmicdrift/kumiko-framework/engine";
13
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
11
14
  import {
12
15
  createMetricsHandle,
13
16
  createNoopMetricsHandle,
14
17
  } from "@cosmicdrift/kumiko-framework/observability";
18
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
19
+ import { createConfigAccessor } from "../config/index.js";
15
20
  import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks.js";
16
21
  import {
17
22
  EXPORT_SECTION_EXTENSION_NAME,
@@ -45,6 +50,8 @@ import { createRunForgetCleanupHandler } from "./handlers/run-forget-cleanup.wri
45
50
  import { USER_DATA_RIGHTS_I18N } from "./i18n.js";
46
51
  import {
47
52
  type GdprMailDefaults,
53
+ isGdprMailDeliveryAvailable,
54
+ isMailRendererAvailable,
48
55
  isMailTransportAvailable,
49
56
  makeDefaultDeletionExecutedEmail,
50
57
  makeDefaultExportFailedEmail,
@@ -53,6 +60,7 @@ import {
53
60
  import { makeTenantMailTransportResolver } from "./lib/mail-transport-resolver.js";
54
61
  import { resolveAppTenantModel } from "./lib/resolve-tenant-model.js";
55
62
  import { makeTenantStorageProviderResolver } from "./lib/storage-provider-resolver.js";
63
+ import { registerGdprMails } from "./mail-notifications.js";
56
64
  import {
57
65
  EXPORT_CLEANUP_BACKLOG_AGE_METRIC,
58
66
  EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC,
@@ -163,11 +171,22 @@ export type UserDataRightsOptions = {
163
171
  readonly privacyCenterShowDeletion?: boolean;
164
172
  };
165
173
 
174
+ function isMailTransportMountedWithoutDelivery(features: readonly FeatureDefinition[]): boolean {
175
+ const names = new Set(features.map((feature) => feature.name));
176
+ const hasTransport = [...names].some((name) => name.startsWith("mail-transport-"));
177
+ return (
178
+ names.has("mail-foundation") &&
179
+ hasTransport &&
180
+ !(names.has("delivery") && names.has("channel-email"))
181
+ );
182
+ }
183
+
166
184
  export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): FeatureDefinition {
167
185
  // One-shot operator warning (the export cron fires daily — warn once per
168
186
  // process, not every run). Lives in the factory scope so the cron closure
169
187
  // shares it across runs.
170
188
  let warnedMissingExportUrl = false;
189
+ let warnedMissingMailDelivery = false;
171
190
  const showDeletion = opts.privacyCenterShowDeletion ?? true;
172
191
  return defineFeature("user-data-rights", (r) => {
173
192
  r.describe(
@@ -197,7 +216,15 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
197
216
  // EXT_USER_DATA channel owner: any r.useExtension(EXT_USER_DATA, ...)
198
217
  // usage already requires user-data-rights to be mounted.
199
218
  r.bootCheck(({ features }) => validateGdprHookCompleteness(features));
200
- r.bootCheck(({ features }) => validateGdprPiiHookCoverage(features));
219
+ r.bootCheck(({ features }) => {
220
+ validateGdprPiiHookCoverage(features);
221
+ if (!warnedMissingMailDelivery && isMailTransportMountedWithoutDelivery(features)) {
222
+ warnedMissingMailDelivery = true;
223
+ createFallbackLogger("user-data-rights").warn(
224
+ "mail-foundation and a mail transport are mounted but delivery/channel-email are not — the default export and deletion-request mails need them and will not be sent",
225
+ );
226
+ }
227
+ });
201
228
 
202
229
  // App-level tenant-occupancy model. Default "multi-user" → tenant-scoped
203
230
  // contributors (e.g. credit) NEVER erase tenant data on a per-user forget
@@ -312,6 +339,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
312
339
  });
313
340
 
314
341
  r.translations({ keys: USER_DATA_RIGHTS_I18N });
342
+ registerGdprMails(r);
315
343
  // Privacy-center dot-form labels (i18nKey()-marked below), duplicated in
316
344
  // web/i18n.ts's client bundle since privacy-center-screen.tsx renders
317
345
  // them client-side.
@@ -520,10 +548,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
520
548
  handler: async (c, { app, clientIp }) => {
521
549
  const url = new URL(c.req.url);
522
550
  const parsedBody = await c.req.json().catch(() => null);
523
- const token =
524
- parsedBody && typeof parsedBody === "object" && "token" in parsedBody
525
- ? (parsedBody as { token?: unknown }).token
526
- : undefined;
551
+ const token = isPlainObject(parsedBody) ? parsedBody["token"] : undefined;
527
552
  if (typeof token !== "string" || token.length === 0) {
528
553
  return c.json({ error: "missing_token" }, 400);
529
554
  }
@@ -605,28 +630,25 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
605
630
  // versendet der Cron die Export-Notifications selbst (Default-Templates).
606
631
  // Der Resolver baut den per-Tenant-Transport aus configResolver — gleiche
607
632
  // Bruecke wie buildStorageProvider.
608
- const exportMailResolver = isMailTransportAvailable(exportRegistry)
609
- ? makeTenantMailTransportResolver({
610
- registry: exportRegistry,
611
- configResolver: ctx.configResolver,
612
- secrets: ctx.secrets,
613
- db: exportDb,
614
- userId: exportUserId,
615
- handlerName: "user-data-rights:run-export-jobs",
616
- })
617
- : undefined;
633
+ // The cron runs across tenants: each mail needs a notify bound to the
634
+ // recipient's tenant (template override, channel config).
635
+ const notifyFactory = ctx._notifyFactory;
636
+ const exportNotifyFor =
637
+ notifyFactory !== undefined && isGdprMailDeliveryAvailable(exportRegistry)
638
+ ? (tenantId: TenantId) => notifyFactory(createSystemUser(tenantId), tenantId)
639
+ : undefined;
618
640
  const sendExportReadyEmail =
619
641
  opts.sendExportReadyEmail ??
620
- (exportMailResolver && opts.appExportDownloadUrl !== undefined
621
- ? makeDefaultExportReadyEmail(exportMailResolver, opts.mailDefaults)
642
+ (exportNotifyFor && opts.appExportDownloadUrl !== undefined
643
+ ? makeDefaultExportReadyEmail(exportNotifyFor, opts.mailDefaults)
622
644
  : undefined);
623
645
  const sendExportFailedEmail =
624
646
  opts.sendExportFailedEmail ??
625
- (exportMailResolver
626
- ? makeDefaultExportFailedEmail(exportMailResolver, opts.mailDefaults)
647
+ (exportNotifyFor
648
+ ? makeDefaultExportFailedEmail(exportNotifyFor, opts.mailDefaults)
627
649
  : undefined);
628
650
  if (
629
- exportMailResolver &&
651
+ exportNotifyFor &&
630
652
  !opts.sendExportReadyEmail &&
631
653
  opts.appExportDownloadUrl === undefined &&
632
654
  !warnedMissingExportUrl
@@ -634,7 +656,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
634
656
  warnedMissingExportUrl = true;
635
657
  // biome-ignore lint/suspicious/noConsole: one-shot operator visibility for misconfig
636
658
  console.warn(
637
- "[user-data-rights:run-export-jobs] mail transport mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)",
659
+ "[user-data-rights:run-export-jobs] mail delivery mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)",
638
660
  );
639
661
  }
640
662
 
@@ -697,22 +719,37 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
697
719
  userId: forgetUserId,
698
720
  });
699
721
 
700
- // C6 — Default-Mail beim delete-flip wenn kein Callback gesetzt + ein
701
- // mail-transport gemountet ist (gleiche per-Tenant-Bruecke wie Export).
702
- const forgetMailResolver = isMailTransportAvailable(forgetRegistry)
703
- ? makeTenantMailTransportResolver({
704
- registry: forgetRegistry,
705
- configResolver: ctx.configResolver,
706
- secrets: ctx.secrets,
707
- db: forgetDb,
708
- userId: forgetUserId,
709
- handlerName: "user-data-rights:run-forget-cleanup",
710
- })
711
- : undefined;
722
+ // Without an app callback the default mail goes out when a mail transport and a
723
+ // mail renderer are mounted. The user is already erased here, so it bypasses the
724
+ // delivery log: an attempt row would be new personal data.
725
+ const forgetConfigResolver = ctx.configResolver;
712
726
  const sendDeletionExecutedEmail =
713
727
  opts.sendDeletionExecutedEmail ??
714
- (forgetMailResolver
715
- ? makeDefaultDeletionExecutedEmail(forgetMailResolver, opts.mailDefaults)
728
+ (isMailTransportAvailable(forgetRegistry) && isMailRendererAvailable(forgetRegistry)
729
+ ? makeDefaultDeletionExecutedEmail(
730
+ {
731
+ registry: forgetRegistry,
732
+ db: forgetDb,
733
+ resolveTransport: makeTenantMailTransportResolver({
734
+ registry: forgetRegistry,
735
+ configResolver: forgetConfigResolver,
736
+ secrets: ctx.secrets,
737
+ db: forgetDb,
738
+ userId: forgetUserId,
739
+ handlerName: "user-data-rights:run-forget-cleanup",
740
+ }),
741
+ configFor: (tenantId) =>
742
+ forgetConfigResolver &&
743
+ createConfigAccessor(
744
+ forgetRegistry,
745
+ forgetConfigResolver,
746
+ tenantId,
747
+ forgetUserId,
748
+ forgetDb,
749
+ ),
750
+ },
751
+ opts.mailDefaults,
752
+ )
716
753
  : undefined);
717
754
 
718
755
  const forgetResult = await runForgetCleanup({
@@ -71,12 +71,13 @@ export const cancelDeletionWrite = defineWriteHandler({
71
71
 
72
72
  // gracePeriodEnd=null im Response symmetrisch zu request-deletion's
73
73
  // ISO-Timestamp — Frontend kann beide Endpoints uniform behandeln.
74
+ const gracePeriodEnd: string | null = null;
74
75
  return {
75
76
  isSuccess: true as const,
76
77
  data: {
77
78
  userId: event.user.id,
78
79
  status: USER_STATUS.Active,
79
- gracePeriodEnd: null as string | null,
80
+ gracePeriodEnd,
80
81
  },
81
82
  };
82
83
  },
@@ -1,11 +1,10 @@
1
- import { createTransportForTenant } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
2
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
3
  import * as z from "zod";
5
4
  import { USER_STATUS } from "../../user/index.js";
6
5
  import {
7
6
  type GdprMailDefaults,
8
- isMailTransportAvailable,
7
+ isGdprMailDeliveryAvailable,
9
8
  makeDefaultDeletionRequestedEmail,
10
9
  } from "../lib/default-mailers.js";
11
10
  import { resolveGracePeriod, startDeletionGracePeriod } from "./deletion-grace-period.js";
@@ -71,17 +70,12 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
71
70
  if (!res.ok) return writeFailure(res.error);
72
71
  const { gracePeriodEnd, userEmail, userLocale } = res;
73
72
 
74
- // App-Callback hat Vorrang; sonst greift die mail-foundation-Default-Mail
75
- // (Request-Lane → ctx.config vorhanden, createTransportForTenant direkt).
76
- // Ohne gemounteten mail-transport bleibt send undefined → keine Mail.
73
+ // The app callback wins; otherwise the default mail goes through ctx.notify after
74
+ // the commit. Without delivery and an email channel there is no sender and no mail.
77
75
  const send =
78
76
  opts.sendDeletionRequestedEmail ??
79
- (isMailTransportAvailable(ctx.registry)
80
- ? makeDefaultDeletionRequestedEmail(
81
- (tenantId) =>
82
- createTransportForTenant(ctx, tenantId, "user-data-rights:request-deletion"),
83
- opts.mailDefaults,
84
- )
77
+ (ctx.notify !== undefined && isGdprMailDeliveryAvailable(ctx.registry)
78
+ ? makeDefaultDeletionRequestedEmail(ctx.notify, opts.mailDefaults)
85
79
  : undefined);
86
80
 
87
81
  // Best-effort Email-Notification. Send-Failure darf das Write nicht