@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (468) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/audit/handlers/list.query.js +9 -5
  8. package/dist/auth-email-password/bootstrap.d.ts +8 -5
  9. package/dist/auth-email-password/bootstrap.js +12 -4
  10. package/dist/auth-email-password/email-rate-limits.d.ts +4 -0
  11. package/dist/auth-email-password/email-rate-limits.js +13 -0
  12. package/dist/auth-email-password/email-templates.d.ts +4 -26
  13. package/dist/auth-email-password/email-templates.js +9 -101
  14. package/dist/auth-email-password/feature.js +24 -6
  15. package/dist/auth-email-password/handlers/change-password.write.d.ts +1 -1
  16. package/dist/auth-email-password/handlers/change-password.write.js +6 -2
  17. package/dist/auth-email-password/handlers/confirm-account-unlock.write.d.ts +1 -1
  18. package/dist/auth-email-password/handlers/invite-accept-with-login.write.d.ts +1 -1
  19. package/dist/auth-email-password/handlers/invite-accept.write.d.ts +1 -1
  20. package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -2
  21. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  22. package/dist/auth-email-password/handlers/invite-signup-complete.write.d.ts +1 -1
  23. package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
  24. package/dist/auth-email-password/handlers/logout.write.d.ts +1 -1
  25. package/dist/auth-email-password/handlers/logout.write.js +6 -2
  26. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -1
  27. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  28. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -1
  29. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  30. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -1
  31. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  32. package/dist/auth-email-password/handlers/reset-password.write.d.ts +1 -1
  33. package/dist/auth-email-password/handlers/signup-confirm.write.d.ts +1 -1
  34. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  35. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -1
  36. package/dist/auth-email-password/handlers/signup-request.write.js +2 -1
  37. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -1
  38. package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -3
  39. package/dist/auth-email-password/handlers/token-request-handler.js +2 -2
  40. package/dist/auth-email-password/handlers/verify-email.write.d.ts +1 -1
  41. package/dist/auth-email-password/index.d.ts +2 -2
  42. package/dist/auth-email-password/index.js +1 -1
  43. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  44. package/dist/auth-email-password/magic-link-mail.js +19 -14
  45. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  46. package/dist/auth-email-password/mail-notifications.js +116 -0
  47. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  48. package/dist/auth-email-password/signup-activation.js +2 -3
  49. package/dist/auth-email-password/web/auth-gate.js +16 -4
  50. package/dist/auth-foundation/types.js +2 -8
  51. package/dist/auth-mfa/feature.js +4 -4
  52. package/dist/auth-mfa/handlers/disable.write.d.ts +1 -1
  53. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.d.ts +1 -1
  54. package/dist/auth-mfa/handlers/enable-confirm.write.d.ts +1 -1
  55. package/dist/auth-mfa/handlers/enable-start-preauth.write.d.ts +1 -1
  56. package/dist/auth-mfa/handlers/enable-start.write.d.ts +1 -1
  57. package/dist/auth-mfa/handlers/regenerate-recovery.write.d.ts +1 -1
  58. package/dist/auth-mfa/handlers/verify.write.d.ts +1 -1
  59. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  60. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  61. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  62. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  63. package/dist/billing-foundation/constants.d.ts +3 -3
  64. package/dist/billing-foundation/constants.js +5 -4
  65. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  66. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +74 -32
  67. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -0
  68. package/dist/billing-foundation/consumer-protection/consent-gate.js +21 -2
  69. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  70. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  71. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  72. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  73. package/dist/billing-foundation/consumer-protection/register-termination.js +11 -1
  74. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +25 -13
  75. package/dist/billing-foundation/consumer-protection/termination-mail.js +11 -23
  76. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  77. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  78. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +6 -0
  79. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +34 -0
  80. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +2 -0
  81. package/dist/billing-foundation/consumer-protection/termination-record.js +7 -1
  82. package/dist/billing-foundation/consumer-protection/termination-request.js +31 -21
  83. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  84. package/dist/billing-foundation/consumer-protection/termination-texts.js +6 -0
  85. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +10 -0
  86. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +26 -0
  87. package/dist/billing-foundation/handlers/create-checkout-session.write.js +11 -5
  88. package/dist/billing-foundation/handlers/process-event.write.js +12 -8
  89. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  90. package/dist/billing-foundation/i18n.js +3 -0
  91. package/dist/billing-foundation/subscription-tier-sync.d.ts +7 -1
  92. package/dist/billing-foundation/subscription-tier-sync.js +16 -9
  93. package/dist/billing-foundation/types.d.ts +7 -2
  94. package/dist/cap-counter/calendar-cap-usage-reservation.js +2 -0
  95. package/dist/cap-counter/enforce-cap.d.ts +18 -1
  96. package/dist/cap-counter/enforce-cap.js +49 -6
  97. package/dist/cap-counter/index.d.ts +1 -1
  98. package/dist/cap-counter/index.js +1 -1
  99. package/dist/cap-counter/with-cap-enforcement.d.ts +2 -0
  100. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  101. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  102. package/dist/channel-email/email-channel.d.ts +2 -2
  103. package/dist/channel-email/email-channel.js +101 -26
  104. package/dist/channel-email/feature.js +3 -3
  105. package/dist/channel-email/index.d.ts +1 -1
  106. package/dist/channel-email/index.js +1 -1
  107. package/dist/channel-email/pii-guard.js +10 -0
  108. package/dist/channel-email/smtp-transport.js +13 -0
  109. package/dist/channel-email/types.d.ts +8 -0
  110. package/dist/channel-in-app/handlers/mark-all-read.write.d.ts +1 -1
  111. package/dist/channel-in-app/handlers/mark-read.write.d.ts +1 -1
  112. package/dist/compliance-profiles/handlers/set-profile.write.d.ts +1 -1
  113. package/dist/config/handlers/reset.write.d.ts +1 -1
  114. package/dist/config/handlers/set.write.d.ts +1 -1
  115. package/dist/config/handlers/set.write.js +3 -4
  116. package/dist/config/resolver.d.ts +1 -0
  117. package/dist/config/resolver.js +11 -14
  118. package/dist/config/write-helpers.d.ts +2 -0
  119. package/dist/config/write-helpers.js +19 -0
  120. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
  121. package/dist/crypto-shredding/handlers/forget-subject.write.js +29 -19
  122. package/dist/custom-fields/handlers/update-tenant-field.write.js +0 -1
  123. package/dist/custom-fields/lib/value-schema.js +10 -11
  124. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  125. package/dist/data-retention/index.d.ts +1 -0
  126. package/dist/data-retention/index.js +1 -0
  127. package/dist/data-retention/run-retention-cleanup.js +6 -6
  128. package/dist/delivery/attachment-ref.d.ts +41 -0
  129. package/dist/delivery/attachment-ref.js +78 -0
  130. package/dist/delivery/channel-context.d.ts +8 -4
  131. package/dist/delivery/channel-context.js +94 -2
  132. package/dist/delivery/constants.d.ts +1 -0
  133. package/dist/delivery/constants.js +1 -0
  134. package/dist/delivery/delivery-service.d.ts +4 -1
  135. package/dist/delivery/delivery-service.js +38 -32
  136. package/dist/delivery/feature.js +1 -1
  137. package/dist/delivery/handlers/resubscribe-address.write.d.ts +1 -1
  138. package/dist/delivery/handlers/resubscribe-user.write.d.ts +1 -1
  139. package/dist/delivery/handlers/set-preference.write.d.ts +1 -1
  140. package/dist/delivery/handlers/unsubscribe-address.write.d.ts +1 -1
  141. package/dist/delivery/handlers/unsubscribe-user.write.d.ts +1 -1
  142. package/dist/delivery/index.d.ts +5 -2
  143. package/dist/delivery/index.js +4 -1
  144. package/dist/delivery/jobs.js +66 -14
  145. package/dist/delivery/notify-factory.js +4 -1
  146. package/dist/delivery/testing.d.ts +4 -1
  147. package/dist/delivery/types.d.ts +26 -14
  148. package/dist/delivery/unsubscribe.js +4 -1
  149. package/dist/derivatives-sharp/render.js +1 -18
  150. package/dist/document-ingest-foundation/pages.js +4 -4
  151. package/dist/feature-toggles/handlers/set.write.d.ts +1 -1
  152. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  153. package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
  154. package/dist/file-foundation/index.d.ts +1 -0
  155. package/dist/file-foundation/index.js +1 -0
  156. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  157. package/dist/file-foundation/read-file-ref-content.js +51 -0
  158. package/dist/files-tenant-data/hooks.js +4 -0
  159. package/dist/folders/web/folder-manager.js +3 -2
  160. package/dist/form-draft/db/queries/cleanup.js +4 -4
  161. package/dist/form-draft/db/queries/draft-count.js +2 -2
  162. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  163. package/dist/form-draft/handlers/discard.write.d.ts +1 -1
  164. package/dist/form-draft/handlers/save.write.d.ts +1 -1
  165. package/dist/form-draft/release-file-refs.js +3 -2
  166. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  167. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  168. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  169. package/dist/inbound-mail-foundation/oauth-access-token.js +8 -3
  170. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  171. package/dist/inbound-mail-foundation/types.js +4 -4
  172. package/dist/inbound-mail-foundation/watch-supervisor.js +24 -13
  173. package/dist/inbound-provider-imap/imap-client.js +4 -3
  174. package/dist/jobs/db/queries/stale-run-sweep.d.ts +1 -1
  175. package/dist/jobs/db/queries/stale-run-sweep.js +12 -5
  176. package/dist/jobs/feature.js +5 -3
  177. package/dist/jobs/handlers/list.query.js +3 -1
  178. package/dist/jobs/handlers/retry.write.d.ts +1 -1
  179. package/dist/jobs/handlers/stale-run-sweep.job.d.ts +1 -1
  180. package/dist/jobs/handlers/stale-run-sweep.job.js +2 -2
  181. package/dist/jobs/handlers/trigger.write.d.ts +1 -1
  182. package/dist/jobs/job-run-logger.js +32 -10
  183. package/dist/jobs/tenant-job-run-table.js +3 -2
  184. package/dist/ledger/entity.js +3 -1
  185. package/dist/ledger/recurring.js +1 -3
  186. package/dist/managed-pages/branding.d.ts +2 -0
  187. package/dist/managed-pages/branding.js +24 -2
  188. package/dist/managed-pages/feature.js +1 -0
  189. package/dist/managed-pages/handlers/set.write.d.ts +1 -1
  190. package/dist/managed-pages/i18n.js +1 -0
  191. package/dist/managed-pages/screens/branding-screen.js +8 -1
  192. package/dist/page-render/branding.d.ts +1 -0
  193. package/dist/page-render/branding.js +1 -0
  194. package/dist/page-render/markdown.js +1 -1
  195. package/dist/personal-access-tokens/feature.js +1 -1
  196. package/dist/personal-access-tokens/handlers/create.write.d.ts +1 -1
  197. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  198. package/dist/personal-access-tokens/handlers/revoke.write.d.ts +1 -1
  199. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  200. package/dist/renderer-foundation/api.d.ts +13 -13
  201. package/dist/renderer-foundation/api.js +1 -1
  202. package/dist/renderer-foundation/constants.js +2 -4
  203. package/dist/renderer-foundation/feature.js +7 -7
  204. package/dist/renderer-foundation/index.d.ts +2 -1
  205. package/dist/renderer-foundation/index.js +1 -0
  206. package/dist/renderer-foundation/types.d.ts +94 -32
  207. package/dist/renderer-pdf/feature.js +7 -1
  208. package/dist/renderer-pdf/render-table-pdf.d.ts +27 -33
  209. package/dist/renderer-pdf/render-table-pdf.js +95 -34
  210. package/dist/secrets/feature.js +4 -3
  211. package/dist/secrets/handlers/delete.write.d.ts +1 -1
  212. package/dist/secrets/handlers/set.write.d.ts +1 -1
  213. package/dist/secrets/secrets-context.js +15 -3
  214. package/dist/sessions/db/queries/cleanup.js +2 -2
  215. package/dist/sessions/feature.js +1 -1
  216. package/dist/sessions/handlers/list.query.js +2 -1
  217. package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -1
  218. package/dist/sessions/handlers/revoke-all-others.write.d.ts +1 -1
  219. package/dist/sessions/handlers/revoke.write.d.ts +1 -1
  220. package/dist/shared/attachment-file-name.d.ts +3 -0
  221. package/dist/shared/attachment-file-name.js +35 -0
  222. package/dist/shared/chunked-entity-migration.d.ts +1 -0
  223. package/dist/shared/chunked-entity-migration.js +1 -1
  224. package/dist/shared/index.d.ts +3 -0
  225. package/dist/shared/index.js +3 -0
  226. package/dist/shared/is-tenant-db.js +1 -1
  227. package/dist/shared/mail-template-body.d.ts +7 -0
  228. package/dist/shared/mail-template-body.js +27 -0
  229. package/dist/shared/sharp-input-limits.d.ts +6 -0
  230. package/dist/shared/sharp-input-limits.js +22 -0
  231. package/dist/shared/signup-handover.js +2 -2
  232. package/dist/subscription-stripe/plugin-methods.js +18 -8
  233. package/dist/subscription-stripe/verify-webhook.d.ts +1 -1
  234. package/dist/tags/handlers/assign-tag.write.js +3 -1
  235. package/dist/tags/web/tag-chip.js +2 -0
  236. package/dist/template-resolver/api.d.ts +30 -2
  237. package/dist/template-resolver/api.js +42 -6
  238. package/dist/template-resolver/compile-document-body.d.ts +12 -0
  239. package/dist/template-resolver/compile-document-body.js +205 -0
  240. package/dist/template-resolver/constants.d.ts +13 -4
  241. package/dist/template-resolver/constants.js +32 -10
  242. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  243. package/dist/template-resolver/document-sanitizer.js +201 -0
  244. package/dist/template-resolver/document-schema.d.ts +21 -0
  245. package/dist/template-resolver/document-schema.js +316 -0
  246. package/dist/template-resolver/feature.d.ts +20 -0
  247. package/dist/template-resolver/feature.js +53 -6
  248. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  249. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  250. package/dist/template-resolver/handlers/collection-set.write.d.ts +1 -1
  251. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  252. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  253. package/dist/template-resolver/handlers/copy-locale.write.js +134 -0
  254. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  255. package/dist/template-resolver/handlers/diff-to-system.query.js +30 -0
  256. package/dist/template-resolver/handlers/discard-draft.write.d.ts +13 -0
  257. package/dist/template-resolver/handlers/discard-draft.write.js +37 -0
  258. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  259. package/dist/template-resolver/handlers/export-bundle.query.js +118 -0
  260. package/dist/template-resolver/handlers/find-by-id.query.d.ts +2 -0
  261. package/dist/template-resolver/handlers/find-by-id.query.js +9 -6
  262. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  263. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  264. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  265. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  266. package/dist/template-resolver/handlers/layout-source.query.d.ts +31 -0
  267. package/dist/template-resolver/handlers/layout-source.query.js +87 -0
  268. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  269. package/dist/template-resolver/handlers/list.query.js +9 -8
  270. package/dist/template-resolver/handlers/override-diff.d.ts +20 -0
  271. package/dist/template-resolver/handlers/override-diff.js +41 -0
  272. package/dist/template-resolver/handlers/overview.query.d.ts +65 -0
  273. package/dist/template-resolver/handlers/overview.query.js +205 -0
  274. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  275. package/dist/template-resolver/handlers/preview.query.js +24 -0
  276. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  277. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  278. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  279. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  280. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  281. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  282. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  283. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  284. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  285. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  286. package/dist/template-resolver/handlers/send-test.write.d.ts +13 -0
  287. package/dist/template-resolver/handlers/send-test.write.js +108 -0
  288. package/dist/template-resolver/handlers/set.write.d.ts +5 -2
  289. package/dist/template-resolver/handlers/set.write.js +25 -3
  290. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  291. package/dist/template-resolver/handlers/shared.js +6 -2
  292. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  293. package/dist/template-resolver/handlers/template-gates.js +142 -0
  294. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  295. package/dist/template-resolver/handlers/template-write.js +141 -0
  296. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +49 -0
  297. package/dist/template-resolver/handlers/tenant-overrides.query.js +112 -0
  298. package/dist/template-resolver/handlers/toggle-status.write.d.ts +27 -2
  299. package/dist/template-resolver/handlers/toggle-status.write.js +47 -15
  300. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  301. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  302. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  303. package/dist/template-resolver/handlers/upsert-tenant.write.js +11 -62
  304. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  305. package/dist/template-resolver/handlers/usage.query.js +24 -0
  306. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  307. package/dist/template-resolver/handlers/validate.query.js +80 -0
  308. package/dist/template-resolver/index.d.ts +19 -1
  309. package/dist/template-resolver/index.js +17 -1
  310. package/dist/template-resolver/layout-appearance.d.ts +10 -0
  311. package/dist/template-resolver/layout-appearance.js +26 -0
  312. package/dist/template-resolver/layout-regions.d.ts +23 -0
  313. package/dist/template-resolver/layout-regions.js +125 -0
  314. package/dist/template-resolver/linked-resources.d.ts +17 -0
  315. package/dist/template-resolver/linked-resources.js +75 -0
  316. package/dist/template-resolver/pdf-constants.d.ts +7 -0
  317. package/dist/template-resolver/pdf-constants.js +8 -0
  318. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  319. package/dist/template-resolver/preview-display-html.js +27 -0
  320. package/dist/template-resolver/print-document.d.ts +1 -0
  321. package/dist/template-resolver/print-document.js +55 -0
  322. package/dist/template-resolver/qualified-names.d.ts +34 -0
  323. package/dist/template-resolver/qualified-names.js +34 -0
  324. package/dist/template-resolver/record-mapping.d.ts +2 -0
  325. package/dist/template-resolver/record-mapping.js +21 -0
  326. package/dist/template-resolver/resource-image.d.ts +15 -0
  327. package/dist/template-resolver/resource-image.js +34 -0
  328. package/dist/template-resolver/seeding.d.ts +14 -1
  329. package/dist/template-resolver/seeding.js +125 -18
  330. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  331. package/dist/template-resolver/store-template-resource.js +264 -0
  332. package/dist/template-resolver/table.d.ts +150 -8
  333. package/dist/template-resolver/table.js +20 -2
  334. package/dist/template-resolver/template-bundle.d.ts +36 -0
  335. package/dist/template-resolver/template-bundle.js +60 -0
  336. package/dist/template-resolver/template-diff.d.ts +7 -0
  337. package/dist/template-resolver/template-diff.js +68 -0
  338. package/dist/template-resolver/template-draft.d.ts +51 -0
  339. package/dist/template-resolver/template-draft.js +72 -0
  340. package/dist/template-resolver/template-meta.d.ts +85 -0
  341. package/dist/template-resolver/template-meta.js +145 -0
  342. package/dist/template-resolver/template-preview.d.ts +103 -0
  343. package/dist/template-resolver/template-preview.js +418 -0
  344. package/dist/template-resolver/template-resources.d.ts +19 -0
  345. package/dist/template-resolver/template-resources.js +73 -0
  346. package/dist/template-resolver/template-usage.d.ts +16 -0
  347. package/dist/template-resolver/template-usage.js +35 -0
  348. package/dist/template-resolver/template-validation.d.ts +22 -0
  349. package/dist/template-resolver/template-validation.js +173 -0
  350. package/dist/template-resolver/testing.js +5 -5
  351. package/dist/template-resolver/user-content-table.d.ts +14 -8
  352. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  353. package/dist/template-resolver/web/client-plugin.js +12 -7
  354. package/dist/template-resolver/web/document-editor-extensions.d.ts +18 -0
  355. package/dist/template-resolver/web/document-editor-extensions.js +145 -0
  356. package/dist/template-resolver/web/document-editor.d.ts +4 -0
  357. package/dist/template-resolver/web/document-editor.js +17 -0
  358. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  359. package/dist/template-resolver/web/document-tiptap-editor.js +492 -0
  360. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +41 -0
  361. package/dist/template-resolver/web/document-toolbar-blocks.js +19 -0
  362. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  363. package/dist/template-resolver/web/editor-handle.js +24 -0
  364. package/dist/template-resolver/web/i18n.js +91 -0
  365. package/dist/template-resolver/web/index.d.ts +5 -0
  366. package/dist/template-resolver/web/index.js +5 -0
  367. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  368. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  369. package/dist/template-resolver/web/placeholder-line-editor.d.ts +17 -0
  370. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  371. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  372. package/dist/template-resolver/web/placeholder-text.js +34 -0
  373. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  374. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  375. package/dist/tenant/handlers/add-member.write.d.ts +1 -1
  376. package/dist/tenant/handlers/cancel-invitation.write.d.ts +1 -1
  377. package/dist/tenant/handlers/create.write.d.ts +1 -1
  378. package/dist/tenant/handlers/remove-member.write.d.ts +1 -1
  379. package/dist/tenant/handlers/team-list.query.js +3 -2
  380. package/dist/tenant/handlers/toggle-enabled.write.d.ts +2 -2
  381. package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
  382. package/dist/tenant/handlers/update.write.d.ts +1 -1
  383. package/dist/tenant/seeding.js +31 -6
  384. package/dist/tenant-handover/handlers/claim.write.d.ts +1 -1
  385. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +2 -2
  386. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  387. package/dist/tenant-lifecycle/handlers/request-destruction.write.d.ts +1 -1
  388. package/dist/tier-engine/feature.js +9 -3
  389. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +1 -1
  390. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  391. package/dist/user/handlers/create.write.d.ts +1 -1
  392. package/dist/user/handlers/detail.query.js +2 -1
  393. package/dist/user/handlers/update.write.d.ts +1 -1
  394. package/dist/user/resolve-display-names.js +5 -9
  395. package/dist/user/seeding.js +20 -1
  396. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  397. package/dist/user-data-rights/email-templates.d.ts +1 -29
  398. package/dist/user-data-rights/email-templates.js +5 -106
  399. package/dist/user-data-rights/feature.js +53 -35
  400. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +2 -2
  401. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  402. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.d.ts +1 -1
  403. package/dist/user-data-rights/handlers/lift-restriction.write.d.ts +1 -1
  404. package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +1 -1
  405. package/dist/user-data-rights/handlers/request-deletion.write.d.ts +1 -1
  406. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  407. package/dist/user-data-rights/handlers/request-export.write.d.ts +1 -1
  408. package/dist/user-data-rights/handlers/restrict-account.write.d.ts +1 -1
  409. package/dist/user-data-rights/handlers/run-forget-cleanup.write.d.ts +1 -1
  410. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  411. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  412. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  413. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  414. package/dist/user-data-rights/mail-notifications.js +118 -0
  415. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  416. package/dist/user-profile/handlers/change-email.write.d.ts +1 -1
  417. package/dist/user-profile/web/profile-screen.js +2 -1
  418. package/dist/waitlist/feature.js +10 -0
  419. package/dist/waitlist/handlers/invite.write.d.ts +1 -1
  420. package/dist/waitlist/handlers/reject.write.d.ts +1 -1
  421. package/dist/waitlist/handlers/submit.write.d.ts +1 -1
  422. package/dist/waitlist/handlers/submit.write.js +3 -4
  423. package/dist/waitlist/mail.d.ts +19 -6
  424. package/dist/waitlist/mail.js +50 -14
  425. package/dist/waitlist/options.js +1 -1
  426. package/dist/workflow-runner/event-trigger.js +2 -2
  427. package/dist/workflow-runner/handlers/resume-run.write.js +7 -2
  428. package/package.json +37 -17
  429. package/src/admin-shell/changes.json +6 -0
  430. package/src/audit/changes.json +6 -0
  431. package/src/auth-email-password/changes.json +51 -0
  432. package/src/billing-foundation/changes.json +43 -0
  433. package/src/cap-counter/changes.json +6 -0
  434. package/src/channel-email/changes.json +19 -0
  435. package/src/config/changes.json +6 -0
  436. package/src/crypto-shredding/changes.json +6 -0
  437. package/src/custom-fields/changes.json +6 -0
  438. package/src/data-retention/changes.json +6 -0
  439. package/src/delivery/changes.json +66 -0
  440. package/src/file-foundation/changes.json +6 -0
  441. package/src/files-tenant-data/changes.json +6 -0
  442. package/src/inbound-mail-foundation/changes.json +6 -0
  443. package/src/jobs/changes.json +19 -0
  444. package/src/ledger/changes.json +7 -0
  445. package/src/managed-pages/changes.json +6 -0
  446. package/src/renderer-foundation/changes.json +39 -0
  447. package/src/renderer-pdf/changes.json +12 -0
  448. package/src/secrets/changes.json +6 -0
  449. package/src/shared/changes.json +6 -0
  450. package/src/subscription-stripe/changes.json +6 -0
  451. package/src/template-resolver/changes.json +186 -0
  452. package/src/tier-engine/changes.json +6 -0
  453. package/src/user/changes.json +6 -0
  454. package/src/user-data-rights/changes.json +13 -0
  455. package/src/user-profile/changes.json +1 -1
  456. package/src/waitlist/changes.json +7 -0
  457. package/src/workflow-runner/changes.json +6 -0
  458. package/dist/renderer-simple/feature.d.ts +0 -10
  459. package/dist/renderer-simple/feature.js +0 -42
  460. package/dist/renderer-simple/index.d.ts +0 -3
  461. package/dist/renderer-simple/index.js +0 -2
  462. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  463. package/dist/renderer-simple/mail-blocks.js +0 -107
  464. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  465. package/dist/renderer-simple/resolve-variables.js +0 -47
  466. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  467. package/dist/renderer-simple/simple-renderer.js +0 -200
  468. package/src/renderer-simple/changes.json +0 -27
