@cosmicdrift/kumiko-bundled-features 0.388.0 → 0.390.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 (415) hide show
  1. package/dist/agent-tools/index.d.ts +1 -1
  2. package/dist/agent-tools/index.js +1 -1
  3. package/dist/agent-tools/tool-dispatch.d.ts +8 -3
  4. package/dist/agent-tools/tool-dispatch.js +36 -25
  5. package/dist/audit/feature.js +2 -1
  6. package/dist/auth-email-password/auth-user-row.js +1 -1
  7. package/dist/auth-email-password/bootstrap.js +6 -1
  8. package/dist/auth-email-password/feature.js +1 -1
  9. package/dist/auth-email-password/handlers/change-password.write.js +7 -7
  10. package/dist/auth-email-password/handlers/confirm-account-unlock.write.js +3 -3
  11. package/dist/auth-email-password/handlers/confirm-token-flow.js +4 -4
  12. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +8 -7
  13. package/dist/auth-email-password/handlers/invite-accept.write.js +10 -7
  14. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  15. package/dist/auth-email-password/handlers/invite-info.query.js +2 -2
  16. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +8 -7
  17. package/dist/auth-email-password/handlers/login.write.js +17 -15
  18. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
  19. package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
  20. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
  21. package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
  22. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
  23. package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
  24. package/dist/auth-email-password/handlers/reset-password.write.js +2 -2
  25. package/dist/auth-email-password/handlers/signup-confirm.write.js +12 -12
  26. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
  27. package/dist/auth-email-password/handlers/signup-request.write.js +16 -8
  28. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +4 -2
  29. package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
  30. package/dist/auth-email-password/handlers/token-request-handler.js +7 -2
  31. package/dist/auth-email-password/handlers/verify-email.write.js +2 -2
  32. package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
  33. package/dist/auth-email-password/magic-link-mail.js +7 -3
  34. package/dist/auth-email-password/seeding.d.ts +1 -1
  35. package/dist/auth-email-password/seeding.js +9 -8
  36. package/dist/auth-email-password/signup-activation.d.ts +1 -0
  37. package/dist/auth-email-password/signup-activation.js +1 -0
  38. package/dist/auth-email-password/web/auth-client.d.ts +2 -2
  39. package/dist/auth-email-password/web/auth-client.js +8 -4
  40. package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
  41. package/dist/auth-email-password/web/auth-redirect.js +20 -25
  42. package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
  43. package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
  44. package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
  45. package/dist/auth-email-password/web/index.d.ts +1 -0
  46. package/dist/auth-email-password/web/index.js +1 -0
  47. package/dist/auth-email-password/web/login-screen.js +11 -6
  48. package/dist/auth-email-password/web/reset-password-screen.js +7 -1
  49. package/dist/auth-email-password/web/return-path.d.ts +4 -0
  50. package/dist/auth-email-password/web/return-path.js +34 -0
  51. package/dist/auth-email-password/web/session.js +6 -1
  52. package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
  53. package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
  54. package/dist/auth-email-password/web/signup-screen.js +8 -3
  55. package/dist/auth-foundation/feature.js +12 -4
  56. package/dist/auth-mfa/config.d.ts +1 -1
  57. package/dist/auth-mfa/config.js +3 -7
  58. package/dist/auth-mfa/handlers/disable.write.js +5 -5
  59. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +19 -14
  60. package/dist/auth-mfa/handlers/enable-confirm.write.js +3 -3
  61. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +6 -1
  62. package/dist/auth-mfa/handlers/enable-start.write.js +1 -1
  63. package/dist/auth-mfa/handlers/reencrypt.job.js +14 -9
  64. package/dist/auth-mfa/handlers/regenerate-recovery.write.js +4 -4
  65. package/dist/auth-mfa/handlers/verify.write.js +19 -14
  66. package/dist/auth-mfa/mfa-code-verifier.js +9 -4
  67. package/dist/auth-mfa/mfa-status-checker.js +13 -6
  68. package/dist/auth-mfa/verify-factor.js +1 -1
  69. package/dist/billing-foundation/billing-info-query.js +2 -2
  70. package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -1
  71. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +5 -5
  72. package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
  73. package/dist/billing-foundation/consumer-protection/register-termination.js +1 -1
  74. package/dist/billing-foundation/consumer-protection/termination-open-cancels.d.ts +4 -0
  75. package/dist/billing-foundation/consumer-protection/termination-open-cancels.js +14 -0
  76. package/dist/billing-foundation/consumer-protection/termination-provider-lock.d.ts +2 -0
  77. package/dist/billing-foundation/consumer-protection/termination-provider-lock.js +3 -0
  78. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +4 -4
  79. package/dist/billing-foundation/consumer-protection/termination-record.js +11 -48
  80. package/dist/billing-foundation/consumer-protection/termination-request.js +10 -8
  81. package/dist/billing-foundation/consumer-protection/termination-revoke.js +10 -12
  82. package/dist/billing-foundation/events.js +1 -1
  83. package/dist/billing-foundation/feature.js +3 -3
  84. package/dist/billing-foundation/get-subscription-for-tenant.d.ts +1 -1
  85. package/dist/billing-foundation/handlers/process-event.write.js +5 -5
  86. package/dist/billing-foundation/handlers/process-payment-event.write.js +3 -3
  87. package/dist/billing-foundation/plan-catalog.js +2 -2
  88. package/dist/billing-foundation/projection.js +1 -1
  89. package/dist/billing-foundation/types.js +1 -1
  90. package/dist/cap-counter/aggregate-id.d.ts +1 -1
  91. package/dist/cap-counter/aggregate-id.js +1 -1
  92. package/dist/cap-counter/book-cap-usage.d.ts +2 -2
  93. package/dist/cap-counter/book-cap-usage.js +3 -3
  94. package/dist/cap-counter/cap-reservation.js +16 -7
  95. package/dist/cap-counter/constants.js +1 -1
  96. package/dist/cap-counter/feature.js +3 -2
  97. package/dist/cap-counter/handlers/increment-rolling.write.js +1 -1
  98. package/dist/cap-counter/reservation-chain.js +1 -1
  99. package/dist/cap-counter/with-cap-enforcement.d.ts +3 -3
  100. package/dist/cap-counter/with-cap-enforcement.js +1 -1
  101. package/dist/cap-overview/feature.js +1 -0
  102. package/dist/cap-overview/handlers/caps-usage.query.js +1 -1
  103. package/dist/cap-overview/handlers/tenant-caps-list.query.js +1 -1
  104. package/dist/compliance-profiles/seeding.d.ts +1 -1
  105. package/dist/compliance-profiles/seeding.js +9 -3
  106. package/dist/config/feature.js +6 -6
  107. package/dist/config/handlers/cascade.query.js +1 -1
  108. package/dist/config/handlers/readiness.query.js +1 -1
  109. package/dist/config/handlers/reencrypt.job.js +23 -13
  110. package/dist/config/handlers/reset.write.js +3 -3
  111. package/dist/config/handlers/selected-extensions.query.js +1 -1
  112. package/dist/config/handlers/set.write.js +15 -5
  113. package/dist/config/handlers/values.query.js +1 -1
  114. package/dist/crypto-shredding/handlers/forget-subject.write.js +9 -9
  115. package/dist/crypto-shredding/resolve-file-bytes-deleter.js +3 -3
  116. package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
  117. package/dist/custom-fields/handlers/delete-system-field.write.js +1 -1
  118. package/dist/custom-fields/handlers/delete-tenant-field.write.js +1 -1
  119. package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
  120. package/dist/custom-fields/index.d.ts +1 -0
  121. package/dist/custom-fields/index.js +1 -0
  122. package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
  123. package/dist/data-retention/feature.js +1 -1
  124. package/dist/data-retention/run-retention-cleanup.js +12 -2
  125. package/dist/delivery/attempt-log.js +1 -1
  126. package/dist/delivery/delivery-service.js +5 -5
  127. package/dist/delivery/feature.js +3 -2
  128. package/dist/delivery/index.d.ts +1 -0
  129. package/dist/delivery/index.js +1 -0
  130. package/dist/delivery/jobs.js +11 -11
  131. package/dist/delivery/system-channel-context.d.ts +15 -0
  132. package/dist/delivery/system-channel-context.js +5 -0
  133. package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
  134. package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
  135. package/dist/document-ingest-foundation/events.d.ts +11 -0
  136. package/dist/document-ingest-foundation/events.js +12 -1
  137. package/dist/document-ingest-foundation/index.d.ts +2 -1
  138. package/dist/document-ingest-foundation/index.js +2 -1
  139. package/dist/feature-toggles/feature.js +1 -0
  140. package/dist/feature-toggles/handlers/registered.query.js +1 -1
  141. package/dist/feature-toggles/handlers/set.write.js +1 -1
  142. package/dist/feature-toggles/toggle-sync-signal.js +1 -1
  143. package/dist/file-derivatives/feature.d.ts +1 -1
  144. package/dist/file-derivatives/feature.js +1 -1
  145. package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
  146. package/dist/file-derivatives/handlers/public-variant.query.js +5 -5
  147. package/dist/file-foundation/feature.js +1 -1
  148. package/dist/file-foundation/read-file-ref-content.d.ts +4 -2
  149. package/dist/file-foundation/read-file-ref-content.js +3 -3
  150. package/dist/file-provider-s3/feature.js +1 -1
  151. package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +8 -8
  152. package/dist/form-draft/feature.js +1 -1
  153. package/dist/form-draft/handlers/cleanup.job.js +22 -12
  154. package/dist/form-draft/handlers/discard.write.js +3 -3
  155. package/dist/form-draft/handlers/get.query.js +1 -1
  156. package/dist/foundation-shared/config-helpers.d.ts +1 -1
  157. package/dist/foundation-shared/config-helpers.js +1 -1
  158. package/dist/host-routing/validate-options.js +3 -0
  159. package/dist/inbound-mail-foundation/events.js +1 -1
  160. package/dist/inbound-mail-foundation/feature.js +2 -1
  161. package/dist/inbound-mail-foundation/handlers/account-state.d.ts +3 -1
  162. package/dist/inbound-mail-foundation/handlers/account-state.js +1 -1
  163. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  164. package/dist/inbound-mail-foundation/handlers/disconnect-account.write.js +1 -1
  165. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +10 -10
  166. package/dist/inbound-mail-foundation/handlers/update-account.write.js +1 -1
  167. package/dist/inbound-mail-foundation/projection.js +1 -1
  168. package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
  169. package/dist/inbound-provider-imap/feature.js +1 -1
  170. package/dist/jobs/events.js +1 -1
  171. package/dist/jobs/feature.js +1 -0
  172. package/dist/jobs/handlers/consumer.write.js +1 -1
  173. package/dist/jobs/handlers/projection-rebuild.job.js +1 -1
  174. package/dist/jobs/handlers/reindex-entity.job.js +5 -5
  175. package/dist/jobs/handlers/retention-cleanup.job.js +1 -1
  176. package/dist/jobs/handlers/retry.write.js +1 -1
  177. package/dist/jobs/handlers/stale-run-sweep.job.js +1 -1
  178. package/dist/jobs/handlers/trigger.write.js +2 -2
  179. package/dist/jobs/job-run-logger.d.ts +1 -0
  180. package/dist/jobs/job-run-logger.js +38 -18
  181. package/dist/ledger/web/index.js +1 -1
  182. package/dist/legal-pages/lib/boot-check.d.ts +10 -4
  183. package/dist/legal-pages/lib/boot-check.js +2 -2
  184. package/dist/legal-pages/seed.d.ts +3 -1
  185. package/dist/legal-pages/seed.js +2 -2
  186. package/dist/mail-foundation/feature.d.ts +2 -2
  187. package/dist/mail-transport-smtp/config-keys.js +1 -1
  188. package/dist/mail-transport-smtp/feature.js +7 -6
  189. package/dist/managed-pages/constants.d.ts +1 -0
  190. package/dist/managed-pages/constants.js +1 -0
  191. package/dist/managed-pages/feature.js +3 -1
  192. package/dist/managed-pages/index.d.ts +1 -0
  193. package/dist/managed-pages/index.js +1 -0
  194. package/dist/managed-pages/seeding.d.ts +1 -1
  195. package/dist/managed-pages/seeding.js +10 -4
  196. package/dist/notes-history/handlers/add-note.write.js +2 -2
  197. package/dist/personal-access-tokens/feature.js +2 -1
  198. package/dist/personal-access-tokens/handlers/create.write.js +2 -2
  199. package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
  200. package/dist/rate-limiting/handlers/status.query.js +2 -2
  201. package/dist/readiness/handlers/status.query.js +1 -1
  202. package/dist/renderer-foundation/api.d.ts +4 -2
  203. package/dist/renderer-foundation/api.js +2 -2
  204. package/dist/secrets/announce-secret-change.d.ts +6 -2
  205. package/dist/secrets/announce-secret-change.js +3 -3
  206. package/dist/secrets/feature.d.ts +1 -1
  207. package/dist/secrets/feature.js +8 -8
  208. package/dist/secrets/handlers/delete.write.js +1 -1
  209. package/dist/secrets/handlers/rotate.job.js +13 -8
  210. package/dist/secrets/handlers/set.write.js +1 -1
  211. package/dist/secrets/secrets-context.js +16 -6
  212. package/dist/secrets/write-gate.js +8 -2
  213. package/dist/seo/feature.js +1 -1
  214. package/dist/sessions/feature.js +2 -1
  215. package/dist/sessions/handlers/cleanup.job.js +2 -2
  216. package/dist/sessions/handlers/detail.query.d.ts +1 -0
  217. package/dist/sessions/handlers/detail.query.js +5 -2
  218. package/dist/sessions/handlers/list.query.d.ts +1 -0
  219. package/dist/sessions/handlers/list.query.js +5 -2
  220. package/dist/sessions/handlers/mine.query.d.ts +1 -0
  221. package/dist/sessions/handlers/mine.query.js +5 -2
  222. package/dist/sessions/handlers/revoke-all-for-user.write.js +3 -3
  223. package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
  224. package/dist/sessions/handlers/revoke.write.js +1 -1
  225. package/dist/sessions/index.d.ts +1 -1
  226. package/dist/sessions/index.js +1 -1
  227. package/dist/sessions/screens.js +8 -4
  228. package/dist/sessions/session-callbacks.d.ts +0 -2
  229. package/dist/sessions/session-callbacks.js +1 -3
  230. package/dist/sessions/session-revoked-event.js +1 -1
  231. package/dist/sessions/web/session-device-cell.js +10 -12
  232. package/dist/sessions/web/user-agent.d.ts +1 -0
  233. package/dist/sessions/web/user-agent.js +11 -0
  234. package/dist/shared/index.d.ts +2 -0
  235. package/dist/shared/index.js +2 -0
  236. package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
  237. package/dist/shared/purge-files-reaching-subject.js +31 -4
  238. package/dist/shared/require-for-tenant.d.ts +1 -1
  239. package/dist/shared/require-for-tenant.js +6 -4
  240. package/dist/shared/run-in-sub-transaction.js +5 -5
  241. package/dist/shared/stream-tenant-backfill.d.ts +28 -0
  242. package/dist/shared/stream-tenant-backfill.js +119 -0
  243. package/dist/shared/stream-tenant-derived-state.d.ts +25 -0
  244. package/dist/shared/stream-tenant-derived-state.js +68 -0
  245. package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
  246. package/dist/step-dispatcher/feature.js +1 -1
  247. package/dist/step-dispatcher/mail-runner.d.ts +1 -0
  248. package/dist/subscription-dodo/plugin-methods.js +1 -1
  249. package/dist/subscription-dodo/runtime.js +4 -4
  250. package/dist/subscription-stripe/plugin-methods.js +3 -3
  251. package/dist/subscription-stripe/runtime.js +4 -4
  252. package/dist/tags/feature.js +1 -0
  253. package/dist/template-resolver/api.d.ts +4 -2
  254. package/dist/template-resolver/api.js +2 -2
  255. package/dist/template-resolver/document-ops.d.ts +5 -0
  256. package/dist/template-resolver/document-ops.js +18 -2
  257. package/dist/template-resolver/feature.js +2 -2
  258. package/dist/template-resolver/handlers/bundle-import.d.ts +4 -3
  259. package/dist/template-resolver/handlers/export-bundle.query.js +1 -1
  260. package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
  261. package/dist/template-resolver/handlers/layout-source.query.js +26 -6
  262. package/dist/template-resolver/handlers/overview.query.js +17 -9
  263. package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
  264. package/dist/template-resolver/handlers/send-test-shared.js +1 -1
  265. package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
  266. package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
  267. package/dist/template-resolver/handlers/send-test.write.js +2 -2
  268. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +3 -1
  269. package/dist/template-resolver/handlers/tenant-override-selection.js +1 -1
  270. package/dist/template-resolver/handlers/tenant-overrides.query.js +1 -1
  271. package/dist/template-resolver/handlers/usage.query.js +1 -1
  272. package/dist/template-resolver/seeding.d.ts +1 -0
  273. package/dist/template-resolver/seeding.js +17 -4
  274. package/dist/template-resolver/store-template-resource.d.ts +7 -6
  275. package/dist/template-resolver/store-template-resource.js +48 -8
  276. package/dist/template-resolver/template-resources.d.ts +2 -2
  277. package/dist/template-resolver/template-resources.js +2 -2
  278. package/dist/template-resolver/testing.js +2 -2
  279. package/dist/template-resolver/web/document-insert-fields.d.ts +4 -0
  280. package/dist/template-resolver/web/document-insert-fields.js +69 -0
  281. package/dist/template-resolver/web/document-insert-model.d.ts +43 -0
  282. package/dist/template-resolver/web/document-insert-model.js +106 -0
  283. package/dist/template-resolver/web/document-insert-panel.d.ts +13 -0
  284. package/dist/template-resolver/web/document-insert-panel.js +49 -0
  285. package/dist/template-resolver/web/document-tiptap-bubble.d.ts +4 -0
  286. package/dist/template-resolver/web/document-tiptap-bubble.js +85 -0
  287. package/dist/template-resolver/web/document-tiptap-content.d.ts +3 -0
  288. package/dist/template-resolver/web/document-tiptap-content.js +12 -0
  289. package/dist/template-resolver/web/document-tiptap-context.d.ts +41 -0
  290. package/dist/template-resolver/web/document-tiptap-context.js +43 -0
  291. package/dist/template-resolver/web/document-tiptap-editor.js +50 -495
  292. package/dist/template-resolver/web/document-tiptap-helpers.d.ts +44 -0
  293. package/dist/template-resolver/web/document-tiptap-helpers.js +129 -0
  294. package/dist/template-resolver/web/document-tiptap-layout.d.ts +4 -0
  295. package/dist/template-resolver/web/document-tiptap-layout.js +24 -0
  296. package/dist/template-resolver/web/document-tiptap-toolbar-groups.d.ts +6 -0
  297. package/dist/template-resolver/web/document-tiptap-toolbar-groups.js +33 -0
  298. package/dist/template-resolver/web/document-tiptap-toolbar.d.ts +3 -0
  299. package/dist/template-resolver/web/document-tiptap-toolbar.js +91 -0
  300. package/dist/template-resolver/web/editor-handle.d.ts +1 -0
  301. package/dist/template-resolver/web/editor-handle.js +15 -0
  302. package/dist/template-resolver/web/i18n.js +3 -0
  303. package/dist/template-resolver/web/placeholder-line-editor.js +16 -5
  304. package/dist/template-resolver/web/qualified-names.d.ts +1 -0
  305. package/dist/template-resolver/web/qualified-names.js +1 -0
  306. package/dist/template-resolver/web/use-document-tiptap-model.d.ts +54 -0
  307. package/dist/template-resolver/web/use-document-tiptap-model.js +96 -0
  308. package/dist/tenant/feature.js +1 -0
  309. package/dist/tenant/handlers/cancel-invitation.write.js +1 -1
  310. package/dist/tenant/handlers/memberships.query.js +1 -1
  311. package/dist/tenant/handlers/remove-member.write.js +2 -2
  312. package/dist/tenant/handlers/team-list.query.js +3 -3
  313. package/dist/tenant/handlers/update-member-roles.write.js +1 -1
  314. package/dist/tenant/index.d.ts +1 -0
  315. package/dist/tenant/index.js +1 -0
  316. package/dist/tenant/invited-membership.js +6 -1
  317. package/dist/tenant/schema/tenant.d.ts +2 -2
  318. package/dist/tenant/schema/tenant.js +4 -1
  319. package/dist/tenant/seeding.d.ts +2 -2
  320. package/dist/tenant/seeding.js +52 -52
  321. package/dist/tenant/stream-tenant-backfill.d.ts +21 -0
  322. package/dist/tenant/stream-tenant-backfill.js +63 -0
  323. package/dist/tenant/testing.d.ts +12 -0
  324. package/dist/tenant/testing.js +33 -0
  325. package/dist/tenant-handover/events.js +1 -1
  326. package/dist/tenant-handover/handlers/claim.write.js +1 -1
  327. package/dist/tenant-lifecycle/feature.js +5 -5
  328. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +14 -5
  329. package/dist/tenant-lifecycle/handlers/request-destruction.write.js +12 -4
  330. package/dist/tenant-lifecycle/run-tenant-destroy.js +23 -13
  331. package/dist/tenant-lifecycle/stages.js +28 -2
  332. package/dist/tenant-lifecycle/testing.d.ts +1 -1
  333. package/dist/tenant-lifecycle/testing.js +6 -5
  334. package/dist/tier-engine/entity.js +1 -1
  335. package/dist/tier-engine/feature.js +8 -3
  336. package/dist/tier-engine/handlers/active-tier.query.js +3 -3
  337. package/dist/tier-engine/handlers/get-tenant-tier.query.js +6 -1
  338. package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -4
  339. package/dist/user/db/queries/stream-tenant-backfill.d.ts +20 -1
  340. package/dist/user/db/queries/stream-tenant-backfill.js +40 -77
  341. package/dist/user/feature.js +1 -0
  342. package/dist/user/handlers/find-for-auth.query.js +2 -2
  343. package/dist/user/handlers/update-roles.js +2 -2
  344. package/dist/user/index.d.ts +1 -1
  345. package/dist/user/index.js +1 -1
  346. package/dist/user/invited-global-roles.js +6 -1
  347. package/dist/user/principal-status.js +1 -1
  348. package/dist/user/seeding.d.ts +1 -1
  349. package/dist/user/seeding.js +20 -5
  350. package/dist/user/testing.d.ts +3 -0
  351. package/dist/user/testing.js +10 -0
  352. package/dist/user-data-rights/audit-download.js +12 -2
  353. package/dist/user-data-rights/feature.js +30 -26
  354. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
  355. package/dist/user-data-rights/handlers/deletion-grace-period.js +1 -1
  356. package/dist/user-data-rights/handlers/download-by-job.query.js +2 -2
  357. package/dist/user-data-rights/handlers/download-by-token.query.js +2 -2
  358. package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
  359. package/dist/user-data-rights/handlers/restrict-account.write.js +4 -4
  360. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +8 -6
  361. package/dist/user-data-rights/index.d.ts +1 -1
  362. package/dist/user-data-rights/index.js +1 -1
  363. package/dist/user-data-rights/lib/mail-transport-resolver.js +1 -1
  364. package/dist/user-data-rights/lib/storage-provider-resolver.js +1 -1
  365. package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -1
  366. package/dist/user-data-rights/run-export-jobs.js +7 -2
  367. package/dist/user-data-rights/run-forget-cleanup.d.ts +1 -0
  368. package/dist/user-data-rights/run-forget-cleanup.js +9 -1
  369. package/dist/user-data-rights/run-user-export.js +7 -1
  370. package/dist/user-data-rights/web/constants.d.ts +14 -0
  371. package/dist/user-data-rights/web/constants.js +14 -0
  372. package/dist/user-data-rights/web/index.d.ts +1 -0
  373. package/dist/user-data-rights/web/index.js +1 -0
  374. package/dist/user-profile/handlers/change-email.write.js +8 -6
  375. package/dist/waitlist/feature.js +1 -0
  376. package/dist/waitlist/handlers/export-purge.write.js +1 -1
  377. package/dist/waitlist/handlers/invite.write.js +2 -2
  378. package/dist/waitlist/handlers/reject.write.js +1 -1
  379. package/dist/waitlist/handlers/submit.write.js +20 -11
  380. package/dist/waitlist/import.d.ts +2 -1
  381. package/dist/waitlist/import.js +22 -2
  382. package/dist/waitlist/legacy-migrate.js +8 -3
  383. package/dist/waitlist/link-on-signup.js +6 -1
  384. package/dist/waitlist/options.d.ts +2 -1
  385. package/dist/workflow-runner/db/queries/due-runs.js +1 -1
  386. package/dist/workflow-runner/event-subscriber.js +4 -3
  387. package/dist/workflow-runner/event-trigger.js +2 -1
  388. package/dist/workflow-runner/feature.js +2 -2
  389. package/dist/workflow-runner/handlers/resume-run.write.js +6 -6
  390. package/dist/workflow-runner/runner.d.ts +3 -1
  391. package/dist/workflow-runner/runner.js +3 -3
  392. package/package.json +41 -23
  393. package/src/agent-tools/changes.json +6 -0
  394. package/src/auth-email-password/changes.json +18 -0
  395. package/src/auth-mfa/changes.json +6 -0
  396. package/src/billing-foundation/changes.json +6 -0
  397. package/src/cap-counter/changes.json +12 -0
  398. package/src/config/changes.json +6 -0
  399. package/src/crypto-shredding/changes.json +12 -0
  400. package/src/custom-fields/changes.json +6 -0
  401. package/src/delivery/changes.json +12 -0
  402. package/src/document-ingest-foundation/changes.json +6 -0
  403. package/src/host-routing/changes.json +6 -0
  404. package/src/jobs/changes.json +6 -0
  405. package/src/mail-transport-smtp/changes.json +12 -0
  406. package/src/managed-pages/changes.json +6 -0
  407. package/src/secrets/changes.json +6 -0
  408. package/src/sessions/changes.json +19 -0
  409. package/src/step-dispatcher/changes.json +6 -0
  410. package/src/template-resolver/changes.json +54 -0
  411. package/src/tenant/changes.json +19 -0
  412. package/src/tenant-lifecycle/changes.json +6 -0
  413. package/src/user-data-rights/changes.json +36 -0
  414. package/src/waitlist/changes.json +18 -0
  415. package/src/workflow-runner/changes.json +6 -0
