@cosmicdrift/kumiko-samples 0.361.0 → 0.363.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +17 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  12. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  13. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  15. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
  18. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  20. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  21. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  22. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  23. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  24. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  25. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  26. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  27. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  28. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  29. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
  33. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  34. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
  35. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  36. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  37. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
  38. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  39. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  40. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  41. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
  42. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  43. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
  44. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  45. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  46. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  47. package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
  48. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  49. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  50. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  51. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  52. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  53. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  54. package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
  55. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  56. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  57. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  58. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  59. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  60. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  61. package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
  62. package/packages/bundled-features/src/delivery/changes.json +48 -0
  63. package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
  64. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  65. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  66. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  67. package/packages/bundled-features/src/delivery/index.ts +17 -2
  68. package/packages/bundled-features/src/delivery/jobs.ts +48 -5
  69. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  70. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  71. package/packages/bundled-features/src/delivery/types.ts +28 -18
  72. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  73. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  74. package/packages/bundled-features/src/document-ingest-foundation/aggregate-id.ts +10 -0
  75. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  76. package/packages/bundled-features/src/document-ingest-foundation/events.ts +3 -1
  77. package/packages/bundled-features/src/document-ingest-foundation/feature.ts +5 -3
  78. package/packages/bundled-features/src/document-ingest-foundation/index.ts +1 -0
  79. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  80. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  81. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  82. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  83. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  84. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  85. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  86. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  87. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  89. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  91. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  92. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  93. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  94. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  95. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  96. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  97. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  98. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  99. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  100. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  101. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  102. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  103. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  104. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  105. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  106. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  107. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  108. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  109. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  110. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  111. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  112. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  113. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  114. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  115. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  116. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  117. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  118. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  119. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  120. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  121. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  122. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  123. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  124. package/packages/bundled-features/src/shared/index.ts +6 -0
  125. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  126. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  127. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  128. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  129. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  130. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  131. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  132. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  133. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  134. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  135. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  136. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  137. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  138. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  139. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  140. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  141. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  144. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  145. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  149. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  153. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  154. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  158. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  159. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  160. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  162. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  163. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  164. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  165. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  166. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  167. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  168. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  169. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  170. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  171. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  172. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  173. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  174. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  175. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  176. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  177. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  178. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  179. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  180. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  181. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  182. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  183. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  184. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  185. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  186. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  187. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  188. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  189. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  190. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  191. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  192. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  193. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  194. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  195. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  196. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  197. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  198. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  199. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  200. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  201. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  202. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  203. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  204. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  205. package/packages/bundled-features/src/user-data-rights/changes.json +14 -0
  206. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  207. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  208. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  209. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  210. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +5 -1
  211. package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +13 -1
  212. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  213. package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
  214. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  215. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  216. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  217. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  218. package/packages/bundled-features/src/user-data-rights/schema/download-attempt.ts +6 -12
  219. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  220. package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +10 -1
  221. package/packages/bundled-features/src/user-data-rights-defaults/hooks/download-attempt.userdata-hook.ts +58 -0
  222. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  223. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  224. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  225. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  226. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  227. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  228. package/samples/apps/ui-walkthrough/package.json +2 -0
  229. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  230. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  231. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  232. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  233. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  234. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  235. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  236. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  237. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  238. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  239. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +1 -1
  240. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  241. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  242. package/samples/apps/use-all-bundled/feature-manifest.json +123 -22
  243. package/samples/apps/use-all-bundled/kumiko/migrations/0044_download-attempt-ip-bidx.rebuild.json +4 -0
  244. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  245. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  246. package/samples/apps/use-all-bundled/package.json +2 -0
  247. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  248. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  249. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  250. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  251. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  252. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  253. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  254. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  255. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  256. package/samples/recipes/delivery-notifications/README.md +26 -25
  257. package/samples/recipes/delivery-notifications/package.json +2 -1
  258. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  259. package/samples/recipes/seed-migration/README.md +1 -1
  260. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  261. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  262. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  263. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  264. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  265. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -1,27 +1,38 @@
1
- // Default send*Email-Callbacks fuer die GDPR-Notifications, backed by einem
2
- // mail-transport (mail-foundation). Damit muss eine App keinen Callback-Code
3
- // schreiben: sie mountet mail-foundation + einen mail-transport-* und setzt
4
- // fuer Export-Mails appExportDownloadUrl — fertig. Uebergibt sie eigene
5
- // send*Email-Opts, greifen diese Defaults nicht (Opt-Override im feature.ts).
1
+ // Default send*Email callbacks for the GDPR notifications. The app writes no
2
+ // callback code: it mounts delivery + channel-email (+ the template stack) and
3
+ // sets appExportDownloadUrl for export mails. Own send*Email options win over
4
+ // these defaults (opt-override in feature.ts).
6
5
  //
