@cosmicdrift/kumiko-bundled-features 0.363.0 → 0.365.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 (361) hide show
  1. package/dist/audit/feature.js +1 -1
  2. package/dist/audit/handlers/list.query.js +9 -5
  3. package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
  4. package/dist/audit/run-escape-hatch-retention.js +39 -12
  5. package/dist/auth-email-password/bootstrap.d.ts +9 -8
  6. package/dist/auth-email-password/bootstrap.js +12 -4
  7. package/dist/auth-email-password/email-rate-limits.d.ts +4 -0
  8. package/dist/auth-email-password/email-rate-limits.js +13 -0
  9. package/dist/auth-email-password/feature.js +2 -0
  10. package/dist/auth-email-password/handlers/change-password.write.d.ts +1 -1
  11. package/dist/auth-email-password/handlers/change-password.write.js +6 -2
  12. package/dist/auth-email-password/handlers/confirm-account-unlock.write.d.ts +1 -1
  13. package/dist/auth-email-password/handlers/invite-accept-with-login.write.d.ts +1 -1
  14. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -3
  15. package/dist/auth-email-password/handlers/invite-accept.write.d.ts +1 -1
  16. package/dist/auth-email-password/handlers/invite-create.write.d.ts +4 -5
  17. package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
  18. package/dist/auth-email-password/handlers/invite-signup-complete.write.d.ts +1 -1
  19. package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
  20. package/dist/auth-email-password/handlers/logout.write.d.ts +1 -1
  21. package/dist/auth-email-password/handlers/logout.write.js +6 -2
  22. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -1
  23. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -1
  24. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -1
  25. package/dist/auth-email-password/handlers/reset-password.write.d.ts +1 -1
  26. package/dist/auth-email-password/handlers/signup-confirm.write.d.ts +1 -1
  27. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -1
  28. package/dist/auth-email-password/handlers/signup-request.write.js +2 -1
  29. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +2 -1
  30. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -1
  31. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
  32. package/dist/auth-email-password/handlers/token-request-handler.js +2 -1
  33. package/dist/auth-email-password/handlers/verify-email.write.d.ts +1 -1
  34. package/dist/auth-email-password/magic-link-mail.js +6 -3
  35. package/dist/auth-email-password/mail-i18n.d.ts +3 -0
  36. package/dist/auth-email-password/mail-i18n.js +10 -0
  37. package/dist/auth-email-password/web/auth-gate.js +16 -4
  38. package/dist/auth-mfa/handlers/disable.write.d.ts +1 -1
  39. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.d.ts +1 -1
  40. package/dist/auth-mfa/handlers/enable-confirm.write.d.ts +1 -1
  41. package/dist/auth-mfa/handlers/enable-start-preauth.write.d.ts +1 -1
  42. package/dist/auth-mfa/handlers/enable-start.write.d.ts +1 -1
  43. package/dist/auth-mfa/handlers/regenerate-recovery.write.d.ts +1 -1
  44. package/dist/auth-mfa/handlers/verify.write.d.ts +1 -1
  45. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +52 -5
  46. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -0
  47. package/dist/billing-foundation/consumer-protection/consent-gate.js +21 -2
  48. package/dist/billing-foundation/consumer-protection/mail-notifications.js +1 -1
  49. package/dist/billing-foundation/consumer-protection/register-termination.js +11 -1
  50. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +5 -3
  51. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -2
  52. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +6 -0
  53. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +34 -0
  54. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +2 -0
  55. package/dist/billing-foundation/consumer-protection/termination-record.js +15 -2
  56. package/dist/billing-foundation/consumer-protection/termination-request.js +30 -20
  57. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  58. package/dist/billing-foundation/consumer-protection/termination-texts.js +6 -0
  59. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +10 -0
  60. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +26 -0
  61. package/dist/billing-foundation/handlers/create-checkout-session.write.js +11 -5
  62. package/dist/billing-foundation/handlers/process-event.write.js +10 -6
  63. package/dist/billing-foundation/i18n.js +9 -0
  64. package/dist/billing-foundation/subscription-tier-sync.d.ts +7 -1
  65. package/dist/billing-foundation/subscription-tier-sync.js +21 -7
  66. package/dist/billing-foundation/types.d.ts +7 -2
  67. package/dist/cap-counter/book-cap-usage.js +8 -0
  68. package/dist/cap-counter/calendar-cap-usage-reservation.js +2 -0
  69. package/dist/cap-counter/cap-reservation.js +7 -3
  70. package/dist/cap-counter/enforce-cap.d.ts +18 -1
  71. package/dist/cap-counter/enforce-cap.js +49 -6
  72. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +1 -0
  73. package/dist/cap-counter/handlers/increment-rolling.write.js +2 -0
  74. package/dist/cap-counter/index.d.ts +1 -1
  75. package/dist/cap-counter/index.js +1 -1
  76. package/dist/cap-counter/with-cap-enforcement.d.ts +2 -0
  77. package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -1
  78. package/dist/cap-overview/handlers/tenant-options.query.js +3 -1
  79. package/dist/channel-discord/discord-channel.js +3 -2
  80. package/dist/channel-email/email-channel.js +40 -23
  81. package/dist/channel-email/smtp-transport.js +6 -1
  82. package/dist/channel-in-app/handlers/mark-all-read.write.d.ts +1 -1
  83. package/dist/channel-in-app/handlers/mark-read.write.d.ts +1 -1
  84. package/dist/channel-slack/slack-channel.js +3 -2
  85. package/dist/channel-teams/teams-channel.js +3 -2
  86. package/dist/compliance-profiles/handlers/set-profile.write.d.ts +1 -1
  87. package/dist/config/handlers/reset.write.d.ts +1 -1
  88. package/dist/config/handlers/set.write.d.ts +1 -1
  89. package/dist/config/handlers/set.write.js +2 -2
  90. package/dist/config/resolver.d.ts +1 -0
  91. package/dist/config/resolver.js +11 -14
  92. package/dist/config/write-helpers.d.ts +2 -0
  93. package/dist/config/write-helpers.js +19 -0
  94. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
  95. package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -20
  96. package/dist/custom-fields/db/queries/retention.d.ts +1 -1
  97. package/dist/custom-fields/db/queries/retention.js +2 -1
  98. package/dist/custom-fields/handlers/update-tenant-field.write.js +0 -1
  99. package/dist/data-retention/run-retention-cleanup.js +4 -5
  100. package/dist/delivery/attachment-ref.d.ts +2 -0
  101. package/dist/delivery/attachment-ref.js +30 -1
  102. package/dist/delivery/channel-context.js +19 -1
  103. package/dist/delivery/delivery-service.js +71 -82
  104. package/dist/delivery/handlers/resubscribe-address.write.d.ts +1 -1
  105. package/dist/delivery/handlers/resubscribe-user.write.d.ts +1 -1
  106. package/dist/delivery/handlers/set-preference.write.d.ts +1 -1
  107. package/dist/delivery/handlers/unsubscribe-address.write.d.ts +1 -1
  108. package/dist/delivery/handlers/unsubscribe-user.write.d.ts +1 -1
  109. package/dist/delivery/index.d.ts +1 -1
  110. package/dist/delivery/index.js +1 -1
  111. package/dist/delivery/jobs.js +43 -11
  112. package/dist/delivery/types.d.ts +7 -1
  113. package/dist/feature-toggles/handlers/set.write.d.ts +1 -1
  114. package/dist/feature-toggles/toggle-runtime.d.ts +5 -3
  115. package/dist/feature-toggles/toggle-runtime.js +25 -12
  116. package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
  117. package/dist/file-foundation/read-file-ref-content.d.ts +1 -1
  118. package/dist/file-foundation/read-file-ref-content.js +6 -4
  119. package/dist/files-tenant-data/hooks.js +4 -0
  120. package/dist/files-tenant-data/index.d.ts +1 -0
  121. package/dist/files-tenant-data/index.js +2 -1
  122. package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
  123. package/dist/form-draft/db/queries/owned-file-refs.js +9 -2
  124. package/dist/form-draft/handlers/discard.write.d.ts +1 -1
  125. package/dist/form-draft/handlers/discard.write.js +1 -1
  126. package/dist/form-draft/handlers/save.write.d.ts +1 -1
  127. package/dist/inbound-mail-foundation/oauth-access-token.js +8 -3
  128. package/dist/inbound-mail-foundation/watch-supervisor.js +3 -1
  129. package/dist/jobs/db/queries/stale-run-sweep.d.ts +1 -1
  130. package/dist/jobs/db/queries/stale-run-sweep.js +12 -5
  131. package/dist/jobs/feature.js +5 -3
  132. package/dist/jobs/handlers/list.query.js +3 -1
  133. package/dist/jobs/handlers/retry.write.d.ts +1 -1
  134. package/dist/jobs/handlers/stale-run-sweep.job.d.ts +1 -1
  135. package/dist/jobs/handlers/stale-run-sweep.job.js +2 -2
  136. package/dist/jobs/handlers/trigger.write.d.ts +1 -1
  137. package/dist/jobs/job-run-logger.js +35 -10
  138. package/dist/jobs/tenant-job-run-table.js +3 -2
  139. package/dist/ledger/entity.js +3 -1
  140. package/dist/managed-pages/handlers/set.write.d.ts +1 -1
  141. package/dist/metrics/filters.d.ts +16 -1
  142. package/dist/metrics/filters.js +27 -11
  143. package/dist/personal-access-tokens/handlers/create.write.d.ts +1 -1
  144. package/dist/personal-access-tokens/handlers/revoke.write.d.ts +1 -1
  145. package/dist/personal-access-tokens/revoke-for-user.js +3 -7
  146. package/dist/renderer-foundation/types.d.ts +2 -0
  147. package/dist/renderer-pdf/render-table-pdf.d.ts +27 -33
  148. package/dist/renderer-pdf/render-table-pdf.js +95 -34
  149. package/dist/secrets/feature.js +4 -3
  150. package/dist/secrets/handlers/delete.write.d.ts +1 -1
  151. package/dist/secrets/handlers/delete.write.js +3 -1
  152. package/dist/secrets/handlers/set.write.d.ts +1 -1
  153. package/dist/secrets/secrets-context.js +15 -3
  154. package/dist/secrets/write-gate.d.ts +3 -1
  155. package/dist/secrets/write-gate.js +6 -3
  156. package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -1
  157. package/dist/sessions/handlers/revoke-all-others.write.d.ts +1 -1
  158. package/dist/sessions/handlers/revoke.write.d.ts +1 -1
  159. package/dist/shared/chunked-entity-migration.d.ts +1 -0
  160. package/dist/shared/chunked-entity-migration.js +1 -1
  161. package/dist/shared/index.d.ts +1 -0
  162. package/dist/shared/index.js +1 -0
  163. package/dist/shared/mail-template-body.d.ts +3 -0
  164. package/dist/shared/mail-template-body.js +10 -0
  165. package/dist/shared/resource-image-formats.d.ts +23 -0
  166. package/dist/shared/resource-image-formats.js +21 -0
  167. package/dist/shared/sharp-input-limits.js +7 -8
  168. package/dist/step-dispatcher/webhook-runner.js +2 -2
  169. package/dist/subscription-stripe/plugin-methods.js +18 -8
  170. package/dist/subscription-stripe/verify-webhook.d.ts +1 -1
  171. package/dist/template-resolver/api.js +3 -2
  172. package/dist/template-resolver/compile-document-body.d.ts +7 -0
  173. package/dist/template-resolver/compile-document-body.js +114 -21
  174. package/dist/template-resolver/compile-steps-block.d.ts +10 -0
  175. package/dist/template-resolver/compile-steps-block.js +45 -0
  176. package/dist/template-resolver/constants.d.ts +6 -3
  177. package/dist/template-resolver/constants.js +13 -2
  178. package/dist/template-resolver/document-sanitizer.js +3 -0
  179. package/dist/template-resolver/document-schema.d.ts +3 -2
  180. package/dist/template-resolver/document-schema.js +29 -4
  181. package/dist/template-resolver/document-steps.d.ts +17 -0
  182. package/dist/template-resolver/document-steps.js +93 -0
  183. package/dist/template-resolver/feature.d.ts +2 -0
  184. package/dist/template-resolver/feature.js +13 -1
  185. package/dist/template-resolver/handlers/bundle-import.d.ts +7 -1
  186. package/dist/template-resolver/handlers/bundle-import.js +63 -38
  187. package/dist/template-resolver/handlers/collection-set.write.d.ts +1 -1
  188. package/dist/template-resolver/handlers/collection-set.write.js +4 -3
  189. package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
  190. package/dist/template-resolver/handlers/copy-locale.write.js +95 -49
  191. package/dist/template-resolver/handlers/diff-to-system.query.js +5 -38
  192. package/dist/template-resolver/handlers/discard-draft.write.d.ts +13 -0
  193. package/dist/template-resolver/handlers/discard-draft.write.js +37 -0
  194. package/dist/template-resolver/handlers/export-bundle.query.js +51 -1
  195. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
  196. package/dist/template-resolver/handlers/find-by-id.query.js +6 -3
  197. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
  198. package/dist/template-resolver/handlers/import-bundle-system.write.js +16 -5
  199. package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
  200. package/dist/template-resolver/handlers/import-bundle.write.js +11 -1
  201. package/dist/template-resolver/handlers/layout-source.query.d.ts +5 -0
  202. package/dist/template-resolver/handlers/layout-source.query.js +46 -23
  203. package/dist/template-resolver/handlers/list.query.js +4 -1
  204. package/dist/template-resolver/handlers/override-diff.d.ts +20 -0
  205. package/dist/template-resolver/handlers/override-diff.js +41 -0
  206. package/dist/template-resolver/handlers/overview.query.d.ts +9 -0
  207. package/dist/template-resolver/handlers/overview.query.js +60 -12
  208. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +1 -1
  209. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +1 -1
  210. package/dist/template-resolver/handlers/resource-remove.write.d.ts +1 -1
  211. package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -2
  212. package/dist/template-resolver/handlers/resource-upload.write.js +36 -6
  213. package/dist/template-resolver/handlers/send-test.write.d.ts +3 -1
  214. package/dist/template-resolver/handlers/send-test.write.js +37 -10
  215. package/dist/template-resolver/handlers/set.write.d.ts +1 -1
  216. package/dist/template-resolver/handlers/template-gates.js +13 -2
  217. package/dist/template-resolver/handlers/template-write.js +36 -4
  218. package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +32 -0
  219. package/dist/template-resolver/handlers/tenant-overrides.query.js +80 -16
  220. package/dist/template-resolver/handlers/toggle-status.write.d.ts +17 -4
  221. package/dist/template-resolver/handlers/toggle-status.write.js +35 -19
  222. package/dist/template-resolver/handlers/upsert-system.write.d.ts +1 -1
  223. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +1 -1
  224. package/dist/template-resolver/handlers/upsert-tenant.write.js +1 -1
  225. package/dist/template-resolver/handlers/usage.query.js +11 -2
  226. package/dist/template-resolver/handlers/validate.query.js +4 -1
  227. package/dist/template-resolver/index.d.ts +5 -4
  228. package/dist/template-resolver/index.js +5 -4
  229. package/dist/template-resolver/layout-appearance.d.ts +10 -0
  230. package/dist/template-resolver/layout-appearance.js +36 -0
  231. package/dist/template-resolver/mail-constants.d.ts +2 -0
  232. package/dist/template-resolver/mail-constants.js +3 -0
  233. package/dist/template-resolver/pdf-constants.d.ts +7 -0
  234. package/dist/template-resolver/pdf-constants.js +8 -0
  235. package/dist/template-resolver/qualified-names.d.ts +4 -0
  236. package/dist/template-resolver/qualified-names.js +4 -0
  237. package/dist/template-resolver/resource-image.d.ts +2 -9
  238. package/dist/template-resolver/resource-image.js +2 -11
  239. package/dist/template-resolver/seeding.js +23 -4
  240. package/dist/template-resolver/store-template-resource.d.ts +53 -9
  241. package/dist/template-resolver/store-template-resource.js +250 -97
  242. package/dist/template-resolver/table.d.ts +103 -0
  243. package/dist/template-resolver/table.js +9 -2
  244. package/dist/template-resolver/template-bundle.js +4 -0
  245. package/dist/template-resolver/template-draft.d.ts +54 -0
  246. package/dist/template-resolver/template-draft.js +72 -0
  247. package/dist/template-resolver/template-meta.d.ts +35 -6
  248. package/dist/template-resolver/template-meta.js +67 -16
  249. package/dist/template-resolver/template-preview.d.ts +19 -2
  250. package/dist/template-resolver/template-preview.js +98 -55
  251. package/dist/template-resolver/template-resources.js +3 -4
  252. package/dist/template-resolver/template-usage.d.ts +5 -1
  253. package/dist/template-resolver/template-usage.js +14 -9
  254. package/dist/template-resolver/template-validation.js +13 -9
  255. package/dist/template-resolver/web/client-plugin.js +4 -2
  256. package/dist/template-resolver/web/document-editor-extensions.d.ts +5 -1
  257. package/dist/template-resolver/web/document-editor-extensions.js +76 -13
  258. package/dist/template-resolver/web/document-editor.d.ts +2 -1
  259. package/dist/template-resolver/web/document-editor.js +12 -2
  260. package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
  261. package/dist/template-resolver/web/document-tiptap-editor.js +173 -34
  262. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +13 -5
  263. package/dist/template-resolver/web/document-toolbar-blocks.js +6 -0
  264. package/dist/template-resolver/web/i18n.js +24 -4
  265. package/dist/template-resolver/web/index.d.ts +2 -0
  266. package/dist/template-resolver/web/index.js +2 -0
  267. package/dist/template-resolver/web/placeholder-line-editor.d.ts +6 -2
  268. package/dist/template-resolver/web/placeholder-line-editor.js +9 -7
  269. package/dist/tenant/handlers/add-member.write.d.ts +1 -1
  270. package/dist/tenant/handlers/cancel-invitation.write.d.ts +1 -1
  271. package/dist/tenant/handlers/create.write.d.ts +1 -1
  272. package/dist/tenant/handlers/remove-member.write.d.ts +1 -1
  273. package/dist/tenant/handlers/toggle-enabled.write.d.ts +2 -2
  274. package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
  275. package/dist/tenant/handlers/update-member-roles.write.js +3 -6
  276. package/dist/tenant/handlers/update.write.d.ts +1 -1
  277. package/dist/tenant/seeding.js +23 -24
  278. package/dist/tenant-handover/handlers/claim.write.d.ts +1 -1
  279. package/dist/tenant-handover/move-entity-graph.js +14 -4
  280. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
  281. package/dist/tenant-lifecycle/handlers/request-destruction.write.d.ts +1 -1
  282. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
  283. package/dist/tenant-lifecycle/run-tenant-destroy.js +8 -1
  284. package/dist/tenant-lifecycle/stages.js +3 -2
  285. package/dist/tier-engine/constants.d.ts +2 -0
  286. package/dist/tier-engine/constants.js +4 -0
  287. package/dist/tier-engine/entity.d.ts +15 -14
  288. package/dist/tier-engine/entity.js +4 -3
  289. package/dist/tier-engine/feature.js +9 -3
  290. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +6 -1
  291. package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
  292. package/dist/tier-engine/index.d.ts +1 -1
  293. package/dist/tier-engine/index.js +1 -1
  294. package/dist/tier-engine/tier-resolver.d.ts +13 -13
  295. package/dist/user/constants.d.ts +1 -0
  296. package/dist/user/handlers/create.write.d.ts +1 -1
  297. package/dist/user/handlers/update.write.d.ts +1 -1
  298. package/dist/user/invited-global-roles.d.ts +2 -4
  299. package/dist/user/resolve-display-names.js +5 -9
  300. package/dist/user/seeding.js +20 -1
  301. package/dist/user-data-rights/email-templates.js +0 -1
  302. package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
  303. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.d.ts +1 -1
  304. package/dist/user-data-rights/handlers/lift-restriction.write.d.ts +1 -1
  305. package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +1 -1
  306. package/dist/user-data-rights/handlers/request-deletion.write.d.ts +1 -1
  307. package/dist/user-data-rights/handlers/request-export.write.d.ts +1 -1
  308. package/dist/user-data-rights/handlers/restrict-account.write.d.ts +1 -1
  309. package/dist/user-data-rights/handlers/run-forget-cleanup.write.d.ts +1 -1
  310. package/dist/user-data-rights/i18n.js +5 -0
  311. package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +2 -0
  312. package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -2
  313. package/dist/user-data-rights/run-forget-cleanup.js +2 -2
  314. package/dist/user-profile/handlers/change-email.write.d.ts +1 -1
  315. package/dist/waitlist/feature.js +1 -1
  316. package/dist/waitlist/handlers/invite.write.d.ts +1 -1
  317. package/dist/waitlist/handlers/invite.write.js +21 -3
  318. package/dist/waitlist/handlers/reject.write.d.ts +1 -1
  319. package/dist/waitlist/handlers/reject.write.js +3 -16
  320. package/dist/waitlist/handlers/submit.write.d.ts +1 -1
  321. package/dist/waitlist/handlers/submit.write.js +29 -7
  322. package/dist/waitlist/i18n.js +9 -0
  323. package/dist/waitlist/lib.d.ts +2 -1
  324. package/dist/waitlist/lib.js +15 -0
  325. package/dist/waitlist/options.js +1 -1
  326. package/dist/workflow-runner/event-trigger.js +2 -2
  327. package/dist/workflow-runner/feature.js +1 -1
  328. package/dist/workflow-runner/handlers/resume-run.write.js +7 -2
  329. package/package.json +19 -10
  330. package/src/audit/changes.json +18 -0
  331. package/src/auth-email-password/changes.json +55 -0
  332. package/src/billing-foundation/changes.json +48 -0
  333. package/src/cap-counter/changes.json +19 -0
  334. package/src/cap-overview/changes.json +6 -0
  335. package/src/channel-email/changes.json +18 -0
  336. package/src/config/changes.json +6 -0
  337. package/src/crypto-shredding/changes.json +6 -0
  338. package/src/custom-fields/changes.json +6 -0
  339. package/src/delivery/changes.json +30 -0
  340. package/src/document-ingest-foundation/changes.json +6 -0
  341. package/src/feature-toggles/changes.json +7 -0
  342. package/src/file-foundation/changes.json +6 -0
  343. package/src/files-tenant-data/changes.json +6 -0
  344. package/src/inbound-mail-foundation/changes.json +6 -0
  345. package/src/jobs/changes.json +31 -0
  346. package/src/ledger/changes.json +7 -0
  347. package/src/metrics/changes.json +6 -0
  348. package/src/personal-access-tokens/changes.json +6 -0
  349. package/src/renderer-pdf/changes.json +12 -0
  350. package/src/secrets/changes.json +12 -0
  351. package/src/shared/changes.json +6 -0
  352. package/src/subscription-stripe/changes.json +6 -0
  353. package/src/template-resolver/changes.json +149 -0
  354. package/src/tenant/changes.json +6 -0
  355. package/src/tenant-lifecycle/changes.json +6 -0
  356. package/src/tier-engine/changes.json +18 -0
  357. package/src/user/changes.json +6 -0
  358. package/src/user-data-rights/changes.json +12 -0
  359. package/src/user-profile/changes.json +1 -1
  360. package/src/waitlist/changes.json +18 -0
  361. package/src/workflow-runner/changes.json +6 -0
