@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
@@ -23,7 +23,7 @@ export function createVerifyEmailHandler(opts) {
23
23
  escapeHatch: {
24
24
  grants: ["systemIdentity"],
25
25
  reason: "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
26
- "target user via ctx.writeAs(SYSTEM, user:update) to set emailVerified.",
26
+ "target user via ctx.dispatch.writeAs(SYSTEM, user:update) to set emailVerified.",
27
27
  },
28
28
  agent: { expose: false },
29
29
  handler: async (event, ctx) => {
@@ -37,7 +37,7 @@ export function createVerifyEmailHandler(opts) {
37
37
  return invalidVerificationToken();
38
38
  return runConfirmTokenFlow(ctx, verify.userId, verify.expiresAtMs, {
39
39
  purpose: "verify",
40
- redisRequiredMessage: "email-verification requires ctx.redis to enforce token single-use",
40
+ redisRequiredMessage: "email-verification requires ctx.services.redis to enforce token single-use",
41
41
  invalidToken: invalidVerificationToken,
42
42
  buildChanges: async () => ({ emailVerified: true }),
43
43
  successData: { kind: "verified" },
@@ -1,5 +1,5 @@
1
1
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
- import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type SessionUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { type SeedTenantHooks } from "../tenant/seeding.ts";
4
4
  export { seedTenant, seedTenantMembership } from "../tenant/seeding.ts";
5
5
  export { seedUser } from "../user/seeding.ts";
@@ -13,9 +13,9 @@
13
13
  // sub-paths themselves.
14
14
  import { configuredBlindIndexKey, configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
15
15
  import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
16
+ import { createSystemUser, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
16
17
  import { ConflictError } from "@cosmicdrift/kumiko-framework/errors";
17
18
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
18
- import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
19
19
  import { hashPassword } from "../shared/index.js";
20
20
  // kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
21
21
  import { seedTenant, seedTenantMembership } from "../tenant/seeding.js";
@@ -129,9 +129,9 @@ hooks) {
129
129
  * persistent-DB mode succeeds. Returns the userId.
130
130
  */
131
131
  export async function seedAdmin(db, options) {
132
- const by = options.by ?? TestUsers.systemAdmin;
132
+ const byOption = options.by === undefined ? {} : { by: options.by };
133
133
  for (const m of options.memberships) {
134
- await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, by });
134
+ await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, ...byOption });
135
135
  }
136
136
  const { id: userId } = await seedUserWithPassword(db, {
137
137
  email: options.email,
@@ -139,14 +139,14 @@ export async function seedAdmin(db, options) {
139
139
  displayName: options.displayName,
140
140
  ...(options.globalRoles !== undefined && { roles: options.globalRoles }),
141
141
  ...(options.emailVerified !== undefined && { emailVerified: options.emailVerified }),
142
- by,
142
+ ...byOption,
143
143
  });
144
144
  for (const m of options.memberships) {
145
145
  await seedTenantMembership(db, {
146
146
  userId,
147
147
  tenantId: m.tenantId,
148
148
  roles: m.roles,
149
- by,
149
+ ...byOption,
150
150
  });
151
151
  }
152
152
  return { id: userId };
@@ -239,7 +239,8 @@ export async function seedAdminGuarded(db, options) {
239
239
  const existingId = await findExistingUserIdForEmail(db, kms, options.email);
240
240
  if (existingId === undefined)
241
241
  return seedAdmin(db, options);
242
- const by = options.by ?? TestUsers.systemAdmin;
242
+ const byOption = options.by === undefined ? {} : { by: options.by };
243
+ const by = options.by ?? createSystemUser(SYSTEM_TENANT_ID);
243
244
  const existingRow = await fetchOne(db, userTable, { id: existingId });
244
245
  if (existingRow) {
245
246
  // Re-write the email so email_bidx is recomputed; without it the account
@@ -251,12 +252,12 @@ export async function seedAdminGuarded(db, options) {
251
252
  }
252
253
  }
253
254
  for (const m of options.memberships) {
254
- await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, by });
255
+ await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, ...byOption });
255
256
  await seedTenantMembership(db, {
256
257
  userId: existingId,
257
258
  tenantId: m.tenantId,
258
259
  roles: m.roles,
259
- by,
260
+ ...byOption,
260
261
  });
261
262
  }
262
263
  return { id: existingId };
@@ -10,6 +10,7 @@
10
10
  // Middleware / runProdApp resolve providers from the registry at boot
11
11
  // (tokenVerifier/sessionStore) or request-time (tokenVerifier shape match).
12
12
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
13
+ import { systemScopedRunner } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
13
14
  import { validateSessionStoreMultiplicity, validateTenantExistenceMultiplicity, validateTenantResolverMultiplicity, validateTokenVerifierMultiplicity, } from "./boot-checks.js";
14
15
  import { EXT_SESSION_STORE, EXT_TENANT_EXISTENCE, EXT_TENANT_RESOLVER, EXT_TOKEN_VERIFIER, isAuthProviderPlugin, isSessionStoreProvider, isTenantExistenceProvider, isTenantResolverProvider, tokenShapeMatches, } from "./types.js";
15
16
  export { EXT_SESSION_STORE, EXT_TENANT_EXISTENCE, EXT_TENANT_RESOLVER, EXT_TOKEN_VERIFIER, } from "./types.js";
@@ -49,6 +50,13 @@ export function createAuthFoundationFeature() {
49
50
  r.bootCheck(({ features }) => validateTenantExistenceMultiplicity(features));
50
51
  }, { dedupeOptions: {} });
51
52
  }
