@cosmicdrift/kumiko-bundled-features 0.389.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 (315) hide show
  1. package/dist/audit/feature.js +1 -1
  2. package/dist/auth-email-password/auth-user-row.js +1 -1
  3. package/dist/auth-email-password/bootstrap.js +6 -1
  4. package/dist/auth-email-password/feature.js +1 -1
  5. package/dist/auth-email-password/handlers/change-password.write.js +7 -7
  6. package/dist/auth-email-password/handlers/confirm-account-unlock.write.js +3 -3
  7. package/dist/auth-email-password/handlers/confirm-token-flow.js +4 -4
  8. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +8 -7
  9. package/dist/auth-email-password/handlers/invite-accept.write.js +10 -7
  10. package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
  11. package/dist/auth-email-password/handlers/invite-info.query.js +2 -2
  12. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +8 -7
  13. package/dist/auth-email-password/handlers/login.write.js +17 -15
  14. package/dist/auth-email-password/handlers/reset-password.write.js +2 -2
  15. package/dist/auth-email-password/handlers/signup-confirm.write.js +12 -12
  16. package/dist/auth-email-password/handlers/signup-request.write.js +12 -8
  17. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +4 -2
  18. package/dist/auth-email-password/handlers/token-request-handler.js +2 -2
  19. package/dist/auth-email-password/handlers/verify-email.write.js +2 -2
  20. package/dist/auth-email-password/seeding.d.ts +1 -1
  21. package/dist/auth-email-password/seeding.js +9 -8
  22. package/dist/auth-foundation/feature.js +12 -4
  23. package/dist/auth-mfa/handlers/disable.write.js +5 -5
  24. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +19 -14
  25. package/dist/auth-mfa/handlers/enable-confirm.write.js +3 -3
  26. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +6 -1
  27. package/dist/auth-mfa/handlers/enable-start.write.js +1 -1
  28. package/dist/auth-mfa/handlers/reencrypt.job.js +14 -9
  29. package/dist/auth-mfa/handlers/regenerate-recovery.write.js +4 -4
  30. package/dist/auth-mfa/handlers/verify.write.js +19 -14
  31. package/dist/auth-mfa/mfa-code-verifier.js +9 -4
  32. package/dist/auth-mfa/mfa-status-checker.js +8 -3
  33. package/dist/auth-mfa/verify-factor.js +1 -1
  34. package/dist/billing-foundation/billing-info-query.js +2 -2
  35. package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -1
  36. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +5 -5
  37. package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
  38. package/dist/billing-foundation/consumer-protection/register-termination.js +1 -1
  39. package/dist/billing-foundation/consumer-protection/termination-open-cancels.d.ts +4 -0
  40. package/dist/billing-foundation/consumer-protection/termination-open-cancels.js +14 -0
  41. package/dist/billing-foundation/consumer-protection/termination-provider-lock.d.ts +2 -0
  42. package/dist/billing-foundation/consumer-protection/termination-provider-lock.js +3 -0
  43. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +4 -4
  44. package/dist/billing-foundation/consumer-protection/termination-record.js +11 -48
  45. package/dist/billing-foundation/consumer-protection/termination-request.js +10 -8
  46. package/dist/billing-foundation/consumer-protection/termination-revoke.js +10 -12
  47. package/dist/billing-foundation/events.js +1 -1
  48. package/dist/billing-foundation/feature.js +3 -3
  49. package/dist/billing-foundation/get-subscription-for-tenant.d.ts +1 -1
  50. package/dist/billing-foundation/handlers/process-event.write.js +5 -5
  51. package/dist/billing-foundation/handlers/process-payment-event.write.js +3 -3
  52. package/dist/billing-foundation/plan-catalog.js +2 -2
  53. package/dist/billing-foundation/projection.js +1 -1
  54. package/dist/billing-foundation/types.js +1 -1
  55. package/dist/cap-counter/aggregate-id.d.ts +1 -1
  56. package/dist/cap-counter/aggregate-id.js +1 -1
  57. package/dist/cap-counter/book-cap-usage.d.ts +2 -2
  58. package/dist/cap-counter/book-cap-usage.js +3 -3
  59. package/dist/cap-counter/cap-reservation.js +1 -1
  60. package/dist/cap-counter/constants.js +1 -1
  61. package/dist/cap-counter/feature.js +2 -2
  62. package/dist/cap-counter/handlers/increment-rolling.write.js +1 -1
  63. package/dist/cap-counter/reservation-chain.js +1 -1
  64. package/dist/cap-counter/with-cap-enforcement.d.ts +3 -3
  65. package/dist/cap-counter/with-cap-enforcement.js +1 -1
  66. package/dist/cap-overview/handlers/caps-usage.query.js +1 -1
  67. package/dist/cap-overview/handlers/tenant-caps-list.query.js +1 -1
  68. package/dist/compliance-profiles/seeding.d.ts +1 -1
  69. package/dist/compliance-profiles/seeding.js +9 -3
  70. package/dist/config/feature.js +6 -6
  71. package/dist/config/handlers/cascade.query.js +1 -1
  72. package/dist/config/handlers/readiness.query.js +1 -1
  73. package/dist/config/handlers/reencrypt.job.js +23 -13
  74. package/dist/config/handlers/reset.write.js +3 -3
  75. package/dist/config/handlers/selected-extensions.query.js +1 -1
  76. package/dist/config/handlers/set.write.js +4 -4
  77. package/dist/config/handlers/values.query.js +1 -1
  78. package/dist/crypto-shredding/handlers/forget-subject.write.js +8 -8
  79. package/dist/crypto-shredding/resolve-file-bytes-deleter.js +3 -3
  80. package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
  81. package/dist/custom-fields/handlers/delete-system-field.write.js +1 -1
  82. package/dist/custom-fields/handlers/delete-tenant-field.write.js +1 -1
  83. package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
  84. package/dist/custom-fields/index.d.ts +1 -0
  85. package/dist/custom-fields/index.js +1 -0
  86. package/dist/data-retention/feature.js +1 -1
  87. package/dist/data-retention/run-retention-cleanup.js +12 -2
  88. package/dist/delivery/attempt-log.js +1 -1
  89. package/dist/delivery/delivery-service.js +5 -5
  90. package/dist/delivery/feature.js +2 -2
  91. package/dist/delivery/jobs.js +11 -11
  92. package/dist/document-ingest-foundation/events.js +1 -1
  93. package/dist/feature-toggles/handlers/registered.query.js +1 -1
  94. package/dist/feature-toggles/handlers/set.write.js +1 -1
  95. package/dist/feature-toggles/toggle-sync-signal.js +1 -1
  96. package/dist/file-derivatives/feature.d.ts +1 -1
  97. package/dist/file-derivatives/feature.js +1 -1
  98. package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
  99. package/dist/file-derivatives/handlers/public-variant.query.js +5 -5
  100. package/dist/file-foundation/feature.js +1 -1
  101. package/dist/file-foundation/read-file-ref-content.d.ts +4 -2
  102. package/dist/file-foundation/read-file-ref-content.js +3 -3
  103. package/dist/file-provider-s3/feature.js +1 -1
  104. package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +8 -8
  105. package/dist/form-draft/feature.js +1 -1
  106. package/dist/form-draft/handlers/cleanup.job.js +22 -12
  107. package/dist/form-draft/handlers/discard.write.js +3 -3
  108. package/dist/form-draft/handlers/get.query.js +1 -1
  109. package/dist/foundation-shared/config-helpers.d.ts +1 -1
  110. package/dist/foundation-shared/config-helpers.js +1 -1
  111. package/dist/inbound-mail-foundation/events.js +1 -1
  112. package/dist/inbound-mail-foundation/feature.js +1 -1
  113. package/dist/inbound-mail-foundation/handlers/account-state.d.ts +3 -1
  114. package/dist/inbound-mail-foundation/handlers/account-state.js +1 -1
  115. package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
  116. package/dist/inbound-mail-foundation/handlers/disconnect-account.write.js +1 -1
  117. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +10 -10
  118. package/dist/inbound-mail-foundation/handlers/update-account.write.js +1 -1
  119. package/dist/inbound-mail-foundation/projection.js +1 -1
  120. package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
  121. package/dist/inbound-provider-imap/feature.js +1 -1
  122. package/dist/jobs/events.js +1 -1
  123. package/dist/jobs/handlers/consumer.write.js +1 -1
  124. package/dist/jobs/handlers/projection-rebuild.job.js +1 -1
  125. package/dist/jobs/handlers/reindex-entity.job.js +5 -5
  126. package/dist/jobs/handlers/retention-cleanup.job.js +1 -1
  127. package/dist/jobs/handlers/retry.write.js +1 -1
  128. package/dist/jobs/handlers/stale-run-sweep.job.js +1 -1
  129. package/dist/jobs/handlers/trigger.write.js +2 -2
  130. package/dist/jobs/job-run-logger.d.ts +1 -0
  131. package/dist/jobs/job-run-logger.js +38 -18
  132. package/dist/legal-pages/lib/boot-check.d.ts +10 -4
  133. package/dist/legal-pages/lib/boot-check.js +2 -2
  134. package/dist/mail-foundation/feature.d.ts +2 -2
  135. package/dist/mail-transport-smtp/feature.js +1 -1
  136. package/dist/managed-pages/seeding.d.ts +1 -1
  137. package/dist/managed-pages/seeding.js +10 -4
  138. package/dist/notes-history/handlers/add-note.write.js +1 -1
  139. package/dist/personal-access-tokens/feature.js +1 -1
  140. package/dist/personal-access-tokens/handlers/create.write.js +2 -2
  141. package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
  142. package/dist/rate-limiting/handlers/status.query.js +2 -2
  143. package/dist/readiness/handlers/status.query.js +1 -1
  144. package/dist/renderer-foundation/api.d.ts +4 -2
  145. package/dist/renderer-foundation/api.js +2 -2
  146. package/dist/secrets/announce-secret-change.d.ts +6 -2
  147. package/dist/secrets/announce-secret-change.js +3 -3
  148. package/dist/secrets/feature.d.ts +1 -1
  149. package/dist/secrets/feature.js +8 -8
  150. package/dist/secrets/handlers/delete.write.js +1 -1
  151. package/dist/secrets/handlers/rotate.job.js +13 -8
  152. package/dist/secrets/handlers/set.write.js +1 -1
  153. package/dist/secrets/secrets-context.js +16 -6
  154. package/dist/seo/feature.js +1 -1
  155. package/dist/sessions/feature.js +1 -1
  156. package/dist/sessions/handlers/cleanup.job.js +2 -2
  157. package/dist/sessions/handlers/detail.query.d.ts +1 -0
  158. package/dist/sessions/handlers/detail.query.js +5 -2
  159. package/dist/sessions/handlers/list.query.d.ts +1 -0
  160. package/dist/sessions/handlers/list.query.js +5 -2
  161. package/dist/sessions/handlers/mine.query.d.ts +1 -0
  162. package/dist/sessions/handlers/mine.query.js +5 -2
  163. package/dist/sessions/handlers/revoke-all-for-user.write.js +3 -3
  164. package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
  165. package/dist/sessions/handlers/revoke.write.js +1 -1
  166. package/dist/sessions/screens.js +8 -4
  167. package/dist/sessions/session-revoked-event.js +1 -1
  168. package/dist/sessions/web/session-device-cell.js +10 -12
  169. package/dist/sessions/web/user-agent.d.ts +1 -0
  170. package/dist/sessions/web/user-agent.js +11 -0
  171. package/dist/shared/index.d.ts +2 -0
  172. package/dist/shared/index.js +2 -0
  173. package/dist/shared/purge-files-reaching-subject.js +12 -2
  174. package/dist/shared/require-for-tenant.d.ts +1 -1
  175. package/dist/shared/require-for-tenant.js +6 -4
  176. package/dist/shared/stream-tenant-backfill.d.ts +28 -0
  177. package/dist/shared/stream-tenant-backfill.js +119 -0
  178. package/dist/shared/stream-tenant-derived-state.d.ts +25 -0
  179. package/dist/shared/stream-tenant-derived-state.js +68 -0
  180. package/dist/step-dispatcher/feature.js +1 -1
  181. package/dist/subscription-dodo/plugin-methods.js +1 -1
  182. package/dist/subscription-dodo/runtime.js +4 -4
  183. package/dist/subscription-stripe/plugin-methods.js +3 -3
  184. package/dist/subscription-stripe/runtime.js +4 -4
  185. package/dist/template-resolver/api.d.ts +4 -2
  186. package/dist/template-resolver/api.js +2 -2
  187. package/dist/template-resolver/handlers/bundle-import.d.ts +4 -3
  188. package/dist/template-resolver/handlers/export-bundle.query.js +1 -1
  189. package/dist/template-resolver/handlers/overview.query.js +2 -2
  190. package/dist/template-resolver/handlers/send-test-shared.js +1 -1
  191. package/dist/template-resolver/handlers/send-test.write.js +2 -2
  192. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +3 -1
  193. package/dist/template-resolver/handlers/tenant-override-selection.js +1 -1
  194. package/dist/template-resolver/handlers/tenant-overrides.query.js +1 -1
  195. package/dist/template-resolver/handlers/usage.query.js +1 -1
  196. package/dist/template-resolver/seeding.js +12 -2
  197. package/dist/template-resolver/store-template-resource.d.ts +7 -6
  198. package/dist/template-resolver/store-template-resource.js +3 -3
  199. package/dist/template-resolver/template-resources.d.ts +2 -2
  200. package/dist/template-resolver/template-resources.js +2 -2
  201. package/dist/template-resolver/testing.js +2 -2
  202. package/dist/template-resolver/web/document-insert-fields.d.ts +4 -0
  203. package/dist/template-resolver/web/document-insert-fields.js +69 -0
  204. package/dist/template-resolver/web/document-insert-model.d.ts +43 -0
  205. package/dist/template-resolver/web/document-insert-model.js +106 -0
  206. package/dist/template-resolver/web/document-insert-panel.d.ts +13 -0
  207. package/dist/template-resolver/web/document-insert-panel.js +49 -0
  208. package/dist/template-resolver/web/document-tiptap-bubble.d.ts +4 -0
  209. package/dist/template-resolver/web/document-tiptap-bubble.js +85 -0
  210. package/dist/template-resolver/web/document-tiptap-content.d.ts +3 -0
  211. package/dist/template-resolver/web/document-tiptap-content.js +12 -0
  212. package/dist/template-resolver/web/document-tiptap-context.d.ts +41 -0
  213. package/dist/template-resolver/web/document-tiptap-context.js +43 -0
  214. package/dist/template-resolver/web/document-tiptap-editor.js +50 -495
  215. package/dist/template-resolver/web/document-tiptap-helpers.d.ts +44 -0
  216. package/dist/template-resolver/web/document-tiptap-helpers.js +129 -0
  217. package/dist/template-resolver/web/document-tiptap-layout.d.ts +4 -0
  218. package/dist/template-resolver/web/document-tiptap-layout.js +24 -0
  219. package/dist/template-resolver/web/document-tiptap-toolbar-groups.d.ts +6 -0
  220. package/dist/template-resolver/web/document-tiptap-toolbar-groups.js +33 -0
  221. package/dist/template-resolver/web/document-tiptap-toolbar.d.ts +3 -0
  222. package/dist/template-resolver/web/document-tiptap-toolbar.js +91 -0
  223. package/dist/template-resolver/web/editor-handle.d.ts +1 -0
  224. package/dist/template-resolver/web/editor-handle.js +15 -0
  225. package/dist/template-resolver/web/placeholder-line-editor.js +16 -5
  226. package/dist/template-resolver/web/use-document-tiptap-model.d.ts +54 -0
  227. package/dist/template-resolver/web/use-document-tiptap-model.js +96 -0
  228. package/dist/tenant/handlers/cancel-invitation.write.js +1 -1
  229. package/dist/tenant/handlers/memberships.query.js +1 -1
  230. package/dist/tenant/handlers/remove-member.write.js +2 -2
  231. package/dist/tenant/handlers/update-member-roles.write.js +1 -1
  232. package/dist/tenant/index.d.ts +1 -0
  233. package/dist/tenant/index.js +1 -0
  234. package/dist/tenant/invited-membership.js +6 -1
  235. package/dist/tenant/schema/tenant.d.ts +2 -2
  236. package/dist/tenant/schema/tenant.js +4 -1
  237. package/dist/tenant/seeding.d.ts +2 -2
  238. package/dist/tenant/seeding.js +52 -52
  239. package/dist/tenant/stream-tenant-backfill.d.ts +21 -0
  240. package/dist/tenant/stream-tenant-backfill.js +63 -0
  241. package/dist/tenant/testing.d.ts +12 -0
  242. package/dist/tenant/testing.js +33 -0
  243. package/dist/tenant-handover/events.js +1 -1
  244. package/dist/tenant-handover/handlers/claim.write.js +1 -1
  245. package/dist/tenant-lifecycle/feature.js +5 -5
  246. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +14 -5
  247. package/dist/tenant-lifecycle/handlers/request-destruction.write.js +12 -4
  248. package/dist/tenant-lifecycle/run-tenant-destroy.js +23 -13
  249. package/dist/tenant-lifecycle/stages.js +6 -1
  250. package/dist/tenant-lifecycle/testing.d.ts +1 -1
  251. package/dist/tenant-lifecycle/testing.js +6 -5
  252. package/dist/tier-engine/entity.js +1 -1
  253. package/dist/tier-engine/feature.js +8 -3
  254. package/dist/tier-engine/handlers/active-tier.query.js +3 -3
  255. package/dist/tier-engine/handlers/get-tenant-tier.query.js +6 -1
  256. package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -4
  257. package/dist/user/db/queries/stream-tenant-backfill.d.ts +20 -1
  258. package/dist/user/db/queries/stream-tenant-backfill.js +40 -77
  259. package/dist/user/handlers/find-for-auth.query.js +2 -2
  260. package/dist/user/handlers/update-roles.js +2 -2
  261. package/dist/user/index.d.ts +1 -1
  262. package/dist/user/index.js +1 -1
  263. package/dist/user/invited-global-roles.js +6 -1
  264. package/dist/user/principal-status.js +1 -1
  265. package/dist/user/seeding.d.ts +1 -1
  266. package/dist/user/seeding.js +20 -5
  267. package/dist/user/testing.d.ts +3 -0
  268. package/dist/user/testing.js +10 -0
  269. package/dist/user-data-rights/audit-download.js +12 -2
  270. package/dist/user-data-rights/feature.js +29 -26
  271. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
  272. package/dist/user-data-rights/handlers/deletion-grace-period.js +1 -1
  273. package/dist/user-data-rights/handlers/download-by-job.query.js +2 -2
  274. package/dist/user-data-rights/handlers/download-by-token.query.js +2 -2
  275. package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
  276. package/dist/user-data-rights/handlers/restrict-account.write.js +4 -4
  277. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +8 -6
  278. package/dist/user-data-rights/index.d.ts +1 -1
  279. package/dist/user-data-rights/index.js +1 -1
  280. package/dist/user-data-rights/lib/mail-transport-resolver.js +1 -1
  281. package/dist/user-data-rights/lib/storage-provider-resolver.js +1 -1
  282. package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -1
  283. package/dist/user-data-rights/run-export-jobs.js +7 -2
  284. package/dist/user-data-rights/run-forget-cleanup.d.ts +1 -0
  285. package/dist/user-data-rights/run-forget-cleanup.js +9 -1
  286. package/dist/user-data-rights/run-user-export.js +7 -1
  287. package/dist/user-data-rights/web/constants.d.ts +8 -0
  288. package/dist/user-data-rights/web/constants.js +8 -0
  289. package/dist/user-data-rights/web/index.d.ts +1 -1
  290. package/dist/user-data-rights/web/index.js +1 -1
  291. package/dist/user-profile/handlers/change-email.write.js +8 -6
  292. package/dist/waitlist/handlers/export-purge.write.js +1 -1
  293. package/dist/waitlist/handlers/invite.write.js +2 -2
  294. package/dist/waitlist/handlers/reject.write.js +1 -1
  295. package/dist/waitlist/handlers/submit.write.js +9 -4
  296. package/dist/waitlist/legacy-migrate.js +6 -1
  297. package/dist/waitlist/link-on-signup.js +6 -1
  298. package/dist/workflow-runner/db/queries/due-runs.js +1 -1
  299. package/dist/workflow-runner/event-subscriber.js +4 -3
  300. package/dist/workflow-runner/event-trigger.js +2 -1
  301. package/dist/workflow-runner/feature.js +2 -2
  302. package/dist/workflow-runner/handlers/resume-run.write.js +6 -6
  303. package/dist/workflow-runner/runner.d.ts +3 -1
  304. package/dist/workflow-runner/runner.js +3 -3
  305. package/package.json +41 -23
  306. package/src/auth-email-password/changes.json +6 -0
  307. package/src/billing-foundation/changes.json +6 -0
  308. package/src/custom-fields/changes.json +6 -0
  309. package/src/jobs/changes.json +6 -0
  310. package/src/mail-transport-smtp/changes.json +6 -0
  311. package/src/sessions/changes.json +12 -0
  312. package/src/template-resolver/changes.json +18 -0
  313. package/src/tenant/changes.json +19 -0
  314. package/src/user-data-rights/changes.json +24 -0
  315. package/src/workflow-runner/changes.json +6 -0