@@ -79,11 +79,20 @@ export function createDeliveryService(options) {
79
79
  }
80
80
  // A key that guarded nothing must not turn the caller's retry into a skipped duplicate.
81
81
  // Partial success keeps it: a retry would re-send to those who got it.
82
- async function releaseIdempotencyIfNothingDelivered(tenantId, key, run) {
82
+ // A failing release is logged and swallowed: it must neither replace the dispatch error the
83
+ // caller retries on nor fail a notify() that already dispatched.
84
+ async function releaseIdempotencyIfNothingDelivered(tenantId, key, run, notificationType) {
83
85
  // skip: no key claimed, or a recipient already got the notification
84
86
  if (!key || run.deliveries.some(isDeliveredOrQueued))
85
87
  return;
86
- await idemRedis?.del(deliveryIdempotencyRedisKey(tenantId, key));
88
+ try {
89
+ await idemRedis?.del(deliveryIdempotencyRedisKey(tenantId, key));
90
+ }
91
+ catch (releaseErr) {
92
+ logError.warn(`idempotency key release failed: ${redactedMessageOf(releaseErr)}`, {
93
+ notificationType,
94
+ });
95
+ }
87
96
  }
88
97
  // Only a single-channel route lets the queued job free the key on terminal failure
89
98
  // without risking a duplicate send on another channel.
@@ -290,35 +299,50 @@ export function createDeliveryService(options) {
290
299
  return addressHash !== undefined && suppressed.has(addressOptOutKey(addressHash, channelName));
291
300
  }
292
301
  const recipientChannelKey = (userId, channelName) => `${userId}|${channelName}`;
293
- async function isChannelDeliverable(run, userId, tenantId, notificationType, channelName) {
294
- if (isChannelKilled && (await isChannelKilled(tenantId, channelName)))
295
- return false;
296
- return isChannelEnabled(run, userId, tenantId, notificationType, channelName);
302
+ // Kill switch, then the user's preference, then the rate limit — the order deliverToUser reports
303
+ // skips in. The rate limit consumes a token, so each recipient/channel pair is evaluated once.
304
+ async function evaluateChannelGate(run, userId, tenantId, notificationType, channelName, priority) {
305
+ if (isChannelKilled && (await isChannelKilled(tenantId, channelName))) {
306
+ return "channel_disabled";
307
+ }
308
+ // Critical priority skips the preference check
309
+ if (priority !== "critical" &&
310
+ !(await isChannelEnabled(run, userId, tenantId, notificationType, channelName))) {
311
+ return "preference_disabled";
312
+ }
313
+ if (rateLimit && !(await checkRateLimit(rateLimit, tenantId, channelName))) {
314
+ return "rate_limited";
315
+ }
316
+ return "open";
297
317
  }
298
318
  // Resolves every user's channel addresses up front so the opt-out lookup can
299
- // cover them all at once. A resolve that throws stays out of the map: the
300
- // delivery loop resolves it again and reports channel_error as before.
301
- // Channels the kill switch or the user's preferences disable are never resolved
302
- // here: resolve may have side effects and deliverToUser skips those channels.
303
- async function prefetchUserAddresses(run, userIds, tenantId, notificationType) {
319
+ // cover them all at once. A resolve that throws stays out of the address map:
320
+ // the delivery loop resolves it again and reports channel_error as before.
321
+ // Channels that are gated (kill switch, preferences, rate limit) are never resolved
322
+ // here: resolve may have side effects and deliverToUser skips those channels. The
323
+ // gate verdict is kept so deliverToUser does not query it a second time.
324
+ async function prefetchRecipients(run, userIds, tenantId, notificationType, priority) {
304
325
  const addresses = new Map();
326
+ const gates = new Map();
305
327
  const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
306
328
  for (const userId of userIds) {
307
329
  for (const channel of channels) {
308
330
  if (!channel.resolve)
309
331
  continue;
332
+ const key = recipientChannelKey(userId, channel.name);
310
333
  try {
311
- if (!(await isChannelDeliverable(run, userId, tenantId, notificationType, channel.name))) {
334
+ const gate = await evaluateChannelGate(run, userId, tenantId, notificationType, channel.name, priority);
335
+ gates.set(key, gate);
336
+ if (gate !== "open")
312
337
  continue;
313
- }
314
- addresses.set(recipientChannelKey(userId, channel.name), await channel.resolve(userId, channelCtx));
338
+ addresses.set(key, await channel.resolve(userId, channelCtx));
315
339
  }
316
340
  catch {
317
341
  // skip: re-resolved (and logged) by deliverToUser
318
342
  }
319
343
  }
320
344
  }
321
- return addresses;
345
+ return { addresses, gates };
322
346
  }
323
347
  // Check if user has disabled this notification+channel combo.
324
348
  // Specificity order: exact > any wildcard. When only wildcards match and they
@@ -341,68 +365,33 @@ export function createDeliveryService(options) {
341
365
  // and conservative — DB ordering no longer decides the outcome).
342
366
  return !prefs.some((p) => p.enabled === false);
343
367
  }
344
- async function deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed) {
368
+ async function deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetched, suppressed) {
345
369
  const channelCtx = buildChannelContext(run.db, registry, sseBroker, tenantId, secrets, tenantConfigFor?.(tenantId), run.filesForTenant);
346
370
  for (const channel of channels) {
347
371
  // Route-only channel (no per-user address): not a user-notification target.
348
372
  if (!channel.resolve)
349
373
  continue;
350
374
  const message = buildMessage(notificationType, data, channel.name, run.locale);
351
- // Kill switch: tenant admin disabled this channel entirely
352
- if (isChannelKilled) {
353
- const killed = await isChannelKilled(tenantId, channel.name);
354
- if (killed) {
355
- await logDelivery(run, {
356
- tenantId,
357
- notificationType,
358
- channel: channel.name,
359
- recipientId: userId,
360
- recipientAddress: null,
361
- status: "skipped",
362
- error: "channel_disabled",
363
- priority,
364
- });
365
- continue;
366
- }
367
- }
368
- // Check preferences (critical priority skips preference check)
369
- if (priority !== "critical") {
370
- const enabled = await isChannelEnabled(run, userId, tenantId, notificationType, channel.name);
371
- if (!enabled) {
372
- await logDelivery(run, {
373
- tenantId,
374
- notificationType,
375
- channel: channel.name,
376
- recipientId: userId,
377
- recipientAddress: null,
378
- status: "skipped",
379
- error: "preference_disabled",
380
- priority,
381
- });
382
- continue;
383
- }
384
- }
385
- // Rate limiting
386
- if (rateLimit) {
387
- const allowed = await checkRateLimit(rateLimit, tenantId, channel.name);
388
- if (!allowed) {
389
- await logDelivery(run, {
390
- tenantId,
391
- notificationType,
392
- channel: channel.name,
393
- recipientId: userId,
394
- recipientAddress: null,
395
- status: "skipped",
396
- error: "rate_limited",
397
- priority,
398
- });
399
- continue;
400
- }
375
+ const prefetchKey = recipientChannelKey(userId, channel.name);
376
+ const gate = prefetched.gates.get(prefetchKey) ??
377
+ (await evaluateChannelGate(run, userId, tenantId, notificationType, channel.name, priority));
378
+ if (gate !== "open") {
379
+ await logDelivery(run, {
380
+ tenantId,
381
+ notificationType,
382
+ channel: channel.name,
383
+ recipientId: userId,
384
+ recipientAddress: null,
385
+ status: "skipped",
386
+ error: gate,
387
+ priority,
388
+ });
389
+ continue;
401
390
  }
402
391
  try {
403
- const prefetchKey = recipientChannelKey(userId, channel.name);
404
- const address = prefetchedAddresses.has(prefetchKey)
405
- ? prefetchedAddresses.get(prefetchKey)
392
+ const wasPrefetched = prefetched.addresses.has(prefetchKey);
393
+ const address = wasPrefetched
394
+ ? prefetched.addresses.get(prefetchKey)
406
395
  : await channel.resolve(userId, channelCtx);
407
396
  if (!address) {
408
397
  await logDelivery(run, {
@@ -417,7 +406,12 @@ export function createDeliveryService(options) {
417
406
  });
418
407
  continue;
419
408
  }
420
- if (priority !== "critical" && isAddressSuppressed(suppressed, address, channel.name)) {
409
+ // A fallback resolve yields an address the batched opt-out lookup never covered.
410
+ const suppressedForAddress = wasPrefetched
411
+ ? suppressed
412
+ : await loadSuppressedAddresses(run, tenantId, notificationType, priority, [address]);
413
+ if (priority !== "critical" &&
414
+ isAddressSuppressed(suppressedForAddress, address, channel.name)) {
421
415
  await logDelivery(run, {
422
416
  tenantId,
423
417
  notificationType,
@@ -551,12 +545,12 @@ export function createDeliveryService(options) {
551
545
  else {
552
546
  userIds = to;
553
547
  }
554
- const prefetchedAddresses = priority === "critical"
555
- ? new Map()
556
- : await prefetchUserAddresses(run, userIds, tenantId, notificationType);
557
- const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
548
+ const prefetched = priority === "critical"
549
+ ? { addresses: new Map(), gates: new Map() }
550
+ : await prefetchRecipients(run, userIds, tenantId, notificationType, priority);
551
+ const suppressed = await loadSuppressedAddresses(run, tenantId, notificationType, priority, [...prefetched.addresses.values()].filter((address) => !!address));
558
552
  for (const userId of userIds) {
559
- await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed);
553
+ await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetched, suppressed);
560
554
  }
561
555
  }
562
556
  }
@@ -599,15 +593,10 @@ export function createDeliveryService(options) {
599
593
  await dispatchNotification(notificationType, options, tenantId, priority, run);
600
594
  }
601
595
  catch (err) {
602
- // A failing release must not replace the dispatch error the caller retries on.
603
- await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run).catch((releaseErr) => {
604
- logError.warn(`idempotency key release failed: ${redactedMessageOf(releaseErr)}`, {
605
- notificationType,
606
- });
607
- });
596
+ await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run, notificationType);
608
597
  throw err;
609
598
  }
610
- await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run);
599
+ await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run, notificationType);
611
600
  return dispatched(run);
612
601
  },
613
602
  };
@@ -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,6 +1,6 @@
1
1
  export { sanitizePdfAttachmentFileName } from "../shared/index.js";
2
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";
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";
4
4
  export { type AttemptLogRetentionOptions, DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
5
5
  export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
6
6
  export { type ChatWebhookChannelOptions, chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
@@ -1,6 +1,6 @@
1
1
  export { sanitizePdfAttachmentFileName } from "../shared/index.js";
2
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";
3
+ export { DELIVERY_ENVELOPE_DATA_KEYS, emailAttachmentRefSchema, fileAttachmentRefSchema, inlineAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, MAX_RENDERED_PDF_VARIABLES_BYTES, renderedPdfAttachmentRefSchema, toAttachmentRef, withoutDeliveryEnvelopeKeys, } from "./attachment-ref.js";
4
4
  export { DEFAULT_ATTEMPT_LOG_RETENTION_DAYS, runAttemptLogRetention, } from "./attempt-log-retention.js";
5
5
  export { createEmailAttachmentReader, createTemplateMailRenderer } from "./channel-context.js";
6
6
  export { chatMessageText, createChatWebhookChannel, resolveChatWebhookTarget, toChannelResult, } from "./chat-webhook-channel.js";
@@ -36,6 +36,8 @@ const sendJobPayloadSchema = renderJobPayloadSchema.extend({
36
36
  html: z.string(),
37
37
  subject: z.string(),
38
38
  text: z.string().optional(),
39
+ fromName: z.string().optional(),
40
+ replyTo: z.string().optional(),
39
41
  attachments: z.array(emailAttachmentRefSchema).max(MAX_EMAIL_ATTACHMENTS).optional(),
40
42
  })
41
43
  .optional(),
@@ -63,6 +65,8 @@ function toSendRendered(rendered) {
63
65
  html: rendered.html,
64
66
  subject: rendered.subject,
65
67
  ...(rendered.text !== undefined && { text: rendered.text }),
68
+ ...(rendered.fromName !== undefined && { fromName: rendered.fromName }),
69
+ ...(rendered.replyTo !== undefined && { replyTo: rendered.replyTo }),
66
70
  ...(rendered.attachments !== undefined && {
67
71
  attachments: rendered.attachments.map(toAttachmentRef),
68
72
  }),
@@ -96,12 +100,11 @@ function entryFor(p, status, error, address, confirmed) {
96
100
  ...(confirmed === false && { confirmed }),
97
101
  };
98
102
  }
99
- // Only the final retry frees the key: earlier failures are retried by the runner and
100
- // must still dedup. Assumes ctx.redis is the handle the service claimed on; runProdApp
101
- // wires the same instance into both.
102
- async function releaseIdempotencyOnTerminalFailure(ctx, p) {
103
- // skip: nothing to release before the last attempt or without an idempotency key
104
- 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)
105
108
  return;
106
109
  if (!ctx.redis) {
107
110
  ctx.log.warn("delivery: idempotency key not released, job context has no redis", {
@@ -118,6 +121,26 @@ async function releaseIdempotencyOnTerminalFailure(ctx, p) {
118
121
  });
119
122
  }
120
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
+ }
121
144
  // The job runner stores err.message in the run row, and BullMQ keeps failedReason and
122
145
  // the stack trace in Redis in plain text, so the rethrown error carries only the code.
123
146
  // The redacted message goes to the log.
@@ -128,7 +151,6 @@ async function failAttempt(ctx, db, registry, p, code, err) {
128
151
  channel: p.channelName,
129
152
  });
130
153
  await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, "failed", code, p.address));
131
- await releaseIdempotencyOnTerminalFailure(ctx, p);
132
154
  throw new Error(code);
133
155
  }
