@cosmicdrift/kumiko-bundled-features 0.362.0 → 0.363.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/dist/admin-shell/feature.d.ts +3 -0
  2. package/dist/admin-shell/feature.js +6 -1
  3. package/dist/agent-tools/agent-manifest.js +2 -1
  4. package/dist/agent-tools/filter-ops.d.ts +1 -1
  5. package/dist/agent-tools/filter-ops.js +1 -3
  6. package/dist/agent-tools/tool-dispatch.js +2 -1
  7. package/dist/auth-email-password/email-templates.d.ts +4 -26
  8. package/dist/auth-email-password/email-templates.js +9 -101
  9. package/dist/auth-email-password/feature.js +24 -6
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/request-account-unlock.write.js +2 -4
  12. package/dist/auth-email-password/handlers/request-email-verification.write.js +2 -4
  13. package/dist/auth-email-password/handlers/request-password-reset.write.js +2 -4
  14. package/dist/auth-email-password/handlers/signup-confirm.write.js +3 -4
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -2
  16. package/dist/auth-email-password/handlers/token-request-handler.js +0 -1
  17. package/dist/auth-email-password/index.d.ts +2 -2
  18. package/dist/auth-email-password/index.js +1 -1
  19. package/dist/auth-email-password/magic-link-mail.d.ts +3 -2
  20. package/dist/auth-email-password/magic-link-mail.js +19 -14
  21. package/dist/auth-email-password/mail-notifications.d.ts +32 -0
  22. package/dist/auth-email-password/mail-notifications.js +116 -0
  23. package/dist/auth-email-password/signup-activation.d.ts +1 -1
  24. package/dist/auth-email-password/signup-activation.js +2 -3
  25. package/dist/auth-foundation/types.js +2 -8
  26. package/dist/auth-mfa/feature.js +4 -4
  27. package/dist/auth-mfa/hmac-token-codec.js +4 -4
  28. package/dist/auth-mfa/mfa-challenge-token.js +3 -3
  29. package/dist/auth-mfa/mfa-preauth-setup-token.js +3 -3
  30. package/dist/auth-mfa/mfa-setup-token.js +8 -8
  31. package/dist/billing-foundation/constants.d.ts +3 -3
  32. package/dist/billing-foundation/constants.js +5 -4
  33. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +32 -14
  34. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +27 -32
  35. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +3 -3
  36. package/dist/billing-foundation/consumer-protection/mail-notifications.d.ts +8 -0
  37. package/dist/billing-foundation/consumer-protection/mail-notifications.js +95 -0
  38. package/dist/billing-foundation/consumer-protection/register-consent.js +2 -0
  39. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +20 -10
  40. package/dist/billing-foundation/consumer-protection/termination-mail.js +10 -21
  41. package/dist/billing-foundation/consumer-protection/termination-notify.js +2 -2
  42. package/dist/billing-foundation/consumer-protection/termination-pages.js +1 -3
  43. package/dist/billing-foundation/consumer-protection/termination-request.js +2 -2
  44. package/dist/billing-foundation/handlers/process-event.write.js +2 -2
  45. package/dist/billing-foundation/handlers/process-payment-event.write.js +1 -1
  46. package/dist/billing-foundation/subscription-tier-sync.js +3 -4
  47. package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -1
  48. package/dist/cap-overview/web/cap-usage-cell.js +5 -4
  49. package/dist/channel-email/email-channel.d.ts +2 -2
  50. package/dist/channel-email/email-channel.js +89 -26
  51. package/dist/channel-email/feature.js +3 -3
  52. package/dist/channel-email/index.d.ts +1 -1
  53. package/dist/channel-email/index.js +1 -1
  54. package/dist/channel-email/pii-guard.js +10 -0
  55. package/dist/channel-email/smtp-transport.js +13 -0
  56. package/dist/channel-email/types.d.ts +8 -0
  57. package/dist/config/handlers/set.write.js +1 -2
  58. package/dist/custom-fields/lib/value-schema.js +10 -11
  59. package/dist/custom-fields/wire-user-data-rights.js +7 -6
  60. package/dist/data-retention/index.d.ts +1 -0
  61. package/dist/data-retention/index.js +1 -0
  62. package/dist/data-retention/run-retention-cleanup.js +6 -6
  63. package/dist/delivery/attachment-ref.d.ts +39 -0
  64. package/dist/delivery/attachment-ref.js +64 -0
  65. package/dist/delivery/channel-context.d.ts +8 -4
  66. package/dist/delivery/channel-context.js +76 -2
  67. package/dist/delivery/constants.d.ts +1 -0
  68. package/dist/delivery/constants.js +1 -0
  69. package/dist/delivery/delivery-service.d.ts +4 -1
  70. package/dist/delivery/delivery-service.js +38 -32
  71. package/dist/delivery/feature.js +1 -1
  72. package/dist/delivery/index.d.ts +5 -2
  73. package/dist/delivery/index.js +4 -1
  74. package/dist/delivery/jobs.js +25 -5
  75. package/dist/delivery/notify-factory.js +4 -1
  76. package/dist/delivery/testing.d.ts +4 -1
  77. package/dist/delivery/types.d.ts +20 -14
  78. package/dist/delivery/unsubscribe.js +4 -1
  79. package/dist/derivatives-sharp/render.js +1 -18
  80. package/dist/document-ingest-foundation/pages.js +4 -4
  81. package/dist/feature-toggles/toggle-sync-signal.js +4 -4
  82. package/dist/file-foundation/index.d.ts +1 -0
  83. package/dist/file-foundation/index.js +1 -0
  84. package/dist/file-foundation/read-file-ref-content.d.ts +27 -0
  85. package/dist/file-foundation/read-file-ref-content.js +49 -0
  86. package/dist/folders/web/folder-manager.js +3 -2
  87. package/dist/form-draft/db/queries/cleanup.js +4 -4
  88. package/dist/form-draft/db/queries/draft-count.js +2 -2
  89. package/dist/form-draft/db/queries/owned-file-refs.js +2 -2
  90. package/dist/form-draft/release-file-refs.js +3 -2
  91. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  92. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +6 -6
  93. package/dist/inbound-mail-foundation/handlers/scope-visibility.d.ts +1 -1
  94. package/dist/inbound-mail-foundation/oauth-state.js +7 -7
  95. package/dist/inbound-mail-foundation/types.js +4 -4
  96. package/dist/inbound-mail-foundation/watch-supervisor.js +22 -13
  97. package/dist/inbound-provider-imap/imap-client.js +4 -3
  98. package/dist/ledger/recurring.js +1 -3
  99. package/dist/managed-pages/branding.d.ts +2 -0
  100. package/dist/managed-pages/branding.js +24 -2
  101. package/dist/managed-pages/feature.js +1 -0
  102. package/dist/managed-pages/i18n.js +1 -0
  103. package/dist/managed-pages/screens/branding-screen.js +8 -1
  104. package/dist/page-render/branding.d.ts +1 -0
  105. package/dist/page-render/branding.js +1 -0
  106. package/dist/page-render/markdown.js +1 -1
  107. package/dist/personal-access-tokens/feature.js +1 -1
  108. package/dist/personal-access-tokens/handlers/list.query.js +2 -1
  109. package/dist/personal-access-tokens/revoke-for-user.js +2 -1
  110. package/dist/renderer-foundation/api.d.ts +13 -13
  111. package/dist/renderer-foundation/api.js +1 -1
  112. package/dist/renderer-foundation/constants.js +2 -4
  113. package/dist/renderer-foundation/feature.js +7 -7
  114. package/dist/renderer-foundation/index.d.ts +2 -1
  115. package/dist/renderer-foundation/index.js +1 -0
  116. package/dist/renderer-foundation/types.d.ts +92 -32
  117. package/dist/renderer-pdf/feature.js +7 -1
  118. package/dist/sessions/db/queries/cleanup.js +2 -2
  119. package/dist/sessions/feature.js +1 -1
  120. package/dist/sessions/handlers/list.query.js +2 -1
  121. package/dist/shared/attachment-file-name.d.ts +3 -0
  122. package/dist/shared/attachment-file-name.js +35 -0
  123. package/dist/shared/index.d.ts +3 -0
  124. package/dist/shared/index.js +3 -0
  125. package/dist/shared/is-tenant-db.js +1 -1
  126. package/dist/shared/mail-template-body.d.ts +4 -0
  127. package/dist/shared/mail-template-body.js +17 -0
  128. package/dist/shared/sharp-input-limits.d.ts +6 -0
  129. package/dist/shared/sharp-input-limits.js +22 -0
  130. package/dist/shared/signup-handover.js +2 -2
  131. package/dist/tags/handlers/assign-tag.write.js +3 -1
  132. package/dist/tags/web/tag-chip.js +2 -0
  133. package/dist/template-resolver/api.d.ts +30 -2
  134. package/dist/template-resolver/api.js +40 -5
  135. package/dist/template-resolver/compile-document-body.d.ts +8 -0
  136. package/dist/template-resolver/compile-document-body.js +163 -0
  137. package/dist/template-resolver/constants.d.ts +13 -4
  138. package/dist/template-resolver/constants.js +30 -10
  139. package/dist/template-resolver/document-sanitizer.d.ts +32 -0
  140. package/dist/template-resolver/document-sanitizer.js +201 -0
  141. package/dist/template-resolver/document-schema.d.ts +21 -0
  142. package/dist/template-resolver/document-schema.js +296 -0
  143. package/dist/template-resolver/feature.d.ts +18 -0
  144. package/dist/template-resolver/feature.js +50 -6
  145. package/dist/template-resolver/handlers/bundle-import.d.ts +20 -0
  146. package/dist/template-resolver/handlers/bundle-import.js +147 -0
  147. package/dist/template-resolver/handlers/collection-set.write.js +25 -2
  148. package/dist/template-resolver/handlers/copy-locale.write.d.ts +19 -0
  149. package/dist/template-resolver/handlers/copy-locale.write.js +116 -0
  150. package/dist/template-resolver/handlers/diff-to-system.query.d.ts +22 -0
  151. package/dist/template-resolver/handlers/diff-to-system.query.js +64 -0
  152. package/dist/template-resolver/handlers/export-bundle.query.d.ts +35 -0
  153. package/dist/template-resolver/handlers/export-bundle.query.js +68 -0
  154. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  155. package/dist/template-resolver/handlers/find-by-id.query.js +3 -3
  156. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +39 -0
  157. package/dist/template-resolver/handlers/import-bundle-system.write.js +36 -0
  158. package/dist/template-resolver/handlers/import-bundle.write.d.ts +40 -0
  159. package/dist/template-resolver/handlers/import-bundle.write.js +19 -0
  160. package/dist/template-resolver/handlers/layout-source.query.d.ts +26 -0
  161. package/dist/template-resolver/handlers/layout-source.query.js +64 -0
  162. package/dist/template-resolver/handlers/list.query.d.ts +4 -1
  163. package/dist/template-resolver/handlers/list.query.js +5 -7
  164. package/dist/template-resolver/handlers/overview.query.d.ts +59 -0
  165. package/dist/template-resolver/handlers/overview.query.js +165 -0
  166. package/dist/template-resolver/handlers/preview.query.d.ts +12 -0
  167. package/dist/template-resolver/handlers/preview.query.js +24 -0
  168. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +8 -0
  169. package/dist/template-resolver/handlers/reset-to-system.write.js +50 -0
  170. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +8 -0
  171. package/dist/template-resolver/handlers/resource-link-pdf.write.js +32 -0
  172. package/dist/template-resolver/handlers/resource-remove.write.d.ts +9 -0
  173. package/dist/template-resolver/handlers/resource-remove.write.js +44 -0
  174. package/dist/template-resolver/handlers/resource-shared.d.ts +9 -0
  175. package/dist/template-resolver/handlers/resource-shared.js +14 -0
  176. package/dist/template-resolver/handlers/resource-upload.write.d.ts +13 -0
  177. package/dist/template-resolver/handlers/resource-upload.write.js +98 -0
  178. package/dist/template-resolver/handlers/send-test.write.d.ts +11 -0
  179. package/dist/template-resolver/handlers/send-test.write.js +81 -0
  180. package/dist/template-resolver/handlers/set.write.d.ts +4 -1
  181. package/dist/template-resolver/handlers/set.write.js +25 -3
  182. package/dist/template-resolver/handlers/shared.d.ts +9 -5
  183. package/dist/template-resolver/handlers/shared.js +6 -2
  184. package/dist/template-resolver/handlers/template-gates.d.ts +36 -0
  185. package/dist/template-resolver/handlers/template-gates.js +131 -0
  186. package/dist/template-resolver/handlers/template-write.d.ts +45 -0
  187. package/dist/template-resolver/handlers/template-write.js +109 -0
  188. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +17 -0
  189. package/dist/template-resolver/handlers/tenant-overrides.query.js +48 -0
  190. package/dist/template-resolver/handlers/toggle-status.write.d.ts +12 -0
  191. package/dist/template-resolver/handlers/toggle-status.write.js +27 -11
  192. package/dist/template-resolver/handlers/upsert-system.write.d.ts +5 -8
  193. package/dist/template-resolver/handlers/upsert-system.write.js +14 -59
  194. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +5 -8
  195. package/dist/template-resolver/handlers/upsert-tenant.write.js +10 -61
  196. package/dist/template-resolver/handlers/usage.query.d.ts +17 -0
  197. package/dist/template-resolver/handlers/usage.query.js +15 -0
  198. package/dist/template-resolver/handlers/validate.query.d.ts +74 -0
  199. package/dist/template-resolver/handlers/validate.query.js +80 -0
  200. package/dist/template-resolver/index.d.ts +18 -1
  201. package/dist/template-resolver/index.js +16 -1
  202. package/dist/template-resolver/layout-regions.d.ts +23 -0
  203. package/dist/template-resolver/layout-regions.js +125 -0
  204. package/dist/template-resolver/linked-resources.d.ts +17 -0
  205. package/dist/template-resolver/linked-resources.js +75 -0
  206. package/dist/template-resolver/preview-display-html.d.ts +7 -0
  207. package/dist/template-resolver/preview-display-html.js +27 -0
  208. package/dist/template-resolver/print-document.d.ts +1 -0
  209. package/dist/template-resolver/print-document.js +55 -0
  210. package/dist/template-resolver/qualified-names.d.ts +30 -0
  211. package/dist/template-resolver/qualified-names.js +30 -0
  212. package/dist/template-resolver/record-mapping.d.ts +2 -0
  213. package/dist/template-resolver/record-mapping.js +21 -0
  214. package/dist/template-resolver/resource-image.d.ts +15 -0
  215. package/dist/template-resolver/resource-image.js +34 -0
  216. package/dist/template-resolver/seeding.d.ts +14 -1
  217. package/dist/template-resolver/seeding.js +105 -15
  218. package/dist/template-resolver/store-template-resource.d.ts +64 -0
  219. package/dist/template-resolver/store-template-resource.js +264 -0
  220. package/dist/template-resolver/table.d.ts +47 -8
  221. package/dist/template-resolver/table.js +11 -0
  222. package/dist/template-resolver/template-bundle.d.ts +36 -0
  223. package/dist/template-resolver/template-bundle.js +60 -0
  224. package/dist/template-resolver/template-diff.d.ts +7 -0
  225. package/dist/template-resolver/template-diff.js +68 -0
  226. package/dist/template-resolver/template-meta.d.ts +60 -0
  227. package/dist/template-resolver/template-meta.js +101 -0
  228. package/dist/template-resolver/template-preview.d.ts +86 -0
  229. package/dist/template-resolver/template-preview.js +374 -0
  230. package/dist/template-resolver/template-resources.d.ts +14 -0
  231. package/dist/template-resolver/template-resources.js +42 -0
  232. package/dist/template-resolver/template-usage.d.ts +12 -0
  233. package/dist/template-resolver/template-usage.js +30 -0
  234. package/dist/template-resolver/template-validation.d.ts +22 -0
  235. package/dist/template-resolver/template-validation.js +172 -0
  236. package/dist/template-resolver/testing.js +5 -5
  237. package/dist/template-resolver/user-content-table.d.ts +14 -8
  238. package/dist/template-resolver/web/client-plugin.d.ts +1 -0
  239. package/dist/template-resolver/web/client-plugin.js +9 -6
  240. package/dist/template-resolver/web/document-editor-extensions.d.ts +15 -0
  241. package/dist/template-resolver/web/document-editor-extensions.js +107 -0
  242. package/dist/template-resolver/web/document-editor.d.ts +3 -0
  243. package/dist/template-resolver/web/document-editor.js +11 -0
  244. package/dist/template-resolver/web/document-tiptap-editor.d.ts +3 -0
  245. package/dist/template-resolver/web/document-tiptap-editor.js +376 -0
  246. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +33 -0
  247. package/dist/template-resolver/web/document-toolbar-blocks.js +15 -0
  248. package/dist/template-resolver/web/editor-handle.d.ts +3 -0
  249. package/dist/template-resolver/web/editor-handle.js +24 -0
  250. package/dist/template-resolver/web/i18n.js +76 -0
  251. package/dist/template-resolver/web/index.d.ts +4 -0
  252. package/dist/template-resolver/web/index.js +4 -0
  253. package/dist/template-resolver/web/placeholder-line-editor-lazy.d.ts +4 -0
  254. package/dist/template-resolver/web/placeholder-line-editor-lazy.js +16 -0
  255. package/dist/template-resolver/web/placeholder-line-editor.d.ts +15 -0
  256. package/dist/template-resolver/web/placeholder-line-editor.js +94 -0
  257. package/dist/template-resolver/web/placeholder-text.d.ts +4 -0
  258. package/dist/template-resolver/web/placeholder-text.js +34 -0
  259. package/dist/template-resolver/web/use-stable-by-key.d.ts +1 -0
  260. package/dist/template-resolver/web/use-stable-by-key.js +13 -0
  261. package/dist/tenant/handlers/team-list.query.js +3 -2
  262. package/dist/tenant/seeding.js +31 -6
  263. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  264. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +2 -1
  265. package/dist/user/db/queries/stream-tenant-backfill.js +4 -4
  266. package/dist/user/handlers/detail.query.js +2 -1
  267. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  268. package/dist/user-data-rights/email-templates.d.ts +1 -29
  269. package/dist/user-data-rights/email-templates.js +5 -106
  270. package/dist/user-data-rights/feature.js +53 -35
  271. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  272. package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
  273. package/dist/user-data-rights/handlers/request-deletion.write.js +5 -7
  274. package/dist/user-data-rights/lib/default-mailers.d.ts +15 -7
  275. package/dist/user-data-rights/lib/default-mailers.js +100 -48
  276. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  277. package/dist/user-data-rights/mail-notifications.d.ts +13 -0
  278. package/dist/user-data-rights/mail-notifications.js +118 -0
  279. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  280. package/dist/user-profile/web/profile-screen.js +2 -1
  281. package/dist/waitlist/feature.js +10 -0
  282. package/dist/waitlist/handlers/submit.write.js +3 -4
  283. package/dist/waitlist/mail.d.ts +19 -6
  284. package/dist/waitlist/mail.js +50 -14
  285. package/package.json +28 -17
  286. package/src/admin-shell/changes.json +6 -0
  287. package/src/auth-email-password/changes.json +14 -0
  288. package/src/billing-foundation/changes.json +7 -0
  289. package/src/channel-email/changes.json +19 -0
  290. package/src/data-retention/changes.json +6 -0
  291. package/src/delivery/changes.json +48 -0
  292. package/src/managed-pages/changes.json +6 -0
  293. package/src/renderer-foundation/changes.json +39 -0
  294. package/src/template-resolver/changes.json +118 -0
  295. package/src/user-data-rights/changes.json +7 -0
  296. package/src/waitlist/changes.json +7 -0
  297. package/dist/renderer-simple/feature.d.ts +0 -10
  298. package/dist/renderer-simple/feature.js +0 -42
  299. package/dist/renderer-simple/index.d.ts +0 -3
  300. package/dist/renderer-simple/index.js +0 -2
  301. package/dist/renderer-simple/mail-blocks.d.ts +0 -21
  302. package/dist/renderer-simple/mail-blocks.js +0 -107
  303. package/dist/renderer-simple/resolve-variables.d.ts +0 -7
  304. package/dist/renderer-simple/resolve-variables.js +0 -47
  305. package/dist/renderer-simple/simple-renderer.d.ts +0 -24
  306. package/dist/renderer-simple/simple-renderer.js +0 -200
  307. package/src/renderer-simple/changes.json +0 -27