53
+ // Providers run before any tenant or session exists (token verify, host to tenant), so their reads are cross-tenant by nature.
54
+ function systemScopedDeps(deps) {
55
+ return {
56
+ ...deps,
57
+ db: systemScopedRunner(deps.db, "auth lookups run before a tenant is established"),
58
+ };
59
+ }
52
60
  /**
53
61
  * Tries every registered tokenVerifier provider's shape against `rawToken`
54
62
  * in registration order and calls the first match's verifier. Returns null
@@ -62,7 +70,7 @@ export async function resolveTokenVerifier(deps, rawToken) {
62
70
  continue;
63
71
  if (!tokenShapeMatches(usage.options.shape, rawToken))
64
72
  continue;
65
- const verify = await usage.options.build(deps);
73
+ const verify = await usage.options.build(systemScopedDeps(deps));
66
74
  return verify(rawToken);
67
75
  }
68
76
  return null;
@@ -78,7 +86,7 @@ export async function resolveSessionStore(deps) {
78
86
  if (!usage || !isSessionStoreProvider(usage.options)) {
79
87
  throw new Error("[auth-foundation] no sessionStore provider registered — did the boot validator run?");
80
88
  }
81
- return usage.options.build(deps);
89
+ return usage.options.build(systemScopedDeps(deps));
82
90
  }
83
91
  /** Optional tenantResolver — null when no provider registered. */