134
156
  // Render the message and hand off to delivery.send. On failure: record the
@@ -136,10 +158,13 @@ async function failAttempt(ctx, db, registry, p, code, err) {
136
158
  // the send step is never reached, so a stuck render can't half-send.
137
159
  export const deliveryRenderJob = async (payload, ctx) => {
138
160
  const p = renderJobPayloadSchema.parse(payload);
161
+ return releasingKeyOnTerminalFailure(ctx, p, () => renderStep(p, ctx));
162
+ };
163
+ async function renderStep(p, ctx) {
139
164
  const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
140
165
  const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
141
166
  const channel = resolveChannel(registry, p.channelName);
142
- const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, fileTenantId));
167
+ const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId));
143
168
  let rendered;
144
169
  try {
145
170
  if (!channel.render) {
@@ -157,21 +182,28 @@ export const deliveryRenderJob = async (payload, ctx) => {
157
182
  catch (err) {
158
183
  return failAttempt(ctx, db, registry, p, "channel_error", err);
159
184
  }
160
- };
185
+ }
161
186
  // Deliver via the channel using the rendered payload (if any). On failure:
162
187
  // record the terminal failed event and rethrow so BullMQ retries the send with
163
188
  // the same already-rendered HTML — no re-render needed.
164
189
  export const deliverySendJob = async (payload, ctx) => {
165
190
  const p = sendJobPayloadSchema.parse(payload);
191
+ return releasingKeyOnTerminalFailure(ctx, p, () => sendStep(p, ctx));
192
+ };
193
+ async function sendStep(p, ctx) {
166
194
  const tenantId = p.tenantId; // @cast-boundary engine-payload — stream tenant
167
195
  const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
168
196
  const channel = resolveChannel(registry, p.channelName);
169
- const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, fileTenantId));
197
+ const channelCtx = buildChannelContext(db, registry, undefined, tenantId, ctx.secrets, ctx.config, (fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId));
170
198
  try {
171
199
  const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
172
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);
173
205
  }
