@cosmicdrift/kumiko-samples 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 (252) 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/pages.ts +6 -3
  75. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  76. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  78. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  79. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  80. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  82. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  83. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  84. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  85. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  89. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  91. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  92. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  93. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  94. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  95. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  96. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  97. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  98. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  99. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  100. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  101. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  102. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  103. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  104. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  105. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  106. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  107. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  108. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  109. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  110. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  111. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  112. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  113. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  114. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  115. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  116. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  117. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  118. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  119. package/packages/bundled-features/src/shared/index.ts +6 -0
  120. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  121. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  122. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  123. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  124. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  125. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  126. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  127. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  128. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  129. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  130. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  131. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  132. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  133. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  134. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  135. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  136. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  137. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  138. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  140. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  141. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  144. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  145. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  149. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  153. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  158. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  159. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  160. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  162. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  163. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  164. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  165. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  166. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  167. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  168. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  169. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  170. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  171. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  172. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  173. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  174. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  175. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  176. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  177. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  178. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  179. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  180. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  181. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  182. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  183. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  184. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  186. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  188. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  189. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  190. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  191. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  193. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  194. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  195. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  196. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  197. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  198. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  199. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  200. package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
  201. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  202. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  203. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  204. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  205. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  206. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  207. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  208. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  209. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  210. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  211. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  212. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  213. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  214. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  215. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  216. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  217. package/samples/apps/ui-walkthrough/package.json +2 -0
  218. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  219. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  220. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  221. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  222. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  223. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  224. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  225. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  226. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  227. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  228. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  229. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  230. package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
  231. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  232. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  233. package/samples/apps/use-all-bundled/package.json +2 -0
  234. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  235. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  237. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  238. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  239. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  240. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  241. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  242. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  243. package/samples/recipes/delivery-notifications/README.md +26 -25
  244. package/samples/recipes/delivery-notifications/package.json +2 -1
  245. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  246. package/samples/recipes/seed-migration/README.md +1 -1
  247. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  248. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  249. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  250. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  251. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  252. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -3,6 +3,7 @@ import {
3
3
  defineEntityDetailHandler,
4
4
  type HandlerContext,
5
5
  } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  import { userEntity } from "../schema/user.js";
7
8
  import { attachTenantLabels, dbForList } from "./list.query.js";
8
9
 
@@ -24,12 +25,9 @@ export const detailQuery = {
24
25
  ctx: HandlerContext,
25
26
  ) => {
26
27
  const result = await baseDetail.handler?.(query, ctx);
27
- if (result === null || typeof result !== "object") return result;
28
+ if (!isPlainObject(result)) return result;
28
29
  if (!ctx.registry.features.has("tenant")) return result;
29
- const [enriched] = await attachTenantLabels(
30
- [result as Record<string, unknown>],
31
- dbForList(ctx),
32
- );
30
+ const [enriched] = await attachTenantLabels([result], dbForList(ctx));
33
31
  return enriched ?? result;
34
32
  },
35
33
  };