7
- // Jeder Callback rendert das Template (email-templates.ts) und versendet ueber
8
- // den per-Tenant aufgeloesten EmailTransport. `resolveTransport` kommt im Job-
9
- // Lane aus makeTenantMailTransportResolver, im Request-Lane direkt aus
10
- // createTransportForTenant(ctx, ...).
6
+ // Export-ready/-failed and deletion-requested go through ctx.notify addressed
7
+ // to the user, so delivery resolves the address and the attempt row carries
8
+ // the recipientId the forget path erases. Deletion-executed cannot: the user
9
+ // is gone by then, and an attempt row written after the erasure would be new
10
+ // personal data. It renders the seeded template and sends over the tenant's
11
+ // mail transport, with no delivery log.
11
12
 
12
13
  import type { EmailTransport } from "@cosmicdrift/kumiko-bundled-features/channel-email";
13
14
  import { MAIL_TRANSPORT_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
14
- import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
15
- import { hasMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
15
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
16
+ import type {
17
+ ConfigAccessor,
18
+ NotifyFn,
19
+ NotifyOptions,
20
+ Registry,
21
+ TenantId,
22
+ } from "@cosmicdrift/kumiko-framework/engine";
23
+ import { hasMailTranslations, mailT } from "@cosmicdrift/kumiko-framework/i18n";
24
+ import { DELIVERY_CHANNEL_EXTENSION } from "../../delivery/index.js";
16
25
  import {
26
+ collectRendererPlugins,
27
+ createRendererFoundationApi,
28
+ } from "../../renderer-foundation/index.js";
29
+ import {
30
+ formatGdprMailTimestamp,
17
31
  type GdprMailLocale,
18
32
  normalizeGdprMailLocale,
19
- renderDeletionExecutedEmail,
20
- renderDeletionRequestedEmail,
21
- renderExportFailedEmail,
22
- renderExportReadyEmail,
23
33
  } from "../email-templates.js";
24
34
  import type { SendDeletionRequestedEmailFn } from "../handlers/request-deletion.write.js";
35
+ import { GdprMailNotificationTypes, gdprMailTemplateLocale } from "../mail-notifications.js";
25
36
  import type { SendExportFailedEmailFn, SendExportReadyEmailFn } from "../run-export-jobs.js";
26
37
  import type { SendDeletionExecutedEmailFn } from "../run-forget-cleanup.js";
27
38
 
@@ -30,74 +41,116 @@ export type GdprMailDefaults = {
30
41
  readonly appName?: string;
31
42
  };
32
43
 
33
- // mail-foundation ist eine Soft-Dep von user-data-rights. Die Default-Mailer
34
- // greifen nur wenn mindestens ein mail-transport-* registriert ist — sonst
35
- // kann ohnehin nichts versendet werden und die App bleibt beim bisherigen
36
- // "kein Callback → keine Email"-Verhalten.
44
+ // The notify-based defaults need the delivery pipeline with an email channel.
45
+ export function isGdprMailDeliveryAvailable(registry: Registry): boolean {
46
+ return registry
47
+ .getExtensionUsages(DELIVERY_CHANNEL_EXTENSION)
48
+ .some((usage) => usage.entityName === "email");
49
+ }
50
+
37
51
  export function isMailTransportAvailable(registry: Registry): boolean {
38
52
  return registry.getExtensionUsages(MAIL_TRANSPORT_EXTENSION).length > 0;
39
53
  }
40
54
 
41
- type TransportResolver = (tenantId: string) => Promise<EmailTransport>;
55
+ export function isMailRendererAvailable(registry: Registry): boolean {
56
+ return collectRendererPlugins(registry).some((plugin) => plugin.kinds.includes("mail"));
57
+ }
58
+
59
+ export type NotifyForTenant = (tenantId: TenantId) => NotifyFn;
42
60
 
43
61
  // Per-recipient locale wins (user.locale); mailDefaults.locale is the fallback
44
- // for unknown/unsupported user.locale values; the template itself defaults to en.
45
- function localeFor(
62
+ // for unknown/unsupported user.locale values; a locale without a template for
63
+ // this notification falls back to en.
64
+ function mailLocaleFor(
65
+ notificationType: string,
46
66
  userLocale: string | null,
47
67
  defaults: GdprMailDefaults,
48
- ): GdprMailLocale | undefined {
68
+ ): string {
49
69
  const user = normalizeGdprMailLocale(userLocale);
50
- if (user !== undefined && hasMailTranslations(user)) return user;
51
- return defaults.locale;
70
+ const preferred = user !== undefined && hasMailTranslations(user) ? user : defaults.locale;
71
+ return gdprMailTemplateLocale(notificationType, preferred ?? "en");
72
+ }
73
+
74
+ function appNameFor(locale: string, defaults: GdprMailDefaults): string {
75
+ return defaults.appName ?? mailT(locale, "gdpr.mail.appNameDefault");
76
+ }
77
+
78
+ // Delivery failures come back as results, not throws; the callers' best-effort
79
+ // handling only sees them if they throw.
80
+ async function notifyImmediately(
81
+ notify: NotifyFn,
82
+ notificationType: string,
83
+ options: NotifyOptions,
84
+ ): Promise<void> {
85
+ const result = await notify(notificationType, { ...options, immediate: true });
86
+ const failed = result.deliveries.find((delivery) => delivery.status === "failed");
87
+ if (failed) {
88
+ throw new Error(`${notificationType} delivery failed: ${failed.error ?? "unknown"}`);
89
+ }
52
90
  }
53
91
 
54
92
  export function makeDefaultExportReadyEmail(
55
- resolveTransport: TransportResolver,
93
+ notifyFor: NotifyForTenant,
56
94
  defaults: GdprMailDefaults = {},
57
95
  ): SendExportReadyEmailFn {
58
96
  return async (args) => {
59
- const transport = await resolveTransport(args.tenantId);
60
- const { subject, html, text } = renderExportReadyEmail({
61
- downloadUrl: args.downloadUrl,
62
- expiresAt: args.expiresAt,
63
- locale: localeFor(args.userLocale, defaults),
64
- appName: defaults.appName,
97
+ const type = GdprMailNotificationTypes.exportReady;
98
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
99
+ // immediate: the plain magic-link token must not land in a delivery job payload.
100
+ await notifyImmediately(notifyFor(args.tenantId), type, {
101
+ to: args.userId,
102
+ locale,
103
+ data: {
104
+ appName: appNameFor(locale, defaults),
105
+ actionUrl: args.downloadUrl,
106
+ validUntil: formatGdprMailTimestamp(args.expiresAt),
107
+ },
65
108
  });
66
- await transport.send({ to: args.userEmail, subject, html, text });
67
109
  };
68
110
  }
69
111
 
70
112
  export function makeDefaultExportFailedEmail(
71
- resolveTransport: TransportResolver,
113
+ notifyFor: NotifyForTenant,
72
114
  defaults: GdprMailDefaults = {},
73
115
  ): SendExportFailedEmailFn {
74
116
  return async (args) => {
75
- const transport = await resolveTransport(args.tenantId);
76
- const { subject, html, text } = renderExportFailedEmail({
77
- locale: localeFor(args.userLocale, defaults),
78
- appName: defaults.appName,
117
+ const type = GdprMailNotificationTypes.exportFailed;
118
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
119
+ await notifyImmediately(notifyFor(args.tenantId), type, {
120
+ to: args.userId,
121
+ locale,
122
+ data: { appName: appNameFor(locale, defaults) },
79
123
  });
80
- await transport.send({ to: args.userEmail, subject, html, text });
81
124
  };
82
125
  }
83
126
 
84
127
  export function makeDefaultDeletionRequestedEmail(
85
- resolveTransport: TransportResolver,
128
+ notify: NotifyFn,
86
129
  defaults: GdprMailDefaults = {},
87
130
  ): SendDeletionRequestedEmailFn {
88
131
  return async (args) => {
89
- const transport = await resolveTransport(args.tenantId);
90
- const { subject, html, text } = renderDeletionRequestedEmail({
91
- gracePeriodEnd: args.gracePeriodEnd,
92
- locale: localeFor(args.userLocale, defaults),
93
- appName: defaults.appName,
132
+ const type = GdprMailNotificationTypes.deletionRequested;
133
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
134
+ await notify(type, {
135
+ to: args.userId,
136
+ locale,
137
+ data: {
138
+ appName: appNameFor(locale, defaults),
139
+ deletionDate: formatGdprMailTimestamp(args.gracePeriodEnd),
140
+ },
94
141
  });
95
- await transport.send({ to: args.userEmail, subject, html, text });
96
142
  };
97
143
  }
98
144
 
145
+ export type DeletionExecutedMailDeps = {
146
+ readonly registry: Registry;
147
+ readonly db: DbConnection;
148
+ readonly resolveTransport: (tenantId: string) => Promise<EmailTransport>;
149
+ readonly configFor: (tenantId: TenantId) => ConfigAccessor | undefined;
150
+ };
151
+
99
152
  export function makeDefaultDeletionExecutedEmail(
100
- resolveTransport: TransportResolver,
153
+ deps: DeletionExecutedMailDeps,
101
154
  defaults: GdprMailDefaults = {},
102
155
  ): SendDeletionExecutedEmailFn {
103
156
  return async (args) => {
@@ -109,12 +162,40 @@ export function makeDefaultDeletionExecutedEmail(
109
162
  // skip: orphan user has no tenant whose transport could send the mail
110
163
  return;
111
164
  }
112
- const transport = await resolveTransport(tenantId);
113
- const { subject, html, text } = renderDeletionExecutedEmail({
114
- executedAt: args.executedAt,
115
- locale: localeFor(args.userLocale, defaults),
116
- appName: defaults.appName,
165
+ const type = GdprMailNotificationTypes.deletionExecuted;
166
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
167
+ const plugin = createRendererFoundationApi(
168
+ collectRendererPlugins(deps.registry),
169
+ ).createRendererForTenant({ tenantId, kind: "mail" });
170
+ const config = deps.configFor(tenantId);
171
+ const rendered = await plugin.render(
172
+ {
173
+ kind: "mail",
174
+ payload: {
175
+ slug: type,
176
+ locale,
177
+ variables: {
178
+ appName: appNameFor(locale, defaults),
179
+ executedAt: formatGdprMailTimestamp(args.executedAt),
180
+ },
181
+ },
182
+ },
183
+ {
184
+ db: deps.db,
185
+ registry: deps.registry,
186
+ tenantId,
187
+ ...(config !== undefined && { config }),
188
+ },
189
+ );
190
+ if (rendered.kind !== "mail") {
191
+ throw new Error(`deletion-executed mail: renderer answered with kind "${rendered.kind}"`);
192
+ }
193
+ const transport = await deps.resolveTransport(tenantId);
194
+ await transport.send({
195
+ to: args.userEmail,
196
+ subject: rendered.subject,
197
+ html: rendered.html,
198
+ text: rendered.text,
117
199
  });
118
- await transport.send({ to: args.userEmail, subject, html, text });
119
200
  };
120
201
  }
@@ -92,12 +92,12 @@ export type BackfillResult = {
92
92
  };
93
93
 
94
94
  export async function backfillUserLifecycleEvents(conn: DbRunner): Promise<BackfillResult> {
95
- const rows = (await selectMany(conn, userTable, {})) as Array<{
95
+ const rows = await selectMany<{
96
96
  id: string;
97
97
  status: string;
98
98
  gracePeriodEnd: unknown;
99
99
  pendingDeletionRequestId: unknown;
100
- }>;
100
+ }>(conn, userTable, {});
101
101
 
102
102
  let backfilled = 0;
103
103
  const failed: Array<{ id: string; error: string }> = [];
@@ -0,0 +1,157 @@
1
+ import {
2
+ type FeatureRegistrar,
3
+ type NotificationLocalizedTemplate,
4
+ QnTypes,
5
+ qn,
6
+ type TemplateVariables,
7
+ templateVariable,
8
+ } from "@cosmicdrift/kumiko-framework/engine";
9
+ import {
10
+ hasMailTranslationKey,
11
+ listMailLocales,
12
+ mailT,
13
+ resolveMailLocale,
14
+ } from "@cosmicdrift/kumiko-framework/i18n";
15
+ import { escapeHtml } from "@cosmicdrift/kumiko-headless";
16
+ import { mailParagraph } from "../shared/index.js";
17
+ import { USER_DATA_RIGHTS_FEATURE } from "./constants.js";
18
+
19
+ const ALL_GDPR_MAIL_KINDS = [
20
+ "exportReady",
21
+ "exportFailed",
22
+ "deletionRequested",
23
+ "deletionExecuted",
24
+ ] as const;
25
+
26
+ export type GdprMailKind = (typeof ALL_GDPR_MAIL_KINDS)[number];
27
+
28
+ export const GDPR_MAIL_KINDS: Readonly<
29
+ Record<GdprMailKind, { readonly name: string; readonly translationKey: string }>
30
+ > = {
31
+ exportReady: { name: "export-ready", translationKey: "exportReady" },
32
+ exportFailed: { name: "export-failed", translationKey: "exportFailed" },
33
+ deletionRequested: { name: "deletion-requested", translationKey: "deletionRequested" },
34
+ deletionExecuted: { name: "deletion-executed", translationKey: "deletionExecuted" },
35
+ };
36
+
37
+ export const GdprMailNotificationTypes: Readonly<Record<GdprMailKind, string>> = {
38
+ exportReady: gdprMailType("exportReady"),
39
+ exportFailed: gdprMailType("exportFailed"),
40
+ deletionRequested: gdprMailType("deletionRequested"),
41
+ deletionExecuted: gdprMailType("deletionExecuted"),
42
+ };
43
+
44
+ function gdprMailType(kind: GdprMailKind): string {
45
+ return qn(USER_DATA_RIGHTS_FEATURE, QnTypes.notify, GDPR_MAIL_KINDS[kind].name);
46
+ }
47
+
48
+ function subjectKey(kind: GdprMailKind): string {
49
+ return `gdpr.mail.${GDPR_MAIL_KINDS[kind].translationKey}.subject`;
50
+ }
51
+
52
+ const SUBJECT_KEY_BY_TYPE: ReadonlyMap<string, string> = new Map(
53
+ ALL_GDPR_MAIL_KINDS.map((kind) => [GdprMailNotificationTypes[kind], subjectKey(kind)]),
54
+ );
55
+
56
+ // A registered locale whose bundle lacks the subject key has no template (see
57
+ // gdprMailLocalizedTemplate); the renderer has no locale fallback, so the
58
+ // sender must pick a locale that has one or the mail would not go out at all.
59
+ export function gdprMailTemplateLocale(notificationType: string, requestedLocale: string): string {
60
+ const locale = resolveMailLocale(requestedLocale);
61
+ const key = SUBJECT_KEY_BY_TYPE.get(notificationType);
62
+ return key !== undefined && hasMailTranslationKey(locale, key) ? locale : "en";
63
+ }
64
+
65
+ const appName = () => templateVariable.text({ required: true });
66
+
67
+ export const GDPR_MAIL_VARIABLES: Readonly<Record<GdprMailKind, TemplateVariables>> = {
68
+ exportReady: {
69
+ appName: appName(),
70
+ actionUrl: templateVariable.link({ required: true }),
71
+ validUntil: templateVariable.text({ required: true }),
72
+ },
73
+ exportFailed: { appName: appName() },
74
+ deletionRequested: {
75
+ appName: appName(),
76
+ deletionDate: templateVariable.text({ required: true }),
77
+ },
78
+ deletionExecuted: { appName: appName(), executedAt: templateVariable.text({ required: true }) },
79
+ };
80
+
81
+ // Translation placeholder -> template variable per kind.
82
+ const PLACEHOLDER_SLOTS: Readonly<Record<GdprMailKind, Readonly<Record<string, string>>>> = {
83
+ exportReady: { app: "{appName}", when: "{validUntil}" },
84
+ exportFailed: { app: "{appName}" },
85
+ deletionRequested: { app: "{appName}", when: "{deletionDate}" },
86
+ deletionExecuted: { app: "{appName}", when: "{executedAt}" },
87
+ };
88
+
89
+ function paragraph(kind: GdprMailKind, locale: string, part: string): string {
90
+ const { translationKey } = GDPR_MAIL_KINDS[kind];
91
+ return mailParagraph(
92
+ mailT(locale, `gdpr.mail.${translationKey}.${part}`, PLACEHOLDER_SLOTS[kind]),
93
+ );
94
+ }
95
+
96
+ function greeting(locale: string): string {
97
+ return mailParagraph(mailT(locale, "gdpr.mail.greeting"));
98
+ }
99
+
100
+ function mailBody(kind: GdprMailKind, locale: string): string {
101
+ switch (kind) {
102
+ case "exportReady":
103
+ return [
104
+ greeting(locale),
105
+ paragraph(kind, locale, "intro"),
106
+ `<p><a data-button href="{{actionUrl}}">${escapeHtml(mailT(locale, "gdpr.mail.exportReady.button"))}</a></p>`,
107
+ paragraph(kind, locale, "expiry"),
108
+ ].join("");
109
+ case "exportFailed":
110
+ return [greeting(locale), paragraph(kind, locale, "intro")].join("");
111
+ case "deletionRequested":
112
+ return [
113
+ greeting(locale),
114
+ paragraph(kind, locale, "intro"),
115
+ paragraph(kind, locale, "cancel"),
116
+ ].join("");
117
+ case "deletionExecuted":
118
+ return [greeting(locale), paragraph(kind, locale, "intro")].join("");
119
+ }
120
+ }
121
+
122
+ // Evaluated when the registry is built, after every locale feature has
123
+ // registered its mail translations. English always ships; another locale only
124
+ // gets a template when it has its own subject, so a missing translation never
125
+ // silently becomes an English mail in that locale's slot.
126
+ export function gdprMailLocalizedTemplate(kind: GdprMailKind): () => NotificationLocalizedTemplate {
127
+ const kindSubjectKey = subjectKey(kind);
128
+ return () => {
129
+ const locales = [
130
+ "en",
131
+ ...listMailLocales().filter(
132
+ (locale) => locale !== "en" && hasMailTranslationKey(locale, kindSubjectKey),
133
+ ),
134
+ ];
135
+ return Object.fromEntries(
136
+ locales.map((locale) => [
137
+ locale,
138
+ {
139
+ body: mailBody(kind, locale),
140
+ meta: {
141
+ subject: mailT(locale, kindSubjectKey, { app: "{{appName}}" }),
142
+ category: "transactional" as const,
143
+ },
144
+ },
145
+ ]),
146
+ );
147
+ };
148
+ }
149
+
150
+ export function registerGdprMails(r: FeatureRegistrar): void {
151
+ for (const kind of ALL_GDPR_MAIL_KINDS) {
152
+ r.notification(GDPR_MAIL_KINDS[kind].name, {
153
+ variables: GDPR_MAIL_VARIABLES[kind],
154
+ localizedTemplate: gdprMailLocalizedTemplate(kind),
155
+ });
156
+ }
157
+ }
@@ -254,7 +254,7 @@ export async function runForgetCleanup(
254
254
 
255
255
  // Step 1: Find users with expired grace period.
256
256
  const dueUsers = await selectUsersDueForForgetCleanup(
257
- db as import("@cosmicdrift/kumiko-types/db-connection").DbConnection,
257
+ db,
258
258
  USER_STATUS.DeletionRequested,
259
259
  now.toString(),
260
260
  );
@@ -35,23 +35,17 @@ export const downloadAttemptEntity = createEntity({
35
35
  jobId: createTextField({ personal: false, reason: "technical_reference" }),
36
36
  // User-ID wenn auth-Pfad (job). NULL bei anonymous (token-Pfad).
37
37
  attemptedByUserId: createTextField({ personal: false, reason: "pseudonymous_fk" }),
38
- // No subject annotation on this buildEntityTable-managed entity:
39
- // attemptedByUserId is null-by-design for the anonymous token path, so
40
- // `personal: { of: "attemptedByUserId" }` would throw SubjectResolutionError
41
- // on every such row once a KMS is configured
42
- // (crypto/subject-resolver.ts resolveUserOwnedSubject).
43
- // The job path knows the requester and an IP is personal data, so this is a
44
- // deliberate waiver, not business data: a user forget does not shred these
45
- // values, the 90d hardDelete below is what removes them.
38
+ // The anonymous token path has no owner, so those rows fall back to the tenant key;
39
+ // find "exact" on ip because list-download-attempts filters by IP.
46
40
  ip: createTextField({
47
41
  maxLength: 64,
48
- personal: false,
49
- reason: "security_audit_retention_bounded",
42
+ personal: { of: "attemptedByUserId", whenAbsent: "tenant" },
43
+ find: "exact",
50
44
  }),
51
45
  userAgent: createTextField({
52
46
  maxLength: 256,
53
- personal: false,
54
- reason: "security_audit_retention_bounded",
47
+ personal: { of: "attemptedByUserId", whenAbsent: "tenant" },
48
+ find: "none",
55
49
  }),
56
50
  attemptedAt: createTimestampField({ required: true }),
57
51
  },
@@ -17,6 +17,7 @@ import {
17
17
  useTranslation,
18
18
  } from "@cosmicdrift/kumiko-renderer";
19
19
  import { postWithDownload } from "@cosmicdrift/kumiko-renderer-web";
20
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
20
21
  import { type ReactNode, useEffect, useState } from "react";
21
22
  import {
22
23
  EXPORT_JOB_STATUS,
@@ -49,7 +50,7 @@ type SectionStatus =
49
50
  // Dispatcher-Failures tragen i18nKey nur wenn der Handler einen setzt —
50
51
  // Boundary-Read mit generischem Fallback.
51
52
  function failureKey(error: unknown): string {
52
- const key = (error as { i18nKey?: unknown } | null)?.i18nKey; // @cast-boundary dispatcher-error
53
+ const key = propertyOf(error, "i18nKey");
53
54
  return typeof key === "string" ? key : "userDataRights.privacyCenter.errors.generic";
54
55
  }
55
56
 
@@ -12,6 +12,10 @@ import {
12
12
  deliveryAttemptDeleteHook,
13
13
  deliveryAttemptExportHook,
14
14
  } from "./hooks/delivery-attempt.userdata-hook.js";
15
+ import {
16
+ downloadAttemptDeleteHook,
17
+ downloadAttemptExportHook,
18
+ } from "./hooks/download-attempt.userdata-hook.js";
15
19
  import { fileRefDeleteHook, fileRefExportHook } from "./hooks/file-ref.userdata-hook.js";
16
20
  import {
17
21
  inAppMessageDeleteHook,
@@ -57,7 +61,7 @@ import {
57
61
  export function createUserDataRightsDefaultsFeature(): FeatureDefinition {
58
62
  return defineFeature("user-data-rights-defaults", (r) => {
59
63
  r.describe(
60
- "Registers ready-made `EXT_USER_DATA` export and delete hooks for the bundled entities that hold per-user data: `user` (delete strategy sets email to `deleted-<id>@anonymized.invalid`, nulls `passwordHash`, sets status to `Deleted`; anonymize strategy sets email to `anonymized-<id>@anonymized.invalid` without touching `passwordHash`), `fileRef` (delete removes both the DB row and the storage binary), plus — gated on the source feature being mounted — `user-session` (ip/userAgent, hard-delete), `api-token` (hard-delete = revoke), `in-app-message` (hard-delete), `tenant-invitation` (invitee email forgotten/pseudonymized, inviter link severed), `notification-preference` and user-scoped `config-value` (purged via the forget verb), plus export-only hooks for the events-only aggregates `delivery-attempt` (recipientAddress) and `job-run` (payload) whose erasure runs via crypto-shredding. Mount this alongside `user-data-rights` for standard GDPR compliance; omit it only if your app needs custom anonymization logic for these entities.",
64
+ "Registers ready-made `EXT_USER_DATA` export and delete hooks for the bundled entities that hold per-user data: `user` (delete strategy sets email to `deleted-<id>@anonymized.invalid`, nulls `passwordHash`, sets status to `Deleted`; anonymize strategy sets email to `anonymized-<id>@anonymized.invalid` without touching `passwordHash`), `fileRef` (delete removes both the DB row and the storage binary), plus — gated on the source feature being mounted — `user-session` (ip/userAgent, hard-delete), `api-token` (hard-delete = revoke), `in-app-message` (hard-delete), `tenant-invitation` (invitee email forgotten/pseudonymized, inviter link severed), `notification-preference` and user-scoped `config-value` (purged via the forget verb), plus export-only hooks for the events-only aggregates `delivery-attempt` (recipientAddress) and `job-run` (payload) whose erasure runs via crypto-shredding, and `download-attempt` (rejected export-download attempts of the user: result, path, source IP, user agent, time; delete purges the user's own attempts via the forget verb, token-path rows without an owner expire by retention). Mount this alongside `user-data-rights` for standard GDPR compliance; omit it only if your app needs custom anonymization logic for these entities.",
61
65
  );
62
66
  r.uiHints({
63
67
  displayLabel: "User Data Rights · Default Hooks",
@@ -128,6 +132,11 @@ export function createUserDataRightsDefaultsFeature(): FeatureDefinition {
128
132
  delete: deliveryAttemptDeleteHook,
129
133
  });
130
134
 
135
+ r.useExtension(EXT_USER_DATA, "download-attempt", {
136
+ export: downloadAttemptExportHook,
137
+ delete: downloadAttemptDeleteHook,
138
+ });
139
+
131
140
  r.useExtension(EXT_USER_DATA, "job-run", {
132
141
  export: jobRunExportHook,
133
142
  delete: jobRunDeleteHook,
@@ -0,0 +1,58 @@
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
+ import {
3
+ createSystemUser,
4
+ type UserDataDeleteHook,
5
+ type UserDataExportHook,
6
+ } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { collectErasureFailure, throwIfErasureFailed } from "../../shared/index.js";
8
+ import { downloadAttemptEntity, downloadAttemptsTable } from "../../user-data-rights/index.js";
9
+
10
+ // Event-sourced entity: forget goes through the executor so the erasure replays
11
+ // on rebuild and removes the projection row including ip_bidx.
12
+ const crud = createEventStoreExecutor(downloadAttemptsTable, downloadAttemptEntity, {
13
+ entityName: "download-attempt",
14
+ });
15
+
16
+ // Only job-path attempts name a user; token-path rows have no owner and sit
17
+ // under the tenant key. ip may be ciphertext (kumiko-pii:), which the export
18
+ // runner's central decrypt sweep resolves.
19
+ export const downloadAttemptExportHook: UserDataExportHook = async (ctx) => {
20
+ const rows = await ctx.db.selectMany<Record<string, unknown>>(downloadAttemptsTable, {
21
+ tenantId: ctx.tenantId,
22
+ attemptedByUserId: ctx.userId,
23
+ });
24
+ if (rows.length === 0) return null;
25
+ return {
26
+ entity: "download-attempt",
27
+ rows: rows.map((r) => ({
28
+ result: r["result"],
29
+ via: r["via"],
30
+ ip: r["ip"],
31
+ userAgent: r["userAgent"],
32
+ attemptedAt: r["attemptedAt"],
33
+ })),
34
+ };
35
+ };
36
+
37
+ // Both strategies purge: key erasure alone leaves attemptedByUserId and the
38
+ // blind index linking the user to the row until retention, and not every forget
39
+ // path shreds the subject key.
40
+ export const downloadAttemptDeleteHook: UserDataDeleteHook = async (ctx) => {
41
+ const systemUser = createSystemUser(ctx.tenantId);
42
+ const rows = await ctx.db.selectMany<Record<string, unknown>>(downloadAttemptsTable, {
43
+ tenantId: ctx.tenantId,
44
+ attemptedByUserId: ctx.userId,
45
+ });
46
+ const failures: string[] = [];
47
+ for (const row of rows) {
48
+ const id = row["id"];
49
+ if (typeof id !== "string") continue;
50
+ collectErasureFailure(
51
+ await crud.forget({ id }, systemUser, ctx.db),
52
+ "download-attempt",
53
+ id,
54
+ failures,
55
+ );
56
+ }
57
+ throwIfErasureFailed(failures);
58
+ };
@@ -13,6 +13,7 @@ import {
13
13
  usePrimitives,
14
14
  useTranslation,
15
15
  } from "@cosmicdrift/kumiko-renderer";
16
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
16
17
  import { type ReactNode, useState } from "react";
17
18
  import { AuthHandlers } from "../../auth-email-password/constants.js";
18
19
  import { requestEmailVerification } from "../../auth-email-password/web/index.js";
@@ -27,7 +28,7 @@ type SectionStatus =
27
28
  // Dispatcher failures carry an i18nKey only when the handler sets one —
28
29
  // boundary read with a generic fallback.
29
30
  function failureKey(error: unknown): string {
30
- const key = (error as { i18nKey?: unknown } | null)?.i18nKey; // @cast-boundary dispatcher-error
31
+ const key = propertyOf(error, "i18nKey");
31
32
  return typeof key === "string" ? key : "profile.errors.generic";
32
33
  }
33
34
 
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.363.0",
4
+ "type": "breaking",
5
+ "title": "Waitlist confirmation and admin notice use template variables",
6
+ "detail": "The confirmation and admin-notice mails are r.notification declarations (waitlist:notify:confirmation, waitlist:notify:admin-notice) with variables and localized templates. The notify data is the variable object; the confirmation wording is computed in code and passed as variables. The admin notice subject is the submitter name.",
7
+ "migration": "Update delivery preferences and tests that referenced the old types or read the sections data. 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`)."
8
+ },
2
9
  {
3
10
  "version": "0.342.0",
4
11
  "type": "improvement",
@@ -12,6 +12,13 @@ import { rejectHandler } from "./handlers/reject.write.js";
12
12
  import { createSubmitHandler } from "./handlers/submit.write.js";
13
13
  import { WAITLIST_I18N } from "./i18n.js";
14
14
  import { adminAccess } from "./lib.js";
15
+ import {
16
+ WAITLIST_ADMIN_NOTICE_RECORD,
17
+ WAITLIST_ADMIN_NOTICE_VARIABLES,
18
+ WAITLIST_CONFIRMATION_VARIABLES,
19
+ waitlistAdminNoticeTemplate,
20
+ waitlistConfirmationTemplate,
21
+ } from "./mail.js";
15
22
  import type { WaitlistOptions } from "./options.js";
16
23
  import { waitlistListScreen } from "./screens.js";
17
24
  import { waitlistEntryDeleteHook, waitlistEntryExportHook } from "./user-data-hooks.js";
@@ -26,6 +33,16 @@ export function createWaitlistFeature(opts: WaitlistOptions = {}): FeatureDefini
26
33
  r.requires("tenant", "auth-email-password", "user-data-rights");
27
34
  r.translations({ keys: WAITLIST_I18N });
28
35
 
36
+ r.notification("confirmation", {
37
+ variables: WAITLIST_CONFIRMATION_VARIABLES,
38
+ localizedTemplate: waitlistConfirmationTemplate,
39
+ });
40
+ r.notification("admin-notice", {
41
+ variables: WAITLIST_ADMIN_NOTICE_VARIABLES,
42
+ record: WAITLIST_ADMIN_NOTICE_RECORD,
43
+ localizedTemplate: waitlistAdminNoticeTemplate,
44
+ });
45
+
29
46
  r.entity("waitlistEntry", waitlistEntryEntity);
30
47
  r.writeHandler(createSubmitHandler(opts));
31
48
  r.writeHandler(createInviteHandler(opts.invite));