@@ -1,9 +1,10 @@
1
1
  import { createSystemDbView, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
- import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { assertNotificationVariables, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
4
- import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
4
+ import { assertNotifyTargetsOwnTenant, createEscapeHatchReporter, } from "@cosmicdrift/kumiko-framework/pipeline";
5
5
  import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
6
6
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
7
+ import { fileContextFromResolver } from "../file-foundation/index.js";
7
8
  import { redactErrorText } from "../shared/index.js";
8
9
  import { hashUnsubscribeAddress } from "./address-opt-out.js";
9
10
  import { appendAttemptEvent, logAttempt } from "./attempt-log.js";
@@ -25,12 +26,8 @@ export function collectChannels(registry) {
25
26
  return { name: usage.entityName, ...usage.options };
26
27
  });
27
28
  }
28
- // `collectRenderers` entfernt 2026-05-19: notificationRenderer-Extension-Point
29
- // wurde nie konsumiert (channel-email nimmt renderer als Konstruktor-Option,
30
- // nicht aus Extension-Usages). Multi-Kind-Plugin-Pool lebt jetzt im
31
- // `renderer-foundation`-Bundle via `collectRendererPlugins`.
32
29
  export function createDeliveryService(options) {
33
- const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, secrets, escapeHatchAuditSink, log, } = options;
30
+ const { db, registry, sseBroker, channels, tenantUserIdsQuery, rateLimit, isChannelKilled, idempotencyRedis, jobRunner, secrets, filesForTenant, tenantConfigFor, escapeHatchAuditSink, log, } = options;
34
31
  const idemRedis = idempotencyRedis ?? rateLimit?.redis;