84
92
  export async function resolveTenantResolver(deps) {
@@ -88,7 +96,7 @@ export async function resolveTenantResolver(deps) {
88
96
  if (!isTenantResolverProvider(usage.options)) {
89
97
  throw new Error("[auth-foundation] tenantResolver provider registered without a valid TenantResolverProvider");
90
98
  }
91
- const resolve = await usage.options.build(deps);
99
+ const resolve = await usage.options.build(systemScopedDeps(deps));
92
100
  return { resolve, trust: usage.options.trust };
93
101
  }
94
102
  /** Optional tenantExistence — null when no provider registered. */
@@ -99,5 +107,5 @@ export async function resolveTenantExistence(deps) {
99
107
  if (!isTenantExistenceProvider(usage.options)) {
100
108
  throw new Error("[auth-foundation] tenantExistence provider registered without a valid TenantExistenceProvider");
101
109
  }
102
- return usage.options.build(deps);
110
+ return usage.options.build(systemScopedDeps(deps));
103
111
  }
@@ -30,18 +30,18 @@ export function createDisableHandler(opts) {
30
30
  if (!row)
31
31
  return mfaNotEnabled();
32
32
  // Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
33
- // without ctx.redis, verifyMfaFactor's TOTP-replay burn is skipped, and
33
+ // without ctx.services.redis, verifyMfaFactor's TOTP-replay burn is skipped, and
34
34
  // a code observed once (phishing proxy/shoulder-surfing) could be
35
35
  // replayed within the ±1-step window to disable MFA outright.
36
- // "no ctx.redis means no one gets through MFA login" doesn't cover
36
+ // "no ctx.services.redis means no one gets through MFA login" doesn't cover
37
37
  // this route — it's session-authed, and JWTs issued before Redis was
38
38
  // unwired stay valid until they expire.
39
- if (!ctx.redis) {
39
+ if (!ctx.services.redis) {
40
40
  return writeFailure(new InternalError({
41
- message: "disable (mfa) requires ctx.redis for TOTP replay protection",
41
+ message: "disable (mfa) requires ctx.services.redis for TOTP replay protection",
42
42
  }));
43
43
  }
44
- const replay = { redis: ctx.redis, userId: event.user.id };
44
+ const replay = { redis: ctx.services.redis, userId: event.user.id };
45
45
  const verify = await verifyMfaFactor(row, event.payload.code, replay);
46
46
  if (!verify.ok)
47
47
  return invalidTotpCode();
@@ -37,8 +37,8 @@ export function createEnableConfirmPreauthHandler(opts) {
37
37
  rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
38
38
  escapeHatch: {
39
39
  grants: ["systemIdentity", "unsafeRaw"],
40
- reason: "Pre-auth MFA enrollment step has no session yet — re-checks status via ctx.queryAs(SYSTEM, " +
41
- "user:findForAuth) and membership via ctx.resolveActiveMembership for the setup token's user. " +
40
+ reason: "Pre-auth MFA enrollment step has no session yet — re-checks status via ctx.dispatch.queryAs(SYSTEM, " +
41
+ "user:findForAuth) and membership via ctx.auth.resolveActiveMembership for the setup token's user. " +
42
42
  "Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
43
43
  "guest dispatch tenant.",
44
44
  },
@@ -60,19 +60,19 @@ export function createEnableConfirmPreauthHandler(opts) {
60
60
  // endpoint has NO gate before it (access: { roles: ["anonymous"] }, no JWT,
61
61
  // no prior challenge-token from a successful login) — its only secret
62
62
  // is a 6-digit TOTP code. Silently skipping the brute-force cap and
63
- // single-use burn when ctx.redis is absent would let the setup token
63
+ // single-use burn when ctx.services.redis is absent would let the setup token
64
64
  // be guessed against unboundedly until its TTL expires. The route's
65
65
  // IP rate limiter is not a substitute: it's IP-scoped and resets on
66
66
  // every success.
67
- if (!ctx.redis) {
67
+ if (!ctx.services.redis) {
68
68
  return writeFailure(new InternalError({
69
- message: "enable-confirm-preauth requires ctx.redis for the brute-force cap and single-use burn",
69
+ message: "enable-confirm-preauth requires ctx.services.redis for the brute-force cap and single-use burn",
70
70
  }));
71
71
  }
72
72
  // Same guessing surface as /auth/mfa/verify (a short TOTP code) and
73
73
  // the same per-userId cap — deliberately shared counter, see
74
74
  // mfa-verify-attempts.ts.
75
- const state = await getMfaVerifyLockoutState(ctx.redis, userId);
75
+ const state = await getMfaVerifyLockoutState(ctx.services.redis, userId);
76
76
  if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
77
77
  const now = Date.now();
78
78
  if (state.lockedUntil > now) {
@@ -82,17 +82,22 @@ export function createEnableConfirmPreauthHandler(opts) {
82
82
  }
83
83
  const secret = base32Decode(verify.payload.totpSecretBase32);
84
84
  if (verifyTotp(secret, event.payload.code) === false) {
85
- await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
85
+ await recordFailedMfaVerifyAttempt(ctx.services.redis, userId, maxAttempts, lockoutMinutes);
86
86
  return invalidTotpCode();
87
87
  }
88
88
  // Burn the setup token on the first successful confirm — same
89
89
  // single-use guarantee as enable-confirm.write.ts.
90
- const burnResult = await burnToken(ctx.redis, "mfa-setup", userId, verify.expiresAtMs);
90
+ const burnResult = await burnToken(ctx.services.redis, "mfa-setup", userId, verify.expiresAtMs);
91
91
  if (burnResult === "already-used")
92
92
  return invalidSetupToken();
93
93
  // "system" mode: no session exists yet, tenantId comes from the
94
94
  // verified token, mirroring enable-start-preauth.write.ts.
95
- const scopedDb = createTenantDb({ db: ctx.db.unsafeRaw(), tenantId, mode: "system" });
95
+ const scopedDb = createTenantDb({
96
+ db: ctx.db.unsafeRaw(),
97
+ tenantId,
98
+ mode: "system",
99
+ reason: "pre-auth MFA setup has no session, the tenant comes from the verified setup token",
100
+ });
96
101
  const scopedUser = { id: userId, tenantId, roles: ["User"] };
97
102
  const existing = await findUserMfaRow(scopedDb, scopedUser);
98
103
  if (existing)
@@ -112,7 +117,7 @@ export function createEnableConfirmPreauthHandler(opts) {
112
117
  // existing userId-unique-constraint backstop between the
113
118
  // `existing` check above and executor.create() below.
114
119
  const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
115
- const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
120
+ const userRow = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
116
121
  id: userId,
117
122
  })); // @cast-boundary engine-payload
118
123
  if (!userRow)
@@ -123,7 +128,7 @@ export function createEnableConfirmPreauthHandler(opts) {
123
128
  return invalidSetupToken();
124
129
  }
125
130
  const globalRoles = parseRoles(userRow?.roles ?? null);
126
- const active = await ctx.resolveActiveMembership(userId, tenantId);
131
+ const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
127
132
  if (active.kind === "rejected")
128
133
  return invalidSetupToken();
129
134
  const result = await executor.create({
@@ -135,8 +140,8 @@ export function createEnableConfirmPreauthHandler(opts) {
135
140
  }, scopedUser, scopedDb);
136
141
  if (!result.isSuccess)
137
142
  return result;
138
- // ctx.redis is guaranteed by the fail-closed check above.
139
- await clearMfaVerifyAttempts(ctx.redis, userId);
143
+ // ctx.services.redis is guaranteed by the fail-closed check above.
144
+ await clearMfaVerifyAttempts(ctx.services.redis, userId);
140
145
  // Re-derive the full session the way verify.write.ts does — the
141
146
  // setupToken only proves "password + new TOTP secret", not roles;
142
147
  // roles come from the membership lookup above, same as every other
@@ -157,7 +162,7 @@ export function createEnableConfirmPreauthHandler(opts) {
157
162
  ...sessionTimezoneField(userRow?.timezone),
158
163
  ...sessionLocaleField(userRow?.locale),
159
164
  };
160
- const claims = await ctx.resolveAuthClaims(baseSession);
165
+ const claims = await ctx.auth.resolveClaims(baseSession);
161
166
  const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
162
167
  // Last step before returning success: every gate above (status,
163
168
  // membership, entity-write) has passed, so this is the point of no
@@ -45,9 +45,9 @@ export function createEnableConfirmHandler(opts) {
45
45
  // second seeing invalidSetupToken(). Checked before the TOTP verify
46
46
  // for the same reason the other two handlers check it early: fail
47
47
  // closed before doing any of the guarded work, not partway through.
48
- if (!ctx.redis) {
48
+ if (!ctx.services.redis) {
49
49
  return writeFailure(new InternalError({
50
- message: "enable-confirm requires ctx.redis for the single-use setup-token burn",
50
+ message: "enable-confirm requires ctx.services.redis for the single-use setup-token burn",
51
51
  }));
52
52
  }
53
53
  const secret = base32Decode(verify.payload.totpSecretBase32);
@@ -65,7 +65,7 @@ export function createEnableConfirmHandler(opts) {
65
65
  // concurrent mfa_already_enabled() write) is meant to leave the burn
66
66
  // standing. Add unburnToken here if create-failure retries turn out
67
67
  // to matter.
68
- const burnResult = await burnToken(ctx.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
68
+ const burnResult = await burnToken(ctx.services.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
69
69
  if (burnResult === "already-used")
70
70
  return invalidSetupToken();
71
71
  const existing = await findUserMfaRow(ctx.db, event.user);
@@ -44,7 +44,12 @@ export function createEnableStartPreauthHandler(opts) {
44
44
  // "system" mode: the guest dispatch identity's own tenantId is
45
45
  // meaningless here — the preauthSetupToken is the source of truth
46
46
  // for which tenant's row to read, mirroring verify.write.ts.
47
- const scopedDb = createTenantDb({ db: ctx.db.unsafeRaw(), tenantId, mode: "system" });
47
+ const scopedDb = createTenantDb({
48
+ db: ctx.db.unsafeRaw(),
49
+ tenantId,
50
+ mode: "system",
51
+ reason: "pre-auth MFA setup has no session, the tenant comes from the verified setup token",
52
+ });
48
53
  const scopedUser = { id: userId, tenantId, roles: ["User"] };
49
54
  const existing = await findUserMfaRow(scopedDb, scopedUser);
50
55
  if (existing)
@@ -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
  }
@@ -11,7 +11,12 @@ export function createMfaStatusChecker(opts) {
11
11
  declareEscapeHatch({
12
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",
13
13
  });
14
- 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
+ });
15
20
  const scopedUser = { id: userId, tenantId, roles: ["User"] };
16
21
  const row = await findUserMfaRow(scopedDb, scopedUser);
17
22
  if (row) {
@@ -22,12 +27,12 @@ export function createMfaStatusChecker(opts) {
22
27
  return { required: true, challengeToken: token };
23
28
  }
24
29
  // ctx.config is bound to the CALLING user's tenant (the anonymous
25
- // 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
26
31
  // explicitly, which is what a pre-session check against the LOGGING-IN
27
32
  // user's tenant needs.
28
33
  // @cast-boundary engine-payload — "select" config values are validated
29
34
  // against the declared `options` at write-time by the config feature.
30
- 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");
31
36
  if (policy === "optional")
32
37
  return { required: false };
33
38
  const buildSetupToken = () => signMfaPreauthSetupToken({ userId, tenantId }, MFA_PREAUTH_SETUP_TOKEN_TTL_MINUTES, opts.challengeTokenSecret).token;
@@ -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,