@@ -8,8 +8,8 @@ export const rolesInputSchema = z.union([
8
8
  z.string().transform((raw, ctx) => {
9
9
  try {
10
10
  const parsed: unknown = JSON.parse(raw);
11
- if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) {
12
- return parsed as string[];
11
+ if (Array.isArray(parsed) && parsed.every((v): v is string => typeof v === "string")) {
12
+ return parsed;
13
13
  }
14
14
  } catch {
15
15
  /* fall through */
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.363.0",
4
+ "type": "breaking",
5
+ "title": "GDPR default mails are r.notification declarations, render*Email is gone",
6
+ "detail": "export-ready, export-failed, deletion-requested and deletion-executed are declared as trigger-less notifications (user-data-rights:notify:<name>) with variables and localized templates built from the GDPR translations, so a tenant can override the text. The default export-ready, export-failed and deletion-requested mails send through ctx.notify to the user (export mails with immediate so the plain magic-link token never lands in a job payload). deletion-executed renders the seeded template through the mail renderer and sends over the tenant mail transport without a delivery log, because the user is already gone. App-provided send*Email callbacks still win. The defaults now need delivery with channel-email; a boot warning is logged once when mail-foundation and a mail transport are mounted without them. New exports are GdprMailNotificationTypes, gdprMailLocalizedTemplate, gdprMailTemplateLocale, registerGdprMails and formatGdprMailTimestamp.",
7
+ "migration": "renderExportReadyEmail, renderExportFailedEmail, renderDeletionRequestedEmail, renderDeletionExecutedEmail and RenderedEmail are removed from user-data-rights/email-templates; the translations stay. An app that mounts it without includeBundled needs template-resolver, renderer-foundation and @cosmicdrift/kumiko-renderer-mail-html at boot (composeRendererStack() from dev-server mounts all of them), plus a delivery with the channel-email feature; apps without the template tables need a schema migration (`kumiko schema generate`). Delivery preferences that referenced the mails use the new user-data-rights:notify:<name> types."
8
+ },
2
9
  {
3
10
  "version": "0.362.0",
4
11
  "type": "fix",
@@ -1,10 +1,10 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  import { userTable } from "../../../user/index.js";
5
5
 
6
6
  export async function selectUsersDueForForgetCleanup(
7
- db: DbConnection,
7
+ db: DbRunner,
8
8
  status: string,
9
9
  gracePeriodEndCutoff: Temporal.Instant | string,
10
10
  ): Promise<readonly { id: string }[]> {
@@ -1,10 +1,10 @@
1
- // Default HTML renderers for GDPR notification mails. English ships here;
2
- // other languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
1
+ // Translations for the GDPR notification mails (the templates in
2
+ // mail-notifications.ts are built from them). English ships here; other
3
+ // languages register via @cosmicdrift/kumiko-locale-* (localeDe() /
3
4
  // localeEs() call registerMailTranslations). Apps that want their own
4
5
  // branding pass send*Email callbacks and these defaults do not run.
5
6
 
6
- import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
7
- import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
7
+ import { registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
8
8
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
9
9
 
10
10
  export type GdprMailLocale = string;
@@ -41,151 +41,7 @@ export const GDPR_MAIL_EN: Readonly<Record<string, string>> = {
41
41
 
42
42
  registerMailTranslations("en", GDPR_MAIL_EN);
43
43
 
44
- export type RenderedEmail = {
45
- readonly subject: string;
46
- readonly html: string;
47
- readonly text: string;
48
- };
49
-
50
- export type RenderExportReadyEmailArgs = {
51
- readonly downloadUrl: string;
52
- readonly expiresAt: string;
53
- readonly locale?: GdprMailLocale;
54
- readonly appName?: string;
55
- };
56
-
57
- export type RenderExportFailedEmailArgs = {
58
- readonly locale?: GdprMailLocale;
59
- readonly appName?: string;
60
- };
61
-
62
- export type RenderDeletionRequestedEmailArgs = {
63
- readonly gracePeriodEnd: string;
64
- readonly locale?: GdprMailLocale;
65
- readonly appName?: string;
66
- };
67
-
68
- export type RenderDeletionExecutedEmailArgs = {
69
- readonly executedAt: string;
70
- readonly locale?: GdprMailLocale;
71
- readonly appName?: string;
72
- };
73
-
74
- function t(locale: string, key: string, params?: Readonly<Record<string, string>>): string {
75
- return mailT(locale, key, params);
76
- }
77
-
78
- function appNameFor(args: { locale?: GdprMailLocale; appName?: string }): string {
79
- const locale = args.locale ?? "en";
80
- return args.appName ?? t(locale, "gdpr.mail.appNameDefault");
81
- }
82
-
83
- export function renderExportReadyEmail(args: RenderExportReadyEmailArgs): RenderedEmail {
84
- const locale = args.locale ?? "en";
85
- const app = appNameFor(args);
86
- const subject = t(locale, "gdpr.mail.exportReady.subject", { app });
87
- const body = `
88
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
89
- <p style="margin: 0 0 24px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportReady.intro", { app }))}</p>
90
- <p style="margin: 0 0 24px;">${renderButton({ url: args.downloadUrl, label: t(locale, "gdpr.mail.exportReady.button") })}</p>
91
- <p style="margin: 0 0 8px; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }))}</p>
92
- ${renderFallbackUrl({ url: args.downloadUrl, label: t(locale, "gdpr.mail.fallbackUrl") })}`;
93
- const text = plainTextBody([
94
- t(locale, "gdpr.mail.greeting"),
95
- t(locale, "gdpr.mail.exportReady.intro", { app }),
96
- `${t(locale, "gdpr.mail.exportReady.button")}: ${args.downloadUrl}`,
97
- t(locale, "gdpr.mail.exportReady.expiry", { when: formatTimestamp(args.expiresAt) }),
98
- ]);
99
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
100
- }
101
-
102
- export function renderExportFailedEmail(args: RenderExportFailedEmailArgs): RenderedEmail {
103
- const locale = args.locale ?? "en";
104
- const app = appNameFor(args);
105
- const subject = t(locale, "gdpr.mail.exportFailed.subject", { app });
106
- const body = `
107
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
108
- <p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.exportFailed.intro", { app }))}</p>`;
109
- const text = plainTextBody([
110
- t(locale, "gdpr.mail.greeting"),
111
- t(locale, "gdpr.mail.exportFailed.intro", { app }),
112
- ]);
113
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
114
- }
115
-
116
- export function renderDeletionRequestedEmail(
117
- args: RenderDeletionRequestedEmailArgs,
118
- ): RenderedEmail {
119
- const locale = args.locale ?? "en";
120
- const app = appNameFor(args);
121
- const when = formatTimestamp(args.gracePeriodEnd);
122
- const subject = t(locale, "gdpr.mail.deletionRequested.subject", { app });
123
- const body = `
124
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
125
- <p style="margin: 0 0 16px; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.intro", { app, when }))}</p>
126
- <p style="margin: 0; font-size: 13px; color: #555;">${escapeHtml(t(locale, "gdpr.mail.deletionRequested.cancel"))}</p>`;
127
- const text = plainTextBody([
128
- t(locale, "gdpr.mail.greeting"),
129
- t(locale, "gdpr.mail.deletionRequested.intro", { app, when }),
130
- t(locale, "gdpr.mail.deletionRequested.cancel"),
131
- ]);
132
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
133
- }
134
-
135
- export function renderDeletionExecutedEmail(args: RenderDeletionExecutedEmailArgs): RenderedEmail {
136
- const locale = args.locale ?? "en";
137
- const app = appNameFor(args);
138
- const when = formatTimestamp(args.executedAt);
139
- const subject = t(locale, "gdpr.mail.deletionExecuted.subject", { app });
140
- const body = `
141
- <p style="margin: 0 0 16px; font-size: 16px;">${escapeHtml(t(locale, "gdpr.mail.greeting"))}</p>
142
- <p style="margin: 0; font-size: 14px; line-height: 1.5;">${escapeHtml(t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }))}</p>`;
143
- const text = plainTextBody([
144
- t(locale, "gdpr.mail.greeting"),
145
- t(locale, "gdpr.mail.deletionExecuted.intro", { app, when }),
146
- ]);
147
- return { subject, html: renderShell({ title: subject, bodyHtml: wrapCell(body), locale }), text };
148
- }
149
-
150
- function plainTextBody(paragraphs: readonly string[]): string {
151
- return paragraphs.join("\n\n");
152
- }
153
-
154
- function wrapCell(bodyHtml: string): string {
155
- return `<tr><td>${bodyHtml}</td></tr>`;
156
- }
157
-
158
- function renderShell(args: { title: string; bodyHtml: string; locale: GdprMailLocale }): string {
159
- return `<!DOCTYPE html>
160
- <html lang="${escapeHtmlAttr(args.locale)}">
161
- <head>
162
- <meta charset="utf-8" />
163
- <meta name="viewport" content="width=device-width, initial-scale=1" />
164
- <title>${escapeHtml(args.title)}</title>
165
- </head>
166
- <body style="margin: 0; padding: 0; background: #f7f7f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a;">
167
- <table width="100%" cellpadding="0" cellspacing="0" style="padding: 24px 0;">
168
- <tr>
169
- <td align="center">
170
- <table width="560" cellpadding="0" cellspacing="0" style="max-width: 560px; background: #ffffff; border-radius: 8px; padding: 32px;">
171
- ${args.bodyHtml}
172
- </table>
173
- </td>
174
- </tr>
175
- </table>
176
- </body>
177
- </html>`;
178
- }
179
-
180
- function renderButton(args: { url: string; label: string }): string {
181
- return `<a href="${escapeHtmlAttr(args.url)}" style="display: inline-block; background: #1a1a1a; color: #ffffff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 500;">${escapeHtml(args.label)}</a>`;
182
- }
183
-
184
- function renderFallbackUrl(args: { url: string; label: string }): string {
185
- return `<p style="margin: 24px 0 0; font-size: 12px; color: #666;">${escapeHtml(args.label)}<br /><a href="${escapeHtmlAttr(args.url)}" style="color: #1a1a1a; word-break: break-all;">${escapeHtml(args.url)}</a></p>`;
186
- }
187
-
188
- function formatTimestamp(iso: string): string {
44
+ export function formatGdprMailTimestamp(iso: string): string {
189
45
  try {
190
46
  const z = Temporal.Instant.from(iso).toZonedDateTimeISO("UTC");
191
47
  return `${z.year}-${pad2(z.month)}-${pad2(z.day)} ${pad2(z.hour)}:${pad2(z.minute)} UTC`;
@@ -2,16 +2,21 @@
2
2
  import { UNKNOWN_CLIENT_IP } from "@cosmicdrift/kumiko-framework/api";
3
3
  import {
4
4
  createSystemConfig,
5
+ createSystemUser,
5
6
  defineFeature,
6
7
  EXT_USER_DATA,
7
8
  type FeatureDefinition,
8
9
  i18nKey,
9
10
  SYSTEM_USER_ID,
11
+ type TenantId,
10
12
  } from "@cosmicdrift/kumiko-framework/engine";
13
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
11
14
  import {
12
15
  createMetricsHandle,
13
16
  createNoopMetricsHandle,
14
17
  } from "@cosmicdrift/kumiko-framework/observability";
18
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
19
+ import { createConfigAccessor } from "../config/index.js";
15
20
  import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks.js";
16
21
  import {
17
22
  EXPORT_SECTION_EXTENSION_NAME,
@@ -45,6 +50,8 @@ import { createRunForgetCleanupHandler } from "./handlers/run-forget-cleanup.wri
45
50
  import { USER_DATA_RIGHTS_I18N } from "./i18n.js";
46
51
  import {
47
52
  type GdprMailDefaults,
53
+ isGdprMailDeliveryAvailable,
54
+ isMailRendererAvailable,
48
55
  isMailTransportAvailable,
49
56
  makeDefaultDeletionExecutedEmail,
50
57
  makeDefaultExportFailedEmail,
@@ -53,6 +60,7 @@ import {
53
60
  import { makeTenantMailTransportResolver } from "./lib/mail-transport-resolver.js";
54
61
  import { resolveAppTenantModel } from "./lib/resolve-tenant-model.js";
55
62
  import { makeTenantStorageProviderResolver } from "./lib/storage-provider-resolver.js";
63
+ import { registerGdprMails } from "./mail-notifications.js";
56
64
  import {
57
65
  EXPORT_CLEANUP_BACKLOG_AGE_METRIC,
58
66
  EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC,
@@ -163,11 +171,22 @@ export type UserDataRightsOptions = {
163
171
  readonly privacyCenterShowDeletion?: boolean;
164
172
  };
165
173
 
174
+ function isMailTransportMountedWithoutDelivery(features: readonly FeatureDefinition[]): boolean {
175
+ const names = new Set(features.map((feature) => feature.name));
176
+ const hasTransport = [...names].some((name) => name.startsWith("mail-transport-"));
177
+ return (
178
+ names.has("mail-foundation") &&
179
+ hasTransport &&
180
+ !(names.has("delivery") && names.has("channel-email"))
181
+ );
182
+ }
183
+
166
184
  export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): FeatureDefinition {
167
185
  // One-shot operator warning (the export cron fires daily — warn once per
168
186
  // process, not every run). Lives in the factory scope so the cron closure
169
187
  // shares it across runs.
170
188
  let warnedMissingExportUrl = false;
189
+ let warnedMissingMailDelivery = false;
171
190
  const showDeletion = opts.privacyCenterShowDeletion ?? true;
172
191
  return defineFeature("user-data-rights", (r) => {
173
192
  r.describe(
@@ -197,7 +216,15 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
197
216
  // EXT_USER_DATA channel owner: any r.useExtension(EXT_USER_DATA, ...)
198
217
  // usage already requires user-data-rights to be mounted.
199
218
  r.bootCheck(({ features }) => validateGdprHookCompleteness(features));
200
- r.bootCheck(({ features }) => validateGdprPiiHookCoverage(features));
219
+ r.bootCheck(({ features }) => {
220
+ validateGdprPiiHookCoverage(features);
221
+ if (!warnedMissingMailDelivery && isMailTransportMountedWithoutDelivery(features)) {
222
+ warnedMissingMailDelivery = true;
223
+ createFallbackLogger("user-data-rights").warn(
224
+ "mail-foundation and a mail transport are mounted but delivery/channel-email are not — the default export and deletion-request mails need them and will not be sent",
225
+ );
226
+ }
227
+ });
201
228
 
202
229
  // App-level tenant-occupancy model. Default "multi-user" → tenant-scoped
203
230
  // contributors (e.g. credit) NEVER erase tenant data on a per-user forget
@@ -312,6 +339,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
312
339
  });
313
340
 
314
341
  r.translations({ keys: USER_DATA_RIGHTS_I18N });
342
+ registerGdprMails(r);
315
343
  // Privacy-center dot-form labels (i18nKey()-marked below), duplicated in
316
344
  // web/i18n.ts's client bundle since privacy-center-screen.tsx renders
317
345
  // them client-side.
@@ -520,10 +548,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
520
548
  handler: async (c, { app, clientIp }) => {
521
549
  const url = new URL(c.req.url);
522
550
  const parsedBody = await c.req.json().catch(() => null);
523
- const token =
524
- parsedBody && typeof parsedBody === "object" && "token" in parsedBody
525
- ? (parsedBody as { token?: unknown }).token
526
- : undefined;
551
+ const token = isPlainObject(parsedBody) ? parsedBody["token"] : undefined;
527
552
  if (typeof token !== "string" || token.length === 0) {
528
553
  return c.json({ error: "missing_token" }, 400);
529
554
  }
@@ -605,28 +630,25 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
605
630
  // versendet der Cron die Export-Notifications selbst (Default-Templates).
606
631
  // Der Resolver baut den per-Tenant-Transport aus configResolver — gleiche
607
632
  // Bruecke wie buildStorageProvider.
608
- const exportMailResolver = isMailTransportAvailable(exportRegistry)
609
- ? makeTenantMailTransportResolver({
610
- registry: exportRegistry,
611
- configResolver: ctx.configResolver,
612
- secrets: ctx.secrets,
613
- db: exportDb,
614
- userId: exportUserId,
615
- handlerName: "user-data-rights:run-export-jobs",
616
- })
617
- : undefined;
633
+ // The cron runs across tenants: each mail needs a notify bound to the
634
+ // recipient's tenant (template override, channel config).
635
+ const notifyFactory = ctx._notifyFactory;
636
+ const exportNotifyFor =
637
+ notifyFactory !== undefined && isGdprMailDeliveryAvailable(exportRegistry)
638
+ ? (tenantId: TenantId) => notifyFactory(createSystemUser(tenantId), tenantId)
639
+ : undefined;
618
640
  const sendExportReadyEmail =
619
641
  opts.sendExportReadyEmail ??
620
- (exportMailResolver && opts.appExportDownloadUrl !== undefined
621
- ? makeDefaultExportReadyEmail(exportMailResolver, opts.mailDefaults)
642
+ (exportNotifyFor && opts.appExportDownloadUrl !== undefined
643
+ ? makeDefaultExportReadyEmail(exportNotifyFor, opts.mailDefaults)
622
644
  : undefined);
623
645
  const sendExportFailedEmail =
624
646
  opts.sendExportFailedEmail ??
625
- (exportMailResolver
626
- ? makeDefaultExportFailedEmail(exportMailResolver, opts.mailDefaults)
647
+ (exportNotifyFor
648
+ ? makeDefaultExportFailedEmail(exportNotifyFor, opts.mailDefaults)
627
649
  : undefined);
628
650
  if (
629
- exportMailResolver &&
651
+ exportNotifyFor &&
630
652
  !opts.sendExportReadyEmail &&
631
653
  opts.appExportDownloadUrl === undefined &&
632
654
  !warnedMissingExportUrl
@@ -634,7 +656,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
634
656
  warnedMissingExportUrl = true;
635
657
  // biome-ignore lint/suspicious/noConsole: one-shot operator visibility for misconfig
636
658
  console.warn(
637
- "[user-data-rights:run-export-jobs] mail transport mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)",
659
+ "[user-data-rights:run-export-jobs] mail delivery mounted but appExportDownloadUrl unset — default export-ready emails disabled (export-failed + deletion mails still send)",
638
660
  );
639
661
  }
640
662
 
@@ -697,22 +719,37 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
697
719
  userId: forgetUserId,
698
720
  });
699
721
 
700
- // C6 — Default-Mail beim delete-flip wenn kein Callback gesetzt + ein
701
- // mail-transport gemountet ist (gleiche per-Tenant-Bruecke wie Export).
702
- const forgetMailResolver = isMailTransportAvailable(forgetRegistry)
703
- ? makeTenantMailTransportResolver({
704
- registry: forgetRegistry,
705
- configResolver: ctx.configResolver,
706
- secrets: ctx.secrets,
707
- db: forgetDb,
708
- userId: forgetUserId,
709
- handlerName: "user-data-rights:run-forget-cleanup",
710
- })
711
- : undefined;
722
+ // Without an app callback the default mail goes out when a mail transport and a
723
+ // mail renderer are mounted. The user is already erased here, so it bypasses the
724
+ // delivery log: an attempt row would be new personal data.
725
+ const forgetConfigResolver = ctx.configResolver;
712
726
  const sendDeletionExecutedEmail =
713
727
  opts.sendDeletionExecutedEmail ??
714
- (forgetMailResolver
715
- ? makeDefaultDeletionExecutedEmail(forgetMailResolver, opts.mailDefaults)
728
+ (isMailTransportAvailable(forgetRegistry) && isMailRendererAvailable(forgetRegistry)
729
+ ? makeDefaultDeletionExecutedEmail(
730
+ {
731
+ registry: forgetRegistry,
732
+ db: forgetDb,
733
+ resolveTransport: makeTenantMailTransportResolver({
734
+ registry: forgetRegistry,
735
+ configResolver: forgetConfigResolver,
736
+ secrets: ctx.secrets,
737
+ db: forgetDb,
738
+ userId: forgetUserId,
739
+ handlerName: "user-data-rights:run-forget-cleanup",
740
+ }),
741
+ configFor: (tenantId) =>
742
+ forgetConfigResolver &&
743
+ createConfigAccessor(
744
+ forgetRegistry,
745
+ forgetConfigResolver,
746
+ tenantId,
747
+ forgetUserId,
748
+ forgetDb,
749
+ ),
750
+ },
751
+ opts.mailDefaults,
752
+ )
716
753
  : undefined);
717
754
 
718
755
  const forgetResult = await runForgetCleanup({
@@ -71,12 +71,13 @@ export const cancelDeletionWrite = defineWriteHandler({
71
71
 
72
72
  // gracePeriodEnd=null im Response symmetrisch zu request-deletion's
73
73
  // ISO-Timestamp — Frontend kann beide Endpoints uniform behandeln.
74
+ const gracePeriodEnd: string | null = null;
74
75
  return {
75
76
  isSuccess: true as const,
76
77
  data: {
77
78
  userId: event.user.id,
78
79
  status: USER_STATUS.Active,
79
- gracePeriodEnd: null as string | null,
80
+ gracePeriodEnd,
80
81
  },
81
82
  };
82
83
  },
@@ -1,11 +1,10 @@
1
- import { createTransportForTenant } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
2
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
3
  import * as z from "zod";
5
4
  import { USER_STATUS } from "../../user/index.js";
6
5
  import {
7
6
  type GdprMailDefaults,
8
- isMailTransportAvailable,
7
+ isGdprMailDeliveryAvailable,
9
8
  makeDefaultDeletionRequestedEmail,
10
9
  } from "../lib/default-mailers.js";
11
10
  import { resolveGracePeriod, startDeletionGracePeriod } from "./deletion-grace-period.js";
@@ -71,17 +70,12 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
71
70
  if (!res.ok) return writeFailure(res.error);
72
71
  const { gracePeriodEnd, userEmail, userLocale } = res;
73
72
 
74
- // App-Callback hat Vorrang; sonst greift die mail-foundation-Default-Mail
75
- // (Request-Lane → ctx.config vorhanden, createTransportForTenant direkt).
76
- // Ohne gemounteten mail-transport bleibt send undefined → keine Mail.
73
+ // The app callback wins; otherwise the default mail goes through ctx.notify after
74
+ // the commit. Without delivery and an email channel there is no sender and no mail.
77
75
  const send =
78
76
  opts.sendDeletionRequestedEmail ??
79
- (isMailTransportAvailable(ctx.registry)
80
- ? makeDefaultDeletionRequestedEmail(
81
- (tenantId) =>
82
- createTransportForTenant(ctx, tenantId, "user-data-rights:request-deletion"),
83
- opts.mailDefaults,
84
- )
77
+ (ctx.notify !== undefined && isGdprMailDeliveryAvailable(ctx.registry)
78
+ ? makeDefaultDeletionRequestedEmail(ctx.notify, opts.mailDefaults)
85
79
  : undefined);
86
80
 
87
81
  // Best-effort Email-Notification. Send-Failure darf das Write nicht