@@ -43,6 +43,7 @@ import { createEventStoreExecutor, createTenantDb, nullBlindIndexesForSubject, W
43
43
  import { deleteStoredFileAndDerivatives } from "@cosmicdrift/kumiko-framework/derivatives";
44
44
  import { createSystemUser, entityHasAnonymizableSubjectField, } from "@cosmicdrift/kumiko-framework/engine";
45
45
  import { fileRefEntity, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
46
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
46
47
  import { runInSubTransaction } from "../shared/index.js";
47
48
  import { computeCutoff } from "./keep-for.js";
48
49
  import { resolveRetentionPolicyForTenant } from "./resolve-for-tenant.js";
@@ -219,10 +220,9 @@ function collectSingleFileHolders(registry) {
219
220
  const { singleFields } = planFileFields(entity.fields);
220
221
  if (singleFields.length === 0)
221
222
  continue;
222
- const table = proj.table; // @cast-boundary column-presence probe
223
223
  holders.push({
224
224
  table: proj.table,
225
- tableHasTenantId: table["tenantId"] !== undefined,
225
+ tableHasTenantId: propertyOf(proj.table, "tenantId") !== undefined,
226
226
  singleFields,
227
227
  });
228
228
  }
@@ -473,10 +473,9 @@ export async function runRetentionCleanup(args) {
473
473
  const entity = registry.getEntity(entityName);
474
474
  if (!entity)
475
475
  continue;
476
- const table = proj.table; // @cast-boundary column-presence probe
477
476
  const declaredReference = policy.reference ?? DEFAULT_REFERENCE_FIELD;
478
477
  const referenceField = FRAMEWORK_REFERENCE_ALIAS[declaredReference] ?? declaredReference;
479
- if (table[referenceField] === undefined) {
478
+ if (propertyOf(proj.table, referenceField) === undefined) {
480
479
  skipped.push({ entityName, reason: "missing_reference_column" });
481
480
  continue;
482
481
  }
@@ -485,7 +484,7 @@ export async function runRetentionCleanup(args) {
485
484
  // Tenant-Scope nur wenn die Tabelle eine tenantId-Spalte hat — identisch zu
486
485
  // soft-delete-cleanup. Ohne diesen Filter wuerde ein Tenant die Rows eines
487
486
  // anderen treffen.
488
- if (table["tenantId"] !== undefined) {
487
+ if (propertyOf(proj.table, "tenantId") !== undefined) {
489
488
  where["tenantId"] = tenantId;
490
489
  }
491
490
  switch (policy.strategy) {
@@ -531,7 +530,8 @@ export async function runRetentionCleanup(args) {
531
530
  break;
532
531
  }
533
532
  case "softDelete": {
534
- if (table["isDeleted"] === undefined || table["deletedAt"] === undefined) {
533
+ if (propertyOf(proj.table, "isDeleted") === undefined ||
534
+ propertyOf(proj.table, "deletedAt") === undefined) {
535
535
  skipped.push({ entityName, reason: "missing_softdelete_columns" });
536
536
  break;
537
537
  }
@@ -0,0 +1,39 @@
1
+ import * as z from "zod";
2
+ import type { FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef } from "../renderer-foundation/index.js";
3
+ export type { FileAttachmentRef, InlineAttachmentRef, RenderedPdfAttachmentRef };
4
+ export type EmailAttachmentRef = InlineAttachmentRef | FileAttachmentRef | RenderedPdfAttachmentRef;
5
+ export declare const MAX_EMAIL_ATTACHMENTS = 20;
6
+ export declare const MAX_RENDERED_PDF_VARIABLES_BYTES = 64000;
7
+ export declare const inlineAttachmentRefSchema: z.ZodObject<{
8
+ kind: z.ZodLiteral<"inline">;
9
+ cid: z.ZodString;
10
+ fileRefId: z.ZodString;
11
+ }, z.core.$strict>;
12
+ export declare const fileAttachmentRefSchema: z.ZodObject<{
13
+ kind: z.ZodLiteral<"file">;
14
+ fileRefId: z.ZodString;
15
+ fileName: z.ZodString;
16
+ }, z.core.$strict>;
17
+ export declare const renderedPdfAttachmentRefSchema: z.ZodObject<{
18
+ kind: z.ZodLiteral<"rendered-pdf">;
19
+ templateSlug: z.ZodString;
20
+ variables: z.ZodRecord<z.ZodString, z.ZodUnknown>;
21
+ fileName: z.ZodString;
22
+ locale: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strict>;
24
+ export declare const emailAttachmentRefSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
25
+ kind: z.ZodLiteral<"inline">;
26
+ cid: z.ZodString;
27
+ fileRefId: z.ZodString;
28
+ }, z.core.$strict>, z.ZodObject<{
29
+ kind: z.ZodLiteral<"file">;
30
+ fileRefId: z.ZodString;
31
+ fileName: z.ZodString;
32
+ }, z.core.$strict>, z.ZodObject<{
33
+ kind: z.ZodLiteral<"rendered-pdf">;
34
+ templateSlug: z.ZodString;
35
+ variables: z.ZodRecord<z.ZodString, z.ZodUnknown>;
36
+ fileName: z.ZodString;
37
+ locale: z.ZodOptional<z.ZodString>;
38
+ }, z.core.$strict>], "kind">;
39
+ export declare function toAttachmentRef(ref: EmailAttachmentRef): EmailAttachmentRef;
@@ -0,0 +1,64 @@
1
+ import * as z from "zod";
2
+ import { attachmentFileNameSchema } from "../shared/index.js";
3
+ import { isDocumentResourceKey, templateSlugSchema } from "../template-resolver/index.js";
4
+ export const MAX_EMAIL_ATTACHMENTS = 20;
5
+ export const MAX_RENDERED_PDF_VARIABLES_BYTES = 64_000;
6
+ const MAX_ID_LENGTH = 64;
7
+ const fileRefIdSchema = z.string().min(1).max(MAX_ID_LENGTH);
8
+ export const inlineAttachmentRefSchema = z
9
+ .object({
10
+ kind: z.literal("inline"),
11
+ cid: z.string().min(1).max(MAX_ID_LENGTH).refine(isDocumentResourceKey),
12
+ fileRefId: fileRefIdSchema,
13
+ })
14
+ .strict();
15
+ export const fileAttachmentRefSchema = z
16
+ .object({
17
+ kind: z.literal("file"),
18
+ fileRefId: fileRefIdSchema,
19
+ fileName: attachmentFileNameSchema,
20
+ })
21
+ .strict();
22
+ export const renderedPdfAttachmentRefSchema = z
23
+ .object({
24
+ kind: z.literal("rendered-pdf"),
25
+ templateSlug: templateSlugSchema,
26
+ variables: z
27
+ .record(z.string(), z.unknown())
28
+ .refine((value) => Buffer.byteLength(JSON.stringify(value)) <= MAX_RENDERED_PDF_VARIABLES_BYTES, {
29
+ message: `variables must not exceed ${MAX_RENDERED_PDF_VARIABLES_BYTES} bytes as JSON`,
30
+ }),
31
+ fileName: attachmentFileNameSchema.refine((name) => name.toLowerCase().endsWith(".pdf"), {
32
+ message: "file name must end with .pdf",
33
+ }),
34
+ locale: z
35
+ .string()
36
+ .min(2)
37
+ .max(8)
38
+ .regex(/^[a-z]{2}(-[a-z]{2})?$/i, "locale must be ISO 639-1 (e.g. de, en, en-us)")
39
+ .optional(),
40
+ })
41
+ .strict();
42
+ export const emailAttachmentRefSchema = z.discriminatedUnion("kind", [
43
+ inlineAttachmentRefSchema,
44
+ fileAttachmentRefSchema,
45
+ renderedPdfAttachmentRefSchema,
46
+ ]);
47
+ // Rebuilds only the contract fields so bytes a caller attached to a ref object
48
+ // can never ride into a job payload.
49
+ export function toAttachmentRef(ref) {
50
+ switch (ref.kind) {
51
+ case "inline":
52
+ return { kind: "inline", cid: ref.cid, fileRefId: ref.fileRefId };
53
+ case "file":
54
+ return { kind: "file", fileRefId: ref.fileRefId, fileName: ref.fileName };
55
+ case "rendered-pdf":
56
+ return {
57
+ kind: "rendered-pdf",
58
+ templateSlug: ref.templateSlug,
59
+ variables: ref.variables,
60
+ fileName: ref.fileName,
61
+ ...(ref.locale !== undefined && { locale: ref.locale }),
62
+ };
63
+ }
64
+ }
@@ -1,6 +1,10 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
- import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
- import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type DbConnection, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { ConfigAccessor, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
5
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
5
- import type { ChannelContext } from "./types.js";
6
- export declare function buildChannelContext(db: DbConnection, registry: Registry, sseBroker: SseBroker | undefined, tenantId: TenantId, secrets?: SecretsContext): ChannelContext;
6
+ import { type MailPayload } from "../renderer-foundation/index.js";
7
+ import type { ChannelContext, RenderedMail } from "./types.js";
8
+ export declare function createTemplateMailRenderer(db: DbConnection | TenantDb, registry: Registry, tenantId: TenantId, config: ConfigAccessor | undefined): (payload: MailPayload) => Promise<RenderedMail>;
9
+ export declare function createEmailAttachmentReader(db: DbConnection | TenantDb, registry: Registry, tenantId: TenantId, config: ConfigAccessor | undefined, filesForTenant: (tenantId: string) => FileContext | undefined): NonNullable<ChannelContext["readAttachment"]>;
10
+ export declare function buildChannelContext(db: DbConnection, registry: Registry, sseBroker: SseBroker | undefined, tenantId: TenantId, secrets?: SecretsContext, tenantConfig?: ConfigAccessor, filesForTenant?: (tenantId: string) => FileContext | undefined): ChannelContext;
@@ -1,8 +1,82 @@
1
1
  import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { readFileRefContent } from "../file-foundation/index.js";
4
+ import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
5
+ import { FALLBACK_LOCALE } from "../template-resolver/index.js";
6
+ export function createTemplateMailRenderer(db, registry, tenantId, config) {
7
+ return async (payload) => {
8
+ const plugin = createRendererFoundationApi(collectRendererPlugins(registry)).createRendererForTenant({ tenantId, kind: "mail" });
9
+ const response = await plugin.render({ kind: "mail", payload }, { db, registry, tenantId, ...(config !== undefined && { config }) });
10
+ if (response.kind !== "mail") {
11
+ throw new RendererError(`[delivery] renderer "${plugin.name}" answered a mail request with kind "${response.kind}"`, "other");
12
+ }
13
+ return {
14
+ subject: response.subject,
15
+ html: response.html,
16
+ text: response.text,
17
+ ...(response.previewText !== undefined && { previewText: response.previewText }),
18
+ ...(response.attachments !== undefined && { attachments: response.attachments }),
19
+ };
20
+ };
21
+ }
22
+ const INLINE_IMAGE_CONTENT_TYPE_PREFIX = "image/";
23
+ const PDF_CONTENT_TYPE = "application/pdf";
24
+ // Renders at send time so the job payload carries variables, not bytes. A failure throws:
25
+ // the send job retries instead of mailing without the document the recipient expects.
26
+ function createPdfRenderer(db, registry, tenantId, config, filesForTenant) {
27
+ return async (ref) => {
28
+ const plugin = createRendererFoundationApi(collectRendererPlugins(registry)).createRendererForTenant({ tenantId, kind: "document-pdf" });
29
+ const response = await plugin.render({
30
+ kind: "document-pdf",
31
+ payload: {
32
+ slug: ref.templateSlug,
33
+ locale: ref.locale ?? FALLBACK_LOCALE,
34
+ variables: ref.variables,
35
+ },
36
+ }, { db, registry, tenantId, filesForTenant, ...(config !== undefined && { config }) });
37
+ if (response.kind !== "document-pdf") {
38
+ throw new RendererError(`[delivery] renderer "${plugin.name}" answered a document-pdf request with kind "${response.kind}"`, "other");
39
+ }
40
+ return { content: response.pdfBytes, contentType: PDF_CONTENT_TYPE };
41
+ };
42
+ }
43
+ // Inline images and file attachments the renderer took from a template may come from the system
44
+ // tenant (system template or tenant override of one). A file ref from notify data stays limited to
45
+ // the job's own tenant, since a caller picks it. Inline content that is not an image is dropped, so
46
+ // a swapped fileRef id cannot smuggle a document into the mail body.
47
+ export function createEmailAttachmentReader(db, registry, tenantId, config, filesForTenant) {
48
+ const renderPdf = createPdfRenderer(db, registry, tenantId, config, filesForTenant);
49
+ return async (ref, origin = "message") => {
50
+ if (ref.kind === "rendered-pdf")
51
+ return renderPdf(ref);
52
+ const found = await readFileRefContent({
53
+ db,
54
+ filesForTenant,
55
+ fileRefId: ref.fileRefId,
56
+ allowedTenantIds: ref.kind === "file" && origin === "message" ? [tenantId] : [tenantId, SYSTEM_TENANT_ID],
57
+ });
58
+ if (found === null)
59
+ return null;
60
+ if (ref.kind === "inline" && !found.contentType.startsWith(INLINE_IMAGE_CONTENT_TYPE_PREFIX)) {
61
+ return null;
62
+ }
63
+ return { content: found.content, contentType: found.contentType };
64
+ };
65
+ }
2
66
  // Build the per-tenant context a channel's resolve/render/send receives.
3
67
  // Shared by the synchronous delivery-service path and the async job handlers
4
68
  // (which pass sseBroker=undefined — only the inline inApp channel uses SSE,
5
69
  // and it never runs through a job).
6
- export function buildChannelContext(db, registry, sseBroker, tenantId, secrets) {
7
- return { db: createTenantDb(db, tenantId), registry, sseBroker, tenantId, secrets };
70
+ export function buildChannelContext(db, registry, sseBroker, tenantId, secrets, tenantConfig, filesForTenant) {
71
+ return {
72
+ db: createTenantDb(db, tenantId),
73
+ registry,
74
+ sseBroker,
75
+ tenantId,
76
+ secrets,
77
+ renderMail: createTemplateMailRenderer(db, registry, tenantId, tenantConfig),
78
+ ...(filesForTenant !== undefined && {
79
+ readAttachment: createEmailAttachmentReader(db, registry, tenantId, tenantConfig, filesForTenant),
80
+ }),
81
+ };
8
82
  }
@@ -1,5 +1,6 @@
1
1
  import type { NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const DELIVERY_CHANNEL_EXTENSION: "deliveryChannel";
3
+ export declare const EMAIL_CHANNEL_NAME: "email";
3
4
  export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
4
5
  export declare function deliveryIdempotencyRedisKey(tenantId: string, key: string): string;
5
6
  export declare const deliveryPriorityRank: Record<NotifyPriority, number>;
@@ -3,6 +3,7 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
3
3
  // Extension-point name for delivery-channel plugins (channel-email,
4
4
  // channel-in-app, channel-push, ...).
5
5
  export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel";
6
+ export const EMAIL_CHANNEL_NAME = "email";
6
7
  export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
7
8
  // Single source for claim (delivery-service) and terminal-failure release (jobs).
8
9
  export function deliveryIdempotencyRedisKey(tenantId, key) {
@@ -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, NotifyJobDispatcher, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { ConfigAccessor, EscapeHatchAuditSink, NotifyJobDispatcher, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
5
  import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
5
6
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
6
7
  import type { Redis } from "ioredis";
@@ -23,6 +24,8 @@ export type DeliveryServiceOptions = {
23
24
  readonly idempotencyRedis?: Redis;
24
25
  readonly jobRunner?: NotifyJobDispatcher;
25
26
  readonly secrets?: SecretsContext;
27
+ readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
28
+ readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
26
29
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
27
30
  readonly log?: Logger;
28
31
  };
@@ -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",
@@ -1,13 +1,16 @@
1
+ export { sanitizePdfAttachmentFileName } from "../shared/index.js";
1
2
  export { hashUnsubscribeAddress } from "./address-opt-out.js";
3
+ export { type EmailAttachmentRef, emailAttachmentRefSchema, type FileAttachmentRef, fileAttachmentRefSchema, type InlineAttachmentRef, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, type RenderedPdfAttachmentRef, renderedPdfAttachmentRefSchema, toAttachmentRef, } 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 { emailAttachmentRefSchema, fileAttachmentRefSchema, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, renderedPdfAttachmentRefSchema, toAttachmentRef, } 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";