@@ -53,7 +53,7 @@ export function createEnableStartHandler(opts) {
53
53
  if (!accountLabel) {
54
54
  // The declarative secretMint screen has no client component left to
55
55
  // read the session email off — derive the otpauth label server-side.
56
- accountLabel = readableAccountLabel(await ctx.queryAs(event.user, UserQueries.me, {}));
56
+ accountLabel = readableAccountLabel(await ctx.dispatch.queryAs(event.user, UserQueries.me, {}));
57
57
  }
58
58
  const secret = generateTotpSecret();
59
59
  const recoveryCodes = generateRecoveryCodes();
@@ -47,12 +47,12 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
47
47
  // hoisted function declarations below capture this — pin the narrowed
48
48
  // type explicitly so TS keeps it inside the closures.
49
49
  const cipher = maybeCipher;
50
- if (!ctx.masterKeyProvider) {
50
+ if (!ctx.services.masterKeyProvider) {
51
51
  throw new InternalError({
52
- message: "[auth-mfa:reencrypt] ctx.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
52
+ message: "[auth-mfa:reencrypt] ctx.services.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
53
53
  });
54
54
  }
55
- const provider = ctx.masterKeyProvider;
55
+ const provider = ctx.services.masterKeyProvider;
56
56
  // Undefined = per-subject crypto-shredding isn't configured for this app —
57
57
  // both fields are then plain envelope strings, no PII layer to peel.
58
58
  const piiKms = configuredPiiSubjectKms();
@@ -65,7 +65,12 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
65
65
  function tdbFor(tenantId) {
66
66
  let existing = tdbCache.get(tenantId);
67
67
  if (!existing) {
68
- existing = createTenantDb({ db, tenantId, mode: "system" });
68
+ existing = createTenantDb({
69
+ db,
70
+ tenantId,
71
+ mode: "system",
72
+ reason: "MFA key re-encryption job rewrites enrollments of every tenant",
73
+ });
69
74
  tdbCache.set(tenantId, existing);
70
75
  }
71
76
  return existing;
@@ -101,7 +106,7 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
101
106
  const totpClass = classifyStoredEnvelope(envelopeValues.totpSecret, targetVersion);
102
107
  const recoveryClass = classifyStoredEnvelope(envelopeValues.recoveryCodes, targetVersion);
103
108
  if (totpClass === "unrecognized" || recoveryClass === "unrecognized") {
104
- ctx.log?.warn?.(`[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`);
109
+ ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`);
105
110
  return "failed";
106
111
  }
107
112
  if (totpClass === "current" && recoveryClass === "current") {
@@ -128,7 +133,7 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
128
133
  if (!result.isSuccess) {
129
134
  if (result.error.code === "version_conflict")
130
135
  return "skipped";
131
- ctx.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
136
+ ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
132
137
  code: result.error.code,
133
138
  });
134
139
  return "failed";
@@ -140,9 +145,9 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
140
145
  migrateRow,
141
146
  maxFailures,
142
147
  deadlineAt: deadline,
143
- signal: ctx.signal,
148
+ signal: ctx.lifecycle.signal,
144
149
  onRowError: (row, err) => {
145
- ctx.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
150
+ ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
146
151
  },
147
152
  });
148
153
  const result = {
@@ -152,5 +157,5 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
152
157
  batchesProcessed: outcome.batchesProcessed,
153
158
  stoppedReason: outcome.stoppedReason,
154
159
  };
155
- ctx.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
160
+ ctx.obs.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
156
161
  }
@@ -37,16 +37,16 @@ export function createRegenerateRecoveryHandler(opts) {
37
37
  // afterward.
38
38
  //
39
39
  // Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts:
40
- // without ctx.redis, verifyMfaFactor's TOTP-replay burn is skipped,
40
+ // without ctx.services.redis, verifyMfaFactor's TOTP-replay burn is skipped,
41
41
  // letting an observed code be replayed within the ±1-step window to
42
42
  // regenerate (and thus invalidate) recovery codes — a session-authed
43
43
  // route, so pre-unwiring JWTs stay a valid attack path.
44
- if (!ctx.redis) {
44
+ if (!ctx.services.redis) {
45
45
  return writeFailure(new InternalError({
46
- message: "regenerate-recovery (mfa) requires ctx.redis for TOTP replay protection",
46
+ message: "regenerate-recovery (mfa) requires ctx.services.redis for TOTP replay protection",
47
47
  }));
48
48
  }
49
- const replay = { redis: ctx.redis, userId: event.user.id };
49
+ const replay = { redis: ctx.services.redis, userId: event.user.id };
50
50
  const verify = await verifyMfaFactor(row, event.payload.code, replay);
51
51
  if (!verify.ok)
52
52
  return invalidTotpCode();
@@ -29,8 +29,8 @@ export function createMfaVerifyHandler(opts) {
29
29
  rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
30
30
  escapeHatch: {
31
31
  grants: ["systemIdentity", "unsafeRaw"],
32
- reason: "Pre-auth MFA step has no session yet — re-derives it via ctx.queryAs(SYSTEM, " +
33
- "user:findForAuth) and ctx.resolveActiveMembership for the user the challenge token names. " +
32
+ reason: "Pre-auth MFA step has no session yet — re-derives it via ctx.dispatch.queryAs(SYSTEM, " +
33
+ "user:findForAuth) and ctx.auth.resolveActiveMembership for the user the challenge token names. " +
34
34
  "Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
35
35
  "guest dispatch tenant.",
36
36
  },
@@ -46,19 +46,19 @@ export function createMfaVerifyHandler(opts) {
46
46
  // this route sits behind a challenge token from a successful login,
47
47
  // but its own secret is still just a short TOTP/recovery code —
48
48
  // silently skipping the brute-force cap and single-use burn when
49
- // ctx.redis is absent would let a stolen challenge token be guessed
49
+ // ctx.services.redis is absent would let a stolen challenge token be guessed
50
50
  // against unboundedly, and let a recovery code be replayed via a
51
51
  // reissued challenge (mfa-verify-attempts.ts's counter is deliberately
52
52
  // NOT reset by reissuance, precisely to survive this).
53
- if (!ctx.redis) {
53
+ if (!ctx.services.redis) {
54
54
  return writeFailure(new InternalError({
55
- message: "verify (mfa) requires ctx.redis for the brute-force cap and single-use burn",
55
+ message: "verify (mfa) requires ctx.services.redis for the brute-force cap and single-use burn",
56
56
  }));
57
57
  }
58
58
  // Per-account brute-force cap — survives challenge-token reissuance
59
59
  // on purpose (see mfa-verify-attempts.ts header). Checked BEFORE the
60
60
  // TOTP/recovery verify so a locked account can't be guessed against.
61
- const state = await getMfaVerifyLockoutState(ctx.redis, userId);
61
+ const state = await getMfaVerifyLockoutState(ctx.services.redis, userId);
62
62
  if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
63
63
  const now = Date.now();
64
64
  if (state.lockedUntil > now) {
@@ -69,7 +69,12 @@ export function createMfaVerifyHandler(opts) {
69
69
  // "system" mode: this handler runs with a guest identity whose own
70
70
  // tenantId is meaningless here — the challenge token is the source
71
71
  // of truth for which tenant's row to read.
72
- const scopedDb = createTenantDb({ db: ctx.db.unsafeRaw(), tenantId, mode: "system" });
72
+ const scopedDb = createTenantDb({
73
+ db: ctx.db.unsafeRaw(),
74
+ tenantId,
75
+ mode: "system",
76
+ reason: "MFA challenge has no session, the tenant comes from the verified challenge token",
77
+ });
73
78
  const scopedUser = { id: userId, tenantId, roles: ["User"] };
74
79
  const row = await findUserMfaRow(scopedDb, scopedUser);
75
80
  // MFA got disabled between login and verify (race, or a stale
@@ -77,10 +82,10 @@ export function createMfaVerifyHandler(opts) {
77
82
  // bad code, no detail leak about account state.
78
83
  if (!row)
79
84
  return invalidChallengeToken();
80
- const replay = { redis: ctx.redis, userId };
85
+ const replay = { redis: ctx.services.redis, userId };
81
86
  const verify = await verifyMfaFactor(row, event.payload.code, replay);
82
87
  if (!verify.ok) {
83
- await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
88
+ await recordFailedMfaVerifyAttempt(ctx.services.redis, userId, maxAttempts, lockoutMinutes);
84
89
  return invalidTotpCode();
85
90
  }
86
91
  // Burn the challenge token only after a successful code check — a
@@ -91,7 +96,7 @@ export function createMfaVerifyHandler(opts) {
91
96
  // is reached, so this burn only needs to guard a DIFFERENT code
92
97
  // (recovery, or a still-valid-but-not-yet-burned TOTP step) reused
93
98
  // against the same challenge token.
94
- const burnResult = await burnToken(ctx.redis, "mfa-challenge", userId, verified.expiresAtMs);
99
+ const burnResult = await burnToken(ctx.services.redis, "mfa-challenge", userId, verified.expiresAtMs);
95
100
  if (burnResult === "already-used")
96
101
  return invalidChallengeToken();
97
102
  // Recovery-code use consumes the code — persist the reduced hash-list
@@ -106,12 +111,12 @@ export function createMfaVerifyHandler(opts) {
106
111
  if (!updateResult.isSuccess)
107
112
  return updateResult;
108
113
  }
109
- await clearMfaVerifyAttempts(ctx.redis, userId);
114
+ await clearMfaVerifyAttempts(ctx.services.redis, userId);
110
115
  // Re-derive the full session the way login.write.ts would have, for
111
116
  // the SAME tenant the challenge token already committed to (no
112
117
  // "pick a tenant" step here — that already happened at login time).
113
118
  const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
114
- const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
119
+ const userRow = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
115
120
  id: userId,
116
121
  })); // @cast-boundary engine-payload
117
122
  // Re-check status + membership the way login.write.ts does after its
@@ -130,7 +135,7 @@ export function createMfaVerifyHandler(opts) {
130
135
  const globalRoles = parseRoles(userRow?.roles ?? null);
131
136
  // Membership revoked or principal blocked between login and verify —
132
137
  // mirror login.write.ts's refusal instead of falling back to globalRoles only.
133
- const active = await ctx.resolveActiveMembership(userId, tenantId);
138
+ const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
134
139
  if (active.kind === "rejected")
135
140
  return invalidChallengeToken();
136
141
  // buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
@@ -144,7 +149,7 @@ export function createMfaVerifyHandler(opts) {
144
149
  ...sessionTimezoneField(userRow?.timezone),
145
150
  ...sessionLocaleField(userRow?.locale),
146
151
  };
147
- const claims = await ctx.resolveAuthClaims(baseSession);
152
+ const claims = await ctx.auth.resolveClaims(baseSession);
148
153
  const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
149
154
  return { isSuccess: true, data: { kind: "mfa-verify-success", session } };
150
155
  },
@@ -14,15 +14,20 @@ export function createMfaCodeVerifier() {
14
14
  declareEscapeHatch({
15
15
  reason: "reads the MFA enrollment of the user being re-authenticated on the caller's handler context; the calling handler declares its own escapeHatch",
16
16
  });
17
- const scopedDb = createTenantDb({ db: ctx.db.unsafeRaw(), tenantId, mode: "system" });
17
+ const scopedDb = createTenantDb({
18
+ db: ctx.db.unsafeRaw(),
19
+ tenantId,
20
+ mode: "system",
21
+ reason: "re-authentication reads the MFA enrollment of the user's own tenant, not the handler's dispatch tenant",
22
+ });
18
23
  const row = await findUserMfaRow(scopedDb, { id: userId, tenantId, roles: [] });
19
24
  if (!row)
20
25
  return { enrolled: false, ok: false };
21
- // Fail closed without ctx.redis, matching disable.write.ts/verify.write.ts —
26
+ // Fail closed without ctx.services.redis, matching disable.write.ts/verify.write.ts —
22
27
  // skipping the TOTP-replay burn would let an observed code be replayed.
23
- if (!code || !ctx.redis)
28
+ if (!code || !ctx.services.redis)
24
29
  return { enrolled: true, ok: false };
25
- const verify = await verifyMfaFactor(row, code, { redis: ctx.redis, userId });
30
+ const verify = await verifyMfaFactor(row, code, { redis: ctx.services.redis, userId });
26
31
  return { enrolled: true, ok: verify.ok && verify.method === "totp" };
27
32
  };
28
33
  }
@@ -1,3 +1,4 @@
1
+ import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
1
2
  import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { access, declareEscapeHatch, } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { mfaRequiredConfigHandle, mfaRequiredConfigKey } from "./config.js";
@@ -10,7 +11,12 @@ export function createMfaStatusChecker(opts) {
10
11
  declareEscapeHatch({
11
12
  reason: "reads the MFA enrollment of the tenant named in the signed login/setup token, not the guest dispatch tenant, on the caller's handler context; the calling handler declares its own escapeHatch",
12
13
  });
13
- const scopedDb = createTenantDb({ db: ctx.db.unsafeRaw(), tenantId, mode: "system" });
14
+ const scopedDb = createTenantDb({
15
+ db: ctx.db.unsafeRaw(),
16
+ tenantId,
17
+ mode: "system",
18
+ reason: "login/setup token names the tenant to check, the guest dispatch tenant is meaningless",
19
+ });
14
20
  const scopedUser = { id: userId, tenantId, roles: ["User"] };
15
21
  const row = await findUserMfaRow(scopedDb, scopedUser);
16
22
  if (row) {
@@ -21,20 +27,21 @@ export function createMfaStatusChecker(opts) {
21
27
  return { required: true, challengeToken: token };
22
28
  }
23
29
  // ctx.config is bound to the CALLING user's tenant (the anonymous
24
- // identity's tenant for login) — wrong tenant here. ctx.configResolver takes tenantId
30
+ // identity's tenant for login) — wrong tenant here. ctx.services.configResolver takes tenantId
25
31
  // explicitly, which is what a pre-session check against the LOGGING-IN
26
32
  // user's tenant needs.
27
33
  // @cast-boundary engine-payload — "select" config values are validated
28
34
  // against the declared `options` at write-time by the config feature.
29
- const policy = ((await ctx.configResolver?.get(mfaRequiredConfigHandle.name, mfaRequiredConfigKey(opts.requiredPolicy), tenantId, userId, scopedDb)) ?? "optional");
35
+ const policy = ((await ctx.services.configResolver?.get(mfaRequiredConfigHandle.name, mfaRequiredConfigKey(opts.requiredPolicy), tenantId, userId, scopedDb)) ?? "optional");
30
36
  if (policy === "optional")
31
37
  return { required: false };
32
38
  const buildSetupToken = () => signMfaPreauthSetupToken({ userId, tenantId }, MFA_PREAUTH_SETUP_TOKEN_TTL_MINUTES, opts.challengeTokenSecret).token;
33
39
  if (policy === "all")
34
40
  return { setupRequired: true, preauthSetupToken: buildSetupToken() };
35
- // policy === "admins"
36
- const isAdmin = roles.some((role) => access.admin.includes(role));
37
- return isAdmin
41
+ const isEnforced = policy === "systemAdmins"
42
+ ? roles.includes(ROLES.SystemAdmin)
43
+ : roles.some((role) => access.admin.includes(role));
44
+ return isEnforced
38
45
  ? { setupRequired: true, preauthSetupToken: buildSetupToken() }
39
46
  : { required: false };
40
47
  };
@@ -13,7 +13,7 @@ import { STEP_SECONDS, verifyTotp } from "./totp.js";
13
13
  // must not verify again within its ±1-step window — otherwise a code an
14
14
  // attacker observed once, via phishing proxy or shoulder-surfing, stays
15
15
  // usable for ~90s of parallel logins). Omitted where a caller has no
16
- // ctx.redis available; the code-verify itself still works, just without
16
+ // ctx.services.redis available; the code-verify itself still works, just without
17
17
  // replay protection for that call.
18
18
  export async function verifyMfaFactor(row, code, replay) {
19
19
  const secret = base32Decode(row.totpSecret);
@@ -39,8 +39,8 @@ export function createBillingInfoQueryConfig(deps) {
39
39
  const billingLive = ctx.config
40
40
  ? (await ctx.config(STRIPE_BILLING_LIVE_CONFIG_QN)) === true
41
41
  : false;
42
- const apiKeySet = ctx.secrets
43
- ? await ctx.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
42
+ const apiKeySet = ctx.services.secrets
43
+ ? await ctx.services.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
44
44
  : false;
45
45
  const sub = await ctx.db.fetchOne(subscriptionsProjectionTable, { id: subscriptionAggregateId(ctx.user.tenantId) });
46
46
  return {
@@ -112,7 +112,7 @@ export async function recordConsent(ctx, prepared, details) {
112
112
  };
113
113
  const tenantId = ctx.user.tenantId;
114
114
  const isSubscription = prepared.mode === "subscription";
115
- await ctx.unsafeAppendEvent({
115
+ await ctx.events.unsafeAppend({
116
116
  aggregateId: isSubscription ? subscriptionAggregateId(tenantId) : paymentAggregateId(tenantId),
117
117
  aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
118
118
  type: CHECKOUT_CONSENT_RECORDED_EVENT_QN,
@@ -64,7 +64,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
64
64
  message: `billing-foundation: ${payload.sourceAggregateId} is not the subscription or payment stream of tenant ${tenantId}`,
65
65
  });
66
66
  }
67
- const stream = await ctx.fetchForWriting({
67
+ const stream = await ctx.events.fetchForWriting({
68
68
  aggregateId: payload.sourceAggregateId,
69
69
  aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
70
70
  });
@@ -81,7 +81,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
81
81
  }
82
82
  }
83
83
  if (!consentEvent || !consent) {
84
- ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] no recorded consent ${payload.consentId} on the stream; no mail sent`);
84
+ ctx.obs.log?.warn(`[billing-foundation:issue-contract-confirmation] no recorded consent ${payload.consentId} on the stream; no mail sent`);
85
85
  return {
86
86
  isSuccess: true,
87
87
  data: {
@@ -104,14 +104,14 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
104
104
  }
105
105
  // Everything that can fail runs before the append, so a failed run
106
106
  // leaves no marker behind and the job retry starts clean.
107
- const recipient = recipientRowSchema.safeParse(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
107
+ const recipient = recipientRowSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
108
108
  id: consent.actorUserId,
109
109
  }));
110
110
  const recipientEmail = recipient.success && recipient.data.isDeleted !== true
111
111
  ? await decryptStoredUserEmail(recipient.data.email, "billing-foundation:issue-contract-confirmation")
112
112
  : null;
113
113
  if (!recipient.success || recipientEmail === null) {
114
- ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`);
114
+ ctx.obs.log?.warn(`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`);
115
115
  return {
116
116
  isSuccess: true,
117
117
  data: {
@@ -128,7 +128,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
128
128
  locale,
129
129
  });
130
130
  if (terms.version !== consent.termsTemplateVersion) {
131
- ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] terms text block changed since consent ${payload.consentId} (version ${consent.termsTemplateVersion} -> ${terms.version}); the mail carries the current text`);
131
+ ctx.obs.log?.warn(`[billing-foundation:issue-contract-confirmation] terms text block changed since consent ${payload.consentId} (version ${consent.termsTemplateVersion} -> ${terms.version}); the mail carries the current text`);
132
132
  }
133
133
  const i18n = createI18n(ctx.registry, { defaultLocale: "en" });
134
134
  const tierLabel = consent.tier !== null && options.catalog
@@ -49,7 +49,7 @@ export function registerConsumerProtection(r, options, consumerProtection) {
49
49
  // user), never from payload data.
50
50
  const tenantId = ctx.systemUser.tenantId;
51
51
  const currentPeriodEndIso = payload["currentPeriodEndIso"];
52
- const result = await ctx.write(SubscriptionFoundationHandlers.issueContractConfirmation, {
52
+ const result = await ctx.dispatch.write(SubscriptionFoundationHandlers.issueContractConfirmation, {
53
53
  consentId,
54
54
  sourceAggregateId: isSubscriptionEvent
55
55
  ? subscriptionAggregateId(tenantId)
@@ -37,7 +37,7 @@ export function registerContractTermination(r, options, consumerProtection) {
37
37
  // skip: trigger event carries no usable declaration
38
38
  if (!declared.success)
39
39
  return;
40
- const result = await ctx.write(SubscriptionFoundationHandlers.recordContractTermination, {
40
+ const result = await ctx.dispatch.write(SubscriptionFoundationHandlers.recordContractTermination, {
41
41
  requestId: declared.data.requestId,
42
42
  declarationType: declared.data.declarationType,
43
43
  terminationKind: declared.data.terminationKind,
@@ -0,0 +1,4 @@
1
+ export declare function hasOtherOpenProviderCancel(events: readonly {
2
+ type: string;
3
+ payload: unknown;
4
+ }[], requestId: string): boolean;
@@ -0,0 +1,14 @@
1
+ import { CONTRACT_TERMINATION_REQUESTED_EVENT_QN, contractTerminationRequestedPayloadSchema, } from "../events.js";
2
+ import { hasRevocation } from "./termination-record.js";
3
+ // A revoked declaration must not resume a subscription another declaration still cancels at the provider.
4
+ export function hasOtherOpenProviderCancel(events, requestId) {
5
+ return events.some((e) => {
6
+ if (e.type !== CONTRACT_TERMINATION_REQUESTED_EVENT_QN)
7
+ return false;
8
+ const recorded = contractTerminationRequestedPayloadSchema.safeParse(e.payload).data;
9
+ return (recorded !== undefined &&
10
+ recorded.requestId !== requestId &&
11
+ recorded.providerCancel !== "none" &&
12
+ !hasRevocation(events, recorded.requestId));
13
+ });
14
+ }
@@ -0,0 +1,2 @@
1
+ export declare const TERMINATION_PROVIDER_LOCK_NAMESPACE = 1651798124;
2
+ export declare const TERMINATION_PROVIDER_LOCK_KEY = "subscription";
@@ -0,0 +1,3 @@
1
+ // pg_advisory_xact_lock namespace (int4): 'btpl' as ASCII, disjoint from the other fixed advisory-lock keys.
2
+ export const TERMINATION_PROVIDER_LOCK_NAMESPACE = 0x6274706c;
3
+ export const TERMINATION_PROVIDER_LOCK_KEY = "subscription";
@@ -14,22 +14,22 @@ function receiptBucketKey(ctx, email) {
14
14
  * recorded, a missing receipt is the lesser harm. */
15
15
  export async function mayMailReceipt(ctx, email) {
16
16
  // skip: no rate limiter wired, nothing to count against
17
- if (!ctx.rateLimit)
17
+ if (!ctx.services.rateLimit)
18
18
  return "allowed";
19
19
  try {
20
20
  const bucketKey = receiptBucketKey(ctx, email);
21
21
  if (bucketKey === undefined) {
22
- ctx.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
22
+ ctx.obs.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
23
23
  return "unavailable";
24
24
  }
25
- const decision = await ctx.rateLimit.check(bucketKey, {
25
+ const decision = await ctx.services.rateLimit.check(bucketKey, {
26
26
  limit: RECEIPT_MAILS_PER_RECIPIENT,
27
27
  windowSeconds: RECEIPT_WINDOW_SECONDS,
28
28
  });
29
29
  return decision.allowed ? "allowed" : "limited";
30
30
  }
31
31
  catch (error) {
32
- ctx.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
32
+ ctx.obs.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
33
33
  return "unavailable";
34
34
  }
35
35
  }
@@ -12,6 +12,7 @@ import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
12
12
  import { isTerminalSubscriptionStatus, SubscriptionCancelTimings } from "../web/constants.js";
13
13
  import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE, } from "../web/consumer-protection/consent-locale.js";
14
14
  import { notifyOperator } from "./termination-notify.js";
15
+ import { TERMINATION_PROVIDER_LOCK_KEY, TERMINATION_PROVIDER_LOCK_NAMESPACE, } from "./termination-provider-lock.js";
15
16
  import { isWithinWithdrawalPeriod } from "./withdrawal-period.js";
16
17
  const REQUESTED_CANCEL_TIMINGS = [
17
18
  SubscriptionCancelTimings.periodEnd,
@@ -85,7 +86,7 @@ async function cancelAtProvider(ctx, tenantId, when, receivedAtIso) {
85
86
  });
86
87
  }
87
88
  catch (error) {
88
- ctx.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
89
+ ctx.obs.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
89
90
  return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_error" };
90
91
  }
91
92
  return {
@@ -105,7 +106,7 @@ export function createDeclareContractTerminationHandler() {
105
106
  handler: async (event, ctx) => {
106
107
  // @cast-boundary engine-payload — dispatcher-zod-validated payload
107
108
  const payload = event.payload;
108
- const stream = await ctx.fetchForWriting({
109
+ const stream = await ctx.events.fetchForWriting({
109
110
  aggregateId: subscriptionAggregateId(event.user.tenantId),
110
111
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
111
112
  });
@@ -127,30 +128,6 @@ function revokedRecordResult(receivedAtIso) {
127
128
  },
128
129
  };
129
130
  }
130
- // A revocation that landed during the provider cancel found nothing to take back yet.
131
- async function takeBackLateRevokedCancel(ctx, consumerProtection, args) {
132
- const { payload, tenantId, receivedAtIso, providerCancel } = args;
133
- // skip: nothing was cancelled at the provider, nothing to resume
134
- if (providerCancel !== "period-end")
135
- return;
136
- const reasons = await resumeAtProvider(ctx, await getSubscriptionForTenant(ctx, tenantId));
137
- // skip: resumed cleanly, the revocation notice already tells the operator
138
- if (reasons.length === 0)
139
- return;
140
- await notifyOperator(ctx, consumerProtection, {
141
- locale: payload.locale ?? FALLBACK_CONSENT_LOCALE,
142
- requestId: payload.requestId,
143
- receivedAtIso,
144
- channel: "public",
145
- declaration: {
146
- declarationType: payload.declarationType,
147
- terminationKind: payload.terminationKind,
148
- },
149
- reasons,
150
- tenantId,
151
- providerCancel,
152
- });
153
- }
154
131
  export function createRecordContractTerminationHandler(options, consumerProtection) {
155
132
  return {
156
133
  name: "record-contract-termination",
@@ -161,13 +138,15 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
161
138
  // @cast-boundary engine-payload — dispatcher-zod-validated payload
162
139
  const payload = event.payload;
163
140
  const tenantId = event.user.tenantId;
141
+ // The provider cancel/resume and the stream append must not interleave across declarations of the same subscription.
142
+ await ctx.db.advisoryXactLock(TERMINATION_PROVIDER_LOCK_NAMESPACE, TERMINATION_PROVIDER_LOCK_KEY);
164
143
  if (payload.channel === "public" && payload.when === SubscriptionCancelTimings.immediately) {
165
144
  throw new UnprocessableError("immediate_cancel_not_allowed", {
166
145
  message: "billing-foundation: the public channel never cancels at the provider immediately",
167
146
  });
168
147
  }
169
148
  const receivedAtIso = payload.receivedAtIso ?? options.now().toString();
170
- const stream = await ctx.fetchForWriting({
149
+ const stream = await ctx.events.fetchForWriting({
171
150
  aggregateId: subscriptionAggregateId(tenantId),
172
151
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
173
152
  });
@@ -193,28 +172,12 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
193
172
  effectiveAtIso,
194
173
  providerCancel,
195
174
  };
196
- // Re-fetch after the external call: a webhook appended meanwhile would
197
- // otherwise fail the append on a stale expectedVersion although the
198
- // provider already cancelled.
199
- const streamAfterProviderCall = await ctx.fetchForWriting({
175
+ // Re-fetch after the external call: webhooks append to this stream without the lock, so a
176
+ // stale expectedVersion would fail the append although the provider already cancelled.
177
+ const streamAfterProviderCall = await ctx.events.fetchForWriting({
200
178
  aggregateId: subscriptionAggregateId(tenantId),
201
179
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
202
180
  });
203
- // A concurrent run with the same requestId may have recorded while we were
204
- // at the provider; a second append would duplicate the event and the notice.
205
- const recordedMeanwhile = findRecordedTermination(streamAfterProviderCall.events, payload.requestId);
206
- if (recordedMeanwhile)
207
- return recordedTerminationResult(recordedMeanwhile);
208
- // A revocation that landed during the provider call found no cancel to take back.
209
- if (hasRevocation(streamAfterProviderCall.events, payload.requestId)) {
210
- await takeBackLateRevokedCancel(ctx, consumerProtection, {
211
- payload,
212
- tenantId,
213
- receivedAtIso,
214
- providerCancel,
215
- });
216
- return revokedRecordResult(receivedAtIso);
217
- }
218
181
  await streamAfterProviderCall.appendOne({
219
182
  type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
220
183
  payload: recorded,
@@ -285,7 +248,7 @@ export async function resumeAtProvider(ctx, subscription) {
285
248
  });
286
249
  }
287
250
  catch (error) {
288
- ctx.log?.warn(`[billing-foundation:revoke-contract-termination] provider resume failed: ${error instanceof Error ? error.message : String(error)}`);
251
+ ctx.obs.log?.warn(`[billing-foundation:revoke-contract-termination] provider resume failed: ${error instanceof Error ? error.message : String(error)}`);
289
252
  return ["revocation_resume_failed"];
290
253
  }
291
254
  return [];
@@ -307,7 +270,7 @@ export function createRecordUnmatchedContractTerminationHandler() {
307
270
  handler: async (event, ctx) => {
308
271
  // @cast-boundary engine-payload — dispatcher-zod-validated payload
309
272
  const payload = event.payload;
310
- const stream = await ctx.fetchForWriting({
273
+ const stream = await ctx.events.fetchForWriting({
311
274
  aggregateId: terminationUnmatchedAggregateId(payload.requestId),
312
275
  aggregateType: CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE,
313
276
  });
@@ -95,10 +95,12 @@ async function findTerminableTenantIds(ctx, options, requestTenantId, email) {
95
95
  declareEscapeHatch({
96
96
  reason: "anonymous declarant has no session — finds the contract by email via user:find-for-auth and tenant:memberships as a system user; the calling handler declares its own escapeHatch",
97
97
  });
98
- const user = authUserRowSchema.safeParse(await ctx.queryAs(createSystemUser(requestTenantId), UserQueries.findForAuth, { email }));
98
+ const user = authUserRowSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(requestTenantId), UserQueries.findForAuth, {
99
+ email,
100
+ }));
99
101
  if (!user.success || user.data.isDeleted === true)
100
102
  return [];
101
- const memberships = membershipRowsSchema.safeParse(await ctx.queryAs(createSystemUser(requestTenantId), TenantQueries.memberships, {
103
+ const memberships = membershipRowsSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(requestTenantId), TenantQueries.memberships, {
102
104
  userId: user.data.id,
103
105
  }));
104
106
  if (!memberships.success)
@@ -111,7 +113,7 @@ async function findTerminableTenantIds(ctx, options, requestTenantId, email) {
111
113
  ];
112
114
  const terminable = [];
113
115
  for (const tenantId of adminTenantIds) {
114
- const probe = terminableResultSchema.safeParse(await ctx.queryAs(createSystemUser(tenantId), SubscriptionFoundationQueries.terminableSubscription, {}));
116
+ const probe = terminableResultSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(tenantId), SubscriptionFoundationQueries.terminableSubscription, {}));
115
117
  if (probe.success && probe.data.terminable)
116
118
  terminable.push(tenantId);
117
119
  }
@@ -126,7 +128,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
126
128
  rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
127
129
  ...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
128
130
  escapeHatch: {
129
- reason: "Anonymous declarant has no session — finds the contract by email via ctx.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
131
+ reason: "Anonymous declarant has no session — finds the contract by email via ctx.dispatch.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
130
132
  },
131
133
  handler: async (event, ctx) => {
132
134
  // @cast-boundary engine-payload — dispatcher-zod-validated payload
@@ -143,7 +145,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
143
145
  if (candidates.length === 1 && onlyCandidate !== undefined) {
144
146
  matchedTenantId = onlyCandidate;
145
147
  const revocation = createRevocationToken();
146
- const declared = await ctx.writeAs(createSystemUser(onlyCandidate), SubscriptionFoundationHandlers.declareContractTermination, {
148
+ const declared = await ctx.dispatch.writeAs(createSystemUser(onlyCandidate), SubscriptionFoundationHandlers.declareContractTermination, {
147
149
  requestId,
148
150
  declarationType,
149
151
  terminationKind,
@@ -166,7 +168,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
166
168
  }
167
169
  else {
168
170
  reasons.push(candidates.length === 0 ? "unmatched" : "ambiguous");
169
- const recorded = await ctx.writeAs(createSystemUser(SYSTEM_TENANT_ID), SubscriptionFoundationHandlers.recordUnmatchedContractTermination, {
171
+ const recorded = await ctx.dispatch.writeAs(createSystemUser(SYSTEM_TENANT_ID), SubscriptionFoundationHandlers.recordUnmatchedContractTermination, {
170
172
  requestId,
171
173
  declarationType,
172
174
  terminationKind,
@@ -218,7 +220,7 @@ export function createTerminateContractHandler(options, consumerProtection) {
218
220
  const tenantId = event.user.tenantId;
219
221
  const requestId = generateId();
220
222
  const { declarationType, terminationKind } = payload;
221
- const caller = authUserRowSchema.safeParse(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
223
+ const caller = authUserRowSchema.safeParse(await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
222
224
  id: event.user.id,
223
225
  }));
224
226
  const callerEmail = caller.success && caller.data.email && caller.data.isDeleted !== true
@@ -229,7 +231,7 @@ export function createTerminateContractHandler(options, consumerProtection) {
229
231
  message: "billing-foundation:terminate-contract: calling user has no email on record",
230
232
  });
231
233
  }
232
- const recorded = await ctx.writeAs(createSystemUser(tenantId), SubscriptionFoundationHandlers.recordContractTermination, {
234
+ const recorded = await ctx.dispatch.writeAs(createSystemUser(tenantId), SubscriptionFoundationHandlers.recordContractTermination, {
233
235
  requestId,
234
236
  declarationType,
235
237
  terminationKind,