@cosmicdrift/kumiko-samples 0.388.0 → 0.390.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +25 -13
  3. package/packages/bundled-features/src/agent-tools/changes.json +6 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +42 -26
  6. package/packages/bundled-features/src/audit/feature.ts +2 -1
  7. package/packages/bundled-features/src/auth-email-password/auth-user-row.ts +1 -1
  8. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +13 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +18 -0
  10. package/packages/bundled-features/src/auth-email-password/feature.ts +2 -2
  11. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +7 -7
  12. package/packages/bundled-features/src/auth-email-password/handlers/confirm-account-unlock.write.ts +3 -3
  13. package/packages/bundled-features/src/auth-email-password/handlers/confirm-token-flow.ts +5 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +9 -7
  15. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +11 -7
  16. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +2 -2
  18. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +9 -7
  19. package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +23 -16
  20. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  21. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  22. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  23. package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +3 -2
  24. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +12 -12
  25. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +16 -8
  26. package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +4 -2
  27. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +10 -2
  28. package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +3 -2
  29. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  30. package/packages/bundled-features/src/auth-email-password/seeding.ts +14 -9
  31. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  32. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  33. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  34. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  35. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  36. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  37. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  38. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  39. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  40. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  41. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  42. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  43. package/packages/bundled-features/src/auth-foundation/feature.ts +13 -4
  44. package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
  45. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  46. package/packages/bundled-features/src/auth-mfa/handlers/disable.write.ts +5 -5
  47. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +24 -14
  48. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +9 -3
  49. package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +6 -1
  50. package/packages/bundled-features/src/auth-mfa/handlers/enable-start.write.ts +3 -1
  51. package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +14 -9
  52. package/packages/bundled-features/src/auth-mfa/handlers/regenerate-recovery.write.ts +5 -4
  53. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +25 -14
  54. package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +10 -4
  55. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +17 -8
  56. package/packages/bundled-features/src/auth-mfa/verify-factor.ts +1 -1
  57. package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +2 -2
  58. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  59. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +1 -1
  60. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +5 -5
  61. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +10 -7
  62. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +15 -12
  63. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-open-cancels.ts +22 -0
  64. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-provider-lock.ts +4 -0
  65. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +4 -4
  66. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +17 -58
  67. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +10 -8
  68. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +18 -15
  69. package/packages/bundled-features/src/billing-foundation/events.ts +1 -1
  70. package/packages/bundled-features/src/billing-foundation/feature.ts +8 -3
  71. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +1 -1
  72. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +5 -5
  73. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +3 -3
  74. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +2 -2
  75. package/packages/bundled-features/src/billing-foundation/projection.ts +1 -1
  76. package/packages/bundled-features/src/billing-foundation/types.ts +1 -1
  77. package/packages/bundled-features/src/cap-counter/aggregate-id.ts +1 -1
  78. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +5 -4
  79. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +21 -8
  80. package/packages/bundled-features/src/cap-counter/changes.json +12 -0
  81. package/packages/bundled-features/src/cap-counter/constants.ts +1 -1
  82. package/packages/bundled-features/src/cap-counter/feature.ts +3 -2
  83. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +1 -1
  84. package/packages/bundled-features/src/cap-counter/reservation-chain.ts +1 -1
  85. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +4 -4
  86. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  87. package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -1
  88. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +1 -1
  89. package/packages/bundled-features/src/compliance-profiles/seeding.ts +12 -7
  90. package/packages/bundled-features/src/config/changes.json +6 -0
  91. package/packages/bundled-features/src/config/feature.ts +6 -6
  92. package/packages/bundled-features/src/config/handlers/cascade.query.ts +1 -1
  93. package/packages/bundled-features/src/config/handlers/readiness.query.ts +1 -1
  94. package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +23 -13
  95. package/packages/bundled-features/src/config/handlers/reset.write.ts +3 -3
  96. package/packages/bundled-features/src/config/handlers/selected-extensions.query.ts +1 -1
  97. package/packages/bundled-features/src/config/handlers/set.write.ts +22 -5
  98. package/packages/bundled-features/src/config/handlers/values.query.ts +1 -1
  99. package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
  100. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -9
  101. package/packages/bundled-features/src/crypto-shredding/resolve-file-bytes-deleter.ts +3 -3
  102. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  103. package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
  104. package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +1 -1
  105. package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +1 -1
  106. package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
  107. package/packages/bundled-features/src/custom-fields/index.ts +4 -0
  108. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  109. package/packages/bundled-features/src/data-retention/feature.ts +1 -1
  110. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +12 -2
  111. package/packages/bundled-features/src/delivery/attempt-log.ts +1 -1
  112. package/packages/bundled-features/src/delivery/changes.json +12 -0
  113. package/packages/bundled-features/src/delivery/delivery-service.ts +6 -5
  114. package/packages/bundled-features/src/delivery/feature.ts +3 -2
  115. package/packages/bundled-features/src/delivery/index.ts +5 -0
  116. package/packages/bundled-features/src/delivery/jobs.ts +11 -11
  117. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  118. package/packages/bundled-features/src/delivery/testing.ts +1 -1
  119. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  120. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  121. package/packages/bundled-features/src/document-ingest-foundation/events.ts +16 -1
  122. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  123. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  124. package/packages/bundled-features/src/feature-toggles/handlers/registered.query.ts +1 -1
  125. package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
  126. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +1 -1
  127. package/packages/bundled-features/src/file-derivatives/feature.ts +2 -2
  128. package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +5 -1
  129. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +7 -5
  130. package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
  131. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -5
  132. package/packages/bundled-features/src/file-provider-s3/feature.ts +4 -1
  133. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +8 -8
  134. package/packages/bundled-features/src/form-draft/feature.ts +1 -1
  135. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +22 -12
  136. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +3 -3
  137. package/packages/bundled-features/src/form-draft/handlers/get.query.ts +1 -1
  138. package/packages/bundled-features/src/foundation-shared/config-helpers.ts +1 -1
  139. package/packages/bundled-features/src/host-routing/changes.json +6 -0
  140. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  141. package/packages/bundled-features/src/inbound-mail-foundation/events.ts +1 -1
  142. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +2 -1
  143. package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-state.ts +2 -2
  144. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  145. package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +1 -1
  146. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +10 -10
  147. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +1 -1
  148. package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +1 -1
  149. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
  150. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +4 -1
  151. package/packages/bundled-features/src/jobs/changes.json +6 -0
  152. package/packages/bundled-features/src/jobs/events.ts +1 -1
  153. package/packages/bundled-features/src/jobs/feature.ts +1 -0
  154. package/packages/bundled-features/src/jobs/handlers/consumer.write.ts +7 -4
  155. package/packages/bundled-features/src/jobs/handlers/projection-rebuild.job.ts +1 -1
  156. package/packages/bundled-features/src/jobs/handlers/reindex-entity.job.ts +11 -5
  157. package/packages/bundled-features/src/jobs/handlers/retention-cleanup.job.ts +1 -1
  158. package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
  159. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +1 -1
  160. package/packages/bundled-features/src/jobs/handlers/trigger.write.ts +2 -2
  161. package/packages/bundled-features/src/jobs/job-run-logger.ts +139 -90
  162. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  163. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  164. package/packages/bundled-features/src/legal-pages/lib/boot-check.ts +12 -8
  165. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  166. package/packages/bundled-features/src/mail-foundation/feature.ts +2 -2
  167. package/packages/bundled-features/src/mail-transport-smtp/changes.json +12 -0
  168. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  169. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +13 -12
  170. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  171. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  172. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  173. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  174. package/packages/bundled-features/src/managed-pages/seeding.ts +13 -8
  175. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +2 -2
  176. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -1
  177. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +2 -2
  178. package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
  179. package/packages/bundled-features/src/rate-limiting/handlers/status.query.ts +2 -2
  180. package/packages/bundled-features/src/readiness/handlers/status.query.ts +4 -1
  181. package/packages/bundled-features/src/renderer-foundation/api.ts +3 -3
  182. package/packages/bundled-features/src/secrets/announce-secret-change.ts +4 -4
  183. package/packages/bundled-features/src/secrets/changes.json +6 -0
  184. package/packages/bundled-features/src/secrets/feature.ts +9 -9
  185. package/packages/bundled-features/src/secrets/handlers/delete.write.ts +4 -3
  186. package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +13 -8
  187. package/packages/bundled-features/src/secrets/handlers/set.write.ts +4 -3
  188. package/packages/bundled-features/src/secrets/secrets-context.ts +16 -6
  189. package/packages/bundled-features/src/secrets/write-gate.ts +10 -3
  190. package/packages/bundled-features/src/seo/feature.ts +1 -1
  191. package/packages/bundled-features/src/sessions/changes.json +19 -0
  192. package/packages/bundled-features/src/sessions/feature.ts +2 -1
  193. package/packages/bundled-features/src/sessions/handlers/cleanup.job.ts +2 -2
  194. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +5 -2
  195. package/packages/bundled-features/src/sessions/handlers/list.query.ts +5 -2
  196. package/packages/bundled-features/src/sessions/handlers/mine.query.ts +5 -2
  197. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +3 -3
  198. package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  199. package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
  200. package/packages/bundled-features/src/sessions/index.ts +1 -1
  201. package/packages/bundled-features/src/sessions/screens.ts +8 -4
  202. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  203. package/packages/bundled-features/src/sessions/session-revoked-event.ts +1 -1
  204. package/packages/bundled-features/src/sessions/web/session-device-cell.tsx +10 -12
  205. package/packages/bundled-features/src/sessions/web/user-agent.ts +12 -0
  206. package/packages/bundled-features/src/shared/index.ts +15 -0
  207. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +36 -4
  208. package/packages/bundled-features/src/shared/require-for-tenant.ts +7 -5
  209. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  210. package/packages/bundled-features/src/shared/stream-tenant-backfill.ts +201 -0
  211. package/packages/bundled-features/src/shared/stream-tenant-derived-state.ts +107 -0
  212. package/packages/bundled-features/src/step-dispatcher/changes.json +6 -0
  213. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  214. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -1
  215. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  216. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +1 -1
  217. package/packages/bundled-features/src/subscription-dodo/runtime.ts +7 -7
  218. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +5 -3
  219. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -7
  220. package/packages/bundled-features/src/tags/feature.ts +1 -0
  221. package/packages/bundled-features/src/template-resolver/api.ts +3 -3
  222. package/packages/bundled-features/src/template-resolver/changes.json +54 -0
  223. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  224. package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
  225. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +5 -4
  226. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +2 -2
  227. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
  228. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  229. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +32 -7
  230. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  231. package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +1 -1
  232. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  233. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +2 -2
  234. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +6 -2
  235. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +4 -2
  236. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
  237. package/packages/bundled-features/src/template-resolver/seeding.ts +22 -5
  238. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +68 -19
  239. package/packages/bundled-features/src/template-resolver/template-resources.ts +4 -4
  240. package/packages/bundled-features/src/template-resolver/testing.ts +2 -2
  241. package/packages/bundled-features/src/template-resolver/web/document-insert-fields.tsx +222 -0
  242. package/packages/bundled-features/src/template-resolver/web/document-insert-model.ts +164 -0
  243. package/packages/bundled-features/src/template-resolver/web/document-insert-panel.tsx +115 -0
  244. package/packages/bundled-features/src/template-resolver/web/document-tiptap-bubble.tsx +177 -0
  245. package/packages/bundled-features/src/template-resolver/web/document-tiptap-content.tsx +38 -0
  246. package/packages/bundled-features/src/template-resolver/web/document-tiptap-context.ts +119 -0
  247. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +51 -1098
  248. package/packages/bundled-features/src/template-resolver/web/document-tiptap-helpers.ts +161 -0
  249. package/packages/bundled-features/src/template-resolver/web/document-tiptap-layout.tsx +83 -0
  250. package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar-groups.tsx +153 -0
  251. package/packages/bundled-features/src/template-resolver/web/document-tiptap-toolbar.tsx +182 -0
  252. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +18 -0
  253. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  254. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +19 -6
  255. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  256. package/packages/bundled-features/src/template-resolver/web/use-document-tiptap-model.ts +138 -0
  257. package/packages/bundled-features/src/tenant/changes.json +19 -0
  258. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  259. package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  260. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
  261. package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -2
  262. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  263. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -1
  264. package/packages/bundled-features/src/tenant/index.ts +5 -0
  265. package/packages/bundled-features/src/tenant/invited-membership.ts +6 -1
  266. package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -1
  267. package/packages/bundled-features/src/tenant/seeding.ts +65 -62
  268. package/packages/bundled-features/src/tenant/stream-tenant-backfill.ts +115 -0
  269. package/packages/bundled-features/src/tenant/testing.ts +77 -0
  270. package/packages/bundled-features/src/tenant-handover/events.ts +1 -1
  271. package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -1
  272. package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
  273. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +5 -5
  274. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +22 -10
  275. package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +22 -11
  276. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +23 -12
  277. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +42 -4
  278. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +9 -8
  279. package/packages/bundled-features/src/tier-engine/entity.ts +1 -1
  280. package/packages/bundled-features/src/tier-engine/feature.ts +8 -3
  281. package/packages/bundled-features/src/tier-engine/handlers/active-tier.query.ts +3 -3
  282. package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +7 -1
  283. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +3 -3
  284. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +71 -97
  285. package/packages/bundled-features/src/user/feature.ts +1 -0
  286. package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +2 -2
  287. package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -2
  288. package/packages/bundled-features/src/user/index.ts +2 -0
  289. package/packages/bundled-features/src/user/invited-global-roles.ts +6 -1
  290. package/packages/bundled-features/src/user/principal-status.ts +1 -1
  291. package/packages/bundled-features/src/user/seeding.ts +24 -6
  292. package/packages/bundled-features/src/user/testing.ts +22 -0
  293. package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +1 -1
  294. package/packages/bundled-features/src/user-data-rights/README.md +1 -1
  295. package/packages/bundled-features/src/user-data-rights/audit-download.ts +12 -2
  296. package/packages/bundled-features/src/user-data-rights/changes.json +36 -0
  297. package/packages/bundled-features/src/user-data-rights/feature.ts +30 -24
  298. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -1
  299. package/packages/bundled-features/src/user-data-rights/handlers/deletion-grace-period.ts +1 -1
  300. package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +2 -2
  301. package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +2 -2
  302. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  303. package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +4 -4
  304. package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +12 -6
  305. package/packages/bundled-features/src/user-data-rights/index.ts +8 -1
  306. package/packages/bundled-features/src/user-data-rights/lib/mail-transport-resolver.ts +1 -1
  307. package/packages/bundled-features/src/user-data-rights/lib/storage-provider-resolver.ts +1 -1
  308. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +6 -1
  309. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +8 -2
  310. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +14 -3
  311. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +11 -1
  312. package/packages/bundled-features/src/user-data-rights/web/constants.ts +16 -0
  313. package/packages/bundled-features/src/user-data-rights/web/index.ts +6 -0
  314. package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -6
  315. package/packages/bundled-features/src/waitlist/changes.json +18 -0
  316. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  317. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +1 -1
  318. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +2 -2
  319. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
  320. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +34 -10
  321. package/packages/bundled-features/src/waitlist/import.ts +23 -3
  322. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +8 -2
  323. package/packages/bundled-features/src/waitlist/link-on-signup.ts +7 -1
  324. package/packages/bundled-features/src/waitlist/options.ts +2 -1
  325. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  326. package/packages/bundled-features/src/workflow-runner/db/queries/due-runs.ts +1 -1
  327. package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +4 -3
  328. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  329. package/packages/bundled-features/src/workflow-runner/feature.ts +2 -2
  330. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +8 -8
  331. package/packages/bundled-features/src/workflow-runner/runner.ts +6 -4
  332. package/samples/apps/cap-billing-demo/src/feature.ts +19 -5
  333. package/samples/apps/config-demo/src/app/server.ts +1 -1
  334. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  335. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  336. package/samples/apps/styleguide/src/app/server.ts +6 -3
  337. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  338. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  339. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  340. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  341. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  342. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  343. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  344. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  345. package/samples/apps/use-all-bundled/feature-manifest.json +17 -1
  346. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  347. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  348. package/samples/apps/use-all-bundled/package.json +2 -1
  349. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  350. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  351. package/samples/apps/use-all-bundled/src/app/seed.ts +12 -2
  352. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  353. package/samples/apps/user-data-rights-demo/README.md +1 -1
  354. package/samples/apps/user-data-rights-demo/src/feature.ts +3 -2
  355. package/samples/recipes/api-worker-split/README.md +2 -1
  356. package/samples/recipes/api-worker-split/src/feature.ts +2 -1
  357. package/samples/recipes/cross-feature-events/README.md +5 -5
  358. package/samples/recipes/cross-feature-events/package.json +1 -1
  359. package/samples/recipes/cross-feature-events/src/feature.ts +4 -4
  360. package/samples/recipes/event-sourcing/README.md +5 -5
  361. package/samples/recipes/event-sourcing/package.json +1 -1
  362. package/samples/recipes/event-sourcing/src/feature.ts +15 -15
  363. package/samples/recipes/files-post-processing/README.md +1 -1
  364. package/samples/recipes/files-post-processing/package.json +1 -1
  365. package/samples/recipes/files-post-processing/src/feature.ts +6 -4
  366. package/samples/recipes/golden-path/src/feature.ts +2 -1
  367. package/samples/recipes/lane-routing/package.json +1 -1
  368. package/samples/recipes/legal-pages/README.md +2 -2
  369. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  370. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  371. package/samples/recipes/list-table-options/README.md +50 -0
  372. package/samples/recipes/list-table-options/package.json +11 -0
  373. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  374. package/samples/recipes/managed-config/README.md +3 -3
  375. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  376. package/samples/recipes/search/README.md +1 -1
  377. package/samples/recipes/search/src/feature.ts +3 -3
  378. package/samples/recipes/secrets-demo/README.md +1 -1
  379. package/samples/recipes/secrets-demo/src/feature.ts +5 -2
  380. package/samples/recipes/state-machine/src/events.ts +4 -3
  381. package/samples/recipes/state-machine/src/reducer.ts +1 -1
  382. package/samples/recipes/tenant-isolation/README.md +1 -0
  383. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  384. package/samples/recipes/workflow-engine/src/resume-loop.ts +4 -4
  385. package/samples/recipes/workflow-engine/src/workflow-runner.ts +2 -2