@@ -57,7 +57,7 @@ export function createAuditFeature() {
57
57
  await runEscapeHatchRetention({
58
58
  db: ctx.db.unsafeRaw(),
59
59
  registry: ctx.registry,
60
- configResolver: ctx.configResolver,
60
+ configResolver: ctx.services.configResolver,
61
61
  userId: SYSTEM_USER_ID,
62
62
  });
63
63
  },
@@ -1,4 +1,4 @@
1
- // Narrow cross-feature query results (ctx.queryAs → Promise<unknown>) into
1
+ // Narrow cross-feature query results (ctx.dispatch.queryAs → Promise<unknown>) into
2
2
  // the shape the auth handlers actually read. Replaces a bare
3
3
  // `as AuthUserRow | null` at the system boundary — the coding standard
4
4
  // requires a TypeGuard in place of unchecked casts from unknown.
@@ -7,6 +7,7 @@
7
7
  import { makeDispatchSystemWrite, makeDispatchWriteAs, } from "@cosmicdrift/kumiko-framework/api";
8
8
  import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
9
9
  import { ConfigScopes, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
10
+ import { runInSystemRowLevelSecurityScope } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
10
11
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
11
12
  // kumiko-lint-ignore cross-feature-import bootstrap sets per-tenant config through the config feature's write handler
12
13
  import { ConfigHandlers } from "../config/web/constants.js";
@@ -139,7 +140,11 @@ async function decideInvite(deps, invite) {
139
140
  function isMailingOutcome(outcome) {
140
141
  return outcome === "invited" || outcome === "resent";
141
142
  }
142
- export async function bootstrapTenants(deps, plan) {
143
+ // A one-shot operator process, not a tenant request: it looks up users, memberships and invitations across tenants.
144
+ export function bootstrapTenants(deps, plan) {
145
+ return runInSystemRowLevelSecurityScope("boot tenant bootstrap creates tenants and memberships before any tenant exists", () => bootstrapTenantsInScope(deps, plan));
146
+ }
147
+ async function bootstrapTenantsInScope(deps, plan) {
143
148
  const plannedInvites = planInvites(plan);
144
149
  const dispatchSystemWrite = makeDispatchSystemWrite(deps.dispatcher);
145
150
  const dispatchWriteAsRoles = makeDispatchWriteAs(deps.dispatcher);
@@ -48,7 +48,7 @@ export const authEmailPasswordEnvSchema = z.object({
48
48
  .describe("Optional `iss` claim pinned on every minted JWT."),
49
49
  });
50
50
  // Auth feature — email+password login. Depends on the user feature for
51
- // identity lookups (via ctx.queryAs) and on the tenant feature for
51
+ // identity lookups (via ctx.dispatch.queryAs) and on the tenant feature for
52
52
  // membership resolution. No direct imports of foreign tables.
53
53
  export function createAuthEmailPasswordFeature(opts = {}) {
54
54
  // ≥32 chars, symmetric to the JWT_SECRET env check: a short HMAC secret
@@ -5,7 +5,7 @@ import { UserHandlers, UserQueries } from "../../user/index.js";
5
5
  import { invalidCredentials } from "../errors.js";
6
6
  import { passwordSchema } from "../password-policy.js";
7
7
  // Change-password — authenticated. The user supplies their current password
8
- // (re-auth) and the new one. The new hash is written via ctx.writeAs(system)
8
+ // (re-auth) and the new one. The new hash is written via ctx.dispatch.writeAs(system)
9
9
  // against the user feature's update handler; field-access on passwordHash
10
10
  // (privileged-only) lets the system identity through.
11
11
  export const changePasswordWrite = defineWriteHandler({
@@ -21,8 +21,8 @@ export const changePasswordWrite = defineWriteHandler({
21
21
  },
22
22
  escapeHatch: {
23
23
  grants: ["systemIdentity"],
24
- reason: "Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, ...) " +
25
- "and writes the new hash via ctx.writeAs(SYSTEM, user:update) — the caller's own " +
24
+ reason: "Reads the caller's own passwordHash (privileged-only field) via ctx.dispatch.queryAs(SYSTEM, ...) " +
25
+ "and writes the new hash via ctx.dispatch.writeAs(SYSTEM, user:update) — the caller's own " +
26
26
  "identity can't reach either, field-access on passwordHash is privileged-only.",
27
27
  },
28
28
  // Payload carries the old and new password.
@@ -31,7 +31,7 @@ export const changePasswordWrite = defineWriteHandler({
31
31
  handler: async (event, ctx) => {
32
32
  const systemUser = createSystemUser(event.user.tenantId);
33
33
  // Load self with passwordHash — only visible to the privileged caller.
34
- const me = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
34
+ const me = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
35
35
  id: event.user.id,
36
36
  })); // @cast-boundary db-runner
37
37
  if (!me?.passwordHash) {
@@ -43,13 +43,13 @@ export const changePasswordWrite = defineWriteHandler({
43
43
  }
44
44
  const newHash = await hashPassword(event.payload.newPassword);
45
45
  // The user aggregate is systemStream: its event stream lives on
46
- // SYSTEM_TENANT_ID deterministically. Previously scattered streams need the
47
- // one-time backfillUserStreamTenants migration.
46
+ // SYSTEM_TENANT_ID deterministically. Previously scattered streams are moved by the
47
+ // boot-time stream-tenant backfill (runStreamTenantBackfillOnBoot).
48
48
  const writer = createSystemUser(SYSTEM_TENANT_ID);
49
49
  // Apply via user feature's update handler — writeAs(system) satisfies
50
50
  // the privileged-only write rule on passwordHash. Pass the current version
51
51
  // through so optimistic locking still applies end-to-end.
52
- const writeRes = await ctx.writeAs(writer, UserHandlers.update, {
52
+ const writeRes = await ctx.dispatch.writeAs(writer, UserHandlers.update, {
53
53
  id: me.id,
54
54
  version: me.version,
55
55
  changes: { passwordHash: newHash },
@@ -29,9 +29,9 @@ export function createConfirmAccountUnlockHandler(opts) {
29
29
  i18nKey: "auth.errors.unlockNotConfigured",
30
30
  }));
31
31
  }
32
- if (!ctx.redis) {
32
+ if (!ctx.services.redis) {
33
33
  return writeFailure(new InternalError({
34
- message: "account-unlock requires ctx.redis to clear lockout state",
34
+ message: "account-unlock requires ctx.services.redis to clear lockout state",
35
35
  }));
36
36
  }
37
37
  // All verify failures (malformed / bad_signature / expired) fold into
@@ -40,7 +40,7 @@ export function createConfirmAccountUnlockHandler(opts) {
40
40
  const verify = verifyUnlockToken(event.payload.token, opts.hmacSecret);
41
41
  if (!verify.ok)
42
42
  return invalidUnlockToken();
43
- await clearLockoutState(ctx.redis, verify.userId);
43
+ await clearLockoutState(ctx.services.redis, verify.userId);
44
44
  return { isSuccess: true, data: { kind: "account-unlocked" } };
45
45
  },
46
46
  });
@@ -30,10 +30,10 @@ export async function runConfirmTokenFlow(ctx, userId, expiresAtMs, spec) {
30
30
  declareEscapeHatch({
31
31
  reason: "writes the confirming user's row via UserHandlers.update on the caller's handler context, using a SYSTEM_TENANT-scoped identity; the calling handler declares its own escapeHatch",
32
32
  });
33
- if (!ctx.redis) {
33
+ if (!ctx.services.redis) {
34
34
  return writeFailure(new InternalError({ message: spec.redisRequiredMessage }));
35
35
  }
36
- const redis = ctx.redis;
36
+ const redis = ctx.services.redis;
37
37
  const burn = await burnToken(redis, spec.purpose, userId, expiresAtMs);
38
38
  if (burn === "already-used")
39
39
  return spec.invalidToken();
@@ -52,7 +52,7 @@ export async function runConfirmTokenFlow(ctx, userId, expiresAtMs, spec) {
52
52
  return { isSuccess: true, data: spec.alreadyDone.data };
53
53
  }
54
54
  const changes = await spec.buildChanges(me);
55
- const writeRes = await ctx.writeAs(systemUser, UserHandlers.update, {
55
+ const writeRes = await ctx.dispatch.writeAs(systemUser, UserHandlers.update, {
56
56
  id: me.id,
57
57
  version: me.version,
58
58
  changes,
@@ -89,7 +89,7 @@ async function loadValidatedUser(ctx, systemUser, userId) {
89
89
  declareEscapeHatch({
90
90
  reason: "reads the confirming user's row via UserQueries.findForAuth on the caller's handler context, using a SYSTEM_TENANT-scoped identity; the calling handler declares its own escapeHatch",
91
91
  });
92
- const me = parseAuthUserRow(await ctx.queryAs(systemUser, UserQueries.findForAuth, { id: userId }));
92
+ const me = parseAuthUserRow(await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, { id: userId }));
93
93
  if (!me || me.isDeleted || me.version === undefined || me.version < 1)
94
94
  return null;
95
95
  return { ...me, version: me.version };
@@ -64,13 +64,13 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
64
64
  agent: { expose: false },
65
65
  // kumiko-lint-ignore complexity-budget reuses login.write.ts's gate chain (lockout/password/email/status/membership/mfa) plus invite-specific branches (email match, already-member check, invitation update, unburn-on-failure) — splitting would scatter gate order across functions without reducing risk
66
66
  handler: async (event, ctx) => {
67
- if (!ctx.redis) {
68
- return writeFailure(new InternalError({ message: "invite-accept-with-login requires ctx.redis" }));
67
+ if (!ctx.services.redis) {
68
+ return writeFailure(new InternalError({ message: "invite-accept-with-login requires ctx.services.redis" }));
69
69
  }
70
- const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
70
+ const invitationId = await getInvitationIdForToken(ctx.services.redis, event.payload.token);
71
71
  if (!invitationId)
72
72
  return invalidInviteToken();
73
- const burn = await burnInviteToken(ctx.redis, event.payload.token);
73
+ const burn = await burnInviteToken(ctx.services.redis, event.payload.token);
74
74
  if (burn === "already-used")
75
75
  return invalidInviteToken();
76
76
  let committed = false;
@@ -131,6 +131,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
131
131
  db: dbConn,
132
132
  tenantId: invitationTenantId,
133
133
  mode: "system",
134
+ reason: "invitation tenant differs from the caller's tenant, the accept step updates the invitation row there",
134
135
  });
135
136
  const updateResult = await invitationExecutor.update({
136
137
  id: invitationId,
@@ -139,7 +140,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
139
140
  }, createSystemUser(invitationTenantId), invitationTdb);
140
141
  if (!updateResult.isSuccess)
141
142
  return updateResult;
142
- await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
143
+ await deleteInviteToken(ctx.services.redis, { invitationId, token: event.payload.token });
143
144
  // buildSessionRoles calls stripForbiddenMembershipRoles internally —
144
145
  // a reserved role on an existing membership must never reach the session.
145
146
  const mergedRoles = buildSessionRoles(globalGrant.data.roles, grant.data.roles);
@@ -172,8 +173,8 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
172
173
  };
173
174
  }
174
175
  finally {
175
- if (!committed && ctx.redis) {
176
- await unburnInviteToken(ctx.redis, event.payload.token);
176
+ if (!committed && ctx.services.redis) {
177
+ await unburnInviteToken(ctx.services.redis, event.payload.token);
177
178
  }
178
179
  }
179
180
  },
@@ -55,13 +55,15 @@ export function createInviteAcceptHandler() {
55
55
  },
56
56
  // kumiko-lint-ignore complexity-budget invite branches (auth/anon/burn) stay in one handler
57
57
  handler: async (event, ctx) => {
58
- if (!ctx.redis) {
59
- return writeFailure(new InternalError({ message: "invite-accept requires ctx.redis for token consumption" }));
58
+ if (!ctx.services.redis) {
59
+ return writeFailure(new InternalError({
60
+ message: "invite-accept requires ctx.services.redis for token consumption",
61
+ }));
60
62
  }
61
- const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
63
+ const invitationId = await getInvitationIdForToken(ctx.services.redis, event.payload.token);
62
64
  if (!invitationId)
63
65
  return invalidInviteToken();
64
- const burn = await burnInviteToken(ctx.redis, event.payload.token);
66
+ const burn = await burnInviteToken(ctx.services.redis, event.payload.token);
65
67
  if (burn === "already-used")
66
68
  return invalidInviteToken();
67
69
  let committed = false;
@@ -111,6 +113,7 @@ export function createInviteAcceptHandler() {
111
113
  db: dbConn,
112
114
  tenantId: invitationTenantId,
113
115
  mode: "system",
116
+ reason: "invitation tenant differs from the caller's tenant, the accept step updates the invitation row there",
114
117
  });
115
118
  const updateResult = await invitationExecutor.update({
116
119
  id: invitationId,
@@ -119,7 +122,7 @@ export function createInviteAcceptHandler() {
119
122
  }, createSystemUser(invitationTenantId), invitationTdb);
120
123
  if (!updateResult.isSuccess)
121
124
  return updateResult;
122
- await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
125
+ await deleteInviteToken(ctx.services.redis, { invitationId, token: event.payload.token });
123
126
  committed = true;
124
127
  return {
125
128
  isSuccess: true,
@@ -132,8 +135,8 @@ export function createInviteAcceptHandler() {
132
135
  };
133
136
  }
134
137
  finally {
135
- if (!committed && ctx.redis) {
136
- await unburnInviteToken(ctx.redis, event.payload.token);
138
+ if (!committed && ctx.services.redis) {
139
+ await unburnInviteToken(ctx.services.redis, event.payload.token);
137
140
  }
138
141
  }
139
142
  },
@@ -76,8 +76,8 @@ async function currentMembershipVersion(ctx, tenantId, email) {
76
76
  return membershipVersionOf(ctx.db, tenantId, user.id);
77
77
  }
78
78
  async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAccepted = false) {
79
- if (!ctx.redis) {
80
- return writeFailure(new InternalError({ message: "invite-create requires ctx.redis for token store" }));
79
+ if (!ctx.services.redis) {
80
+ return writeFailure(new InternalError({ message: "invite-create requires ctx.services.redis for token store" }));
81
81
  }
82
82
  const email = request.email.toLowerCase();
83
83
  const tenantId = inviter.tenantId;
@@ -100,7 +100,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
100
100
  const existingVersion = existing["version"]; // @cast-boundary db-row
101
101
  // At most one live invite token per invitation: invalidate
102
102
  // whatever's there before minting the new one.
103
- await invalidateExistingInviteToken(ctx.redis, invitationId);
103
+ await invalidateExistingInviteToken(ctx.services.redis, invitationId);
104
104
  const updateResult = await executor.update({
105
105
  id: invitationId,
106
106
  version: existingVersion,
@@ -133,7 +133,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
133
133
  invitationId = createResult.data.id; // @cast-boundary engine-payload
134
134
  }
135
135
  const token = generateToken();
136
- await storeInviteToken(ctx.redis, { invitationId, token, ttlSeconds });
136
+ await storeInviteToken(ctx.services.redis, { invitationId, token, ttlSeconds });
137
137
  const locale = request.locale ?? resolveHandlerMailLocale(ctx, opts.locale);
138
138
  await dispatchMagicLinkMail(ctx.notify, {
139
139
  handlerName: "invite-create",
@@ -40,9 +40,9 @@ export const inviteInfoQuery = defineQueryHandler({
40
40
  description: "Anonymous, read-only invite lookup for the invite-acceptance page: reveals the invited " +
41
41
  "email and whether an account already exists for it, without consuming the token.",
42
42
  handler: async (query, ctx) => {
43
- if (!ctx.redis)
43
+ if (!ctx.services.redis)
44
44
  throwInvalidInviteToken();
45
- const invitationId = await getInvitationIdForToken(ctx.redis, query.payload.token);
45
+ const invitationId = await getInvitationIdForToken(ctx.services.redis, query.payload.token);
46
46
  if (!invitationId)
47
47
  throwInvalidInviteToken();
48
48
  const invitation = await fetchOne(ctx.db.unsafeRaw(), tenantInvitationsTable, {
@@ -55,13 +55,13 @@ export function createInviteSignupCompleteHandler(opts = {}) {
55
55
  reason: INVITE_SIGNUP_COMPLETE_ESCAPE_HATCH_REASON,
56
56
  },
57
57
  handler: async (event, ctx) => {
58
- if (!ctx.redis) {
59
- return writeFailure(new InternalError({ message: "invite-signup-complete requires ctx.redis" }));
58
+ if (!ctx.services.redis) {
59
+ return writeFailure(new InternalError({ message: "invite-signup-complete requires ctx.services.redis" }));
60
60
  }
61
- const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
61
+ const invitationId = await getInvitationIdForToken(ctx.services.redis, event.payload.token);
62
62
  if (!invitationId)
63
63
  return invalidInviteToken();
64
- const burn = await burnInviteToken(ctx.redis, event.payload.token);
64
+ const burn = await burnInviteToken(ctx.services.redis, event.payload.token);
65
65
  if (burn === "already-used")
66
66
  return invalidInviteToken();
67
67
  let committed = false;
@@ -115,6 +115,7 @@ export function createInviteSignupCompleteHandler(opts = {}) {
115
115
  db: dbConn,
116
116
  tenantId: invitationTenantId,
117
117
  mode: "system",
118
+ reason: "invitation tenant differs from the caller's tenant, the accept step updates the invitation row there",
118
119
  });
119
120
  const updateResult = await invitationExecutor.update({
120
121
  id: invitationId,
@@ -130,7 +131,7 @@ export function createInviteSignupCompleteHandler(opts = {}) {
130
131
  // call, but an MFA-gated role gets the MFA step instead of a session.
131
132
  // It runs before the token is deleted so a throwing check leaves the link retryable.
132
133
  const mfaGate = await gateEnforceMfa(ctx, { mfaStatusChecker: opts.mfaStatusChecker }, userId, invitationTenantId, mergedRoles);
133
- await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
134
+ await deleteInviteToken(ctx.services.redis, { invitationId, token: event.payload.token });
134
135
  if (mfaGate !== undefined) {
135
136
  committed = true;
136
137
  return { isSuccess: true, data: mfaGate };
@@ -153,8 +154,8 @@ export function createInviteSignupCompleteHandler(opts = {}) {
153
154
  };
154
155
  }
155
156
  finally {
156
- if (!committed && ctx.redis) {
157
- await unburnInviteToken(ctx.redis, event.payload.token);
157
+ if (!committed && ctx.services.redis) {
158
+ await unburnInviteToken(ctx.services.redis, event.payload.token);
158
159
  }
159
160
  }
160
161
  },
@@ -34,12 +34,12 @@ function unknownEmailLockoutSubject(ctx, email) {
34
34
  }
35
35
  export async function gateCountUnknownUser(ctx, email, maxFailedAttempts, lockoutDurationMinutes) {
36
36
  const subject = unknownEmailLockoutSubject(ctx, email);
37
- if (!ctx.redis || subject === undefined)
37
+ if (!ctx.services.redis || subject === undefined)
38
38
  return ok(undefined);
39
39
  const locked = await gateEnforceLockout(ctx, subject);
40
40
  if (!locked.ok)
41
41
  return locked;
42
- await recordFailedAttempt(ctx.redis, subject, maxFailedAttempts, lockoutDurationMinutes);
42
+ await recordFailedAttempt(ctx.services.redis, subject, maxFailedAttempts, lockoutDurationMinutes);
43
43
  return ok(undefined);
44
44
  }
45
45
  /**
@@ -47,9 +47,9 @@ export async function gateCountUnknownUser(ctx, email, maxFailedAttempts, lockou
47
47
  * Fail-open without Redis (IP rate-limit still covers partially).
48
48
  */
49
49
  export async function gateEnforceLockout(ctx, userId) {
50
- if (!ctx.redis)
50
+ if (!ctx.services.redis)
51
51
  return ok(undefined);
52
- const state = await getLockoutState(ctx.redis, userId);
52
+ const state = await getLockoutState(ctx.services.redis, userId);
53
53
  if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
54
54
  const now = Date.now();
55
55
  if (state.lockedUntil > now) {
@@ -63,15 +63,15 @@ export async function gateEnforceLockout(ctx, userId) {
63
63
  export async function gateVerifyPassword(ctx, found, password, maxFailedAttempts, lockoutDurationMinutes) {
64
64
  const passwordOk = await verifyPassword(found.passwordHash, password);
65
65
  if (!passwordOk) {
66
- if (ctx.redis) {
67
- await recordFailedAttempt(ctx.redis, found.id, maxFailedAttempts, lockoutDurationMinutes);
66
+ if (ctx.services.redis) {
67
+ await recordFailedAttempt(ctx.services.redis, found.id, maxFailedAttempts, lockoutDurationMinutes);
68
68
  }
69
69
  return reject(invalidCredentials());
70
70
  }
71
71
  // Clear before MFA — MFA has its own attempt-cap; don't password-lock
72
72
  // users who occasionally mistype across otherwise-successful logins.
73
- if (ctx.redis) {
74
- await clearLockoutState(ctx.redis, found.id);
73
+ if (ctx.services.redis) {
74
+ await clearLockoutState(ctx.services.redis, found.id);
75
75
  }
76
76
  return ok(undefined);
77
77
  }
@@ -105,7 +105,7 @@ export async function gateResolveMembership(ctx, found, memberships) {
105
105
  : memberships;
106
106
  const globalRoles = parseRoles(found.roles ?? null);
107
107
  for (const candidate of candidates) {
108
- const active = await ctx.resolveActiveMembership(found.id, candidate.tenantId);
108
+ const active = await ctx.auth.resolveActiveMembership(found.id, candidate.tenantId);
109
109
  if (active.kind === "rejected") {
110
110
  // A blocked principal is blocked for every tenant, so stop here;
111
111
  // not_a_member/tenant_teardown are per-tenant — try the next candidate.
@@ -149,14 +149,14 @@ export async function gateBuildSession(ctx, userId, tenantId, mergedRoles, timez
149
149
  ...(timezone !== null && timezone !== undefined && { timezone }),
150
150
  ...(locale !== null && locale !== undefined && { locale }),
151
151
  };
152
- const claims = await ctx.resolveAuthClaims(baseSession);
152
+ const claims = await ctx.auth.resolveClaims(baseSession);
153
153
  const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
154
154
  return { kind: "auth-session", session };
155
155
  }
156
156
  // Login — unauthenticated entry point. The route is wired public (no JWT
157
157
  // middleware), dispatching with the framework's anonymous identity for the
158
158
  // handler's access check. Everything inside the handler goes through
159
- // ctx.queryAs(system, ...) so the user feature stays the single owner of its
159
+ // ctx.dispatch.queryAs(system, ...) so the user feature stays the single owner of its
160
160
  // table.
161
161
  export function createLoginHandler(opts = {}) {
162
162
  const strictVerification = opts.strictEmailVerification === true;
@@ -173,8 +173,8 @@ export function createLoginHandler(opts = {}) {
173
173
  escapeHatch: {
174
174
  grants: ["systemIdentity", "unsafeRaw"],
175
175
  reason: "Unauthenticated login has no caller identity yet — it looks up the user row by " +
176
- "email via ctx.queryAs(SYSTEM, ...) and resolves tenant membership via " +
177
- "ctx.resolveActiveMembership before a session exists. It also reads the MFA " +
176
+ "email via ctx.dispatch.queryAs(SYSTEM, ...) and resolves tenant membership via " +
177
+ "ctx.auth.resolveActiveMembership before a session exists. It also reads the MFA " +
178
178
  "enrollment of the tenant named in the signed login/setup token, not the guest " +
179
179
  "dispatch tenant, via the mfaStatusChecker callback.",
180
180
  },
@@ -183,7 +183,9 @@ export function createLoginHandler(opts = {}) {
183
183
  description: "Signs a user in with email and password, running the lockout, email-verification, account-status, tenant-membership and MFA gates, and answering with a session or with an MFA challenge or setup requirement.",
184
184
  handler: async (event, ctx) => {
185
185
  const systemUser = createSystemUser(SYSTEM_USER_ID);
186
- const foundRow = parseAuthUserRow(await ctx.queryAs(systemUser, UserQueries.findForAuth, { email: event.payload.email }));
186
+ const foundRow = parseAuthUserRow(await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
187
+ email: event.payload.email,
188
+ }));
187
189
  if (!foundRow?.passwordHash || foundRow.isDeleted) {
188
190
  const unknown = await gateCountUnknownUser(ctx, event.payload.email, maxFailedAttempts, lockoutDurationMinutes);
189
191
  if (!unknown.ok)
@@ -206,7 +208,7 @@ export function createLoginHandler(opts = {}) {
206
208
  if (!statusGate.ok)
207
209
  return statusGate.result;
208
210
  // Still needed for candidate ORDER (preferred tenant first) in gateResolveMembership.
209
- const memberships = (await ctx.queryAs(systemUser, TENANT_MEMBERSHIPS_QUERY, {
211
+ const memberships = (await ctx.dispatch.queryAs(systemUser, TENANT_MEMBERSHIPS_QUERY, {
210
212
  userId: found.id,
211
213
  })); // @cast-boundary db-runner
212
214
  const membershipGate = await gateResolveMembership(ctx, found, memberships);
@@ -25,7 +25,7 @@ export function createResetPasswordHandler(opts) {
25
25
  escapeHatch: {
26
26
  grants: ["systemIdentity"],
27
27
  reason: "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
28
- "target user via ctx.writeAs(SYSTEM, user:update) to set the new password hash.",
28
+ "target user via ctx.dispatch.writeAs(SYSTEM, user:update) to set the new password hash.",
29
29
  },
30
30
  agent: { expose: false },
31
31
  handler: async (event, ctx) => {
@@ -42,7 +42,7 @@ export function createResetPasswordHandler(opts) {
42
42
  return invalidResetToken();
43
43
  return runConfirmTokenFlow(ctx, verify.userId, verify.expiresAtMs, {
44
44
  purpose: "reset",
45
- redisRequiredMessage: "password-reset requires ctx.redis to enforce token single-use",
45
+ redisRequiredMessage: "password-reset requires ctx.services.redis to enforce token single-use",
46
46
  invalidToken: invalidResetToken,
47
47
  buildChanges: async () => ({
48
48
  passwordHash: await hashPassword(event.payload.newPassword),
@@ -55,16 +55,16 @@ function isBenignClaimRejection(error) {
55
55
  async function claimBoundHandover(ctx, session, binding) {
56
56
  const provider = findSignupHandoverProvider(ctx.registry);
57
57
  if (!provider) {
58
- ctx.log?.warn("signup-confirm: handover binding present but no signupHandover provider mounted");
58
+ ctx.obs.log?.warn("signup-confirm: handover binding present but no signupHandover provider mounted");
59
59
  return undefined;
60
60
  }
61
61
  const claim = provider.mintClaim(binding);
62
- const claimResult = await ctx.writeAs(session, claim.qualifiedName, claim.payload);
62
+ const claimResult = await ctx.dispatch.writeAs(session, claim.qualifiedName, claim.payload);
63
63
  if (claimResult.isSuccess)
64
64
  return { entityType: binding.entityType, id: binding.rowId };
65
65
  if (!isBenignClaimRejection(claimResult.error))
66
66
  throw reraiseAsKumikoError(claimResult.error);
67
- ctx.log?.warn("signup-confirm: handover grant did not redeem", {
67
+ ctx.obs.log?.warn("signup-confirm: handover grant did not redeem", {
68
68
  entityType: binding.entityType,
69
69
  });
70
70
  return undefined;
@@ -81,19 +81,19 @@ export function createSignupConfirmHandler(opts = {}) {
81
81
  reason: SIGNUP_CONFIRM_PROVISION_REASON,
82
82
  },
83
83
  handler: async (event, ctx) => {
84
- if (!ctx.redis) {
84
+ if (!ctx.services.redis) {
85
85
  return writeFailure(new InternalError({
86
- message: "signup-confirm requires ctx.redis for token consumption",
86
+ message: "signup-confirm requires ctx.services.redis for token consumption",
87
87
  }));
88
88
  }
89
89
  // Token lookup: nonexistent, expired, and already-consumed tokens all
90
90
  // collapse to invalid_signup_token to prevent enumeration.
91
- const email = await getEmailForSignupToken(ctx.redis, event.payload.token);
91
+ const email = await getEmailForSignupToken(ctx.services.redis, event.payload.token);
92
92
  if (!email)
93
93
  return invalidSignupToken();
94
94
  // Single-use burn: of two parallel confirmations from different tabs,
95
95
  // one succeeds and the other receives invalid_signup_token.
96
- const burn = await burnSignupToken(ctx.redis, event.payload.token);
96
+ const burn = await burnSignupToken(ctx.services.redis, event.payload.token);
97
97
  if (burn === "already-used")
98
98
  return invalidSignupToken();
99
99
  let committed = false;
@@ -154,7 +154,7 @@ export function createSignupConfirmHandler(opts = {}) {
154
154
  // Claim BEFORE spending the signup token or the binding below and
155
155
  // before `committed` flips: if the claim throws, the `finally` still
156
156
  // unburns and the activation link stays retryable.
157
- const handoverBinding = await getSignupHandover(ctx.redis, event.payload.token);
157
+ const handoverBinding = await getSignupHandover(ctx.services.redis, event.payload.token);
158
158
  const handover = handoverBinding
159
159
  ? await claimBoundHandover(ctx, session, handoverBinding)
160
160
  : undefined;
@@ -162,9 +162,9 @@ export function createSignupConfirmHandler(opts = {}) {
162
162
  const mfaGate = await gateEnforceMfa(ctx, { mfaStatusChecker: opts.mfaStatusChecker }, provisioned.userId, provisioned.tenantId, session.roles);
163
163
  // Drop both token lookup keys; the burn key stays for its remaining
164
164
  // TTL as replay protection.
165
- await deleteSignupToken(ctx.redis, { email, token: event.payload.token });
165
+ await deleteSignupToken(ctx.services.redis, { email, token: event.payload.token });
166
166
  if (handoverBinding)
167
- await deleteSignupHandover(ctx.redis, event.payload.token);
167
+ await deleteSignupHandover(ctx.services.redis, event.payload.token);
168
168
  if (mfaGate !== undefined) {
169
169
  committed = true;
170
170
  return {
@@ -192,10 +192,10 @@ export function createSignupConfirmHandler(opts = {}) {
192
192
  };
193
193
  }
194
194
  finally {
195
- if (!committed && ctx.redis) {
195
+ if (!committed && ctx.services.redis) {
196
196
  // Release the burn so a retry after a DB hiccup is not blocked.
197
197
  // Keep the token lookup keys so the user can click the email link again.
198
- await unburnSignupToken(ctx.redis, event.payload.token);
198
+ await unburnSignupToken(ctx.services.redis, event.payload.token);
199
199
  }
200
200
  }
201
201
  },
@@ -73,12 +73,12 @@ export function createSignupRequestHandler(opts) {
73
73
  // No email address here — this fires on every no-op request, PII
74
74
  // must not accumulate in logs just for the "was this toggled off"
75
75
  // signal a support ticket needs.
76
- ctx.log?.info("signup-request skipped: self-registration disabled");
76
+ ctx.obs.log?.info("signup-request skipped: self-registration disabled");
77
77
  return { isSuccess: true, data: { kind: "no-op" } };
78
78
  }
79
- if (!ctx.redis) {
79
+ if (!ctx.services.redis) {
80
80
  return writeFailure(new InternalError({
81
- message: "signup-request requires ctx.redis for the activation-token store",
81
+ message: "signup-request requires ctx.services.redis for the activation-token store",
82
82
  }));
83
83
  }
84
84
  // Email normalization lives in the store (signup-token-store). The
@@ -89,9 +89,9 @@ export function createSignupRequestHandler(opts) {
89
89
  // resend without a fresh `handover` in the payload (the common case:
90
90
  // the visitor just retyped their email) must still carry the earlier
91
91
  // verified grant over to the new token instead of silently dropping it.
92
- const priorBinding = await takeOverSignupHandoverForResend(ctx.redis, email);
92
+ const priorBinding = await takeOverSignupHandoverForResend(ctx.services.redis, email);
93
93
  const locale = resolveHandlerMailLocale(ctx, opts.locale);
94
- const redis = ctx.redis;
94
+ const redis = ctx.services.redis;
95
95
  const issued = await issueSignupActivation({
96
96
  redis,
97
97
  notify: ctx.notify,
@@ -115,7 +115,7 @@ export function createSignupRequestHandler(opts) {
115
115
  const { entityType, token: grantToken } = event.payload.handover;
116
116
  const provider = findSignupHandoverProvider(ctx.registry);
117
117
  if (!provider) {
118
- ctx.log?.warn("signup-request: handover payload present but no signupHandover provider mounted");
118
+ ctx.obs.log?.warn("signup-request: handover payload present but no signupHandover provider mounted");
119
119
  }
120
120
  else {
121
121
  try {
@@ -129,11 +129,15 @@ export function createSignupRequestHandler(opts) {
129
129
  binding = verified;
130
130
  }
131
131
  else {
132
- ctx.log?.warn("signup-request: handover grant did not verify", { entityType });
132
+ ctx.obs.log?.warn("signup-request: handover grant did not verify", {
133
+ entityType,
134
+ });
133
135
  }
134
136
  }
135
137
  catch {
136
- ctx.log?.warn("signup-request: handover grant verification threw", { entityType });
138
+ ctx.obs.log?.warn("signup-request: handover grant verification threw", {
139
+ entityType,
140
+ });
137
141
  }
138
142
  }
139
143
  }
@@ -24,10 +24,12 @@ export function createSwitchTenantMfaGateHandler(opts) {
24
24
  description: "Runs the MFA gate of the login for a tenant switch and answers with an MFA challenge, a setup requirement, or clearance.",
25
25
  handler: async (event, ctx) => {
26
26
  const { userId, tenantId } = event.payload;
27
- const active = await ctx.resolveActiveMembership(userId, tenantId);
27
+ const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
28
28
  if (active.kind === "rejected")
29
29
  return noMembership();
30
- const userRow = parseAuthUserRow(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, { id: userId }));
30
+ const userRow = parseAuthUserRow(await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
31
+ id: userId,
32
+ }));
31
33
  const roles = buildSessionRoles(parseRoles(userRow?.roles ?? null), active.membership.roles);
32
34
  const mfaGate = await gateEnforceMfa(ctx, opts, userId, tenantId, roles);
33
35
  return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear", roles } };
@@ -41,7 +41,7 @@ export function createTokenRequestHandler(spec, opts) {
41
41
  escapeHatch: {
42
42
  grants: ["systemIdentity"],
43
43
  reason: "Anonymous requester has no session — looks up the target user by email via " +
44
- "ctx.queryAs(SYSTEM, user:findForAuth) to mint the out-of-band token.",
44
+ "ctx.dispatch.queryAs(SYSTEM, user:findForAuth) to mint the out-of-band token.",
45
45
  },
46
46
  description: spec.description,
47
47
  handler: async (event, ctx) => {
@@ -53,7 +53,7 @@ export function createTokenRequestHandler(spec, opts) {
53
53
  }));
54
54
  }
55
55
  const systemUser = createSystemUser(event.user.tenantId);
56
- const user = parseAuthUserRow(await ctx.queryAs(systemUser, UserQueries.findForAuth, {
56
+ const user = parseAuthUserRow(await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
57
57
  email: event.payload.email,
58
58
  }));
59
59
  // Silent-success branches all return the SAME shape with kind="no-op".