@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/auth-email-password/email-templates.d.ts +4 -26
  8. package/dist/auth-email-password/email-templates.js +9 -101
  9. package/dist/auth-email-password/feature.js +24 -6
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  12. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  13. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  14. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
  16. package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
  17. package/dist/auth-email-password/index.d.ts +2 -2
  18. package/dist/auth-email-password/index.js +1 -1
  19. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  20. package/dist/auth-email-password/magic-link-mail.js +19 -14
  21. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  22. package/dist/auth-email-password/mail-notifications.js +116 -0
  23. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  24. package/dist/auth-email-password/signup-activation.js +2 -3
  25. package/dist/auth-foundation/types.js +2 -8
  26. package/dist/auth-mfa/feature.js +4 -4
  27. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  28. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  29. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  30. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  31. package/dist/billing-foundation/constants.d.ts +3 -3
  32. package/dist/billing-foundation/constants.js +5 -4
  33. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  34. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
  35. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  36. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  37. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  38. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  39. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
  40. package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
  41. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  42. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  43. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
  44. package/dist/billing-foundation/handlers/process-event.write.js +2 -2
  45. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  46. package/dist/billing-foundation/subscription-tier-sync.js +3 -4
  47. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  48. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  49. package/dist/channel-email/email-channel.d.ts +2 -2
  50. package/dist/channel-email/email-channel.js +89 -26
  51. package/dist/channel-email/feature.js +3 -3
  52. package/dist/channel-email/index.d.ts +1 -1
  53. package/dist/channel-email/index.js +1 -1
  54. package/dist/channel-email/pii-guard.js +10 -0
  55. package/dist/channel-email/smtp-transport.js +13 -0
  56. package/dist/channel-email/types.d.ts +8 -0
  57. package/dist/config/handlers/set.write.js +1 -2
  58. package/dist/custom-fields/lib/value-schema.js +10 -11
  59. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  60. package/dist/data-retention/index.d.ts +1 -0
  61. package/dist/data-retention/index.js +1 -0
  62. package/dist/data-retention/run-retention-cleanup.js +6 -6
  63. package/dist/delivery/attachment-ref.d.ts +39 -0
  64. package/dist/delivery/attachment-ref.js +64 -0
  65. package/dist/delivery/channel-context.d.ts +8 -4
  66. package/dist/delivery/channel-context.js +76 -2
  67. package/dist/delivery/constants.d.ts +1 -0
  68. package/dist/delivery/constants.js +1 -0
  69. package/dist/delivery/delivery-service.d.ts +4 -1
  70. package/dist/delivery/delivery-service.js +38 -32
  71. package/dist/delivery/feature.js +1 -1
  72. package/dist/delivery/index.d.ts +5 -2
  73. package/dist/delivery/index.js +4 -1
  74. package/dist/delivery/jobs.js +25 -5
  75. package/dist/delivery/notify-factory.js +4 -1
  76. package/dist/delivery/testing.d.ts +4 -1
  77. package/dist/delivery/types.d.ts +20 -14
  78. package/dist/delivery/unsubscribe.js +4 -1
  79. package/dist/derivatives-sharp/render.js +1 -18
  80. package/dist/document-ingest-foundation/pages.js +4 -4
  81. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  82. package/dist/file-foundation/index.d.ts +1 -0
  83. package/dist/file-foundation/index.js +1 -0
  84. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  85. package/dist/file-foundation/read-file-ref-content.js +49 -0
  86. package/dist/folders/web/folder-manager.js +3 -2
  87. package/dist/form-draft/db/queries/cleanup.js +4 -4
  88. package/dist/form-draft/db/queries/draft-count.js +2 -2
  89. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  90. package/dist/form-draft/release-file-refs.js +3 -2
  91. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  92. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  93. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  94. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  95. package/dist/inbound-mail-foundation/types.js +4 -4
  96. package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
  97. package/dist/inbound-provider-imap/imap-client.js +4 -3
  98. package/dist/ledger/recurring.js +1 -3
  99. package/dist/managed-pages/branding.d.ts +2 -0
  100. package/dist/managed-pages/branding.js +24 -2
  101. package/dist/managed-pages/feature.js +1 -0
  102. package/dist/managed-pages/i18n.js +1 -0
  103. package/dist/managed-pages/screens/branding-screen.js +8 -1
  104. package/dist/page-render/branding.d.ts +1 -0
  105. package/dist/page-render/branding.js +1 -0
  106. package/dist/page-render/markdown.js +1 -1
  107. package/dist/personal-access-tokens/feature.js +1 -1
  108. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  109. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  110. package/dist/renderer-foundation/api.d.ts +13 -13
  111. package/dist/renderer-foundation/api.js +1 -1
  112. package/dist/renderer-foundation/constants.js +2 -4
  113. package/dist/renderer-foundation/feature.js +7 -7
  114. package/dist/renderer-foundation/index.d.ts +2 -1
  115. package/dist/renderer-foundation/index.js +1 -0
  116. package/dist/renderer-foundation/types.d.ts +92 -32
  117. package/dist/renderer-pdf/feature.js +7 -1
  118. package/dist/sessions/db/queries/cleanup.js +2 -2
  119. package/dist/sessions/feature.js +1 -1
  120. package/dist/sessions/handlers/list.query.js +2 -1
  121. package/dist/shared/attachment-file-name.d.ts +3 -0
  122. package/dist/shared/attachment-file-name.js +35 -0
  123. package/dist/shared/index.d.ts +3 -0
  124. package/dist/shared/index.js +3 -0
  125. package/dist/shared/is-tenant-db.js +1 -1
  126. package/dist/shared/mail-template-body.d.ts +4 -0
  127. package/dist/shared/mail-template-body.js +17 -0
  128. package/dist/shared/sharp-input-limits.d.ts +6 -0
  129. package/dist/shared/sharp-input-limits.js +22 -0
  130. package/dist/shared/signup-handover.js +2 -2
  131. package/dist/tags/handlers/assign-tag.write.js +3 -1
  132. package/dist/tags/web/tag-chip.js +2 -0
  133. package/dist/template-resolver/api.d.ts +30 -2
  134. package/dist/template-resolver/api.js +40 -5
  135. package/dist/template-resolver/compile-document-body.d.ts +8 -0
  136. package/dist/template-resolver/compile-document-body.js +163 -0
  137. package/dist/template-resolver/constants.d.ts +13 -4
  138. package/dist/template-resolver/constants.js +30 -10
  139. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  140. package/dist/template-resolver/document-sanitizer.js +201 -0
  141. package/dist/template-resolver/document-schema.d.ts +21 -0
  142. package/dist/template-resolver/document-schema.js +296 -0
  143. package/dist/template-resolver/feature.d.ts +18 -0
  144. package/dist/template-resolver/feature.js +50 -6
  145. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  146. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  147. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  148. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  149. package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
  150. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  151. package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
  152. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  153. package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
  154. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  155. package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
  156. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  157. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  158. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  159. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  160. package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
  161. package/dist/template-resolver/handlers/layout-source.query.js +64 -0
  162. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  163. package/dist/template-resolver/handlers/list.query.js +5 -7
  164. package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
  165. package/dist/template-resolver/handlers/overview.query.js +165 -0
  166. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  167. package/dist/template-resolver/handlers/preview.query.js +24 -0
  168. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  169. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  170. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  171. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  172. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  173. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  174. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  175. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  176. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  177. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  178. package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
  179. package/dist/template-resolver/handlers/send-test.write.js +81 -0
  180. package/dist/template-resolver/handlers/set.write.d.ts +4 -1
  181. package/dist/template-resolver/handlers/set.write.js +25 -3
  182. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  183. package/dist/template-resolver/handlers/shared.js +6 -2
  184. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  185. package/dist/template-resolver/handlers/template-gates.js +131 -0
  186. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  187. package/dist/template-resolver/handlers/template-write.js +109 -0
  188. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
  189. package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
  190. package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
  191. package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
  192. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  193. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  194. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  195. package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
  196. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  197. package/dist/template-resolver/handlers/usage.query.js +15 -0
  198. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  199. package/dist/template-resolver/handlers/validate.query.js +80 -0
  200. package/dist/template-resolver/index.d.ts +18 -1
  201. package/dist/template-resolver/index.js +16 -1
  202. package/dist/template-resolver/layout-regions.d.ts +23 -0
  203. package/dist/template-resolver/layout-regions.js +125 -0
  204. package/dist/template-resolver/linked-resources.d.ts +17 -0
  205. package/dist/template-resolver/linked-resources.js +75 -0
  206. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  207. package/dist/template-resolver/preview-display-html.js +27 -0
  208. package/dist/template-resolver/print-document.d.ts +1 -0
  209. package/dist/template-resolver/print-document.js +55 -0
  210. package/dist/template-resolver/qualified-names.d.ts +30 -0
  211. package/dist/template-resolver/qualified-names.js +30 -0
  212. package/dist/template-resolver/record-mapping.d.ts +2 -0
  213. package/dist/template-resolver/record-mapping.js +21 -0
  214. package/dist/template-resolver/resource-image.d.ts +15 -0
  215. package/dist/template-resolver/resource-image.js +34 -0
  216. package/dist/template-resolver/seeding.d.ts +14 -1
  217. package/dist/template-resolver/seeding.js +105 -15
  218. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  219. package/dist/template-resolver/store-template-resource.js +264 -0
  220. package/dist/template-resolver/table.d.ts +47 -8
  221. package/dist/template-resolver/table.js +11 -0
  222. package/dist/template-resolver/template-bundle.d.ts +36 -0
  223. package/dist/template-resolver/template-bundle.js +60 -0
  224. package/dist/template-resolver/template-diff.d.ts +7 -0
  225. package/dist/template-resolver/template-diff.js +68 -0
  226. package/dist/template-resolver/template-meta.d.ts +60 -0
  227. package/dist/template-resolver/template-meta.js +101 -0
  228. package/dist/template-resolver/template-preview.d.ts +86 -0
  229. package/dist/template-resolver/template-preview.js +374 -0
  230. package/dist/template-resolver/template-resources.d.ts +14 -0
  231. package/dist/template-resolver/template-resources.js +42 -0
  232. package/dist/template-resolver/template-usage.d.ts +12 -0
  233. package/dist/template-resolver/template-usage.js +30 -0
  234. package/dist/template-resolver/template-validation.d.ts +22 -0
  235. package/dist/template-resolver/template-validation.js +172 -0
  236. package/dist/template-resolver/testing.js +5 -5
  237. package/dist/template-resolver/user-content-table.d.ts +14 -8
  238. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  239. package/dist/template-resolver/web/client-plugin.js +9 -6
  240. package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
  241. package/dist/template-resolver/web/document-editor-extensions.js +107 -0
  242. package/dist/template-resolver/web/document-editor.d.ts +3 -0
  243. package/dist/template-resolver/web/document-editor.js +11 -0
  244. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  245. package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
  246. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
  247. package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
  248. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  249. package/dist/template-resolver/web/editor-handle.js +24 -0
  250. package/dist/template-resolver/web/i18n.js +76 -0
  251. package/dist/template-resolver/web/index.d.ts +4 -0
  252. package/dist/template-resolver/web/index.js +4 -0
  253. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  254. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  255. package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
  256. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  257. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  258. package/dist/template-resolver/web/placeholder-text.js +34 -0
  259. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  260. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  261. package/dist/tenant/handlers/team-list.query.js +3 -2
  262. package/dist/tenant/seeding.js +31 -6
  263. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  264. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  265. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  266. package/dist/user/handlers/detail.query.js +2 -1
  267. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  268. package/dist/user-data-rights/email-templates.d.ts +1 -29
  269. package/dist/user-data-rights/email-templates.js +5 -106
  270. package/dist/user-data-rights/feature.js +53 -35
  271. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  272. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  273. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  274. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  275. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  276. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  277. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  278. package/dist/user-data-rights/mail-notifications.js +118 -0
  279. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  280. package/dist/user-profile/web/profile-screen.js +2 -1
  281. package/dist/waitlist/feature.js +10 -0
  282. package/dist/waitlist/handlers/submit.write.js +3 -4
  283. package/dist/waitlist/mail.d.ts +19 -6
  284. package/dist/waitlist/mail.js +50 -14
  285. package/package.json +28 -17
  286. package/src/admin-shell/changes.json +6 -0
  287. package/src/auth-email-password/changes.json +14 -0
  288. package/src/billing-foundation/changes.json +7 -0
  289. package/src/channel-email/changes.json +19 -0
  290. package/src/data-retention/changes.json +6 -0
  291. package/src/delivery/changes.json +48 -0
  292. package/src/managed-pages/changes.json +6 -0
  293. package/src/renderer-foundation/changes.json +39 -0
  294. package/src/template-resolver/changes.json +118 -0
  295. package/src/user-data-rights/changes.json +7 -0
  296. package/src/waitlist/changes.json +7 -0
  297. package/dist/renderer-simple/feature.d.ts +0 -10
  298. package/dist/renderer-simple/feature.js +0 -42
  299. package/dist/renderer-simple/index.d.ts +0 -3
  300. package/dist/renderer-simple/index.js +0 -2
  301. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  302. package/dist/renderer-simple/mail-blocks.js +0 -107
  303. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  304. package/dist/renderer-simple/resolve-variables.js +0 -47
  305. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  306. package/dist/renderer-simple/simple-renderer.js +0 -200
  307. package/src/renderer-simple/changes.json +0 -27