35
32
  const logError = createFallbackLogger("delivery", log);
36
33
  // Rate limit check: atomic INCR + TTL + over-limit rollback via server-side
@@ -96,9 +93,9 @@ export function createDeliveryService(options) {
96
93
  const routedChannelCount = channels.filter((channel) => route[channel.name]).length;
97
94
  return routedChannelCount === 1 ? idempotencyKey : undefined;
98
95
  }
99
- async function resolveUserIdsForTenant(tenantId) {
96
+ async function resolveUserIdsForTenant(run, tenantId) {
100
97
  if (!tenantUserIdsQuery) {
101
- throw new Error("Tenant broadcast requires tenantUserIdsQuery in DeliveryServiceOptions");
98
+ throw new Error("Tenant broadcast (to: { tenant }) requires the tenant feature (default wiring) or an explicit tenantUserIdsQuery in DeliveryServiceOptions");
102
99
  }
103
100
  const handler = registry.getQueryHandler(tenantUserIdsQuery);
104
101
  if (!handler) {
@@ -117,7 +114,7 @@ export function createDeliveryService(options) {
117
114
  // path a cross-tenant read the dispatcher never allows. Cross-tenant reach only
118
115
  // exists through the grant-carrying systemDb view below.
119
116
  const isSystem = registry.isHandlerSystemScoped(tenantUserIdsQuery);
120
- const tenantDb = createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
117
+ const tenantDb = createTenantDb(run.db, tenantId, "tenant", undefined, undefined, undefined, {
121
118
  report,
122
119
  });
123
120
  // The dispatcher grants a systemScope handler's ctx.systemDb.unsafeRaw() ungated —
@@ -125,7 +122,7 @@ export function createDeliveryService(options) {
125
122
  // caller. Only the systemDb view's own source TenantDb carries the grant, so it
126
123
  // can't leak onto ctx.db.
127
124
  const systemDbSourceTenantDb = isSystem
128
- ? createTenantDb(db, tenantId, "system", undefined, undefined, undefined, {
125
+ ? createTenantDb(run.db, tenantId, "system", undefined, undefined, undefined, {
129
126
  report,
130
127
  unsafeRaw: { reason: `r.systemScope() handler "${tenantUserIdsQuery}"` },
131
128
  })
@@ -157,7 +154,7 @@ export function createDeliveryService(options) {
157
154
  // attempts instead append a queued event up front and a terminal event from
158
155
  // the send job — see deliverViaChannel + jobs.ts.
159
156
  async function logDelivery(run, entry) {
160
- const attemptId = await logAttempt(db, registry, entry);
157
+ const attemptId = await logAttempt(run.db, registry, entry);
161
158
  recordDelivery(run, entry, attemptId);
162
159
  }
163
160
  // The stored/returned error is only the code; the redacted message goes to the log.
@@ -199,7 +196,7 @@ export function createDeliveryService(options) {
199
196
  error: null,
200
197
  priority,
201
198
  };
202
- await appendAttemptEvent(db, registry, deliveryAttemptId, queuedEntry);
199
+ await appendAttemptEvent(run.db, registry, deliveryAttemptId, queuedEntry);
203
200
  await run.jobDispatcher.dispatch(channel.render ? DeliveryJobs.render : DeliveryJobs.send, {
204
201
  channelName: channel.name,
205
202
  address,
@@ -276,7 +273,7 @@ export function createDeliveryService(options) {
276
273
  // route address with no user account) — same suppression rule either way:
277
274
  // no blind-index key configured means no hash, so nothing to look up. One
278
275
  // query per notify() call, not per recipient and channel.
279
- async function loadSuppressedAddresses(tenantId, notificationType, priority, addresses) {
276
+ async function loadSuppressedAddresses(run, tenantId, notificationType, priority, addresses) {
280
277
  // Critical priority skips address opt-outs, same rule as user preferences.
281
278
  if (priority === "critical")
282
279
  return new Set();
@@ -286,32 +283,32 @@ export function createDeliveryService(options) {
286
283
  if (addressHash !== undefined)
287
284
  addressHashes.add(addressHash);
288
285
  }
289
- return selectOptedOutAddresses(db, tenantId, notificationType, [...addressHashes]);
286
+ return selectOptedOutAddresses(run.db, tenantId, notificationType, [...addressHashes]);
290
287
  }
291
288
  function isAddressSuppressed(suppressed, address, channelName) {
292
289
  const addressHash = hashUnsubscribeAddress(address);
293
290
  return addressHash !== undefined && suppressed.has(addressOptOutKey(addressHash, channelName));
294
291
  }
295
292
  const recipientChannelKey = (userId, channelName) => `${userId}|${channelName}`;
296
- async function isChannelDeliverable(userId, tenantId, notificationType, channelName) {
293
+ async function isChannelDeliverable(run, userId, tenantId, notificationType, channelName) {
297
294
  if (isChannelKilled && (await isChannelKilled(tenantId, channelName)))
298
295
  return false;
299
- return isChannelEnabled(userId, tenantId, notificationType, channelName);
296
+ return isChannelEnabled(run, userId, tenantId, notificationType, channelName);
300
297
  }
301
298
  // Resolves every user's channel addresses up front so the opt-out lookup can
302
299
  // cover them all at once. A resolve that throws stays out of the map: the
303
300
  // delivery loop resolves it again and reports channel_error as before.
304
301
  // Channels the kill switch or the user's preferences disable are never resolved
305
302
  // here: resolve may have side effects and deliverToUser skips those channels.
306
- async function prefetchUserAddresses(userIds, tenantId, notificationType) {
303
+ async function prefetchUserAddresses(run, userIds, tenantId, notificationType) {
307
304
  const addresses = new Map();
308
- const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
305
+ const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
309
306
  for (const userId of userIds) {
310
307
  for (const channel of channels) {
311
308
  if (!channel.resolve)
312
309
  continue;
313
310
  try {
314
- if (!(await isChannelDeliverable(userId, tenantId, notificationType, channel.name))) {
311
+ if (!(await isChannelDeliverable(run, userId, tenantId, notificationType, channel.name))) {
315
312
  continue;
316
313
  }
317
314
  addresses.set(recipientChannelKey(userId, channel.name), await channel.resolve(userId, channelCtx));
@@ -332,8 +329,8 @@ export function createDeliveryService(options) {
332
329
  // { type: "*", channel: "inApp", enabled: false } disables inApp globally
333
330
  // { type: "orderAssigned", channel: "*", enabled: true } enables orderAssigned everywhere
334
331
  // → orderAssigned on inApp: disabled (conservative) unless an exact entry overrides.
335
- async function isChannelEnabled(userId, tenantId, notificationType, channelName) {
336
- const prefs = await selectNotificationPreferences(db, tenantId, userId, notificationType, channelName);
332
+ async function isChannelEnabled(run, userId, tenantId, notificationType, channelName) {
333
+ const prefs = await selectNotificationPreferences(run.db, tenantId, userId, notificationType, channelName);
337
334
  if (prefs.length === 0)
338
335
  return true;
339
336
  // Exact match (both specific) wins over any wildcard
@@ -345,7 +342,7 @@ export function createDeliveryService(options) {
345
342
  return !prefs.some((p) => p.enabled === false);
346
343
  }
347
344
  async function deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed) {
348
- const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
345
+ const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
349
346
  for (const channel of channels) {
350
347
  // Route-only channel (no per-user address): not a user-notification target.
351
348
  if (!channel.resolve)
@@ -370,7 +367,7 @@ export function createDeliveryService(options) {
370
367
  }
371
368
  // Check preferences (critical priority skips preference check)
372
369
  if (priority !== "critical") {
373
- const enabled = await isChannelEnabled(userId, tenantId, notificationType, channel.name);
370
+ const enabled = await isChannelEnabled(run, userId, tenantId, notificationType, channel.name);
374
371
  if (!enabled) {
375
372
  await logDelivery(run, {
376
373
  tenantId,
@@ -464,8 +461,8 @@ export function createDeliveryService(options) {
464
461
  }
465
462
  }
466
463
  async function deliverDirect(route, notificationType, data, tenantId, priority, recipientId, run) {
467
- const channelCtx = buildChannelContext(db, registry, sseBroker, tenantId, secrets);
468
- const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, Object.values(route));
464
+ const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
465
+ const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, Object.values(route));
469
466
  // Direct routing skips preferences (no user account) but NOT rate limit
470
467
  // — direct sends can still be abused (webhook replays, test harnesses).
471
468
  for (const channel of channels) {
@@ -549,29 +546,38 @@ export function createDeliveryService(options) {
549
546
  userIds = [to];
550
547
  }
551
548
  else if ("tenant" in to) {
552
- userIds = await resolveUserIdsForTenant(to.tenant);
549
+ userIds = await resolveUserIdsForTenant(run, to.tenant);
553
550
  }
554
551
  else {
555
552
  userIds = to;
556
553
  }
557
554
  const prefetchedAddresses = priority === "critical"
558
555
  ? new Map()
559
- : await prefetchUserAddresses(userIds, tenantId, notificationType);
560
- const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
556
+ : await prefetchUserAddresses(run, userIds, tenantId, notificationType);
557
+ const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
561
558
  for (const userId of userIds) {
562
559
  await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed);
563
560
  }
564
561
  }
565
562
  }
563
+ function dispatched(run) {
564
+ return { kind: "dispatched", deliveries: run.deliveries };
565
+ }
566
566
  return {
567
- async notify(notificationType, options, _user, tenantId, jobDispatcher) {
567
+ async notify(notificationType, options, _user, tenantId, jobDispatcher, deliveryDb, fileProviderResolver) {
568
+ assertNotifyTargetsOwnTenant(options, tenantId);
569
+ assertNotificationVariables(registry, notificationType, options.data);
568
570
  const { route, idempotencyKey } = options;
569
571
  const priority = options.priority ?? "normal";
570
572
  const run = {
573
+ db: deliveryDb ?? db,
571
574
  deliveries: [],
572
575
  jobDispatcher: options.immediate ? undefined : (jobDispatcher ?? jobRunner),
573
576
  locale: options.locale,
574
577
  jobIdempotencyKey: jobIdempotencyKeyFor(idempotencyKey, route),
578
+ filesForTenant: fileProviderResolver === undefined
579
+ ? filesForTenant
580
+ : (fileTenantId) => fileContextFromResolver(fileProviderResolver, fileTenantId),
575
581
  };
576
582
  if (idempotencyKey) {
577
583
  const first = await claimIdempotency(tenantId, idempotencyKey);
@@ -586,7 +592,7 @@ export function createDeliveryService(options) {
586
592
  error: "duplicate_idempotency_key",
587
593
  priority,
588
594
  });
589
- return { deliveries: run.deliveries };
595
+ return dispatched(run);
590
596
  }
591
597
  }
592
598
  try {
@@ -602,7 +608,7 @@ export function createDeliveryService(options) {
602
608
  throw err;
603
609
  }
604
610
  await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run);
605
- return { deliveries: run.deliveries };
611
+ return dispatched(run);
606
612
  },
607
613
  };
608
614
  }
@@ -19,7 +19,7 @@ export function createDeliveryFeature(options) {
19
19
  const resolvedAccess = options?.access ?? access.admin;
20
20
  const attemptLogRetentionDays = resolveAttemptLogRetentionDays(options?.attemptLogRetentionDays);
21
21
  return defineFeature("delivery", (r) => {
22
- r.describe("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.");
22
+ r.describe('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.');
23
23
  r.uiHints({
24
24
  displayLabel: "Notifications \u00b7 Dispatch Core",
25
25
  category: "notifications",
@@ -6,4 +6,4 @@ export declare const resubscribeAddressWrite: import("@cosmicdrift/kumiko-types/
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare const resubscribeUserWrite: import("@cosmicdrift/kumiko-types/def
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -7,4 +7,4 @@ export declare const setPreferenceWrite: import("@cosmicdrift/kumiko-types/defin
7
7
  notificationType: string;
8
8
  channel: string;
9
9
  enabled: boolean;
10
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
10
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare const unsubscribeAddressWrite: import("@cosmicdrift/kumiko-types/
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -6,4 +6,4 @@ export declare const unsubscribeUserWrite: import("@cosmicdrift/kumiko-types/def
6
6
  }, z.core.$strip>, {
7
7
  notificationType: string;
8
8
  channel: string;
9
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
9
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,13 +1,16 @@
1
+ export { sanitizePdfAttachmentFileName } from "../shared/index.js";
1
2
  export { hashUnsubscribeAddress } from "./address-opt-out.js";
3
+ export { DELIVERY_ENVELOPE_DATA_KEYS, type EmailAttachmentRef, emailAttachmentRefSchema, type FileAttachmentRef, fileAttachmentRefSchema, type InlineAttachmentRef, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, type RenderedPdfAttachmentRef, renderedPdfAttachmentRefSchema, toAttachmentRef, withoutDeliveryEnvelopeKeys, } from "./attachment-ref.js";
2
4
  export { type AttemptLogRetentionOptions, DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
5
+ export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
3
6
  export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
4
7
  export { type ChatSendFailureCode, type ChatSendResult, type ChatWebhookRequest, type ChatWebhookResponse, type ChatWebhookTarget, chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
5
8
  export type { DeliveryStatusValue } from "./constants.js";
6
- export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
9
+ export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, EMAIL_CHANNEL_NAME, } from "./constants.js";
7
10
  export { collectChannels, createDeliveryService, type DeliveryServiceOptions, type KillSwitchResolver, type RateLimitConfig, } from "./delivery-service.js";
8
11
  export { createDeliveryFeature, type DeliveryFeatureOptions } from "./feature.js";
9
12
  export { createDeliveryNotifyFactory } from "./notify-factory.js";
10
13
  export { deliveryAttemptsTable, notificationAddressOptOutEntity, notificationAddressOptOutsTable, notificationPreferenceEntity, notificationPreferencesTable, } from "./tables.js";
11
14
  export { type CreateDeliveryTestContextOptions, createDeliveryTestContext } from "./testing.js";
12
- export { type ChannelContext, type ChannelMessage, type ChannelResult, type DeliveryChannel, type DeliveryChannelMode, type DeliveryChannelPlugin, type DeliveryLogEntry, type DeliveryService, isDeliveryChannelPlugin, type NotificationRenderer, type RenderedMessage, type RendererInput, } from "./types.js";
15
+ export { type AttachmentOrigin, type ChannelContext, type ChannelMessage, type ChannelResult, type DeliveryChannel, type DeliveryChannelMode, type DeliveryChannelPlugin, type DeliveryLogEntry, type DeliveryService, isDeliveryChannelPlugin, type RenderedMessage, } from "./types.js";
13
16
  export { type AddressUnsubscribeTokenPayload, createUnsubscribeRoutes, DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, signAddressUnsubscribeToken, signUnsubscribeToken, type UnsubscribeRouteOptions, type UnsubscribeTokenPayload, } from "./unsubscribe.js";
@@ -1,8 +1,11 @@
1
+ export { sanitizePdfAttachmentFileName } from "../shared/index.js";
1
2
  export { hashUnsubscribeAddress } from "./address-opt-out.js";
3
+ export { DELIVERY_ENVELOPE_DATA_KEYS, emailAttachmentRefSchema, fileAttachmentRefSchema, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, renderedPdfAttachmentRefSchema, toAttachmentRef, withoutDeliveryEnvelopeKeys, } from "./attachment-ref.js";
2
4
  export { DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
5
+ export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
3
6
  export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
4
7
  export { chatConnectionNameSchema, chatWebhookUrlSchema, checkChatWebhookTarget, DEFAULT_CHAT_TIMEOUT_MS, postChatWebhook, truncateChars, } from "./chat-webhook-sender.js";
5
- export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, } from "./constants.js";
8
+ export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_CHANNEL_EXTENSION, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobs, DeliveryQueries, DeliveryStatus, EMAIL_CHANNEL_NAME, } from "./constants.js";
6
9
  export { collectChannels, createDeliveryService, } from "./delivery-service.js";
7
10
  export { createDeliveryFeature } from "./feature.js";
8
11
  export { createDeliveryNotifyFactory } from "./notify-factory.js";
@@ -6,6 +6,8 @@
6
6
  // these handlers append the terminal sent/failed event on the same stream.
7
7
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
8
8
  import * as z from "zod";
9
+ import { fileContextForTenant } from "../file-foundation/index.js";
10
+ import { emailAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, toAttachmentRef, } from "./attachment-ref.js";
9
11
  import { appendAttemptEvent } from "./attempt-log.js";
10
12
  import { buildChannelContext } from "./channel-context.js";
11
13
  import { DeliveryJobs, deliveryIdempotencyRedisKey, deliveryPriorityRank } from "./constants.js";
@@ -30,7 +32,14 @@ const renderJobPayloadSchema = z.object({
30
32
  });
31
33
  const sendJobPayloadSchema = renderJobPayloadSchema.extend({
32
34
  rendered: z
33
- .object({ html: z.string(), subject: z.string(), text: z.string().optional() })
35
+ .object({
36
+ html: z.string(),
37
+ subject: z.string(),
38
+ text: z.string().optional(),
39
+ fromName: z.string().optional(),
40
+ replyTo: z.string().optional(),
41
+ attachments: z.array(emailAttachmentRefSchema).max(MAX_EMAIL_ATTACHMENTS).optional(),
42
+ })
34
43
  .optional(),
35
44
  });
36
45
  function requireTenantScopedDeps(ctx, tenantId) {
@@ -39,15 +48,30 @@ function requireTenantScopedDeps(ctx, tenantId) {
39
48
  throw new Error("delivery job: missing registry in job context");
40
49
  // delivery is r.systemScope()'d, so JobContext.systemDb is guaranteed; unsafeRaw
41
50
  // hands the underlying DbConnection to both — safe since they filter by tenantId themselves.
51
+ // Attachment lookups read only this tenant's FileRefs, plus the system tenant's for inline images.
42
52
  if (!ctx.systemDb) {
43
53
  throw new InternalError({
44
54
  message: "delivery job: ctx.systemDb missing on a system-scoped job",
45
55
  });
46
56
  }
57
+ // Also makes ctx.config (built for the job's tenant) the accessor of the payload tenant;
58
+ // job contexts carry no configFor.
47
59
  ctx.systemDb.assertTenantMatch(tenantId);
48
- const db = ctx.systemDb.unsafeRaw("delivery attempt append and channel context filter by this job's tenantId themselves"); // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
60
+ const db = ctx.systemDb.unsafeRaw("attempt append and channel context filter by this job's tenantId; attachment lookup reads only the job tenant's FileRefs plus, for inline images, the system tenant's"); // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
49
61
  return { db, registry };
50
62
  }
63
+ function toSendRendered(rendered) {
64
+ return {
65
+ html: rendered.html,
66
+ subject: rendered.subject,
67
+ ...(rendered.text !== undefined && { text: rendered.text }),
68
+ ...(rendered.fromName !== undefined && { fromName: rendered.fromName }),
69
+ ...(rendered.replyTo !== undefined && { replyTo: rendered.replyTo }),
70
+ ...(rendered.attachments !== undefined && {
71
+ attachments: rendered.attachments.map(toAttachmentRef),
72
+ }),
73
+ };
74
+ }
51
75
  function resolveChannel(registry, name) {
52
76
  const channel = collectChannels(registry).find((c) => c.name === name);
53
77
  if (!channel)
@@ -76,12 +100,11 @@ function entryFor(p, status, error, address, confirmed) {
76
100
  ...(confirmed === false && { confirmed }),
77
101
  };
78
102
  }
79
- // Only the final retry frees the key: earlier failures are retried by the runner and
80
- // must still dedup. Assumes ctx.redis is the handle the service claimed on; runProdApp
81
- // wires the same instance into both.
82
- async function releaseIdempotencyOnTerminalFailure(ctx, p) {
83
- // skip: nothing to release before the last attempt or without an idempotency key
84
- if (!ctx.finalAttempt || !p.idempotencyKey)
103
+ // Assumes ctx.redis is the handle the service claimed on; runProdApp wires the same
104
+ // instance into both.
105
+ async function releaseIdempotencyKey(ctx, p) {
106
+ // skip: no idempotency key was claimed for this notification
107
+ if (!p.idempotencyKey)
85
108
  return;
86
109
  if (!ctx.redis) {
87
110
  ctx.log.warn("delivery: idempotency key not released, job context has no redis", {
@@ -98,6 +121,26 @@ async function releaseIdempotencyOnTerminalFailure(ctx, p) {
98
121
  });
99
122
  }
100
123
  }
124
+ // Only the final retry frees the key: earlier failures are retried by the runner and
125
+ // must still dedup.
126
+ async function releaseIdempotencyOnTerminalFailure(ctx, p) {
127
+ // skip: not the last attempt, the runner retries and the key must keep deduplicating
128
+ if (!ctx.finalAttempt)
129
+ return;
130
+ await releaseIdempotencyKey(ctx, p);
131
+ }
132
+ // Covers every throw after the payload parsed, not just the attempt-logged ones: a
133
+ // missing registry or unknown channel on the last attempt would otherwise hold the key
134
+ // until its TTL.
135
+ async function releasingKeyOnTerminalFailure(ctx, p, run) {
136
+ try {
137
+ return await run();
138
+ }
139
+ catch (err) {
140
+ await releaseIdempotencyOnTerminalFailure(ctx, p);
141
+ throw err;
142
+ }
143
+ }
101
144
  // The job runner stores err.message in the run row, and BullMQ keeps failedReason and
102
145
  // the stack trace in Redis in plain text, so the rethrown error carries only the code.
103
146
  // The redacted message goes to the log.
@@ -108,7 +151,6 @@ async function failAttempt(ctx, db, registry, p, code, err) {
108
151
  channel: p.channelName,
109
152
  });
110
153
  await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, "failed", code, p.address));
111
- await releaseIdempotencyOnTerminalFailure(ctx, p);
112
154
  throw new Error(code);
113
155
  }
114
156
  // Render the message and hand off to delivery.send. On failure: record the
@@ -116,10 +158,13 @@ async function failAttempt(ctx, db, registry, p, code, err) {
116
158
  // the send step is never reached, so a stuck render can't half-send.
117
159
  export const deliveryRenderJob = async (payload, ctx) => {
118
160
  const p = renderJobPayloadSchema.parse(payload);
161
+ return releasingKeyOnTerminalFailure(ctx, p, () => renderStep(p, ctx));
162
+ };
163
+ async function renderStep(p, ctx) {
119
164
  const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
120
165
  const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
121
166
  const channel = resolveChannel(registry, p.channelName);
122
- const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
167
+ const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId));
123
168
  let rendered;
124
169
  try {
125
170
  if (!channel.render) {
@@ -132,26 +177,33 @@ export const deliveryRenderJob = async (payload, ctx) => {
132
177
  }
133
178
  try {
134
179
  const jobRunner = ctx["jobRunner"]; // @cast-boundary dynamic-key — dispatch lives on the concrete runner
135
- await jobRunner.dispatch(DeliveryJobs.send, { ...p, rendered }, { priority: deliveryPriorityRank[p.priority] });
180
+ await jobRunner.dispatch(DeliveryJobs.send, { ...p, rendered: toSendRendered(rendered) }, { priority: deliveryPriorityRank[p.priority] });
136
181
  }
137
182
  catch (err) {
138
183
  return failAttempt(ctx, db, registry, p, "channel_error", err);
139
184
  }
140
- };
185
+ }
141
186
  // Deliver via the channel using the rendered payload (if any). On failure:
142
187
  // record the terminal failed event and rethrow so BullMQ retries the send with
143
188
  // the same already-rendered HTML — no re-render needed.
144
189
  export const deliverySendJob = async (payload, ctx) => {
145
190
  const p = sendJobPayloadSchema.parse(payload);
191
+ return releasingKeyOnTerminalFailure(ctx, p, () => sendStep(p, ctx));
192
+ };
193
+ async function sendStep(p, ctx) {
146
194
  const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
147
195
  const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
148
196
  const channel = resolveChannel(registry, p.channelName);
149
- const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets);
197
+ const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId));
150
198
  try {
151
199
  const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
152
200
  await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, result.status, result.error ?? null, result.address ?? p.address, result.confirmed));
201
+ // A soft failure ends the job successfully, so no retry follows and the caller's
202
+ // retry must not be skipped as a duplicate.
203
+ if (result.status === "failed")
204
+ await releaseIdempotencyKey(ctx, p);
153
205
  }
154
206
  catch (err) {
155
207
  return failAttempt(ctx, db, registry, p, "send_failed", err);
156
208
  }
157
- };
209
+ }
@@ -2,5 +2,8 @@
2
2
  // job dispatcher is handed to notify() per call, so queued channels go through
3
3
  // the delivery jobs. deliverQueuedInline drops the dispatcher and sends inline.
4
4
  export function createDeliveryNotifyFactory(deliveryService, options = {}) {
5
- return (user, tenantId, jobDispatcher) => (notificationType, notifyOptions) => deliveryService.notify(notificationType, notifyOptions, user, tenantId, options.deliverQueuedInline === true ? undefined : jobDispatcher);
5
+ return (user, tenantId, jobDispatcher, deliveryDb, fileProviderResolver) => {
6
+ const notify = (notificationType, notifyOptions) => deliveryService.notify(notificationType, notifyOptions, user, tenantId, options.deliverQueuedInline === true ? undefined : jobDispatcher, deliveryDb, fileProviderResolver);
7
+ return notify;
8
+ };
6
9
  }
@@ -1,6 +1,7 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
- import type { EscapeHatchAuditSink, Registry } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { ConfigAccessor, EscapeHatchAuditSink, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
5
  import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
5
6
  import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
6
7
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
@@ -16,6 +17,8 @@ export type CreateDeliveryTestContextOptions = {
16
17
  readonly jobRunner?: JobRunner;
17
18
  readonly secrets?: SecretsContext;
18
19
  readonly idempotencyRedis?: Redis;
20
+ readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
21
+ readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
19
22
  };
20
23
  /**
21
24
  * Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
@@ -1,15 +1,35 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
- import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
- import type { DeliveryErrorCode, NotifyDeliveryStatus, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { DeliveryErrorCode, NotifyDeliveryStatus, NotifyDispatched, NotifyJobDispatcher, NotifyOptions, NotifyPriority, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
4
5
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
6
+ import type { MailPayload, RenderedTemplateRef } from "../renderer-foundation/index.js";
7
+ import type { EmailAttachmentRef } from "./attachment-ref.js";
5
8
  import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
9
+ export type RenderedMail = {
10
+ readonly subject: string;
11
+ readonly html: string;
12
+ readonly text: string;
13
+ readonly previewText?: string;
14
+ readonly fromName?: string;
15
+ readonly replyTo?: string;
16
+ readonly attachments?: readonly EmailAttachmentRef[];
17
+ /** The layout version the html was rendered through; the editor preview names it. */
18
+ readonly layout?: RenderedTemplateRef;
19
+ };
6
20
  export type ChannelContext = {
7
21
  readonly db: TenantDb;
8
22
  readonly registry: Registry;
9
23
  readonly sseBroker: SseBroker | undefined;
10
24
  readonly tenantId: TenantId;
11
25
  readonly secrets?: SecretsContext | undefined;
26
+ readonly renderMail?: (payload: MailPayload) => Promise<RenderedMail>;
27
+ readonly readAttachment?: (ref: EmailAttachmentRef, origin?: AttachmentOrigin) => Promise<{
28
+ readonly content: Uint8Array;
29
+ readonly contentType: string;
30
+ } | null>;
12
31
  };
32
+ export type AttachmentOrigin = "template" | "message";
13
33
  export type ChannelMessage = {
14
34
  readonly notificationType: string;
15
35
  readonly title: string;
@@ -27,6 +47,9 @@ export type RenderedMessage = {
27
47
  readonly html: string;
28
48
  readonly subject: string;
29
49
  readonly text?: string;
50
+ readonly fromName?: string;
51
+ readonly replyTo?: string;
52
+ readonly attachments?: readonly EmailAttachmentRef[];
30
53
  };
31
54
  export declare const DELIVERY_CHANNEL_MODES: readonly ["inline", "queued"];
32
55
  export type DeliveryChannelMode = (typeof DELIVERY_CHANNEL_MODES)[number];
@@ -38,17 +61,6 @@ export type DeliveryChannel = {
38
61
  render?(message: ChannelMessage, ctx: ChannelContext): Promise<RenderedMessage>;
39
62
  send(address: string, message: ChannelMessage, ctx: ChannelContext, rendered?: RenderedMessage): Promise<ChannelResult>;
40
63
  };
41
- export type RendererInput = {
42
- readonly template: string;
43
- readonly variables: Readonly<Record<string, unknown>>;
44
- readonly locale?: string | undefined;
45
- };
46
- export type NotificationRenderer = {
47
- readonly name: string;
48
- render(input: RendererInput): Promise<string>;
49
- /** Plain-text version of the same mail; the email channel sends it as the text part of a multipart/alternative mail. */
50
- renderText?(input: RendererInput): Promise<string>;
51
- };
52
64
  export type DeliveryLogEntry = {
53
65
  readonly tenantId: TenantId;
54
66
  readonly notificationType: string;
@@ -61,7 +73,7 @@ export type DeliveryLogEntry = {
61
73
  readonly confirmed?: false;
62
74
  };
63
75
  export type DeliveryService = {
64
- notify(notificationType: string, options: NotifyOptions, user: SessionUser, tenantId: TenantId, jobDispatcher?: NotifyJobDispatcher): Promise<NotifyResult>;
76
+ notify(notificationType: string, options: NotifyOptions, user: SessionUser, tenantId: TenantId, jobDispatcher?: NotifyJobDispatcher, deliveryDb?: DbConnection, fileProviderResolver?: FileProviderResolver): Promise<NotifyDispatched>;
65
77
  };
66
78
  export type DeliveryChannelPlugin = Omit<DeliveryChannel, "name">;
67
79
  export declare function isDeliveryChannelPlugin(o: unknown): o is DeliveryChannelPlugin;
@@ -1,5 +1,6 @@
1
1
  import { ExtraRouteRejection, signatureRoute, } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
3
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
3
4
  import * as jose from "jose";
4
5
  import * as z from "zod";
5
6
  import { hashUnsubscribeAddress } from "./address-opt-out.js";
@@ -280,7 +281,9 @@ function tokenFromRequestBody(request) {
280
281
  if (contentType?.includes("application/json")) {
281
282
  try {
282
283
  const parsed = JSON.parse(request.rawBody);
283
- return typeof parsed.token === "string" ? parsed.token : undefined;
284
+ return isPlainObject(parsed) && typeof parsed["token"] === "string"
285
+ ? parsed["token"]
286
+ : undefined;
284
287
  }
285
288
  catch {
286
289
  return undefined;
@@ -3,12 +3,9 @@
3
3
  // region blur (see BlurRegion). Server-only: sharp is a native binding.
4
4
  import QRCode from "qrcode";
5
5
  import sharp from "sharp";
6
+ import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
6
7
  const MIN_SHARP_SIGMA = 0.3;
7
8
  const MAX_SHARP_SIGMA = 1000;
8
- // Above this, a decoded pixel buffer runs into hundreds of MB before any
9
- // resize even happens — sharp's own default (~268MP) is still large enough
10
- // to be an OOM primitive for a single request.
11
- const MAX_INPUT_PIXELS = 100_000_000;
12
9
  // Longest output edge a caller may request via `size`/`maxEdge`. Without a
13
10
  // cap, `{ maxEdge: 50000 }` allocates a multi-gigabyte buffer per encoder
14
11
  // step before any client sees a byte back.
@@ -27,26 +24,12 @@ const MAX_QR_DATA_LENGTH = 1024;
27
24
  // camera capture — the AC claims phone-scannable, so an under-sized layer
28
25
  // throws instead of caching a silently unusable image.
29
26
  const MIN_QR_PIXEL_WIDTH = 160;
30
- // sequentialRead trades random pixel access for lower peak memory on
31
- // streamed formats — applied at every decode entry point below since none
32
- // of them need non-sequential access to the *original* bytes (extract runs
33
- // against a single already-buffered region per call).
34
- const SHARP_INPUT_OPTIONS = { limitInputPixels: MAX_INPUT_PIXELS, sequentialRead: true };
35
27
  function normalizeMimeType(mimeType) {
36
28
  return mimeType.toLowerCase().split(";")[0]?.trim() ?? "";
37
29
  }
38
30
  function clampSigma(sigma) {
39
31
  return Math.min(MAX_SHARP_SIGMA, Math.max(MIN_SHARP_SIGMA, sigma));
40
32
  }
41
- // Shared trust boundary for every image byte source this renderer decodes —
42
- // the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
43
- // alike: a base64 string in a field declaration isn't more trustworthy than
44
- // an upload just because it came from app code.
45
- function assertNotSvg(format) {
46
- if (format === "svg") {
47
- throw new Error("derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.");
48
- }
49
- }
50
33
  function assertValidRegion(region) {
51
34
  const values = [region.x, region.y, region.width, region.height];
52
35
  if (values.some((v) => !Number.isFinite(v)) || region.width <= 0 || region.height <= 0) {