@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
@@ -1,4 +1,52 @@
1
1
  [
2
+ {
3
+ "version": "0.363.0",
4
+ "type": "breaking",
5
+ "title": "NotificationRenderer and RendererInput removed, immediate notify renders with the tenant config",
6
+ "detail": "The inline (immediate, job-less) delivery path now gets the tenant config accessor like the delivery.render job, so the mail layout shows the tenant name in both paths. DeliveryServiceOptions and CreateDeliveryTestContextOptions accept tenantConfigFor; runProdApp wires it from the boot config resolver. The types NotificationRenderer and RendererInput are removed from the delivery exports because the legacy renderer path is gone.",
7
+ "migration": "Drop NotificationRenderer and RendererInput imports; render mails with template + meta on r.notification (see renderer-foundation)."
8
+ },
9
+ {
10
+ "version": "0.363.0",
11
+ "type": "improvement",
12
+ "title": "ChannelContext.renderMail renders template notifications for the email channel",
13
+ "detail": "A notification that declares a template is rendered through the mail renderer plugin with the recipient locale and the notification variables; the email channel fails when renderMail is missing instead of falling back to the legacy renderer. The job path passes the tenant config accessor to the renderer, the inline path renders without one."
14
+ },
15
+ {
16
+ "version": "0.363.0",
17
+ "type": "improvement",
18
+ "title": "Mails can attach a PDF that is rendered from a document template when the mail is sent",
19
+ "detail": "notify data.attachments accepts { kind: \"rendered-pdf\", templateSlug, variables, fileName, locale? }. The send job renders the PDF through the document-pdf renderer, so the job payload carries the variables (at most 64000 bytes as JSON), not the bytes. Variables are stored in the job payload, so keep personal data out of them or accept that retention."
20
+ },
21
+ {
22
+ "version": "0.363.0",
23
+ "type": "improvement",
24
+ "title": "ChannelContext.readAttachment takes an attachment origin",
25
+ "detail": "readAttachment(ref, origin) with origin \"template\" (from the renderer) or \"message\" (default, from notify data). Only template attachments may fall back to the system tenant; file refs from notify data stay limited to the sending tenant."
26
+ },
27
+ {
28
+ "version": "0.363.0",
29
+ "type": "improvement",
30
+ "title": "EMAIL_CHANNEL_NAME, createTemplateMailRenderer and createEmailAttachmentReader are exported",
31
+ "detail": "The email channel name is a shared constant instead of a string literal. The mail renderer and the inline-attachment reader that build the channel context are exported and accept a tenant-bound db, so a handler can send through the same path as a notification."
32
+ },
33
+ {
34
+ "version": "0.363.0",
35
+ "type": "improvement",
36
+ "title": "Notification messages carry attachments (file refs and inline cid images)",
37
+ "detail": "RenderedMessage and the send job payload accept attachments as references (kind file or inline), never bytes. ChannelContext.readAttachment resolves them tenant-scoped at send time; createDeliveryService takes a new filesForTenant option for the inline fallback path."
38
+ },
39
+ {
40
+ "version": "0.363.0",
41
+ "type": "improvement",
42
+ "title": "NotifyFactory takes an optional fileProviderResolver, so immediate and bootstrap mails can read attachments too"
43
+ },
44
+ {
45
+ "version": "0.363.0",
46
+ "type": "fix",
47
+ "title": "Queued mails render with the tenant's config again",
48
+ "detail": "The delivery render and send jobs read the tenant config through ctx.configFor, which job contexts do not carry, so queued mails rendered without tenant name, locale, currency and branding. Both jobs now use ctx.config, which the job runner builds for the payload's tenant."
49
+ },
2
50
  {
3
51
  "version": "0.357.0",
4
52
  "type": "improvement",
@@ -1,8 +1,115 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
- import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
- import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { createTenantDb, type DbConnection, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { ConfigAccessor, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
5
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
6
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
5
- import type { ChannelContext } from "./types.js";
7
+ import { readFileRefContent } from "../file-foundation/index.js";
8
+ import {
9
+ collectRendererPlugins,
10
+ createRendererFoundationApi,
11
+ type MailPayload,
12
+ RendererError,
13
+ } from "../renderer-foundation/index.js";
14
+ import { FALLBACK_LOCALE } from "../template-resolver/index.js";
15
+ import type { EmailAttachmentRef, RenderedPdfAttachmentRef } from "./attachment-ref.js";
16
+ import type { AttachmentOrigin, ChannelContext, RenderedMail } from "./types.js";
17
+
18
+ export function createTemplateMailRenderer(
19
+ db: DbConnection | TenantDb,
20
+ registry: Registry,
21
+ tenantId: TenantId,
22
+ config: ConfigAccessor | undefined,
23
+ ): (payload: MailPayload) => Promise<RenderedMail> {
24
+ return async (payload) => {
25
+ const plugin = createRendererFoundationApi(
26
+ collectRendererPlugins(registry),
27
+ ).createRendererForTenant({ tenantId, kind: "mail" });
28
+ const response = await plugin.render(
29
+ { kind: "mail", payload },
30
+ { db, registry, tenantId, ...(config !== undefined && { config }) },
31
+ );
32
+ if (response.kind !== "mail") {
33
+ throw new RendererError(
34
+ `[delivery] renderer "${plugin.name}" answered a mail request with kind "${response.kind}"`,
35
+ "other",
36
+ );
37
+ }
38
+ return {
39
+ subject: response.subject,
40
+ html: response.html,
41
+ text: response.text,
42
+ ...(response.previewText !== undefined && { previewText: response.previewText }),
43
+ ...(response.attachments !== undefined && { attachments: response.attachments }),
44
+ };
45
+ };
46
+ }
47
+
48
+ const INLINE_IMAGE_CONTENT_TYPE_PREFIX = "image/";
49
+ const PDF_CONTENT_TYPE = "application/pdf";
50
+
51
+ // Renders at send time so the job payload carries variables, not bytes. A failure throws:
52
+ // the send job retries instead of mailing without the document the recipient expects.
53
+ function createPdfRenderer(
54
+ db: DbConnection | TenantDb,
55
+ registry: Registry,
56
+ tenantId: TenantId,
57
+ config: ConfigAccessor | undefined,
58
+ filesForTenant: (tenantId: string) => FileContext | undefined,
59
+ ) {
60
+ return async (ref: RenderedPdfAttachmentRef) => {
61
+ const plugin = createRendererFoundationApi(
62
+ collectRendererPlugins(registry),
63
+ ).createRendererForTenant({ tenantId, kind: "document-pdf" });
64
+ const response = await plugin.render(
65
+ {
66
+ kind: "document-pdf",
67
+ payload: {
68
+ slug: ref.templateSlug,
69
+ locale: ref.locale ?? FALLBACK_LOCALE,
70
+ variables: ref.variables,
71
+ },
72
+ },
73
+ { db, registry, tenantId, filesForTenant, ...(config !== undefined && { config }) },
74
+ );
75
+ if (response.kind !== "document-pdf") {
76
+ throw new RendererError(
77
+ `[delivery] renderer "${plugin.name}" answered a document-pdf request with kind "${response.kind}"`,
78
+ "other",
79
+ );
80
+ }
81
+ return { content: response.pdfBytes, contentType: PDF_CONTENT_TYPE };
82
+ };
83
+ }
84
+
85
+ // Inline images and file attachments the renderer took from a template may come from the system
86
+ // tenant (system template or tenant override of one). A file ref from notify data stays limited to
87
+ // the job's own tenant, since a caller picks it. Inline content that is not an image is dropped, so
88
+ // a swapped fileRef id cannot smuggle a document into the mail body.
89
+ export function createEmailAttachmentReader(
90
+ db: DbConnection | TenantDb,
91
+ registry: Registry,
92
+ tenantId: TenantId,
93
+ config: ConfigAccessor | undefined,
94
+ filesForTenant: (tenantId: string) => FileContext | undefined,
95
+ ): NonNullable<ChannelContext["readAttachment"]> {
96
+ const renderPdf = createPdfRenderer(db, registry, tenantId, config, filesForTenant);
97
+ return async (ref: EmailAttachmentRef, origin: AttachmentOrigin = "message") => {
98
+ if (ref.kind === "rendered-pdf") return renderPdf(ref);
99
+ const found = await readFileRefContent({
100
+ db,
101
+ filesForTenant,
102
+ fileRefId: ref.fileRefId,
103
+ allowedTenantIds:
104
+ ref.kind === "file" && origin === "message" ? [tenantId] : [tenantId, SYSTEM_TENANT_ID],
105
+ });
106
+ if (found === null) return null;
107
+ if (ref.kind === "inline" && !found.contentType.startsWith(INLINE_IMAGE_CONTENT_TYPE_PREFIX)) {
108
+ return null;
109
+ }
110
+ return { content: found.content, contentType: found.contentType };
111
+ };
112
+ }
6
113
 
7
114
  // Build the per-tenant context a channel's resolve/render/send receives.
8
115
  // Shared by the synchronous delivery-service path and the async job handlers
@@ -14,6 +121,24 @@ export function buildChannelContext(
14
121
  sseBroker: SseBroker | undefined,
15
122
  tenantId: TenantId,
16
123
  secrets?: SecretsContext,
124
+ tenantConfig?: ConfigAccessor,
125
+ filesForTenant?: (tenantId: string) => FileContext | undefined,
17
126
  ): ChannelContext {
18
- return { db: createTenantDb(db, tenantId), registry, sseBroker, tenantId, secrets };
127
+ return {
128
+ db: createTenantDb(db, tenantId),
129
+ registry,
130
+ sseBroker,
131
+ tenantId,
132
+ secrets,
133
+ renderMail: createTemplateMailRenderer(db, registry, tenantId, tenantConfig),
134
+ ...(filesForTenant !== undefined && {
135
+ readAttachment: createEmailAttachmentReader(
136
+ db,
137
+ registry,
138
+ tenantId,
139
+ tenantConfig,
140
+ filesForTenant,
141
+ ),
142
+ }),
143
+ };
19
144
  }
@@ -6,6 +6,8 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
6
6
  // channel-in-app, channel-push, ...).
7
7
  export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel" as const;
8
8
 
9
+ export const EMAIL_CHANNEL_NAME = "email" as const;
10
+
9
11
  export {
10
12
  DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT,
11
13
  DELIVERY_ATTEMPT_EVENT,
@@ -2,22 +2,32 @@ import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection, DbRow } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { createSystemDbView, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
4
4
  import type {
5
+ ConfigAccessor,
5
6
  DeliveryErrorCode,
6
7
  EscapeHatchAuditSink,
7
8
  NotifyDelivery,
9
+ NotifyDispatched,
8
10
  NotifyJobDispatcher,
9
11
  NotifyOptions,
10
12
  NotifyPriority,
11
13
  Registry,
12
14
  TenantId,
13
15
  } from "@cosmicdrift/kumiko-framework/engine";
14
- import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
16
+ import {
17
+ assertNotificationVariables,
18
+ createSystemUser,
19
+ } from "@cosmicdrift/kumiko-framework/engine";
20
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
15
21
  import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
16
- import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
22
+ import {
23
+ assertNotifyTargetsOwnTenant,
24
+ createEscapeHatchReporter,
25
+ } from "@cosmicdrift/kumiko-framework/pipeline";
17
26
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
18
27
  import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
19
28
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
20
29
  import type { Redis } from "ioredis";
30
+ import { fileContextFromResolver } from "../file-foundation/index.js";
21
31
  import { redactErrorText } from "../shared/index.js";
22
32
  import { hashUnsubscribeAddress } from "./address-opt-out.js";
23
33
  import { appendAttemptEvent, logAttempt } from "./attempt-log.js";
@@ -77,6 +87,11 @@ export type DeliveryServiceOptions = {
77
87
  // Credentials source for inline-delivered chat channels (webhook URL / bot
78
88
  // token). Jobs get ctx.secrets from the job runner instead.
79
89
  readonly secrets?: SecretsContext;
90
+ // File storage for inline-delivered mails with attachments; jobs use their own ctx instead.
91
+ readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
92
+ // Tenant config for inline rendering (mail layout reads tenant name, locale);
93
+ // job-delivered mails get the same accessor from the job context's configFor.
94
+ readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
80
95
  // Attributed escape-hatch audit for resolveUserIdsForTenant's ctx.systemDb —
81
96
  // absent means an unattributed warn log (see fallbackEscapeHatchReporter).
82
97
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
@@ -96,11 +111,6 @@ export function collectChannels(registry: Registry): DeliveryChannel[] {
96
111
  });
97
112
  }
98
113
 
99
- // `collectRenderers` entfernt 2026-05-19: notificationRenderer-Extension-Point
100
- // wurde nie konsumiert (channel-email nimmt renderer als Konstruktor-Option,
101
- // nicht aus Extension-Usages). Multi-Kind-Plugin-Pool lebt jetzt im
102
- // `renderer-foundation`-Bundle via `collectRendererPlugins`.
103
-
104
114
  export function createDeliveryService(options: DeliveryServiceOptions): DeliveryService {
105
115
  const {
106
116
  db,
@@ -113,6 +123,8 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
113
123
  idempotencyRedis,
114
124
  jobRunner,
115
125
  secrets,
126
+ filesForTenant,
127
+ tenantConfigFor,
116
128
  escapeHatchAuditSink,
117
129
  log,
118
130
  } = options;
@@ -214,9 +226,14 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
214
226
  return routedChannelCount === 1 ? idempotencyKey : undefined;
215
227
  }
216
228
 
217
- async function resolveUserIdsForTenant(tenantId: TenantId): Promise<readonly string[]> {
229
+ async function resolveUserIdsForTenant(
230
+ run: NotifyRun,
231
+ tenantId: TenantId,
232
+ ): Promise<readonly string[]> {
218
233
  if (!tenantUserIdsQuery) {
219
- throw new Error("Tenant broadcast requires tenantUserIdsQuery in DeliveryServiceOptions");
234
+ throw new Error(
235
+ "Tenant broadcast (to: { tenant }) requires the tenant feature (default wiring) or an explicit tenantUserIdsQuery in DeliveryServiceOptions",
236
+ );
220
237
  }
221
238
  const handler = registry.getQueryHandler(tenantUserIdsQuery);
222
239
  if (!handler) {
@@ -235,7 +252,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
235
252
  // path a cross-tenant read the dispatcher never allows. Cross-tenant reach only
236
253
  // exists through the grant-carrying systemDb view below.
237
254
  const isSystem = registry.isHandlerSystemScoped(tenantUserIdsQuery);
238
- const tenantDb = createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
255
+ const tenantDb = createTenantDb(run.db, tenantId, "tenant", undefined, undefined, undefined, {
239
256
  report,
240
257
  });
241
258
  // The dispatcher grants a systemScope handler's ctx.systemDb.unsafeRaw() ungated —
@@ -243,7 +260,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
243
260
  // caller. Only the systemDb view's own source TenantDb carries the grant, so it
244
261
  // can't leak onto ctx.db.
245
262
  const systemDbSourceTenantDb = isSystem
246
- ? createTenantDb(db, tenantId, "system", undefined, undefined, undefined, {
263
+ ? createTenantDb(run.db, tenantId, "system", undefined, undefined, undefined, {
247
264
  report,
248
265
  unsafeRaw: { reason: `r.systemScope() handler "${tenantUserIdsQuery}"` },
249
266
  })
@@ -268,12 +285,16 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
268
285
  // State of one notify() call: collects the per-channel outcome returned to
269
286
  // the caller. Passed explicitly — the service itself is shared across calls.
270
287
  type NotifyRun = {
288
+ // All DB work of the call runs here: inside a write transaction the caller passes the
289
+ // outside-tx pool, because the app pool's slots may all be held by waiting write txs.
290
+ readonly db: DbConnection;
271
291
  readonly deliveries: NotifyDelivery[];
272
292
  readonly jobDispatcher: NotifyJobDispatcher | undefined;
273
293
  readonly locale: string | undefined;
274
294
  // Set only when exactly one channel carries the call: the queued job may then free
275
295
  // the key on terminal failure without risking a duplicate send on another channel.
276
296
  readonly jobIdempotencyKey: string | undefined;
297
+ readonly filesForTenant: ((tenantId: string) => FileContext | undefined) | undefined;
277
298
  };
278
299
 
279
300
  function recordDelivery(
@@ -295,7 +316,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
295
316
  // attempts instead append a queued event up front and a terminal event from
296
317
  // the send job — see deliverViaChannel + jobs.ts.
297
318
  async function logDelivery(run: NotifyRun, entry: DeliveryLogEntry): Promise<void> {
298
- const attemptId = await logAttempt(db, registry, entry);
319
+ const attemptId = await logAttempt(run.db, registry, entry);
299
320
  recordDelivery(run, entry, attemptId);
300
321
  }
301
322
 
@@ -372,7 +393,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
372
393
  error: null,
373
394
  priority,
374
395
  };
375
- await appendAttemptEvent(db, registry, deliveryAttemptId, queuedEntry);
396
+ await appendAttemptEvent(run.db, registry, deliveryAttemptId, queuedEntry);
376
397
  await run.jobDispatcher.dispatch(
377
398
  channel.render ? DeliveryJobs.render : DeliveryJobs.send,
378
399
  {
@@ -460,6 +481,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
460
481
  // no blind-index key configured means no hash, so nothing to look up. One
461
482
  // query per notify() call, not per recipient and channel.
462
483
  async function loadSuppressedAddresses(
484
+ run: NotifyRun,
463
485
  tenantId: TenantId,
464
486
  notificationType: string,
465
487
  priority: NotifyPriority,
@@ -472,7 +494,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
472
494
  const addressHash = hashUnsubscribeAddress(address);
473
495
  if (addressHash !== undefined) addressHashes.add(addressHash);
474
496
  }
475
- return selectOptedOutAddresses(db, tenantId, notificationType, [...addressHashes]);
497
+ return selectOptedOutAddresses(run.db, tenantId, notificationType, [...addressHashes]);
476
498
  }
477
499
 
478
500
  function isAddressSuppressed(
@@ -487,13 +509,14 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
487
509
  const recipientChannelKey = (userId: string, channelName: string) => `${userId}|${channelName}`;
488
510
 
489
511
  async function isChannelDeliverable(
512
+ run: NotifyRun,
490
513
  userId: string,
491
514
  tenantId: TenantId,
492
515
  notificationType: string,
493
516
  channelName: string,
494
517
  ): Promise<boolean> {
495
518
  if (isChannelKilled && (await isChannelKilled(tenantId, channelName))) return false;
496
- return isChannelEnabled(userId, tenantId, notificationType, channelName);
519
+ return isChannelEnabled(run, userId, tenantId, notificationType, channelName);
497
520
  }
498
521
 
499
522
  // Resolves every user's channel addresses up front so the opt-out lookup can
@@ -502,17 +525,28 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
502
525
  // Channels the kill switch or the user's preferences disable are never resolved
503
526
  // here: resolve may have side effects and deliverToUser skips those channels.
504
527
  async function prefetchUserAddresses(
528
+ run: NotifyRun,
505
529
  userIds: readonly string[],
506
530
  tenantId: TenantId,
507
531
  notificationType: string,
508
532
  ): Promise<Map<string, string | null>> {
509
533
  const addresses = new Map<string, string | null>();
510
- const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
534
+ const channelCtx = buildChannelContext(
535
+ run.db,
536
+ registry,
537
+ sseBroker,
538
+ tenantId,
539
+ secrets,
540
+ tenantConfigFor?.(tenantId),
541
+ run.filesForTenant,
542
+ );
511
543
  for (const userId of userIds) {
512
544
  for (const channel of channels) {
513
545
  if (!channel.resolve) continue;
514
546
  try {
515
- if (!(await isChannelDeliverable(userId, tenantId, notificationType, channel.name))) {
547
+ if (
548
+ !(await isChannelDeliverable(run, userId, tenantId, notificationType, channel.name))
549
+ ) {
516
550
  continue;
517
551
  }
518
552
  addresses.set(
@@ -537,13 +571,14 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
537
571
  // { type: "orderAssigned", channel: "*", enabled: true } enables orderAssigned everywhere
538
572
  // → orderAssigned on inApp: disabled (conservative) unless an exact entry overrides.
539
573
  async function isChannelEnabled(
574
+ run: NotifyRun,
540
575
  userId: string,
541
576
  tenantId: TenantId,
542
577
  notificationType: string,
543
578
  channelName: string,
544
579
  ): Promise<boolean> {
545
580
  const prefs = await selectNotificationPreferences(
546
- db,
581
+ run.db,
547
582
  tenantId,
548
583
  userId,
549
584
  notificationType,
@@ -573,7 +608,15 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
573
608
  prefetchedAddresses: ReadonlyMap<string, string | null>,
574
609
  suppressed: ReadonlySet<AddressOptOutKey>,
575
610
  ): Promise<void> {
576
- const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
611
+ const channelCtx = buildChannelContext(
612
+ run.db,
613
+ registry,
614
+ sseBroker,
615
+ tenantId,
616
+ secrets,
617
+ tenantConfigFor?.(tenantId),
618
+ run.filesForTenant,
619
+ );
577
620
 
578
621
  for (const channel of channels) {
579
622
  // Route-only channel (no per-user address): not a user-notification target.
@@ -601,7 +644,13 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
601
644
 
602
645
  // Check preferences (critical priority skips preference check)
603
646
  if (priority !== "critical") {
604
- const enabled = await isChannelEnabled(userId, tenantId, notificationType, channel.name);
647
+ const enabled = await isChannelEnabled(
648
+ run,
649
+ userId,
650
+ tenantId,
651
+ notificationType,
652
+ channel.name,
653
+ );
605
654
  if (!enabled) {
606
655
  await logDelivery(run, {
607
656
  tenantId,
@@ -707,8 +756,17 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
707
756
  recipientId: string | null,
708
757
  run: NotifyRun,
709
758
  ): Promise<void> {
710
- const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
759
+ const channelCtx = buildChannelContext(
760
+ run.db,
761
+ registry,
762
+ sseBroker,
763
+ tenantId,
764
+ secrets,
765
+ tenantConfigFor?.(tenantId),
766
+ run.filesForTenant,
767
+ );
711
768
  const suppressed = await loadSuppressedAddresses(
769
+ run,
712
770
  tenantId,
713
771
  notificationType,
714
772
  priority,
@@ -815,7 +873,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
815
873
  if (typeof to === "string") {
816
874
  userIds = [to];
817
875
  } else if ("tenant" in to) {
818
- userIds = await resolveUserIdsForTenant(to.tenant);
876
+ userIds = await resolveUserIdsForTenant(run, to.tenant);
819
877
  } else {
820
878
  userIds = to;
821
879
  }
@@ -823,8 +881,9 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
823
881
  const prefetchedAddresses =
824
882
  priority === "critical"
825
883
  ? new Map<string, string | null>()
826
- : await prefetchUserAddresses(userIds, tenantId, notificationType);
884
+ : await prefetchUserAddresses(run, userIds, tenantId, notificationType);
827
885
  const suppressed = await loadSuppressedAddresses(
886
+ run,
828
887
  tenantId,
829
888
  notificationType,
830
889
  priority,
@@ -846,15 +905,34 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
846
905
  }
847
906
  }
848
907
 
908
+ function dispatched(run: NotifyRun): NotifyDispatched {
909
+ return { kind: "dispatched", deliveries: run.deliveries };
910
+ }
911
+
849
912
  return {
850
- async notify(notificationType, options, _user, tenantId, jobDispatcher) {
913
+ async notify(
914
+ notificationType,
915
+ options,
916
+ _user,
917
+ tenantId,
918
+ jobDispatcher,
919
+ deliveryDb,
920
+ fileProviderResolver,
921
+ ) {
922
+ assertNotifyTargetsOwnTenant(options, tenantId);
923
+ assertNotificationVariables(registry, notificationType, options.data);
851
924
  const { route, idempotencyKey } = options;
852
925
  const priority: NotifyPriority = options.priority ?? "normal";
853
926
  const run: NotifyRun = {
927
+ db: deliveryDb ?? db,
854
928
  deliveries: [],
855
929
  jobDispatcher: options.immediate ? undefined : (jobDispatcher ?? jobRunner),
856
930
  locale: options.locale,
857
931
  jobIdempotencyKey: jobIdempotencyKeyFor(idempotencyKey, route),
932
+ filesForTenant:
933
+ fileProviderResolver === undefined
934
+ ? filesForTenant
935
+ : (fileTenantId) => fileContextFromResolver(fileProviderResolver, fileTenantId),
858
936
  };
859
937
 
860
938
  if (idempotencyKey) {
@@ -870,7 +948,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
870
948
  error: "duplicate_idempotency_key",
871
949
  priority,
872
950
  });
873
- return { deliveries: run.deliveries };
951
+ return dispatched(run);
874
952
  }
875
953
  }
876
954
 
@@ -888,7 +966,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
888
966
  throw err;
889
967
  }
890
968
  await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run);
891
- return { deliveries: run.deliveries };
969
+ return dispatched(run);
892
970
  },
893
971
  };
894
972
  }
@@ -61,7 +61,7 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
61
61
  const attemptLogRetentionDays = resolveAttemptLogRetentionDays(options?.attemptLogRetentionDays);
62
62
  return defineFeature("delivery", (r) => {
63
63
  r.describe(
64
- "The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app's `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message's `data.unsubscribeUrl` points at the unsubscribe route.",
64
+ 'The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). Inside a write handler the delivery runs after the write has committed (the call returns `{ kind: "deferred" }`, a rolled-back write sends nothing); `immediate: true` delivers synchronously and returns `{ kind: "dispatched", deliveries }`. It stores per-user channel preferences in the `notification-preference` entity, opt-outs for no-account recipient addresses in `notification-address-opt-out` (keyed by a blind-index hash, never the plaintext address), logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything. Unsubscribe/resubscribe links are served by `createUnsubscribeRoutes({ secret })` mounted via the app\'s `extraRoutes` at `/api/delivery/unsubscribe` and `/api/delivery/resubscribe`: `GET /unsubscribe?token=` renders a confirmation page (no write), `POST /unsubscribe` performs the opt-out (RFC 8058 one-click, token from the form body or query), `POST /resubscribe` undoes it (JSON `{token}` or form body only, never the query — reachable by a mail-client link prefetcher) — sign links with `signUnsubscribeToken` / `signAddressUnsubscribeToken` using the same secret. `channel-email` sets `List-Unsubscribe` / `List-Unsubscribe-Post` automatically when a message\'s `data.unsubscribeUrl` points at the unsubscribe route.',
65
65
  );
66
66
  r.uiHints({
67
67
  displayLabel: "Notifications \u00b7 Dispatch Core",
@@ -1,9 +1,24 @@
1
+ export { sanitizePdfAttachmentFileName } from "../shared/index.js";
1
2
  export { hashUnsubscribeAddress } from "./address-opt-out.js";
3
+ export {
4
+ type EmailAttachmentRef,
5
+ emailAttachmentRefSchema,
6
+ type FileAttachmentRef,
7
+ fileAttachmentRefSchema,
8
+ type InlineAttachmentRef,
9
+ inlineAttachmentRefSchema,
10
+ MAX_EMAIL_ATTACHMENTS,
11
+ MAX_RENDERED_PDF_VARIABLES_BYTES,
12
+ type RenderedPdfAttachmentRef,
13
+ renderedPdfAttachmentRefSchema,
14
+ toAttachmentRef,
15
+ } from "./attachment-ref.js";
2
16
  export {
3
17
  type AttemptLogRetentionOptions,
4
18
  DEFAULT_ATTEMPT_LOG_RETENTION_DAYS,
5
19
  runAttemptLogRetention,
6
20
  } from "./attempt-log-retention.js";
21
+ export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
7
22
  export {
8
23
  type ChatWebhookChannelOptions,
9
24
  chatMessageText,
@@ -36,6 +51,7 @@ export {
36
51
  DeliveryJobs,
37
52
  DeliveryQueries,
38
53
  DeliveryStatus,
54
+ EMAIL_CHANNEL_NAME,
39
55
  } from "./constants.js";
40
56
  export {
41
57
  collectChannels,
@@ -55,6 +71,7 @@ export {
55
71
  } from "./tables.js";
56
72
  export { type CreateDeliveryTestContextOptions, createDeliveryTestContext } from "./testing.js";
57
73
  export {
74
+ type AttachmentOrigin,
58
75
  type ChannelContext,
59
76
  type ChannelMessage,
60
77
  type ChannelResult,
@@ -64,9 +81,7 @@ export {
64
81
  type DeliveryLogEntry,
65
82
  type DeliveryService,
66
83
  isDeliveryChannelPlugin,
67
- type NotificationRenderer,
68
84
  type RenderedMessage,
69
- type RendererInput,
70
85
  } from "./types.js";
71
86
  export {
72
87
  type AddressUnsubscribeTokenPayload,