@@ -1,7 +1,10 @@
1
1
  // kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312 (feature.ts > 300 lines)
2
2
  import { UNKNOWN_CLIENT_IP } from "@cosmicdrift/kumiko-framework/api";
3
- import { createSystemConfig, defineFeature, EXT_USER_DATA, i18nKey, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createSystemConfig, createSystemUser, defineFeature, EXT_USER_DATA, i18nKey, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
4
5
  import { createMetricsHandle, createNoopMetricsHandle, } from "@cosmicdrift/kumiko-framework/observability";
6
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
7
+ import { createConfigAccessor } from "../config/index.js";
5
8
  import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks.js";
6
9
  import { EXPORT_SECTION_EXTENSION_NAME, PRIVACY_CENTER_SCREEN_ID, STATUS_OPTION_KEY_PREFIX, USER_ME_QUERY, UserDataRightsHandlers, } from "./constants.js";
7
10
  import { cancelDeletionWrite } from "./handlers/cancel-deletion.write.js";
@@ -21,21 +24,30 @@ import { requestExportWrite } from "./handlers/request-export.write.js";
21
24
  import { restrictAccountWrite } from "./handlers/restrict-account.write.js";
22
25
  import { createRunForgetCleanupHandler } from "./handlers/run-forget-cleanup.write.js";
23
26
  import { USER_DATA_RIGHTS_I18N } from "./i18n.js";
24
- import { isMailTransportAvailable, makeDefaultDeletionExecutedEmail, makeDefaultExportFailedEmail, makeDefaultExportReadyEmail, } from "./lib/default-mailers.js";
27
+ import { isGdprMailDeliveryAvailable, isMailRendererAvailable, isMailTransportAvailable, makeDefaultDeletionExecutedEmail, makeDefaultExportFailedEmail, makeDefaultExportReadyEmail, } from "./lib/default-mailers.js";
25
28
  import { makeTenantMailTransportResolver } from "./lib/mail-transport-resolver.js";
26
29
  import { resolveAppTenantModel } from "./lib/resolve-tenant-model.js";
27
30
  import { makeTenantStorageProviderResolver } from "./lib/storage-provider-resolver.js";
31
+ import { registerGdprMails } from "./mail-notifications.js";
28
32
  import { EXPORT_CLEANUP_BACKLOG_AGE_METRIC, EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC, runExportJobs, } from "./run-export-jobs.js";
29
33
  import { runForgetCleanup, sanitizeReasonForLog, } from "./run-forget-cleanup.js";
30
34
  import { downloadAttemptEntity } from "./schema/download-attempt.js";
31
35
  import { exportDownloadTokenEntity } from "./schema/download-token.js";
32
36
  import { exportJobEntity } from "./schema/export-job.js";
33
37
  import { downloadAttemptListScreen, exportJobDetailScreen, exportJobListScreen, } from "./screens.js";
38
+ function isMailTransportMountedWithoutDelivery(features) {
39
+ const names = new Set(features.map((feature) => feature.name));
40
+ const hasTransport = [...names].some((name) => name.startsWith("mail-transport-"));
41
+ return (names.has("mail-foundation") &&
42
+ hasTransport &&
43
+ !(names.has("delivery") && names.has("channel-email")));
44
+ }
34
45
  export function createUserDataRightsFeature(opts = {}) {
35
46
  // One-shot operator warning (the export cron fires daily — warn once per
36
47
  // process, not every run). Lives in the factory scope so the cron closure
37
48
  // shares it across runs.
38
49
  let warnedMissingExportUrl = false;
50
+ let warnedMissingMailDelivery = false;
39
51
  const showDeletion = opts.privacyCenterShowDeletion ?? true;
40
52
  return defineFeature("user-data-rights", (r) => {
41
53
  r.describe('Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` \u2192 ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })` \u2014 the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When `mail-foundation` and a `mail-transport-*` are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.');
@@ -61,7 +73,13 @@ export function createUserDataRightsFeature(opts = {}) {
61
73
  // EXT_USER_DATA channel owner: any r.useExtension(EXT_USER_DATA, ...)
62
74
  // usage already requires user-data-rights to be mounted.
63
75
  r.bootCheck(({ features }) => validateGdprHookCompleteness(features));
64
- r.bootCheck(({ features }) => validateGdprPiiHookCoverage(features));
76
+ r.bootCheck(({ features }) => {
77
+ validateGdprPiiHookCoverage(features);
78
+ if (!warnedMissingMailDelivery && isMailTransportMountedWithoutDelivery(features)) {
79
+ warnedMissingMailDelivery = true;
80
+ createFallbackLogger("user-data-rights").warn("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");
81
+ }
82
+ });
65
83
  // App-level tenant-occupancy model. Default "multi-user" → tenant-scoped
66
84
  // contributors (e.g. credit) NEVER erase tenant data on a per-user forget
67
85
  // (would harm co-members). An app with one user per tenant sets this to
@@ -147,6 +165,7 @@ export function createUserDataRightsFeature(opts = {}) {
147
165
  access: { roles: ["SystemAdmin"] },
148
166
  });
149
167
  r.translations({ keys: USER_DATA_RIGHTS_I18N });
168
+ registerGdprMails(r);
150
169
  // Privacy-center dot-form labels (i18nKey()-marked below), duplicated in
151
170
  // web/i18n.ts's client bundle since privacy-center-screen.tsx renders
152
171
  // them client-side.
@@ -350,9 +369,7 @@ export function createUserDataRightsFeature(opts = {}) {
350
369
  handler: async (c, { app, clientIp }) => {
351
370
  const url = new URL(c.req.url);
352
371
  const parsedBody = await c.req.json().catch(() => null);
353
- const token = parsedBody && typeof parsedBody === "object" && "token" in parsedBody
354
- ? parsedBody.token
355
- : undefined;
372
+ const token = isPlainObject(parsedBody) ? parsedBody["token"] : undefined;
356
373
  if (typeof token !== "string" || token.length === 0) {
357
374
  return c.json({ error: "missing_token" }, 400);
358
375
  }
@@ -426,31 +443,27 @@ export function createUserDataRightsFeature(opts = {}) {
426
443
  // versendet der Cron die Export-Notifications selbst (Default-Templates).
427
444
  // Der Resolver baut den per-Tenant-Transport aus configResolver — gleiche
428
445
  // Bruecke wie buildStorageProvider.
429
- const exportMailResolver = isMailTransportAvailable(exportRegistry)
430
- ? makeTenantMailTransportResolver({
431
- registry: exportRegistry,
432
- configResolver: ctx.configResolver,
433
- secrets: ctx.secrets,
434
- db: exportDb,
435
- userId: exportUserId,
436
- handlerName: "user-data-rights:run-export-jobs",
437
- })
446
+ // The cron runs across tenants: each mail needs a notify bound to the
447
+ // recipient's tenant (template override, channel config).
448
+ const notifyFactory = ctx._notifyFactory;
449
+ const exportNotifyFor = notifyFactory !== undefined && isGdprMailDeliveryAvailable(exportRegistry)
450
+ ? (tenantId) => notifyFactory(createSystemUser(tenantId), tenantId)
438
451
  : undefined;
439
452
  const sendExportReadyEmail = opts.sendExportReadyEmail ??
440
- (exportMailResolver && opts.appExportDownloadUrl !== undefined
441
- ? makeDefaultExportReadyEmail(exportMailResolver, opts.mailDefaults)
453
+ (exportNotifyFor && opts.appExportDownloadUrl !== undefined
454
+ ? makeDefaultExportReadyEmail(exportNotifyFor, opts.mailDefaults)
442
455
  : undefined);
443
456
  const sendExportFailedEmail = opts.sendExportFailedEmail ??
444
- (exportMailResolver
445
- ? makeDefaultExportFailedEmail(exportMailResolver, opts.mailDefaults)
457
+ (exportNotifyFor
458
+ ? makeDefaultExportFailedEmail(exportNotifyFor, opts.mailDefaults)
446
459
  : undefined);
447
- if (exportMailResolver &&
460
+ if (exportNotifyFor &&
448
461
  !opts.sendExportReadyEmail &&
449
462
  opts.appExportDownloadUrl === undefined &&
450
463
  !warnedMissingExportUrl) {
451
464
  warnedMissingExportUrl = true;
452
465
  // biome-ignore lint/suspicious/noConsole: one-shot operator visibility for misconfig
453
- console.warn("[user-data-rights:run-export-jobs] mail transport mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)");
466
+ console.warn("[user-data-rights:run-export-jobs] mail delivery mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)");
454
467
  }
455
468
  await runExportJobs({
456
469
  db: exportDb,
@@ -508,21 +521,26 @@ export function createUserDataRightsFeature(opts = {}) {
508
521
  db: forgetDb,
509
522
  userId: forgetUserId,
510
523
  });
511
- // C6 — Default-Mail beim delete-flip wenn kein Callback gesetzt + ein
512
- // mail-transport gemountet ist (gleiche per-Tenant-Bruecke wie Export).
513
- const forgetMailResolver = isMailTransportAvailable(forgetRegistry)
514
- ? makeTenantMailTransportResolver({
515
- registry: forgetRegistry,
516
- configResolver: ctx.configResolver,
517
- secrets: ctx.secrets,
518
- db: forgetDb,
519
- userId: forgetUserId,
520
- handlerName: "user-data-rights:run-forget-cleanup",
521
- })
522
- : undefined;
524
+ // Without an app callback the default mail goes out when a mail transport and a
525
+ // mail renderer are mounted. The user is already erased here, so it bypasses the
526
+ // delivery log: an attempt row would be new personal data.
527
+ const forgetConfigResolver = ctx.configResolver;
523
528
  const sendDeletionExecutedEmail = opts.sendDeletionExecutedEmail ??
524
- (forgetMailResolver
525
- ? makeDefaultDeletionExecutedEmail(forgetMailResolver, opts.mailDefaults)
529
+ (isMailTransportAvailable(forgetRegistry) && isMailRendererAvailable(forgetRegistry)
530
+ ? makeDefaultDeletionExecutedEmail({
531
+ registry: forgetRegistry,
532
+ db: forgetDb,
533
+ resolveTransport: makeTenantMailTransportResolver({
534
+ registry: forgetRegistry,
535
+ configResolver: forgetConfigResolver,
536
+ secrets: ctx.secrets,
537
+ db: forgetDb,
538
+ userId: forgetUserId,
539
+ handlerName: "user-data-rights:run-forget-cleanup",
540
+ }),
541
+ configFor: (tenantId) => forgetConfigResolver &&
542
+ createConfigAccessor(forgetRegistry, forgetConfigResolver, tenantId, forgetUserId, forgetDb),
543
+ }, opts.mailDefaults)
526
544
  : undefined);
527
545
  const forgetResult = await runForgetCleanup({
528
546
  db: forgetDb,
@@ -2,5 +2,5 @@ import * as z from "zod";
2
2
  export declare const cancelDeletionWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"cancel-deletion", z.ZodObject<{}, z.core.$strip>, {
3
3
  userId: string;
4
4
  status: "active";
5
- gracePeriodEnd: string | null;
5
+ gracePeriodEnd: null;
6
6
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -54,12 +54,13 @@ export const cancelDeletionWrite = defineWriteHandler({
54
54
  });
55
55
  // gracePeriodEnd=null im Response symmetrisch zu request-deletion's
56
56
  // ISO-Timestamp — Frontend kann beide Endpoints uniform behandeln.
57
+ const gracePeriodEnd = null;
57
58
  return {
58
59
  isSuccess: true,
59
60
  data: {
60
61
  userId: event.user.id,
61
62
  status: USER_STATUS.Active,
62
- gracePeriodEnd: null,
63
+ gracePeriodEnd,
63
64
  },
64
65
  };
65
66
  },
@@ -1,9 +1,8 @@
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
- import { isMailTransportAvailable, makeDefaultDeletionRequestedEmail, } from "../lib/default-mailers.js";
5
+ import { isGdprMailDeliveryAvailable, makeDefaultDeletionRequestedEmail, } from "../lib/default-mailers.js";
7
6
  import { resolveGracePeriod, startDeletionGracePeriod } from "./deletion-grace-period.js";
8
7
  // POST /api/user/request-deletion (S2.U5a) — DSGVO Art. 17 Forget-Antrag.
9
8
  // Flippt status=Active → deletionRequested, setzt gracePeriodEnd aus dem
@@ -34,12 +33,11 @@ export function createRequestDeletionHandler(opts = {}) {
34
33
  if (!res.ok)
35
34
  return writeFailure(res.error);
36
35
  const { gracePeriodEnd, userEmail, userLocale } = res;
37
- // App-Callback hat Vorrang; sonst greift die mail-foundation-Default-Mail
38
- // (Request-Lane → ctx.config vorhanden, createTransportForTenant direkt).
39
- // Ohne gemounteten mail-transport bleibt send undefined → keine Mail.
36
+ // The app callback wins; otherwise the default mail goes through ctx.notify after
37
+ // the commit. Without delivery and an email channel there is no sender and no mail.
40
38
  const send = opts.sendDeletionRequestedEmail ??
41
- (isMailTransportAvailable(ctx.registry)
42
- ? makeDefaultDeletionRequestedEmail((tenantId) => createTransportForTenant(ctx, tenantId, "user-data-rights:request-deletion"), opts.mailDefaults)
39
+ (ctx.notify !== undefined && isGdprMailDeliveryAvailable(ctx.registry)
40
+ ? makeDefaultDeletionRequestedEmail(ctx.notify, opts.mailDefaults)
43
41
  : undefined);
44
42
  // Best-effort Email-Notification. Send-Failure darf das Write nicht
45
43
  // killen — siehe Type-Doc oben.
@@ -1,5 +1,6 @@
1
1
  import type { EmailTransport } from "@cosmicdrift/kumiko-bundled-features/channel-email";
2
- import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { ConfigAccessor, NotifyFn, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { type GdprMailLocale } from "../email-templates.js";
4
5
  import type { SendDeletionRequestedEmailFn } from "../handlers/request-deletion.write.js";
5
6
  import type { SendExportFailedEmailFn, SendExportReadyEmailFn } from "../run-export-jobs.js";
@@ -8,10 +9,17 @@ export type GdprMailDefaults = {
8
9
  readonly locale?: GdprMailLocale;
9
10
  readonly appName?: string;
10
11
  };
12
+ export declare function isGdprMailDeliveryAvailable(registry: Registry): boolean;
11
13
  export declare function isMailTransportAvailable(registry: Registry): boolean;
12
- type TransportResolver = (tenantId: string) => Promise<EmailTransport>;
13
- export declare function makeDefaultExportReadyEmail(resolveTransport: TransportResolver, defaults?: GdprMailDefaults): SendExportReadyEmailFn;
14
- export declare function makeDefaultExportFailedEmail(resolveTransport: TransportResolver, defaults?: GdprMailDefaults): SendExportFailedEmailFn;
15
- export declare function makeDefaultDeletionRequestedEmail(resolveTransport: TransportResolver, defaults?: GdprMailDefaults): SendDeletionRequestedEmailFn;
16
- export declare function makeDefaultDeletionExecutedEmail(resolveTransport: TransportResolver, defaults?: GdprMailDefaults): SendDeletionExecutedEmailFn;
17
- export {};
14
+ export declare function isMailRendererAvailable(registry: Registry): boolean;
15
+ export type NotifyForTenant = (tenantId: TenantId) => NotifyFn;
16
+ export declare function makeDefaultExportReadyEmail(notifyFor: NotifyForTenant, defaults?: GdprMailDefaults): SendExportReadyEmailFn;
17
+ export declare function makeDefaultExportFailedEmail(notifyFor: NotifyForTenant, defaults?: GdprMailDefaults): SendExportFailedEmailFn;
18
+ export declare function makeDefaultDeletionRequestedEmail(notify: NotifyFn, defaults?: GdprMailDefaults): SendDeletionRequestedEmailFn;
19
+ export type DeletionExecutedMailDeps = {
20
+ readonly registry: Registry;
21
+ readonly db: DbConnection;
22
+ readonly resolveTransport: (tenantId: string) => Promise<EmailTransport>;
23
+ readonly configFor: (tenantId: TenantId) => ConfigAccessor | undefined;
24
+ };
25
+ export declare function makeDefaultDeletionExecutedEmail(deps: DeletionExecutedMailDeps, defaults?: GdprMailDefaults): SendDeletionExecutedEmailFn;
@@ -1,65 +1,94 @@
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
  import { MAIL_TRANSPORT_EXTENSION } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
12
- import { hasMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
13
- import { normalizeGdprMailLocale, renderDeletionExecutedEmail, renderDeletionRequestedEmail, renderExportFailedEmail, renderExportReadyEmail, } from "../email-templates.js";
14
- // mail-foundation ist eine Soft-Dep von user-data-rights. Die Default-Mailer
15
- // greifen nur wenn mindestens ein mail-transport-* registriert ist — sonst
16
- // kann ohnehin nichts versendet werden und die App bleibt beim bisherigen
17
- // "kein Callback → keine Email"-Verhalten.
13
+ import { hasMailTranslations, mailT } from "@cosmicdrift/kumiko-framework/i18n";
14
+ import { DELIVERY_CHANNEL_EXTENSION } from "../../delivery/index.js";
15
+ import { collectRendererPlugins, createRendererFoundationApi, } from "../../renderer-foundation/index.js";
16
+ import { formatGdprMailTimestamp, normalizeGdprMailLocale, } from "../email-templates.js";
17
+ import { GdprMailNotificationTypes, gdprMailTemplateLocale } from "../mail-notifications.js";
18
+ // The notify-based defaults need the delivery pipeline with an email channel.
19
+ export function isGdprMailDeliveryAvailable(registry) {
20
+ return registry
21
+ .getExtensionUsages(DELIVERY_CHANNEL_EXTENSION)
22
+ .some((usage) => usage.entityName === "email");
23
+ }
18
24
  export function isMailTransportAvailable(registry) {
19
25
  return registry.getExtensionUsages(MAIL_TRANSPORT_EXTENSION).length > 0;
20
26
  }
27
+ export function isMailRendererAvailable(registry) {
28
+ return collectRendererPlugins(registry).some((plugin) => plugin.kinds.includes("mail"));
29
+ }
21
30
  // Per-recipient locale wins (user.locale); mailDefaults.locale is the fallback
22
- // for unknown/unsupported user.locale values; the template itself defaults to en.
23
- function localeFor(userLocale, defaults) {
31
+ // for unknown/unsupported user.locale values; a locale without a template for
32
+ // this notification falls back to en.
33
+ function mailLocaleFor(notificationType, userLocale, defaults) {
24
34
  const user = normalizeGdprMailLocale(userLocale);
25
- if (user !== undefined && hasMailTranslations(user))
26
- return user;
27
- return defaults.locale;
35
+ const preferred = user !== undefined && hasMailTranslations(user) ? user : defaults.locale;
36
+ return gdprMailTemplateLocale(notificationType, preferred ?? "en");
37
+ }
38
+ function appNameFor(locale, defaults) {
39
+ return defaults.appName ?? mailT(locale, "gdpr.mail.appNameDefault");
28
40
  }
29
- export function makeDefaultExportReadyEmail(resolveTransport, defaults = {}) {
41
+ // Delivery failures come back as results, not throws; the callers' best-effort
42
+ // handling only sees them if they throw.
43
+ async function notifyImmediately(notify, notificationType, options) {
44
+ const result = await notify(notificationType, { ...options, immediate: true });
45
+ const failed = result.deliveries.find((delivery) => delivery.status === "failed");
46
+ if (failed) {
47
+ throw new Error(`${notificationType} delivery failed: ${failed.error ?? "unknown"}`);
48
+ }
49
+ }
50
+ export function makeDefaultExportReadyEmail(notifyFor, defaults = {}) {
30
51
  return async (args) => {
31
- const transport = await resolveTransport(args.tenantId);
32
- const { subject, html, text } = renderExportReadyEmail({
33
- downloadUrl: args.downloadUrl,
34
- expiresAt: args.expiresAt,
35
- locale: localeFor(args.userLocale, defaults),
36
- appName: defaults.appName,
52
+ const type = GdprMailNotificationTypes.exportReady;
53
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
54
+ // immediate: the plain magic-link token must not land in a delivery job payload.
55
+ await notifyImmediately(notifyFor(args.tenantId), type, {
56
+ to: args.userId,
57
+ locale,
58
+ data: {
59
+ appName: appNameFor(locale, defaults),
60
+ actionUrl: args.downloadUrl,
61
+ validUntil: formatGdprMailTimestamp(args.expiresAt),
62
+ },
37
63
  });
38
- await transport.send({ to: args.userEmail, subject, html, text });
39
64
  };
40
65
  }
41
- export function makeDefaultExportFailedEmail(resolveTransport, defaults = {}) {
66
+ export function makeDefaultExportFailedEmail(notifyFor, defaults = {}) {
42
67
  return async (args) => {
43
- const transport = await resolveTransport(args.tenantId);
44
- const { subject, html, text } = renderExportFailedEmail({
45
- locale: localeFor(args.userLocale, defaults),
46
- appName: defaults.appName,
68
+ const type = GdprMailNotificationTypes.exportFailed;
69
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
70
+ await notifyImmediately(notifyFor(args.tenantId), type, {
71
+ to: args.userId,
72
+ locale,
73
+ data: { appName: appNameFor(locale, defaults) },
47
74
  });
48
- await transport.send({ to: args.userEmail, subject, html, text });
49
75
  };
50
76
  }
51
- export function makeDefaultDeletionRequestedEmail(resolveTransport, defaults = {}) {
77
+ export function makeDefaultDeletionRequestedEmail(notify, defaults = {}) {
52
78
  return async (args) => {
53
- const transport = await resolveTransport(args.tenantId);
54
- const { subject, html, text } = renderDeletionRequestedEmail({
55
- gracePeriodEnd: args.gracePeriodEnd,
56
- locale: localeFor(args.userLocale, defaults),
57
- appName: defaults.appName,
79
+ const type = GdprMailNotificationTypes.deletionRequested;
80
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
81
+ await notify(type, {
82
+ to: args.userId,
83
+ locale,
84
+ data: {
85
+ appName: appNameFor(locale, defaults),
86
+ deletionDate: formatGdprMailTimestamp(args.gracePeriodEnd),
87
+ },
58
88
  });
59
- await transport.send({ to: args.userEmail, subject, html, text });
60
89
  };
61
90
  }
62
- export function makeDefaultDeletionExecutedEmail(resolveTransport, defaults = {}) {
91
+ export function makeDefaultDeletionExecutedEmail(deps, defaults = {}) {
63
92
  return async (args) => {
64
93
  // Der User ist global, der Mail-Transport per-Tenant — wir senden ueber den
65
94
  // Transport des ersten Memberships. Orphan-User (0 Memberships) liefert
@@ -69,12 +98,35 @@ export function makeDefaultDeletionExecutedEmail(resolveTransport, defaults = {}
69
98
  // skip: orphan user has no tenant whose transport could send the mail
70
99
  return;
71
100
  }
72
- const transport = await resolveTransport(tenantId);
73
- const { subject, html, text } = renderDeletionExecutedEmail({
74
- executedAt: args.executedAt,
75
- locale: localeFor(args.userLocale, defaults),
76
- appName: defaults.appName,
101
+ const type = GdprMailNotificationTypes.deletionExecuted;
102
+ const locale = mailLocaleFor(type, args.userLocale, defaults);
103
+ const plugin = createRendererFoundationApi(collectRendererPlugins(deps.registry)).createRendererForTenant({ tenantId, kind: "mail" });
104
+ const config = deps.configFor(tenantId);
105
+ const rendered = await plugin.render({
106
+ kind: "mail",
107
+ payload: {
108
+ slug: type,
109
+ locale,
110
+ variables: {
111
+ appName: appNameFor(locale, defaults),
112
+ executedAt: formatGdprMailTimestamp(args.executedAt),
113
+ },
114
+ },
115
+ }, {
116
+ db: deps.db,
117
+ registry: deps.registry,
118
+ tenantId,
119
+ ...(config !== undefined && { config }),
120
+ });
121
+ if (rendered.kind !== "mail") {
122
+ throw new Error(`deletion-executed mail: renderer answered with kind "${rendered.kind}"`);
123
+ }
124
+ const transport = await deps.resolveTransport(tenantId);
125
+ await transport.send({
126
+ to: args.userEmail,
127
+ subject: rendered.subject,
128
+ html: rendered.html,
129
+ text: rendered.text,
77
130
  });
78
- await transport.send({ to: args.userEmail, subject, html, text });
79
131
  };
80
132
  }
@@ -51,7 +51,7 @@ export async function updateUserLifecycle(conn, userId, changes, options) {
51
51
  });
52
52
  }
53
53
  export async function backfillUserLifecycleEvents(conn) {
54
- const rows = (await selectMany(conn, userTable, {}));
54
+ const rows = await selectMany(conn, userTable, {});
55
55
  let backfilled = 0;
56
56
  const failed = [];
57
57
  for (const row of rows) {
@@ -0,0 +1,13 @@
1
+ import { type FeatureRegistrar, type NotificationLocalizedTemplate, type TemplateVariables } from "@cosmicdrift/kumiko-framework/engine";
2
+ declare const ALL_GDPR_MAIL_KINDS: readonly ["exportReady", "exportFailed", "deletionRequested", "deletionExecuted"];
3
+ export type GdprMailKind = (typeof ALL_GDPR_MAIL_KINDS)[number];
4
+ export declare const GDPR_MAIL_KINDS: Readonly<Record<GdprMailKind, {
5
+ readonly name: string;
6
+ readonly translationKey: string;
7
+ }>>;
8
+ export declare const GdprMailNotificationTypes: Readonly<Record<GdprMailKind, string>>;
9
+ export declare function gdprMailTemplateLocale(notificationType: string, requestedLocale: string): string;
10
+ export declare const GDPR_MAIL_VARIABLES: Readonly<Record<GdprMailKind, TemplateVariables>>;
11
+ export declare function gdprMailLocalizedTemplate(kind: GdprMailKind): () => NotificationLocalizedTemplate;
12
+ export declare function registerGdprMails(r: FeatureRegistrar): void;
13
+ export {};
@@ -0,0 +1,118 @@
1
+ import { QnTypes, qn, templateVariable, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { hasMailTranslationKey, listMailLocales, mailT, resolveMailLocale, } from "@cosmicdrift/kumiko-framework/i18n";
3
+ import { escapeHtml } from "@cosmicdrift/kumiko-headless";
4
+ import { mailParagraph } from "../shared/index.js";
5
+ import { USER_DATA_RIGHTS_FEATURE } from "./constants.js";
6
+ const ALL_GDPR_MAIL_KINDS = [
7
+ "exportReady",
8
+ "exportFailed",
9
+ "deletionRequested",
10
+ "deletionExecuted",
11
+ ];
12
+ export const GDPR_MAIL_KINDS = {
13
+ exportReady: { name: "export-ready", translationKey: "exportReady" },
14
+ exportFailed: { name: "export-failed", translationKey: "exportFailed" },
15
+ deletionRequested: { name: "deletion-requested", translationKey: "deletionRequested" },
16
+ deletionExecuted: { name: "deletion-executed", translationKey: "deletionExecuted" },
17
+ };
18
+ export const GdprMailNotificationTypes = {
19
+ exportReady: gdprMailType("exportReady"),
20
+ exportFailed: gdprMailType("exportFailed"),
21
+ deletionRequested: gdprMailType("deletionRequested"),
22
+ deletionExecuted: gdprMailType("deletionExecuted"),
23
+ };
24
+ function gdprMailType(kind) {
25
+ return qn(USER_DATA_RIGHTS_FEATURE, QnTypes.notify, GDPR_MAIL_KINDS[kind].name);
26
+ }
27
+ function subjectKey(kind) {
28
+ return `gdpr.mail.${GDPR_MAIL_KINDS[kind].translationKey}.subject`;
29
+ }
30
+ const SUBJECT_KEY_BY_TYPE = new Map(ALL_GDPR_MAIL_KINDS.map((kind) => [GdprMailNotificationTypes[kind], subjectKey(kind)]));
31
+ // A registered locale whose bundle lacks the subject key has no template (see
32
+ // gdprMailLocalizedTemplate); the renderer has no locale fallback, so the
33
+ // sender must pick a locale that has one or the mail would not go out at all.
34
+ export function gdprMailTemplateLocale(notificationType, requestedLocale) {
35
+ const locale = resolveMailLocale(requestedLocale);
36
+ const key = SUBJECT_KEY_BY_TYPE.get(notificationType);
37
+ return key !== undefined && hasMailTranslationKey(locale, key) ? locale : "en";
38
+ }
39
+ const appName = () => templateVariable.text({ required: true });
40
+ export const GDPR_MAIL_VARIABLES = {
41
+ exportReady: {
42
+ appName: appName(),
43
+ actionUrl: templateVariable.link({ required: true }),
44
+ validUntil: templateVariable.text({ required: true }),
45
+ },
46
+ exportFailed: { appName: appName() },
47
+ deletionRequested: {
48
+ appName: appName(),
49
+ deletionDate: templateVariable.text({ required: true }),
50
+ },
51
+ deletionExecuted: { appName: appName(), executedAt: templateVariable.text({ required: true }) },
52
+ };
53
+ // Translation placeholder -> template variable per kind.
54
+ const PLACEHOLDER_SLOTS = {
55
+ exportReady: { app: "{appName}", when: "{validUntil}" },
56
+ exportFailed: { app: "{appName}" },
57
+ deletionRequested: { app: "{appName}", when: "{deletionDate}" },
58
+ deletionExecuted: { app: "{appName}", when: "{executedAt}" },
59
+ };
60
+ function paragraph(kind, locale, part) {
61
+ const { translationKey } = GDPR_MAIL_KINDS[kind];
62
+ return mailParagraph(mailT(locale, `gdpr.mail.${translationKey}.${part}`, PLACEHOLDER_SLOTS[kind]));
63
+ }
64
+ function greeting(locale) {
65
+ return mailParagraph(mailT(locale, "gdpr.mail.greeting"));
66
+ }
67
+ function mailBody(kind, locale) {
68
+ switch (kind) {
69
+ case "exportReady":
70
+ return [
71
+ greeting(locale),
72
+ paragraph(kind, locale, "intro"),
73
+ `<p><a data-button href="{{actionUrl}}">${escapeHtml(mailT(locale, "gdpr.mail.exportReady.button"))}</a></p>`,
74
+ paragraph(kind, locale, "expiry"),
75
+ ].join("");
76
+ case "exportFailed":
77
+ return [greeting(locale), paragraph(kind, locale, "intro")].join("");
78
+ case "deletionRequested":
79
+ return [
80
+ greeting(locale),
81
+ paragraph(kind, locale, "intro"),
82
+ paragraph(kind, locale, "cancel"),
83
+ ].join("");
84
+ case "deletionExecuted":
85
+ return [greeting(locale), paragraph(kind, locale, "intro")].join("");
86
+ }
87
+ }
88
+ // Evaluated when the registry is built, after every locale feature has
89
+ // registered its mail translations. English always ships; another locale only
90
+ // gets a template when it has its own subject, so a missing translation never
91
+ // silently becomes an English mail in that locale's slot.
92
+ export function gdprMailLocalizedTemplate(kind) {
93
+ const kindSubjectKey = subjectKey(kind);
94
+ return () => {
95
+ const locales = [
96
+ "en",
97
+ ...listMailLocales().filter((locale) => locale !== "en" && hasMailTranslationKey(locale, kindSubjectKey)),
98
+ ];
99
+ return Object.fromEntries(locales.map((locale) => [
100
+ locale,
101
+ {
102
+ body: mailBody(kind, locale),
103
+ meta: {
104
+ subject: mailT(locale, kindSubjectKey, { app: "{{appName}}" }),
105
+ category: "transactional",
106
+ },
107
+ },
108
+ ]));
109
+ };
110
+ }
111
+ export function registerGdprMails(r) {
112
+ for (const kind of ALL_GDPR_MAIL_KINDS) {
113
+ r.notification(GDPR_MAIL_KINDS[kind].name, {
114
+ variables: GDPR_MAIL_VARIABLES[kind],
115
+ localizedTemplate: gdprMailLocalizedTemplate(kind),
116
+ });
117
+ }
118
+ }
@@ -12,6 +12,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
12
12
  // declarative fields/actions instead (feature.ts).
13
13
  import { useDispatcher, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
14
14
  import { postWithDownload } from "@cosmicdrift/kumiko-renderer-web";
15
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
15
16
  import { useEffect, useState } from "react";
16
17
  import { EXPORT_JOB_STATUS, UserDataRightsHandlers, UserDataRightsQueries, } from "../constants.js";
17
18
  // Export-Job läuft async (worker-Lane-Cron, ~1 Min). Solange er pending/running
@@ -21,7 +22,7 @@ const EXPORT_POLL_MS = 4000;
21
22
  // Dispatcher-Failures tragen i18nKey nur wenn der Handler einen setzt —
22
23
  // Boundary-Read mit generischem Fallback.
23
24
  function failureKey(error) {
24
- const key = error?.i18nKey; // @cast-boundary dispatcher-error
25
+ const key = propertyOf(error, "i18nKey");
25
26
  return typeof key === "string" ? key : "userDataRights.privacyCenter.errors.generic";
26
27
  }
27
28
  // Roher ISO-Instant → nur der Datums-Teil ist relevant; reiner String-Slice,
@@ -8,6 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  // (../../user-data-rights/web/privacy-center-screen.tsx), which this file
9
9
  // mirrors.
10
10
  import { useDispatcher, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
11
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
11
12
  import { useState } from "react";
12
13
  import { AuthHandlers } from "../../auth-email-password/constants.js";
13
14
  import { requestEmailVerification } from "../../auth-email-password/web/index.js";
@@ -15,7 +16,7 @@ import { UserProfileHandlers } from "../constants.js";
15
16
  // Dispatcher failures carry an i18nKey only when the handler sets one —
16
17
  // boundary read with a generic fallback.
17
18
  function failureKey(error) {
18
- const key = error?.i18nKey; // @cast-boundary dispatcher-error
19
+ const key = propertyOf(error, "i18nKey");
19
20
  return typeof key === "string" ? key : "profile.errors.generic";
20
21
  }
21
22
  function StatusBanner({ status }) {