@@ -168,9 +168,9 @@ async function issueInvitation(
168
168
  ctx: HandlerContext,
169
169
  refuseAccepted = false,
170
170
  ): Promise<WriteResult<InviteCreateData>> {
171
- if (!ctx.redis) {
171
+ if (!ctx.services.redis) {
172
172
  return writeFailure(
173
- new InternalError({ message: "invite-create requires ctx.redis for token store" }),
173
+ new InternalError({ message: "invite-create requires ctx.services.redis for token store" }),
174
174
  );
175
175
  }
176
176
 
@@ -197,7 +197,7 @@ async function issueInvitation(
197
197
  const existingVersion = existing["version"] as number; // @cast-boundary db-row
198
198
  // At most one live invite token per invitation: invalidate
199
199
  // whatever's there before minting the new one.
200
- await invalidateExistingInviteToken(ctx.redis, invitationId);
200
+ await invalidateExistingInviteToken(ctx.services.redis, invitationId);
201
201
 
202
202
  const updateResult = await executor.update(
203
203
  {
@@ -237,7 +237,7 @@ async function issueInvitation(
237
237
  }
238
238
 
239
239
  const token = generateToken();
240
- await storeInviteToken(ctx.redis, { invitationId, token, ttlSeconds });
240
+ await storeInviteToken(ctx.services.redis, { invitationId, token, ttlSeconds });
241
241
 
242
242
  const locale = request.locale ?? resolveHandlerMailLocale(ctx, opts.locale);
243
243
 
@@ -52,9 +52,9 @@ export const inviteInfoQuery = defineQueryHandler({
52
52
  "Anonymous, read-only invite lookup for the invite-acceptance page: reveals the invited " +
53
53
  "email and whether an account already exists for it, without consuming the token.",
54
54
  handler: async (query, ctx) => {
55
- if (!ctx.redis) throwInvalidInviteToken();
55
+ if (!ctx.services.redis) throwInvalidInviteToken();
56
56
 
57
- const invitationId = await getInvitationIdForToken(ctx.redis, query.payload.token);
57
+ const invitationId = await getInvitationIdForToken(ctx.services.redis, query.payload.token);
58
58
  if (!invitationId) throwInvalidInviteToken();
59
59
 
60
60
  type InvitationRow = {
@@ -103,16 +103,16 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
103
103
  reason: INVITE_SIGNUP_COMPLETE_ESCAPE_HATCH_REASON,
104
104
  },
105
105
  handler: async (event, ctx) => {
106
- if (!ctx.redis) {
106
+ if (!ctx.services.redis) {
107
107
  return writeFailure(
108
- new InternalError({ message: "invite-signup-complete requires ctx.redis" }),
108
+ new InternalError({ message: "invite-signup-complete requires ctx.services.redis" }),
109
109
  );
110
110
  }
111
111
 
112
- const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
112
+ const invitationId = await getInvitationIdForToken(ctx.services.redis, event.payload.token);
113
113
  if (!invitationId) return invalidInviteToken();
114
114
 
115
- const burn = await burnInviteToken(ctx.redis, event.payload.token);
115
+ const burn = await burnInviteToken(ctx.services.redis, event.payload.token);
116
116
  if (burn === "already-used") return invalidInviteToken();
117
117
 
118
118
  type InvitationRow = {
@@ -187,6 +187,8 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
187
187
  db: dbConn,
188
188
  tenantId: invitationTenantId,
189
189
  mode: "system",
190
+ reason:
191
+ "invitation tenant differs from the caller's tenant, the accept step updates the invitation row there",
190
192
  });
191
193
  const updateResult = await invitationExecutor.update(
192
194
  {
@@ -214,7 +216,7 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
214
216
  mergedRoles,
215
217
  );
216
218
 
217
- await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
219
+ await deleteInviteToken(ctx.services.redis, { invitationId, token: event.payload.token });
218
220
 
219
221
  if (mfaGate !== undefined) {
220
222
  committed = true;
@@ -239,8 +241,8 @@ export function createInviteSignupCompleteHandler(opts: InviteSignupCompleteOpti
239
241
  },
240
242
  };
241
243
  } finally {
242
- if (!committed && ctx.redis) {
243
- await unburnInviteToken(ctx.redis, event.payload.token);
244
+ if (!committed && ctx.services.redis) {
245
+ await unburnInviteToken(ctx.services.redis, event.payload.token);
244
246
  }
245
247
  }
246
248
  },
@@ -35,7 +35,7 @@ export type LoginHandlerOptions = {
35
35
  readonly strictEmailVerification?: boolean;
36
36
  // Brute-force protection: after N wrong-password attempts the account
37
37
  // locks for the configured duration. State lives in Redis (see
38
- // lockout-store.ts) — if ctx.redis is unset, lockout is skipped and the
38
+ // lockout-store.ts) — if ctx.services.redis is unset, lockout is skipped and the
39
39
  // handler falls back to classic invalid-credentials. Counter is monotonic
40
40
  // and only resets on a successful login, so a re-lock after the cooldown
41
41
  // happens on the FIRST miss, not the Nth (strict semantic — favours
@@ -123,10 +123,10 @@ export async function gateCountUnknownUser(
123
123
  lockoutDurationMinutes: number,
124
124
  ): Promise<GateOutcome<undefined>> {
125
125
  const subject = unknownEmailLockoutSubject(ctx, email);
126
- if (!ctx.redis || subject === undefined) return ok(undefined);
126
+ if (!ctx.services.redis || subject === undefined) return ok(undefined);
127
127
  const locked = await gateEnforceLockout(ctx, subject);
128
128
  if (!locked.ok) return locked;
129
- await recordFailedAttempt(ctx.redis, subject, maxFailedAttempts, lockoutDurationMinutes);
129
+ await recordFailedAttempt(ctx.services.redis, subject, maxFailedAttempts, lockoutDurationMinutes);
130
130
  return ok(undefined);
131
131
  }
132
132
 
@@ -138,8 +138,8 @@ export async function gateEnforceLockout(
138
138
  ctx: HandlerContext,
139
139
  userId: string,
140
140
  ): Promise<GateOutcome<undefined>> {
141
- if (!ctx.redis) return ok(undefined);
142
- const state = await getLockoutState(ctx.redis, userId);
141
+ if (!ctx.services.redis) return ok(undefined);
142
+ const state = await getLockoutState(ctx.services.redis, userId);
143
143
  if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
144
144
  const now = Date.now();
145
145
  if (state.lockedUntil > now) {
@@ -160,15 +160,20 @@ export async function gateVerifyPassword(
160
160
  ): Promise<GateOutcome<undefined>> {
161
161
  const passwordOk = await verifyPassword(found.passwordHash, password);
162
162
  if (!passwordOk) {
163
- if (ctx.redis) {
164
- await recordFailedAttempt(ctx.redis, found.id, maxFailedAttempts, lockoutDurationMinutes);
163
+ if (ctx.services.redis) {
164
+ await recordFailedAttempt(
165
+ ctx.services.redis,
166
+ found.id,
167
+ maxFailedAttempts,
168
+ lockoutDurationMinutes,
169
+ );
165
170
  }
166
171
  return reject(invalidCredentials());
167
172
  }
168
173
  // Clear before MFA — MFA has its own attempt-cap; don't password-lock
169
174
  // users who occasionally mistype across otherwise-successful logins.
170
- if (ctx.redis) {
171
- await clearLockoutState(ctx.redis, found.id);
175
+ if (ctx.services.redis) {
176
+ await clearLockoutState(ctx.services.redis, found.id);
172
177
  }
173
178
  return ok(undefined);
174
179
  }
@@ -216,7 +221,7 @@ export async function gateResolveMembership(
216
221
  const globalRoles = parseRoles(found.roles ?? null);
217
222
 
218
223
  for (const candidate of candidates) {
219
- const active = await ctx.resolveActiveMembership(found.id, candidate.tenantId);
224
+ const active = await ctx.auth.resolveActiveMembership(found.id, candidate.tenantId);
220
225
  if (active.kind === "rejected") {
221
226
  // A blocked principal is blocked for every tenant, so stop here;
222
227
  // not_a_member/tenant_teardown are per-tenant — try the next candidate.
@@ -275,7 +280,7 @@ export async function gateBuildSession(
275
280
  ...(timezone !== null && timezone !== undefined && { timezone }),
276
281
  ...(locale !== null && locale !== undefined && { locale }),
277
282
  };
278
- const claims = await ctx.resolveAuthClaims(baseSession);
283
+ const claims = await ctx.auth.resolveClaims(baseSession);
279
284
  const session: SessionUser =
280
285
  Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
281
286
  return { kind: "auth-session", session };
@@ -284,7 +289,7 @@ export async function gateBuildSession(
284
289
  // Login — unauthenticated entry point. The route is wired public (no JWT
285
290
  // middleware), dispatching with the framework's anonymous identity for the
286
291
  // handler's access check. Everything inside the handler goes through
287
- // ctx.queryAs(system, ...) so the user feature stays the single owner of its
292
+ // ctx.dispatch.queryAs(system, ...) so the user feature stays the single owner of its
288
293
  // table.
289
294
  export function createLoginHandler(opts: LoginHandlerOptions = {}) {
290
295
  const strictVerification = opts.strictEmailVerification === true;
@@ -305,8 +310,8 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
305
310
  grants: ["systemIdentity", "unsafeRaw"],
306
311
  reason:
307
312
  "Unauthenticated login has no caller identity yet — it looks up the user row by " +
308
- "email via ctx.queryAs(SYSTEM, ...) and resolves tenant membership via " +
309
- "ctx.resolveActiveMembership before a session exists. It also reads the MFA " +
313
+ "email via ctx.dispatch.queryAs(SYSTEM, ...) and resolves tenant membership via " +
314
+ "ctx.auth.resolveActiveMembership before a session exists. It also reads the MFA " +
310
315
  "enrollment of the tenant named in the signed login/setup token, not the guest " +
311
316
  "dispatch tenant, via the mfaStatusChecker callback.",
312
317
  },
@@ -318,7 +323,9 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
318
323
  const systemUser = createSystemUser(SYSTEM_USER_ID);
319
324
 
320
325
  const foundRow = parseAuthUserRow(
321
- await ctx.queryAs(systemUser, UserQueries.findForAuth, { email: event.payload.email }),
326
+ await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
327
+ email: event.payload.email,
328
+ }),
322
329
  );
323
330
  if (!foundRow?.passwordHash || foundRow.isDeleted) {
324
331
  const unknown = await gateCountUnknownUser(
@@ -352,7 +359,7 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
352
359
  if (!statusGate.ok) return statusGate.result;
353
360
 
354
361
  // Still needed for candidate ORDER (preferred tenant first) in gateResolveMembership.
355
- const memberships = (await ctx.queryAs(systemUser, TENANT_MEMBERSHIPS_QUERY, {
362
+ const memberships = (await ctx.dispatch.queryAs(systemUser, TENANT_MEMBERSHIPS_QUERY, {
356
363
  userId: found.id,
357
364
  })) as Array<Membership>; // @cast-boundary db-runner
358
365
 
@@ -31,6 +31,7 @@ export function createRequestAccountUnlockHandler(opts: RequestAccountUnlockOpti
31
31
  // empty) lockout state — harmless no-op if they weren't locked.
32
32
  extraSilentSkip: () => false,
33
33
  notificationType: AuthMailNotificationTypes.accountUnlock,
34
+ forwardsReturnTo: false,
34
35
  },
35
36
  opts,
36
37
  );
@@ -28,6 +28,7 @@ export function createRequestEmailVerificationHandler(opts: RequestEmailVerifica
28
28
  // caller sees the same 200 regardless of whether a token was minted.
29
29
  extraSilentSkip: (user) => user.emailVerified === true,
30
30
  notificationType: AuthMailNotificationTypes.emailVerification,
31
+ forwardsReturnTo: false,
31
32
  },
32
33
  opts,
33
34
  );
@@ -30,6 +30,7 @@ export function createRequestPasswordResetHandler(opts: RequestPasswordResetOpti
30
30
  // state. The sessions feature handles the post-change revocation.
31
31
  extraSilentSkip: () => false,
32
32
  notificationType: AuthMailNotificationTypes.passwordReset,
33
+ forwardsReturnTo: true,
33
34
  },
34
35
  opts,
35
36
  );
@@ -31,7 +31,7 @@ export function createResetPasswordHandler(opts: ResetPasswordOptions) {
31
31
  grants: ["systemIdentity"],
32
32
  reason:
33
33
  "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
34
- "target user via ctx.writeAs(SYSTEM, user:update) to set the new password hash.",
34
+ "target user via ctx.dispatch.writeAs(SYSTEM, user:update) to set the new password hash.",
35
35
  },
36
36
  agent: { expose: false },
37
37
  handler: async (event, ctx) => {
@@ -51,7 +51,8 @@ export function createResetPasswordHandler(opts: ResetPasswordOptions) {
51
51
 
52
52
  return runConfirmTokenFlow(ctx, verify.userId, verify.expiresAtMs, {
53
53
  purpose: "reset",
54
- redisRequiredMessage: "password-reset requires ctx.redis to enforce token single-use",
54
+ redisRequiredMessage:
55
+ "password-reset requires ctx.services.redis to enforce token single-use",
55
56
  invalidToken: invalidResetToken,
56
57
  buildChanges: async () => ({
57
58
  passwordHash: await hashPassword(event.payload.newPassword),
@@ -119,16 +119,16 @@ async function claimBoundHandover(
119
119
  ): Promise<ClaimedHandover | undefined> {
120
120
  const provider = findSignupHandoverProvider(ctx.registry);
121
121
  if (!provider) {
122
- ctx.log?.warn(
122
+ ctx.obs.log?.warn(
123
123
  "signup-confirm: handover binding present but no signupHandover provider mounted",
124
124
  );
125
125
  return undefined;
126
126
  }
127
127
  const claim = provider.mintClaim(binding);
128
- const claimResult = await ctx.writeAs(session, claim.qualifiedName, claim.payload);
128
+ const claimResult = await ctx.dispatch.writeAs(session, claim.qualifiedName, claim.payload);
129
129
  if (claimResult.isSuccess) return { entityType: binding.entityType, id: binding.rowId };
130
130
  if (!isBenignClaimRejection(claimResult.error)) throw reraiseAsKumikoError(claimResult.error);
131
- ctx.log?.warn("signup-confirm: handover grant did not redeem", {
131
+ ctx.obs.log?.warn("signup-confirm: handover grant did not redeem", {
132
132
  entityType: binding.entityType,
133
133
  });
134
134
  return undefined;
@@ -146,22 +146,22 @@ export function createSignupConfirmHandler(opts: SignupConfirmOptions = {}) {
146
146
  reason: SIGNUP_CONFIRM_PROVISION_REASON,
147
147
  },
148
148
  handler: async (event, ctx) => {
149
- if (!ctx.redis) {
149
+ if (!ctx.services.redis) {
150
150
  return writeFailure(
151
151
  new InternalError({
152
- message: "signup-confirm requires ctx.redis for token consumption",
152
+ message: "signup-confirm requires ctx.services.redis for token consumption",
153
153
  }),
154
154
  );
155
155
  }
156
156
 
157
157
  // Token lookup: nonexistent, expired, and already-consumed tokens all
158
158
  // collapse to invalid_signup_token to prevent enumeration.
159
- const email = await getEmailForSignupToken(ctx.redis, event.payload.token);
159
+ const email = await getEmailForSignupToken(ctx.services.redis, event.payload.token);
160
160
  if (!email) return invalidSignupToken();
161
161
 
162
162
  // Single-use burn: of two parallel confirmations from different tabs,
163
163
  // one succeeds and the other receives invalid_signup_token.
164
- const burn = await burnSignupToken(ctx.redis, event.payload.token);
164
+ const burn = await burnSignupToken(ctx.services.redis, event.payload.token);
165
165
  if (burn === "already-used") return invalidSignupToken();
166
166
 
167
167
  let committed = false;
@@ -229,7 +229,7 @@ export function createSignupConfirmHandler(opts: SignupConfirmOptions = {}) {
229
229
  // Claim BEFORE spending the signup token or the binding below and
230
230
  // before `committed` flips: if the claim throws, the `finally` still
231
231
  // unburns and the activation link stays retryable.
232
- const handoverBinding = await getSignupHandover(ctx.redis, event.payload.token);
232
+ const handoverBinding = await getSignupHandover(ctx.services.redis, event.payload.token);
233
233
  const handover = handoverBinding
234
234
  ? await claimBoundHandover(ctx, session, handoverBinding)
235
235
  : undefined;
@@ -245,8 +245,8 @@ export function createSignupConfirmHandler(opts: SignupConfirmOptions = {}) {
245
245
 
246
246
  // Drop both token lookup keys; the burn key stays for its remaining
247
247
  // TTL as replay protection.
248
- await deleteSignupToken(ctx.redis, { email, token: event.payload.token });
249
- if (handoverBinding) await deleteSignupHandover(ctx.redis, event.payload.token);
248
+ await deleteSignupToken(ctx.services.redis, { email, token: event.payload.token });
249
+ if (handoverBinding) await deleteSignupHandover(ctx.services.redis, event.payload.token);
250
250
 
251
251
  if (mfaGate !== undefined) {
252
252
  committed = true;
@@ -275,10 +275,10 @@ export function createSignupConfirmHandler(opts: SignupConfirmOptions = {}) {
275
275
  },
276
276
  };
277
277
  } finally {
278
- if (!committed && ctx.redis) {
278
+ if (!committed && ctx.services.redis) {
279
279
  // Release the burn so a retry after a DB hiccup is not blocked.
280
280
  // Keep the token lookup keys so the user can click the email link again.
281
- await unburnSignupToken(ctx.redis, event.payload.token);
281
+ await unburnSignupToken(ctx.services.redis, event.payload.token);
282
282
  }
283
283
  }
284
284
  },
@@ -31,6 +31,7 @@ import { resolveHandlerMailLocale } from "../magic-link-mail.ts";
31
31
  import { AUTH_SELF_REGISTRATION_FEATURE } from "../self-registration-toggle.ts";
32
32
  import { issueSignupActivation } from "../signup-activation.ts";
33
33
  import { storeSignupHandover, takeOverSignupHandoverForResend } from "../signup-token-store.ts";
34
+ import { MAX_RETURN_PATH_LENGTH } from "../web/return-path.ts";
34
35
 
35
36
  const SignupRequestSchema = z.object({
36
37
  email: z.email(),
@@ -43,6 +44,8 @@ const SignupRequestSchema = z.object({
43
44
  token: z.string().min(1),
44
45
  })
45
46
  .optional(),
47
+ // A bad returnTo must not void the signup; the mail builder drops unsafe values.
48
+ returnTo: z.string().max(MAX_RETURN_PATH_LENGTH).optional().catch(undefined),
46
49
  });
47
50
 
48
51
  const HANDOVER_GRANT_VERIFY_REASON =
@@ -102,13 +105,13 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
102
105
  // No email address here — this fires on every no-op request, PII
103
106
  // must not accumulate in logs just for the "was this toggled off"
104
107
  // signal a support ticket needs.
105
- ctx.log?.info("signup-request skipped: self-registration disabled");
108
+ ctx.obs.log?.info("signup-request skipped: self-registration disabled");
106
109
  return { isSuccess: true, data: { kind: "no-op" } };
107
110
  }
108
- if (!ctx.redis) {
111
+ if (!ctx.services.redis) {
109
112
  return writeFailure(
110
113
  new InternalError({
111
- message: "signup-request requires ctx.redis for the activation-token store",
114
+ message: "signup-request requires ctx.services.redis for the activation-token store",
112
115
  }),
113
116
  );
114
117
  }
@@ -122,10 +125,10 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
122
125
  // resend without a fresh `handover` in the payload (the common case:
123
126
  // the visitor just retyped their email) must still carry the earlier
124
127
  // verified grant over to the new token instead of silently dropping it.
125
- const priorBinding = await takeOverSignupHandoverForResend(ctx.redis, email);
128
+ const priorBinding = await takeOverSignupHandoverForResend(ctx.services.redis, email);
126
129
 
127
130
  const locale = resolveHandlerMailLocale(ctx, opts.locale);
128
- const redis = ctx.redis;
131
+ const redis = ctx.services.redis;
129
132
 
130
133
  const issued = await issueSignupActivation({
131
134
  redis,
@@ -133,6 +136,7 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
133
136
  email,
134
137
  appUrl: opts.appUrl,
135
138
  timeZone: ctx.tz.user,
139
+ ...(event.payload.returnTo !== undefined && { returnTo: event.payload.returnTo }),
136
140
  ...(opts.tokenTtlMinutes !== undefined && { tokenTtlMinutes: opts.tokenTtlMinutes }),
137
141
  ...(opts.appName !== undefined && { appName: opts.appName }),
138
142
  ...(locale !== undefined && { locale }),
@@ -149,7 +153,7 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
149
153
  const { entityType, token: grantToken } = event.payload.handover;
150
154
  const provider = findSignupHandoverProvider(ctx.registry);
151
155
  if (!provider) {
152
- ctx.log?.warn(
156
+ ctx.obs.log?.warn(
153
157
  "signup-request: handover payload present but no signupHandover provider mounted",
154
158
  );
155
159
  } else {
@@ -163,10 +167,14 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
163
167
  if (verified) {
164
168
  binding = verified;
165
169
  } else {
166
- ctx.log?.warn("signup-request: handover grant did not verify", { entityType });
170
+ ctx.obs.log?.warn("signup-request: handover grant did not verify", {
171
+ entityType,
172
+ });
167
173
  }
168
174
  } catch {
169
- ctx.log?.warn("signup-request: handover grant verification threw", { entityType });
175
+ ctx.obs.log?.warn("signup-request: handover grant verification threw", {
176
+ entityType,
177
+ });
170
178
  }
171
179
  }
172
180
  }
@@ -43,10 +43,12 @@ export function createSwitchTenantMfaGateHandler(
43
43
  "Runs the MFA gate of the login for a tenant switch and answers with an MFA challenge, a setup requirement, or clearance.",
44
44
  handler: async (event, ctx) => {
45
45
  const { userId, tenantId } = event.payload;
46
- const active = await ctx.resolveActiveMembership(userId, tenantId);
46
+ const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
47
47
  if (active.kind === "rejected") return noMembership();
48
48
  const userRow = parseAuthUserRow(
49
- await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, { id: userId }),
49
+ await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
50
+ id: userId,
51
+ }),
50
52
  );
51
53
  const roles = buildSessionRoles(parseRoles(userRow?.roles ?? null), active.membership.roles);
52
54
  const mfaGate = await gateEnforceMfa(ctx, opts, userId, tenantId, roles);
@@ -22,9 +22,12 @@ import { type AuthUserRow, parseAuthUserRow } from "../auth-user-row.ts";
22
22
  import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.ts";
23
23
  import type { AuthMailLocale } from "../email-templates.ts";
24
24
  import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.ts";
25
+ import { MAX_RETURN_PATH_LENGTH } from "../web/return-path.ts";
25
26
 
26
27
  const RequestTokenSchema = z.object({
27
28
  email: z.email(),
29
+ // A bad returnTo must not void the request; the mail builder drops unsafe values.
30
+ returnTo: z.string().max(MAX_RETURN_PATH_LENGTH).optional().catch(undefined),
28
31
  });
29
32
 
30
33
  // What the route layer reads off `result.data` after dispatching. Identical
@@ -67,6 +70,9 @@ export type TokenRequestSpec<TName extends string, TSuccessKind extends string>
67
70
  // Trigger-less r.notification dispatched via ctx.notify on success; the
68
71
  // recipient is an anonymous email, so route:{email} delivers directly.
69
72
  readonly notificationType: string;
73
+ // Only flows whose landing page can continue to a page (password reset) put the request's
74
+ // returnTo into the mail link; verification and unlock ignore it.
75
+ readonly forwardsReturnTo: boolean;
70
76
  };
71
77
 
72
78
  export type TokenRequestOptions = {
@@ -105,7 +111,7 @@ export function createTokenRequestHandler<TName extends string, TSuccessKind ext
105
111
  grants: ["systemIdentity"],
106
112
  reason:
107
113
  "Anonymous requester has no session — looks up the target user by email via " +
108
- "ctx.queryAs(SYSTEM, user:findForAuth) to mint the out-of-band token.",
114
+ "ctx.dispatch.queryAs(SYSTEM, user:findForAuth) to mint the out-of-band token.",
109
115
  },
110
116
  description: spec.description,
111
117
  handler: async (event, ctx) => {
@@ -122,7 +128,7 @@ export function createTokenRequestHandler<TName extends string, TSuccessKind ext
122
128
  const systemUser = createSystemUser(event.user.tenantId);
123
129
 
124
130
  const user = parseAuthUserRow(
125
- await ctx.queryAs(systemUser, UserQueries.findForAuth, {
131
+ await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
126
132
  email: event.payload.email,
127
133
  }),
128
134
  );
@@ -159,6 +165,8 @@ export function createTokenRequestHandler<TName extends string, TSuccessKind ext
159
165
  timeZone: recipientTimeZone(user, ctx.tz.user),
160
166
  ...(opts.appName !== undefined && { appName: opts.appName }),
161
167
  locale,
168
+ ...(spec.forwardsReturnTo &&
169
+ event.payload.returnTo !== undefined && { returnTo: event.payload.returnTo }),
162
170
  },
163
171
  );
164
172
 
@@ -32,7 +32,7 @@ export function createVerifyEmailHandler(opts: VerifyEmailOptions) {
32
32
  grants: ["systemIdentity"],
33
33
  reason:
34
34
  "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
35
- "target user via ctx.writeAs(SYSTEM, user:update) to set emailVerified.",
35
+ "target user via ctx.dispatch.writeAs(SYSTEM, user:update) to set emailVerified.",
36
36
  },
37
37
  agent: { expose: false },
38
38
  handler: async (event, ctx) => {
@@ -49,7 +49,8 @@ export function createVerifyEmailHandler(opts: VerifyEmailOptions) {
49
49
 
50
50
  return runConfirmTokenFlow<VerifyEmailData>(ctx, verify.userId, verify.expiresAtMs, {
51
51
  purpose: "verify",
52
- redisRequiredMessage: "email-verification requires ctx.redis to enforce token single-use",
52
+ redisRequiredMessage:
53
+ "email-verification requires ctx.services.redis to enforce token single-use",
53
54
  invalidToken: invalidVerificationToken,
54
55
  buildChanges: async () => ({ emailVerified: true }),
55
56
  successData: { kind: "verified" },
@@ -12,6 +12,7 @@ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
12
12
  import { mailT, resolveMailLocale } from "@cosmicdrift/kumiko-framework/i18n";
13
13
  import { type AuthMailLocale, tokenMailExpiry } from "./email-templates.ts";
14
14
  import { authMailTemplateLocale } from "./mail-notifications.ts";
15
+ import { isSafeReturnPath, RETURN_TO_QUERY_PARAM } from "./web/return-path.ts";
15
16
 
16
17
  // Per-flow constants: which notification type to dispatch. fallbackAppName
17
18
  // replaces the translated default when the flow has no app name of its own.
@@ -38,15 +39,23 @@ export type MagicLinkMailParams = {
38
39
  readonly timeZone?: string;
39
40
  // Invite mails name the role the recipient is invited as.
40
41
  readonly role?: string;
42
+ /** Page the landing page should lead to afterwards; an unsafe value is left out of the link. */
43
+ readonly returnTo?: string;
41
44
  };
42
45
 
43
46
  function resolveAppUrl(appUrl: string | ((locale: string) => string), locale: string): string {
44
47
  return typeof appUrl === "function" ? appUrl(locale) : appUrl;
45
48
  }
46
49
 
47
- function appendToken(appUrl: string, token: string): string {
50
+ function appendTokenAndReturnTo(
51
+ appUrl: string,
52
+ token: string,
53
+ returnTo: string | undefined,
54
+ ): string {
55
+ const query = new URLSearchParams({ token });
56
+ if (isSafeReturnPath(returnTo)) query.set(RETURN_TO_QUERY_PARAM, returnTo);
48
57
  const sep = appUrl.includes("?") ? "&" : "?";
49
- return `${appUrl}${sep}token=${encodeURIComponent(token)}`;
58
+ return `${appUrl}${sep}${query.toString()}`;
50
59
  }
51
60
 
52
61
  // X-Locale on the request wins over the handler's static opts.locale, then
@@ -93,7 +102,11 @@ export async function dispatchMagicLinkMail(
93
102
  data: {
94
103
  appName:
95
104
  params.appName ?? spec.fallbackAppName ?? mailT(templateLocale, "auth.mail.appNameDefault"),
96
- actionUrl: appendToken(resolveAppUrl(params.appUrl, urlLocale), params.token),
105
+ actionUrl: appendTokenAndReturnTo(
106
+ resolveAppUrl(params.appUrl, urlLocale),
107
+ params.token,
108
+ params.returnTo,
109
+ ),
97
110
  validFor,
98
111
  validUntil,
99
112
  ...(params.role !== undefined && { role: params.role }),
@@ -20,10 +20,14 @@ import {
20
20
  } from "@cosmicdrift/kumiko-framework/crypto";
21
21
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
22
22
  import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
23
- import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
23
+ import {
24
+ createSystemUser,
25
+ type SessionUser,
26
+ SYSTEM_TENANT_ID,
27
+ type TenantId,
28
+ } from "@cosmicdrift/kumiko-framework/engine";
24
29
  import { ConflictError } from "@cosmicdrift/kumiko-framework/errors";
25
30
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
26
- import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
27
31
  import { hashPassword } from "../shared/index.ts";
28
32
  // kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
29
33
  import { type SeedTenantHooks, seedTenant, seedTenantMembership } from "../tenant/seeding.ts";
@@ -224,10 +228,10 @@ export async function seedAdmin(
224
228
  db: DbConnection,
225
229
  options: SeedAdminOptions,
226
230
  ): Promise<{ id: string }> {
227
- const by = options.by ?? TestUsers.systemAdmin;
231
+ const byOption = options.by === undefined ? {} : { by: options.by };
228
232
 
229
233
  for (const m of options.memberships) {
230
- await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, by });
234
+ await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, ...byOption });
231
235
  }
232
236
 
233
237
  const { id: userId } = await seedUserWithPassword(db, {
@@ -236,7 +240,7 @@ export async function seedAdmin(
236
240
  displayName: options.displayName,
237
241
  ...(options.globalRoles !== undefined && { roles: options.globalRoles }),
238
242
  ...(options.emailVerified !== undefined && { emailVerified: options.emailVerified }),
239
- by,
243
+ ...byOption,
240
244
  });
241
245
 
242
246
  for (const m of options.memberships) {
@@ -244,7 +248,7 @@ export async function seedAdmin(
244
248
  userId,
245
249
  tenantId: m.tenantId,
246
250
  roles: m.roles,
247
- by,
251
+ ...byOption,
248
252
  });
249
253
  }
250
254
 
@@ -372,7 +376,8 @@ export async function seedAdminGuarded(
372
376
  const existingId = await findExistingUserIdForEmail(db, kms, options.email);
373
377
  if (existingId === undefined) return seedAdmin(db, options);
374
378
 
375
- const by = options.by ?? TestUsers.systemAdmin;
379
+ const byOption = options.by === undefined ? {} : { by: options.by };
380
+ const by = options.by ?? createSystemUser(SYSTEM_TENANT_ID);
376
381
  const existingRow = await fetchOne(db, userTable, { id: existingId });
377
382
  if (existingRow) {
378
383
  // Re-write the email so email_bidx is recomputed; without it the account
@@ -384,12 +389,12 @@ export async function seedAdminGuarded(
384
389
  }
385
390
  }
386
391
  for (const m of options.memberships) {
387
- await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, by });
392
+ await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, ...byOption });
388
393
  await seedTenantMembership(db, {
389
394
  userId: existingId,
390
395
  tenantId: m.tenantId,
391
396
  roles: m.roles,
392
- by,
397
+ ...byOption,
393
398
  });
394
399
  }
395
400
  return { id: existingId };
@@ -28,6 +28,7 @@ export type IssueSignupActivationArgs = {
28
28
  readonly locale?: string;
29
29
  readonly timeZone?: string;
30
30
  readonly tokenTtlMinutes?: number;
31
+ readonly returnTo?: string;
31
32
  /** Runs after the token is stored and before the mail goes out. */
32
33
  readonly onTokenStored?: (stored: {
33
34
  readonly token: string;
@@ -75,6 +76,7 @@ export async function issueSignupActivation(
75
76
  ...(args.timeZone !== undefined && { timeZone: args.timeZone }),
76
77
  ...(args.appName !== undefined && { appName: args.appName }),
77
78
  ...(args.locale !== undefined && { locale: args.locale }),
79
+ ...(args.returnTo !== undefined && { returnTo: args.returnTo }),
78
80
  },
79
81
  );
80
82