174
206
  catch (err) {
175
207
  return failAttempt(ctx, db, registry, p, "send_failed", err);
176
208
  }
177
- };
209
+ }
@@ -3,7 +3,7 @@ import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { DeliveryErrorCode, NotifyDeliveryStatus, NotifyDispatched, NotifyJobDispatcher, NotifyOptions, NotifyPriority, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
5
5
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
6
- import type { MailPayload } from "../renderer-foundation/index.js";
6
+ import type { MailPayload, RenderedTemplateRef } from "../renderer-foundation/index.js";
7
7
  import type { EmailAttachmentRef } from "./attachment-ref.js";
8
8
  import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
9
9
  export type RenderedMail = {
@@ -11,7 +11,11 @@ export type RenderedMail = {
11
11
  readonly html: string;
12
12
  readonly text: string;
13
13
  readonly previewText?: string;
14
+ readonly fromName?: string;
15
+ readonly replyTo?: string;
14
16
  readonly attachments?: readonly EmailAttachmentRef[];
17
+ /** The layout version the html was rendered through; the editor preview names it. */
18
+ readonly layout?: RenderedTemplateRef;
15
19
  };
16
20
  export type ChannelContext = {
17
21
  readonly db: TenantDb;
@@ -43,6 +47,8 @@ export type RenderedMessage = {
43
47
  readonly html: string;
44
48
  readonly subject: string;
45
49
  readonly text?: string;
50
+ readonly fromName?: string;
51
+ readonly replyTo?: string;
46
52
  readonly attachments?: readonly EmailAttachmentRef[];
47
53
  };
48
54
  export declare const DELIVERY_CHANNEL_MODES: readonly ["inline", "queued"];
@@ -7,4 +7,4 @@ export declare function createSetWriteHandler(getRuntime: (() => GlobalFeatureTo
7
7
  featureName: string;
8
8
  enabled: boolean;
9
9
  previousEnabled: boolean | null;
10
- }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
10
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
@@ -1,6 +1,6 @@
1
1
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { type Registry } from "@cosmicdrift/kumiko-framework/engine";
3
- import { type ClosableCacheSyncBus } from "@cosmicdrift/kumiko-framework/redis";
3
+ import { type CacheSyncBus, type ClosableCacheSyncBus } from "@cosmicdrift/kumiko-framework/redis";
4
4
  export type ToggleSyncSignal = {
5
5
  publish(featureName: string, enabled: boolean): void;
6
6
  onMessage(listener: (featureName: string, enabled: boolean) => void): void;
@@ -12,14 +12,16 @@ export declare class GlobalFeatureToggleRuntime {
12
12
  private readonly syncSignal?;
13
13
  private readonly ownedBus?;
14
14
  private snapshot;
15
- private appliedDuringLoad;
15
+ private readonly pendingFlipBuffers;
16
+ private loadInFlight;
16
17
  constructor(db: DbConnection, registry: Registry, syncSignal?: ToggleSyncSignal | undefined, ownedBus?: ClosableCacheSyncBus | undefined);
17
18
  close(): Promise<void>;
18
19
  initialize(): Promise<void>;
20
+ private loadSnapshot;
19
21
  refresh(): Promise<void>;
20
22
  apply(featureName: string, enabled: boolean): void;
21
23
  broadcastToggle(featureName: string, enabled: boolean): void;
22
24
  readOverride(featureName: string): boolean | undefined;
23
25
  effectiveFeatures: () => ReadonlySet<string>;
24
26
  }
25
- export declare function createFeatureToggleRuntime(db: DbConnection, registry: Registry, syncSignal?: ToggleSyncSignal): Promise<GlobalFeatureToggleRuntime>;
27
+ export declare function createFeatureToggleRuntime(db: DbConnection, registry: Registry, syncSignal?: ToggleSyncSignal, cacheSyncBus?: CacheSyncBus): Promise<GlobalFeatureToggleRuntime>;
@@ -22,9 +22,11 @@ export class GlobalFeatureToggleRuntime {
22
22
  syncSignal;
23
23
  ownedBus;
24
24
  snapshot = new Map();
25
- // Flips applied while a snapshot load is in flight. The load's SELECT may have
26
- // started before the flip committed, so its rows can be older than these.
27
- appliedDuringLoad;
25
+ // One buffer per requested load, open from the call until its SELECT finished: the SELECT
26
+ // may have started before a flip committed, so its rows can be older than the flips collected.
27
+ pendingFlipBuffers = new Set();
28
+ // Loads run one at a time so the older rows of one cannot overwrite the result of another.
29
+ loadInFlight = Promise.resolve();
28
30
  constructor(db, registry, syncSignal, ownedBus) {
29
31
  this.db = db;
30
32
  this.registry = registry;
@@ -47,9 +49,14 @@ export class GlobalFeatureToggleRuntime {
47
49
  async close() {
48
50
  await this.ownedBus?.close();
49
51
  }
50
- async initialize() {
52
+ initialize() {
51
53
  const pendingFlips = new Map();
52
- this.appliedDuringLoad = pendingFlips;
54
+ this.pendingFlipBuffers.add(pendingFlips);
55
+ const load = this.loadInFlight.then(() => this.loadSnapshot(pendingFlips));
56
+ this.loadInFlight = load.catch(() => undefined);
57
+ return load;
58
+ }
59
+ async loadSnapshot(pendingFlips) {
53
60
  try {
54
61
  const rows = await selectMany(this.db, globalFeatureStateTable);
55
62
  this.snapshot = new Map([
@@ -58,7 +65,7 @@ export class GlobalFeatureToggleRuntime {
58
65
  ]);
59
66
  }
60
67
  finally {
61
- this.appliedDuringLoad = undefined;
68
+ this.pendingFlipBuffers.delete(pendingFlips);
62
69
  }
63
70
  }
64
71
  // Re-read the full snapshot. Called from the set-handler after a
@@ -74,7 +81,8 @@ export class GlobalFeatureToggleRuntime {
74
81
  // lock. Kept alongside refresh() so both options are explicit.
75
82
  apply(featureName, enabled) {
76
83
  this.snapshot.set(featureName, enabled);
77
- this.appliedDuringLoad?.set(featureName, enabled);
84
+ for (const pendingFlips of this.pendingFlipBuffers)
85
+ pendingFlips.set(featureName, enabled);
78
86
  }
79
87
  // Called by the toggle-cache-sync MSP handler (feature.ts) — the one
80
88
  // process whose shared cursor won a given toggle-set event. With a
@@ -116,11 +124,16 @@ export class GlobalFeatureToggleRuntime {
116
124
  }
117
125
  // Factory for app-boot wiring: instantiate, initialize, return the runtime (for the
118
126
  // set-handler to refresh; its effectiveFeatures feeds createDispatcher). Without an explicit
119
- // syncSignal the runtime builds one on the framework cache-sync bus: Redis-backed when
120
- // REDIS_URL is set, process-local otherwise. close() releases a bus it created.
121
- export async function createFeatureToggleRuntime(db, registry, syncSignal) {
122
- const ownedBus = syncSignal ? undefined : createDefaultCacheSyncBus();
123
- const signal = syncSignal ?? (ownedBus ? createCacheSyncToggleSignal(ownedBus) : undefined);
127
+ // syncSignal the signal runs on the given cacheSyncBus or, failing that, on a bus the runtime
128
+ // creates itself (Redis-backed when REDIS_URL is set, process-local otherwise). close() releases
129
+ // only a bus the runtime created.
130
+ export async function createFeatureToggleRuntime(db, registry, syncSignal,
131
+ // The server's own bus (the one handed to the dispatcher): reusing it avoids a second set of
132
+ // Redis connections that nothing closes at shutdown.
133
+ cacheSyncBus) {
134
+ const ownedBus = syncSignal || cacheSyncBus ? undefined : createDefaultCacheSyncBus();
135
+ const sharedBus = cacheSyncBus ?? ownedBus;
136
+ const signal = syncSignal ?? (sharedBus ? createCacheSyncToggleSignal(sharedBus) : undefined);
124
137
  const runtime = new GlobalFeatureToggleRuntime(db, registry, signal, ownedBus);
125
138
  try {
126
139
  await runtime.initialize();
@@ -99,7 +99,7 @@ export const publicVariantQuery = defineQueryHandler({
99
99
  if (!spec)
100
100
  return null;
101
101
  const result = await derivatives.variant(query.payload.fileRefId, spec, query.payload.variant);
102
- const bytes = await files.ref(result.storageKey).read();
102
+ const bytes = await files.refFromFileRef(result).read();
103
103
  // ponytail: fully buffered (no streaming), +33% over the wire from
104
104
  // base64 — HttpRouteHandlerDeps only exposes {app, systemQuery} (no
105
105
  // direct ctx.files access from the Hono handler itself), and the local
@@ -9,7 +9,7 @@ export type FileRefContent = {
9
9
  export declare function fileContextForTenant(ctx: {
10
10
  readonly _fileProviderResolver?: FileProviderResolver | undefined;
11
11
  readonly files?: FileContext | undefined;
12
- }, tenantId: string): FileContext | undefined;
12
+ }, contextTenantId: string, tenantId: string): FileContext | undefined;
13
13
  export declare function fileContextFromResolver(resolver: FileProviderResolver, tenantId: string): FileContext;
14
14
  export declare class FileRefTooLargeError extends Error {
15
15
  readonly size: number;
@@ -1,11 +1,13 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { isUuid } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { createFileContext, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
4
- // Tenant-bound on purpose: the FileContext of the current request only reaches its own key space.
5
- export function fileContextForTenant(ctx, tenantId) {
4
+ // Tenant-bound on purpose: without a resolver `ctx.files` only reaches the storage of the
5
+ // context's own tenant (`contextTenantId`), so a request for another tenant's files is
6
+ // undefined instead of silently reading through the wrong tenant's storage.
7
+ export function fileContextForTenant(ctx, contextTenantId, tenantId) {
6
8
  const resolver = ctx._fileProviderResolver;
7
9
  if (resolver === undefined)
8
- return ctx.files;
10
+ return tenantId === contextTenantId ? ctx.files : undefined;
9
11
  return fileContextFromResolver(resolver, tenantId);
10
12
  }
11
13
  export function fileContextFromResolver(resolver, tenantId) {
@@ -44,6 +46,6 @@ export async function readFileRefContent(args) {
44
46
  if (files === undefined) {
45
47
  throw new Error("readFileRefContent: no file storage is available for the file's tenant");
46
48
  }
47
- const content = await files.ref(row.storageKey).read();
49
+ const content = await files.refFromFileRef(row).read();
48
50
  return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
49
51
  }
@@ -97,6 +97,7 @@ export const fileRefTenantDestroyHook = async (ctx) => {
97
97
  },
98
98
  maxFailures: 1,
99
99
  deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
100
+ minBatches: 1,
100
101
  onRowError: (_row, err) => {
101
102
  firstFailure = err instanceof Error ? err.message : String(err);
102
103
  },
@@ -176,6 +177,9 @@ export const fileRefStorageDestroyHook = async (tenantId, ctx) => {
176
177
  },
177
178
  maxFailures: 1,
178
179
  deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
180
+ // list() and the survivor lookup above can use up the whole budget; one chunk per tick
181
+ // still deletes, so the stage never reports "no progress".
182
+ minBatches: 1,
179
183
  onRowError: (_key, err) => {
180
184
  firstFailure = err instanceof Error ? err.message : String(err);
181
185
  },
@@ -1,4 +1,5 @@
1
1
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare const FILES_TENANT_DATA_FEATURE: "files-tenant-data";
2
3
  export declare function createFilesTenantDataFeature(): FeatureDefinition;
3
4
  export { sweepOrphanedDerivativesJob, sweepOrphanedDerivativesPayloadSchema, } from "./handlers/sweep-orphaned-derivatives.job.js";
4
5
  export { fileRefStorageDestroyHook, fileRefTenantDestroyHook } from "./hooks.js";
@@ -14,8 +14,9 @@ import { defineFeature, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, } from "@cosmicdr
14
14
  import { sweepOrphanedDerivativesJob, sweepOrphanedDerivativesPayloadSchema, } from "./handlers/sweep-orphaned-derivatives.job.js";
15
15
  import { fileRefStorageDestroyHook, fileRefTenantDestroyHook } from "./hooks.js";
16
16
  const SWEEP_ORPHANED_DERIVATIVES_ESCAPE_HATCH_REASON = "iterates every tenant and checks fileRef owners per tenant";
17
+ export const FILES_TENANT_DATA_FEATURE = "files-tenant-data";
17
18
  export function createFilesTenantDataFeature() {
18
- return defineFeature("files-tenant-data", (r) => {
19
+ return defineFeature(FILES_TENANT_DATA_FEATURE, (r) => {
19
20
  r.describe("GDPR coverage for the `files` feature's `fileRef` entity: tenant-destroy row purge (EXT_TENANT_DATA) and full storage-prefix binary wipe (EXT_STORAGE_PROVIDER), plus a manual backfill/GC job that sweeps derivatives orphaned by a forget/tenant-destroy that ran before #2461 wired binary cleanup into those flows. Requires `tenant-lifecycle`, optionalRequires `files`.");
20
21
  r.uiHints({
21
22
  displayLabel: "Files · Tenant-Destroy & Derivative GC",
@@ -1,4 +1,4 @@
1
- import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
1
+ import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
2
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  export declare function filterOwnedStorageKeys(db: DbRunner, tenantId: TenantId, ownerId: string, candidateKeys: readonly string[], draftInsertedAt: Temporal.Instant, isCreateMode: boolean): Promise<readonly string[]>;