@cosmicdrift/kumiko-bundled-features 0.387.0 → 0.389.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 (320) hide show
  1. package/dist/agent-tools/index.d.ts +1 -1
  2. package/dist/agent-tools/index.js +1 -1
  3. package/dist/agent-tools/tool-dispatch.d.ts +12 -7
  4. package/dist/agent-tools/tool-dispatch.js +42 -31
  5. package/dist/audit/feature.js +1 -0
  6. package/dist/audit/handlers/audit-events.js +1 -1
  7. package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
  8. package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
  9. package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
  10. package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
  11. package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
  12. package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
  13. package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
  14. package/dist/auth-email-password/handlers/signup-request.write.js +4 -0
  15. package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
  16. package/dist/auth-email-password/handlers/token-request-handler.js +5 -0
  17. package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
  18. package/dist/auth-email-password/magic-link-mail.js +7 -3
  19. package/dist/auth-email-password/signup-activation.d.ts +1 -0
  20. package/dist/auth-email-password/signup-activation.js +1 -0
  21. package/dist/auth-email-password/web/auth-client.d.ts +2 -2
  22. package/dist/auth-email-password/web/auth-client.js +8 -4
  23. package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
  24. package/dist/auth-email-password/web/auth-redirect.js +20 -25
  25. package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
  26. package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
  27. package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
  28. package/dist/auth-email-password/web/index.d.ts +1 -0
  29. package/dist/auth-email-password/web/index.js +1 -0
  30. package/dist/auth-email-password/web/login-screen.js +11 -6
  31. package/dist/auth-email-password/web/reset-password-screen.js +7 -1
  32. package/dist/auth-email-password/web/return-path.d.ts +4 -0
  33. package/dist/auth-email-password/web/return-path.js +34 -0
  34. package/dist/auth-email-password/web/session.js +6 -1
  35. package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
  36. package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
  37. package/dist/auth-email-password/web/signup-screen.js +8 -3
  38. package/dist/auth-mfa/config.d.ts +1 -1
  39. package/dist/auth-mfa/config.js +3 -7
  40. package/dist/auth-mfa/feature-exports.d.ts +2 -2
  41. package/dist/auth-mfa/feature.js +8 -0
  42. package/dist/auth-mfa/mfa-status-checker.js +5 -3
  43. package/dist/auth-mfa/web/i18n.js +5 -1
  44. package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
  45. package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
  46. package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
  47. package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
  48. package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
  49. package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
  50. package/dist/billing-foundation/checkout-core.d.ts +9 -14
  51. package/dist/billing-foundation/checkout-core.js +24 -39
  52. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
  53. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
  54. package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
  55. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
  56. package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
  57. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
  58. package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
  59. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
  60. package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
  61. package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
  62. package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
  63. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
  64. package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
  65. package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
  66. package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
  67. package/dist/billing-foundation/events.d.ts +1 -0
  68. package/dist/billing-foundation/events.js +1 -0
  69. package/dist/billing-foundation/feature.js +3 -1
  70. package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
  71. package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
  72. package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
  73. package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
  74. package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
  75. package/dist/billing-foundation/handlers/process-event.write.js +8 -2
  76. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
  77. package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
  78. package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
  79. package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
  80. package/dist/billing-foundation/i18n.js +22 -0
  81. package/dist/billing-foundation/index.d.ts +1 -1
  82. package/dist/billing-foundation/plan-catalog.d.ts +21 -5
  83. package/dist/billing-foundation/plan-catalog.js +25 -16
  84. package/dist/billing-foundation/plan-switch.d.ts +12 -0
  85. package/dist/billing-foundation/plan-switch.js +60 -0
  86. package/dist/billing-foundation/types.d.ts +57 -1
  87. package/dist/billing-foundation/types.js +9 -0
  88. package/dist/billing-foundation/validate-options.js +17 -0
  89. package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
  90. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
  91. package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
  92. package/dist/billing-foundation/web/constants.d.ts +4 -0
  93. package/dist/billing-foundation/web/constants.js +4 -0
  94. package/dist/billing-foundation/web/plan-price.d.ts +15 -0
  95. package/dist/billing-foundation/web/plan-price.js +32 -0
  96. package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
  97. package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
  98. package/dist/cap-counter/cap-reservation.js +20 -7
  99. package/dist/cap-counter/enforce-cap.js +2 -1
  100. package/dist/cap-counter/feature.js +1 -0
  101. package/dist/cap-counter/stock-cap-guard.js +7 -1
  102. package/dist/cap-overview/feature.js +1 -0
  103. package/dist/channel-email/feature.js +1 -1
  104. package/dist/config/handlers/cascade.query.d.ts +5 -0
  105. package/dist/config/handlers/cascade.query.js +4 -3
  106. package/dist/config/handlers/set.write.js +12 -2
  107. package/dist/config/handlers/values.query.d.ts +5 -0
  108. package/dist/config/handlers/values.query.js +4 -3
  109. package/dist/config/write-helpers.d.ts +1 -0
  110. package/dist/config/write-helpers.js +18 -9
  111. package/dist/crypto-shredding/handlers/forget-subject.write.js +14 -11
  112. package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
  113. package/dist/delivery/channel-context.js +6 -3
  114. package/dist/delivery/db/queries/preferences.js +2 -2
  115. package/dist/delivery/delivery-service.js +22 -7
  116. package/dist/delivery/feature.js +1 -0
  117. package/dist/delivery/handlers/preferences.query.d.ts +1 -1
  118. package/dist/delivery/handlers/preferences.query.js +20 -1
  119. package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
  120. package/dist/delivery/index.d.ts +1 -0
  121. package/dist/delivery/index.js +1 -0
  122. package/dist/delivery/system-channel-context.d.ts +15 -0
  123. package/dist/delivery/system-channel-context.js +5 -0
  124. package/dist/delivery/unsubscribe-link.d.ts +3 -1
  125. package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
  126. package/dist/delivery/unsubscribe-page-strings.js +1 -0
  127. package/dist/delivery/unsubscribe-page.d.ts +2 -0
  128. package/dist/delivery/unsubscribe-page.js +5 -0
  129. package/dist/delivery/unsubscribe.js +25 -7
  130. package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
  131. package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
  132. package/dist/document-ingest-foundation/events.d.ts +11 -0
  133. package/dist/document-ingest-foundation/events.js +11 -0
  134. package/dist/document-ingest-foundation/index.d.ts +2 -1
  135. package/dist/document-ingest-foundation/index.js +2 -1
  136. package/dist/feature-toggles/feature.js +1 -0
  137. package/dist/file-foundation/index.d.ts +1 -1
  138. package/dist/file-foundation/index.js +1 -1
  139. package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
  140. package/dist/file-foundation/read-file-ref-content.js +6 -1
  141. package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
  142. package/dist/form-draft/db/queries/cleanup.js +2 -2
  143. package/dist/form-draft/decrypt-draft.js +5 -1
  144. package/dist/host-routing/dispatch.js +11 -20
  145. package/dist/host-routing/validate-options.js +3 -0
  146. package/dist/inbound-mail-foundation/feature.js +1 -0
  147. package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
  148. package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
  149. package/dist/inbound-mail-foundation/i18n.js +0 -1
  150. package/dist/inbound-mail-foundation/screens.js +0 -3
  151. package/dist/inbound-mail-foundation/types.d.ts +4 -3
  152. package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
  153. package/dist/jobs/feature.d.ts +1 -0
  154. package/dist/jobs/feature.js +9 -4
  155. package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
  156. package/dist/jobs/handlers/tenant-failures.query.js +45 -41
  157. package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
  158. package/dist/jobs/handlers/tenant-runs.query.js +54 -50
  159. package/dist/jobs/job-run-logger.js +5 -4
  160. package/dist/ledger/web/index.js +1 -1
  161. package/dist/legal-pages/seed.d.ts +3 -1
  162. package/dist/legal-pages/seed.js +2 -2
  163. package/dist/mail-foundation/feature.d.ts +3 -3
  164. package/dist/mail-foundation/feature.js +3 -0
  165. package/dist/mail-transport-smtp/config-keys.js +1 -1
  166. package/dist/mail-transport-smtp/feature.js +6 -5
  167. package/dist/managed-pages/constants.d.ts +1 -0
  168. package/dist/managed-pages/constants.js +1 -0
  169. package/dist/managed-pages/feature.js +3 -1
  170. package/dist/managed-pages/index.d.ts +1 -0
  171. package/dist/managed-pages/index.js +1 -0
  172. package/dist/notes-history/handlers/add-note.write.js +1 -1
  173. package/dist/personal-access-tokens/feature.d.ts +1 -1
  174. package/dist/personal-access-tokens/feature.js +5 -0
  175. package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
  176. package/dist/renderer-pdf/render-table-pdf.js +22 -10
  177. package/dist/secrets/write-gate.js +8 -2
  178. package/dist/sessions/feature.d.ts +2 -2
  179. package/dist/sessions/feature.js +5 -0
  180. package/dist/sessions/index.d.ts +1 -1
  181. package/dist/sessions/index.js +1 -1
  182. package/dist/sessions/session-callbacks.d.ts +0 -2
  183. package/dist/sessions/session-callbacks.js +1 -3
  184. package/dist/shared/decrypt-stored-pii.d.ts +1 -0
  185. package/dist/shared/decrypt-stored-pii.js +6 -1
  186. package/dist/shared/index.d.ts +1 -1
  187. package/dist/shared/index.js +1 -1
  188. package/dist/shared/mail-template-body.d.ts +6 -2
  189. package/dist/shared/mail-template-body.js +7 -4
  190. package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
  191. package/dist/shared/purge-files-reaching-subject.js +19 -2
  192. package/dist/shared/run-in-sub-transaction.js +5 -5
  193. package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
  194. package/dist/step-dispatcher/feature.js +1 -0
  195. package/dist/step-dispatcher/mail-runner.d.ts +1 -0
  196. package/dist/subscription-dodo/client.d.ts +17 -0
  197. package/dist/subscription-dodo/client.js +31 -0
  198. package/dist/subscription-dodo/constants.d.ts +12 -0
  199. package/dist/subscription-dodo/constants.js +19 -0
  200. package/dist/subscription-dodo/feature.d.ts +37 -0
  201. package/dist/subscription-dodo/feature.js +144 -0
  202. package/dist/subscription-dodo/index.d.ts +2 -0
  203. package/dist/subscription-dodo/index.js +4 -0
  204. package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
  205. package/dist/subscription-dodo/plugin-methods.js +260 -0
  206. package/dist/subscription-dodo/runtime.d.ts +27 -0
  207. package/dist/subscription-dodo/runtime.js +89 -0
  208. package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
  209. package/dist/subscription-dodo/verify-webhook.js +189 -0
  210. package/dist/subscription-stripe/feature.js +1 -0
  211. package/dist/subscription-stripe/runtime.js +7 -6
  212. package/dist/tags/feature.js +1 -0
  213. package/dist/template-resolver/api.d.ts +3 -2
  214. package/dist/template-resolver/api.js +24 -2
  215. package/dist/template-resolver/compile-document-body.js +1 -0
  216. package/dist/template-resolver/document-ops.d.ts +5 -0
  217. package/dist/template-resolver/document-ops.js +18 -2
  218. package/dist/template-resolver/feature.js +4 -4
  219. package/dist/template-resolver/handlers/bundle-import.js +3 -3
  220. package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
  221. package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
  222. package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
  223. package/dist/template-resolver/handlers/document-base.js +3 -2
  224. package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
  225. package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
  226. package/dist/template-resolver/handlers/layout-source.query.js +26 -6
  227. package/dist/template-resolver/handlers/list.query.js +5 -6
  228. package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
  229. package/dist/template-resolver/handlers/overview.query.js +21 -10
  230. package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
  231. package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
  232. package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
  233. package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
  234. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
  235. package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
  236. package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
  237. package/dist/template-resolver/handlers/usage.query.js +26 -16
  238. package/dist/template-resolver/handlers/validate.query.js +7 -2
  239. package/dist/template-resolver/seeding.d.ts +1 -0
  240. package/dist/template-resolver/seeding.js +31 -12
  241. package/dist/template-resolver/store-template-resource.d.ts +1 -0
  242. package/dist/template-resolver/store-template-resource.js +65 -10
  243. package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
  244. package/dist/template-resolver/template-delivery-policy.js +0 -6
  245. package/dist/template-resolver/template-meta.js +7 -4
  246. package/dist/template-resolver/template-preview.js +4 -3
  247. package/dist/template-resolver/web/constants.d.ts +2 -2
  248. package/dist/template-resolver/web/constants.js +3 -3
  249. package/dist/template-resolver/web/i18n.js +3 -0
  250. package/dist/template-resolver/web/index.d.ts +1 -0
  251. package/dist/template-resolver/web/index.js +1 -0
  252. package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
  253. package/dist/template-resolver/web/mail-layout-constants.js +7 -0
  254. package/dist/template-resolver/web/qualified-names.d.ts +1 -0
  255. package/dist/template-resolver/web/qualified-names.js +1 -0
  256. package/dist/tenant/feature.js +1 -0
  257. package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
  258. package/dist/tenant/handlers/member-directory.query.js +10 -3
  259. package/dist/tenant/handlers/team-list.query.js +3 -3
  260. package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
  261. package/dist/tenant/handlers/tenant-directory.query.js +8 -4
  262. package/dist/tenant-lifecycle/constants.d.ts +1 -0
  263. package/dist/tenant-lifecycle/constants.js +4 -0
  264. package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
  265. package/dist/tenant-lifecycle/stages.js +22 -1
  266. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
  267. package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
  268. package/dist/tier-engine/web/constants.js +2 -2
  269. package/dist/user/feature.js +1 -0
  270. package/dist/user-data-rights/feature.js +1 -0
  271. package/dist/user-data-rights/index.d.ts +1 -1
  272. package/dist/user-data-rights/index.js +1 -1
  273. package/dist/user-data-rights/run-export-jobs.js +3 -4
  274. package/dist/user-data-rights/run-forget-cleanup.js +2 -1
  275. package/dist/user-data-rights/web/constants.d.ts +6 -0
  276. package/dist/user-data-rights/web/constants.js +6 -0
  277. package/dist/user-data-rights/web/index.d.ts +1 -0
  278. package/dist/user-data-rights/web/index.js +1 -0
  279. package/dist/waitlist/feature.js +1 -0
  280. package/dist/waitlist/handlers/export-purge.write.js +2 -1
  281. package/dist/waitlist/handlers/submit.write.js +14 -10
  282. package/dist/waitlist/import.d.ts +3 -1
  283. package/dist/waitlist/import.js +26 -2
  284. package/dist/waitlist/legacy-migrate.js +18 -7
  285. package/dist/waitlist/link-on-signup.js +18 -6
  286. package/dist/waitlist/options.d.ts +5 -1
  287. package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
  288. package/package.json +19 -9
  289. package/src/agent-tools/changes.json +12 -0
  290. package/src/audit/changes.json +12 -0
  291. package/src/auth-email-password/changes.json +12 -0
  292. package/src/auth-mfa/changes.json +12 -0
  293. package/src/billing-foundation/changes.json +55 -0
  294. package/src/cap-counter/changes.json +30 -0
  295. package/src/channel-email/changes.json +6 -0
  296. package/src/config/changes.json +18 -0
  297. package/src/crypto-shredding/changes.json +18 -0
  298. package/src/delivery/changes.json +42 -0
  299. package/src/document-ingest-foundation/changes.json +6 -0
  300. package/src/form-draft/changes.json +6 -0
  301. package/src/host-routing/changes.json +12 -0
  302. package/src/inbound-mail-foundation/changes.json +18 -0
  303. package/src/jobs/changes.json +13 -0
  304. package/src/mail-foundation/changes.json +6 -0
  305. package/src/mail-transport-smtp/changes.json +6 -0
  306. package/src/managed-pages/changes.json +6 -0
  307. package/src/renderer-pdf/changes.json +6 -0
  308. package/src/secrets/changes.json +6 -0
  309. package/src/sessions/changes.json +7 -0
  310. package/src/step-dispatcher/changes.json +12 -0
  311. package/src/subscription-dodo/changes.json +14 -0
  312. package/src/subscription-stripe/changes.json +12 -0
  313. package/src/template-resolver/changes.json +84 -0
  314. package/src/tenant/changes.json +6 -0
  315. package/src/tenant-lifecycle/changes.json +12 -0
  316. package/src/tier-engine/changes.json +6 -0
  317. package/src/user-data-rights/changes.json +12 -0
  318. package/src/waitlist/changes.json +36 -0
  319. package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
  320. package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
@@ -22,6 +22,7 @@ export type { RequestAccountUnlockScreenProps } from "./request-account-unlock-s
22
22
  export { RequestAccountUnlockScreen } from "./request-account-unlock-screen.tsx";
23
23
  export type { ResetPasswordScreenProps } from "./reset-password-screen.tsx";
24
24
  export { ResetPasswordScreen } from "./reset-password-screen.tsx";
25
+ export { isSafeReturnPath, RETURN_TO_QUERY_PARAM, readReturnToFromSearch, } from "./return-path.ts";
25
26
  export type { SessionApi, SessionBootstrapFailure, SessionState, SessionStatus, } from "./session.tsx";
26
27
  export { hasLikelyAuthSession, SessionContext, SessionProvider, useSession } from "./session.tsx";
27
28
  export type { SessionBootstrapErrorScreenProps } from "./session-bootstrap-error.tsx";
@@ -16,6 +16,7 @@ export { InviteAcceptScreen } from "./invite-accept-screen.js";
16
16
  export { LoginScreen } from "./login-screen.js";
17
17
  export { RequestAccountUnlockScreen } from "./request-account-unlock-screen.js";
18
18
  export { ResetPasswordScreen } from "./reset-password-screen.js";
19
+ export { isSafeReturnPath, RETURN_TO_QUERY_PARAM, readReturnToFromSearch, } from "./return-path.js";
19
20
  export { hasLikelyAuthSession, SessionContext, SessionProvider, useSession } from "./session.js";
20
21
  export { retryDelayMs, SessionBootstrapErrorScreen } from "./session-bootstrap-error.js";
21
22
  export { SignupCompleteScreen } from "./signup-complete-screen.js";
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Login handler reason codes map 1:1 to i18n keys (reasonToI18nKey): a new
3
3
  // reason code needs a bundle entry plus a line in that mapping.
4
- import { useOptionalDispatcher, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
4
+ import { prependBasePath, useBasePath, useOptionalDispatcher, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
5
  import { useState } from "react";
6
6
  import { requestEmailVerification, } from "./auth-client.js";
7
7
  import { AuthCard } from "./auth-form-primitives.js";
8
8
  import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.js";
9
- import { followNextOrLandingAfterLogin } from "./auth-redirect.js";
9
+ import { appendReturnTo, followNextOrLandingAfterLogin, readNextFromWindow, } from "./auth-redirect.js";
10
10
  import { AuthQueries } from "./constants.js";
11
11
  import { useSession } from "./session.js";
12
12
  // Maps login-handler reason codes to i18n keys and optional interpolation parameters.
@@ -51,14 +51,19 @@ function LoginScreenBody({ title, subtitle, submitLabel, forgotPasswordHref, unl
51
51
  // Hides the resend button once the email input changes; otherwise resend
52
52
  // would silently go to the edited, possibly mistyped address.
53
53
  const [failedLoginEmail, setFailedLoginEmail] = useState(null);
54
+ const basePath = useBasePath();
55
+ // The page the user was sent away from rides along so the reset or signup mail leads back to it.
56
+ const returnTo = readNextFromWindow();
54
57
  const effectiveForgotHref = forgotPasswordHref === undefined
55
58
  ? loginOptions?.passwordReset === true
56
- ? DEFAULT_FORGOT_PASSWORD_PATH
59
+ ? appendReturnTo(prependBasePath(DEFAULT_FORGOT_PASSWORD_PATH, basePath), returnTo)
57
60
  : null
58
- : forgotPasswordHref;
61
+ : forgotPasswordHref === null
62
+ ? null
63
+ : appendReturnTo(forgotPasswordHref, returnTo);
59
64
  const effectiveUnlockHref = unlockAccountHref === undefined
60
65
  ? loginOptions?.accountUnlock === true
61
- ? DEFAULT_AUTH_PATHS.unlockAccount
66
+ ? prependBasePath(DEFAULT_AUTH_PATHS.unlockAccount, basePath)
62
67
  : null
63
68
  : unlockAccountHref;
64
69
  const doSubmit = async () => {
@@ -123,7 +128,7 @@ function LoginScreenBody({ title, subtitle, submitLabel, forgotPasswordHref, unl
123
128
  email.trim().length > 0 &&
124
129
  email === failedLoginEmail && (_jsx("span", { className: "self-start", children: _jsx(Button, { variant: "link", onClick: () => void onResend(), disabled: resendStatus.kind === "sending", children: resendStatus.kind === "sending"
125
130
  ? t("auth.login.submitting")
126
- : t("auth.login.resendVerification") }) })), error.reason === "account_locked" && effectiveUnlockHref !== null && (_jsx("span", { className: "self-start", children: _jsx(Link, { href: effectiveUnlockHref, variant: "default", children: t("auth.login.unlockAccount") }) })), resendStatus.kind === "rateLimited" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendRateLimited") })), resendStatus.kind === "error" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendError") }))] }) })) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.login.submitting") : effectiveSubmit })] }), effectiveForgotHref !== null && (_jsx(Link, { href: effectiveForgotHref, variant: "muted", className: "self-center", children: t("auth.login.forgotPassword") })), signupHref !== undefined && (_jsx(Link, { href: signupHref, variant: "muted", className: "self-center", children: t("auth.signup.title") })), legalLinks !== undefined && legalLinks.length > 0 && (_jsx("nav", { "data-testid": "login-legal-links", className: "flex items-center justify-center gap-3 pt-2 border-t border-border/50", children: legalLinks.map((link) => (_jsx(Link, { href: link.href, variant: "muted", className: "text-xs", children: link.label }, link.href))) }))] }));
131
+ : t("auth.login.resendVerification") }) })), error.reason === "account_locked" && effectiveUnlockHref !== null && (_jsx("span", { className: "self-start", children: _jsx(Link, { href: effectiveUnlockHref, variant: "default", children: t("auth.login.unlockAccount") }) })), resendStatus.kind === "rateLimited" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendRateLimited") })), resendStatus.kind === "error" && (_jsx("span", { className: "text-xs", children: t("auth.login.resendError") }))] }) })) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.login.submitting") : effectiveSubmit })] }), effectiveForgotHref !== null && (_jsx(Link, { href: effectiveForgotHref, variant: "muted", className: "self-center", children: t("auth.login.forgotPassword") })), signupHref !== undefined && (_jsx(Link, { href: appendReturnTo(signupHref, returnTo), variant: "muted", className: "self-center", children: t("auth.signup.title") })), legalLinks !== undefined && legalLinks.length > 0 && (_jsx("nav", { "data-testid": "login-legal-links", className: "flex items-center justify-center gap-3 pt-2 border-t border-border/50", children: legalLinks.map((link) => (_jsx(Link, { href: link.href, variant: "muted", className: "text-xs", children: link.label }, link.href))) }))] }));
127
132
  }
128
133
  function LoginScreenWithQueriedOptions(props) {
129
134
  const { data } = useQuery(AuthQueries.loginOptions, {}, { enabled: props.forgotPasswordHref === undefined || props.unlockAccountHref === undefined });
@@ -13,6 +13,7 @@ import { useState } from "react";
13
13
  import { resetPassword } from "./auth-client.js";
14
14
  import { passwordPairIssue } from "./auth-form-logic.js";
15
15
  import { AuthCard, useUrlToken } from "./auth-form-primitives.js";
16
+ import { buildLoginRedirectUrl, readReturnToFromWindow } from "./auth-redirect.js";
16
17
  export function ResetPasswordScreen({ title, token: tokenProp, loginHref = "/login", }) {
17
18
  const t = useTranslation();
18
19
  const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
@@ -22,6 +23,7 @@ export function ResetPasswordScreen({ title, token: tokenProp, loginHref = "/log
22
23
  const [submitting, setSubmitting] = useState(false);
23
24
  const [done, setDone] = useState(false);
24
25
  const [error, setError] = useState(null);
26
+ const [afterResetHref, setAfterResetHref] = useState(loginHref);
25
27
  const doSubmit = async () => {
26
28
  setError(null);
27
29
  const issue = passwordPairIssue(newPassword, confirmPassword);
@@ -37,6 +39,10 @@ export function ResetPasswordScreen({ title, token: tokenProp, loginHref = "/log
37
39
  const res = await resetPassword(token, newPassword);
38
40
  setSubmitting(false);
39
41
  if (res.ok) {
42
+ const returnTo = readReturnToFromWindow();
43
+ if (returnTo !== null) {
44
+ setAfterResetHref(buildLoginRedirectUrl(loginHref, returnTo, window.location.origin));
45
+ }
40
46
  setDone(true);
41
47
  return;
42
48
  }
@@ -62,5 +68,5 @@ export function ResetPasswordScreen({ title, token: tokenProp, loginHref = "/log
62
68
  if (token === "") {
63
69
  return (_jsxs(AuthCard, { title: effectiveTitle, children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.resetPassword.missingToken") }), _jsx(Link, { href: loginHref, variant: "muted", children: t("auth.resetPassword.goToLogin") })] }));
64
70
  }
65
- return (_jsx(AuthCard, { title: effectiveTitle, children: done ? (_jsxs(_Fragment, { children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.resetPassword.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.resetPassword.successBody") })] }), _jsx(Link, { href: loginHref, variant: "button", children: t("auth.resetPassword.goToLogin") })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.resetPassword.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "reset-new-password", label: t("auth.resetPassword.newPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-new-password", name: "reset-new-password", value: newPassword, onChange: setNewPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), _jsx(Field, { id: "reset-confirm-password", label: t("auth.resetPassword.confirmPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-confirm-password", name: "reset-confirm-password", value: confirmPassword, onChange: setConfirmPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.resetPassword.submitting") : t("auth.resetPassword.submit") })] })] })) }));
71
+ return (_jsx(AuthCard, { title: effectiveTitle, children: done ? (_jsxs(_Fragment, { children: [_jsxs(Banner, { variant: "info", children: [_jsx("p", { className: "font-medium text-foreground", children: t("auth.resetPassword.successTitle") }), _jsx("p", { className: "mt-1", children: t("auth.resetPassword.successBody") })] }), _jsx(Link, { href: afterResetHref, variant: "button", children: t("auth.resetPassword.goToLogin") })] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-sm text-muted-foreground", children: t("auth.resetPassword.intro") }), _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "reset-new-password", label: t("auth.resetPassword.newPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-new-password", name: "reset-new-password", value: newPassword, onChange: setNewPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), _jsx(Field, { id: "reset-confirm-password", label: t("auth.resetPassword.confirmPassword"), required: true, children: _jsx(Input, { kind: "password", id: "reset-confirm-password", name: "reset-confirm-password", value: confirmPassword, onChange: setConfirmPassword, disabled: submitting, required: true, autoComplete: "new-password" }) }), error !== null && _jsx(Banner, { variant: "error", children: error }), _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting ? t("auth.resetPassword.submitting") : t("auth.resetPassword.submit") })] })] })) }));
66
72
  }
@@ -0,0 +1,4 @@
1
+ export declare const RETURN_TO_QUERY_PARAM = "returnTo";
2
+ export declare const MAX_RETURN_PATH_LENGTH = 2048;
3
+ export declare function isSafeReturnPath(candidate: unknown): candidate is string;
4
+ export declare function readReturnToFromSearch(search: string): string | null;
@@ -0,0 +1,34 @@
1
+ // Shared by the server (mail link) and the web pages (landing page), so it is plain string logic:
2
+ // no DOM, no window, no `new URL` (React Native's URL getters throw).
3
+ export const RETURN_TO_QUERY_PARAM = "returnTo";
4
+ export const MAX_RETURN_PATH_LENGTH = 2048;
5
+ const DOT_SEGMENTS = new Set([".", "..", ".%2e", "%2e.", "%2e%2e", "%2e"]);
6
+ function hasForbiddenCharacter(candidate) {
7
+ for (let i = 0; i < candidate.length; i++) {
8
+ const code = candidate.charCodeAt(i);
9
+ if (code <= 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f))
10
+ return true;
11
+ if (code === 0x5c)
12
+ return true;
13
+ }
14
+ return false;
15
+ }
16
+ // A dot segment is resolved by URL normalization, so "/.//evil" would become "//evil".
17
+ function hasDotSegment(candidate) {
18
+ const pathEnd = candidate.search(/[?#]/);
19
+ const path = pathEnd === -1 ? candidate : candidate.slice(0, pathEnd);
20
+ return path.split("/").some((segment) => DOT_SEGMENTS.has(segment.toLowerCase()));
21
+ }
22
+ export function isSafeReturnPath(candidate) {
23
+ if (typeof candidate !== "string")
24
+ return false;
25
+ if (candidate.length < 1 || candidate.length > MAX_RETURN_PATH_LENGTH)
26
+ return false;
27
+ if (!candidate.startsWith("/") || candidate[1] === "/")
28
+ return false;
29
+ return !hasForbiddenCharacter(candidate) && !hasDotSegment(candidate);
30
+ }
31
+ export function readReturnToFromSearch(search) {
32
+ const candidate = new URLSearchParams(search).get(RETURN_TO_QUERY_PARAM);
33
+ return isSafeReturnPath(candidate) ? candidate : null;
34
+ }
@@ -10,6 +10,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
10
10
  // and transitions.
11
11
  import { CurrentUserProvider, useSessionEndedSignal, } from "@cosmicdrift/kumiko-renderer";
12
12
  import { readCsrfToken } from "@cosmicdrift/kumiko-renderer/live";
13
+ import { clearRememberedListState } from "@cosmicdrift/kumiko-renderer/web";
13
14
  import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
14
15
  import { AuthRequestError, fetchCurrentUser, fetchTenants, login as loginApi, logout as logoutApi, switchTenant as switchTenantApi, } from "./auth-client.js";
15
16
  // Exported so screens that render on BOTH authenticated and anonymous public
@@ -127,6 +128,8 @@ export function SessionProvider({ children, postLogoutUrl, }) {
127
128
  }, [doRefresh]);
128
129
  const logout = useCallback(async () => {
129
130
  await logoutApi();
131
+ // The reload below would hide the identity change from the list-state boot.
132
+ clearRememberedListState();
130
133
  if (postLogoutUrl !== undefined && typeof window !== "undefined") {
131
134
  // State stays untouched: flipping to unauthenticated would let an AuthGate
132
135
  // with loginUrl race this navigation to its own redirect.
@@ -160,6 +163,7 @@ export function SessionProvider({ children, postLogoutUrl, }) {
160
163
  const userEmail = profile?.email;
161
164
  const userDisplayName = profile?.displayName;
162
165
  const userTimeZone = profile?.effectiveTimeZone;
166
+ const activeTenantId = state.activeTenantId ?? undefined;
163
167
  const currentUser = useMemo(() => userId === undefined || userEmail === undefined || userDisplayName === undefined
164
168
  ? undefined
165
169
  : {
@@ -167,7 +171,8 @@ export function SessionProvider({ children, postLogoutUrl, }) {
167
171
  email: userEmail,
168
172
  displayName: userDisplayName,
169
173
  ...(userTimeZone !== undefined && { timeZone: userTimeZone }),
170
- }, [userId, userEmail, userDisplayName, userTimeZone]);
174
+ ...(activeTenantId !== undefined && { tenantId: activeTenantId }),
175
+ }, [userId, userEmail, userDisplayName, userTimeZone, activeTenantId]);
171
176
  return (_jsx(SessionContext.Provider, { value: api, children: _jsx(CurrentUserProvider, { user: currentUser, children: children }) }));
172
177
  }
173
178
  export function useSession() {
@@ -21,7 +21,7 @@ import { useState } from "react";
21
21
  import { confirmSignup } from "./auth-client.js";
22
22
  import { passwordPairIssue, resolvePostAuthHref } from "./auth-form-logic.js";
23
23
  import { AuthCard, useUrlToken } from "./auth-form-primitives.js";
24
- import { buildLoginRedirectUrl } from "./auth-redirect.js";
24
+ import { buildLoginRedirectUrl, readReturnToFromWindow } from "./auth-redirect.js";
25
25
  export function SignupCompleteScreen({ title, token: tokenProp, loggedInHref = "/", loginHref = "/login", }) {
26
26
  const t = useTranslation();
27
27
  const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
@@ -47,11 +47,19 @@ export function SignupCompleteScreen({ title, token: tokenProp, loggedInHref = "
47
47
  const res = await confirmSignup(token, password);
48
48
  setSubmitting(false);
49
49
  if (res.ok) {
50
+ // The page the signup started from beats the server's landing path.
51
+ const returnTo = readReturnToFromWindow();
50
52
  if (res.data.kind === "mfa-pending") {
51
53
  setMfaPending(true);
52
- setContinueHref(res.data.landingPath === undefined
54
+ const next = returnTo ?? res.data.landingPath;
55
+ setContinueHref(next === undefined
53
56
  ? loginHref
54
- : buildLoginRedirectUrl(loginHref, res.data.landingPath, window.location.origin));
57
+ : buildLoginRedirectUrl(loginHref, next, window.location.origin));
58
+ return;
59
+ }
60
+ // A claimed handover keeps its app-defined target.
61
+ if (returnTo !== null && res.data.handover === undefined) {
62
+ setContinueHref(returnTo);
55
63
  return;
56
64
  }
57
65
  // Cookies are already set (auto-login). Show a confirmation with an
@@ -5,5 +5,8 @@ export type SignupScreenProps = {
5
5
  /** href for the "Already have an account?" link. App-specific; defaults to
6
6
  * "/login". */
7
7
  readonly loginHref?: string;
8
+ /** Page the activation mail should lead to after signup; defaults to the `returnTo` query
9
+ * parameter of this page. Unsafe values are dropped. */
10
+ readonly returnTo?: string;
8
11
  };
9
- export declare function SignupScreen({ title, subtitle, loginHref, }: SignupScreenProps): ReactNode;
12
+ export declare function SignupScreen({ title, subtitle, loginHref, returnTo, }: SignupScreenProps): ReactNode;
@@ -16,7 +16,12 @@ import { useState } from "react";
16
16
  import { requestSignup } from "./auth-client.js";
17
17
  import { retryAfterMinutes } from "./auth-form-logic.js";
18
18
  import { AuthCard } from "./auth-form-primitives.js";
19
- export function SignupScreen({ title, subtitle, loginHref = "/login", }) {
19
+ import { readReturnToFromWindow } from "./auth-redirect.js";
20
+ import { isSafeReturnPath } from "./return-path.js";
21
+ function resolveReturnTo(prop) {
22
+ return (isSafeReturnPath(prop) ? prop : readReturnToFromWindow()) ?? undefined;
23
+ }
24
+ export function SignupScreen({ title, subtitle, loginHref = "/login", returnTo, }) {
20
25
  const t = useTranslation();
21
26
  const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
22
27
  const [email, setEmail] = useState("");
@@ -27,7 +32,7 @@ export function SignupScreen({ title, subtitle, loginHref = "/login", }) {
27
32
  setSubmitting(true);
28
33
  setError(null);
29
34
  try {
30
- const res = await requestSignup(email);
35
+ const res = await requestSignup(email, undefined, resolveReturnTo(returnTo));
31
36
  if (res.ok) {
32
37
  setDone(true);
33
38
  }
@@ -59,7 +64,7 @@ export function SignupScreen({ title, subtitle, loginHref = "/login", }) {
59
64
  setSubmitting(true);
60
65
  setError(null);
61
66
  try {
62
- await requestSignup(email);
67
+ await requestSignup(email, undefined, resolveReturnTo(returnTo));
63
68
  }
64
69
  finally {
65
70
  setSubmitting(false);
@@ -1,5 +1,5 @@
1
1
  import { type ConfigKeyHandle } from "@cosmicdrift/kumiko-framework/engine";
2
- export declare const MFA_REQUIRED_POLICIES: readonly ["optional", "admins", "all"];
2
+ export declare const MFA_REQUIRED_POLICIES: readonly ["optional", "systemAdmins", "admins", "all"];
3
3
  export type MfaRequiredPolicy = (typeof MFA_REQUIRED_POLICIES)[number];
4
4
  export declare const mfaRequiredConfigHandle: ConfigKeyHandle<"select">;
5
5
  export declare function mfaRequiredConfigKey(defaultPolicy?: MfaRequiredPolicy): import("@cosmicdrift/kumiko-types/config").ConfigKeyDefinition<"select">;
@@ -2,14 +2,10 @@
2
2
  // they haven't opted in themselves? Default "optional" means the feature
3
3
  // is fully opt-in (existing tenants/users unaffected — no migration step).
4
4
  //
5
- // ponytail: "admins"/"all" HARD-BLOCK login for an unenrolled user (see
6
- // mfa-status-checker.ts's setupRequired branch) — there is no in-band way
7
- // for a blocked user to complete enrollment yet (that flow ships in PR3's
8
- // UI). Flipping this away from "optional" before PR3 locks out every
9
- // unenrolled matching user with no recovery path. Fine for a tenant that
10
- // enrolls its admins out-of-band first; a footgun otherwise.
5
+ // Every non-optional policy blocks login for an unenrolled matching user until
6
+ // they finish the pre-auth enrollment step (mfa-status-checker.ts setupRequired).
11
7
  import { access, createTenantConfig, } from "@cosmicdrift/kumiko-framework/engine";
12
- export const MFA_REQUIRED_POLICIES = ["optional", "admins", "all"];
8
+ export const MFA_REQUIRED_POLICIES = ["optional", "systemAdmins", "admins", "all"];
13
9
  export const mfaRequiredConfigHandle = {
14
10
  name: "auth-mfa:config:required",
15
11
  type: "select",
@@ -2,11 +2,11 @@ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import type { MfaCodeVerifier } from "./mfa-code-verifier.ts";
3
3
  import type { MfaStatusChecker } from "./mfa-status-checker.ts";
4
4
  import type { MfaTokenSecretsEnv } from "./token-secrets.ts";
5
- export type BindMfaRevokeAllOtherSessions = (revoker: (userId: string, currentSid: string | undefined) => Promise<number>) => void;
5
+ export type BindMfaRevokeAllOtherSessions = (revoker: (userId: string, currentSid: string | undefined) => Promise<number>) => () => void;
6
6
  export declare function bindMfaRevokeAllOtherSessionsFromFeature(feature: FeatureDefinition): BindMfaRevokeAllOtherSessions | undefined;
7
7
  export declare function mfaStatusCheckerFromFeature(feature: FeatureDefinition): MfaStatusChecker | undefined;
8
8
  export declare function mfaVerifierFromFeature(feature: FeatureDefinition): MfaCodeVerifier | undefined;
9
9
  export type BindMfaTokenSecrets = (masterSecret: string, env: MfaTokenSecretsEnv) => void;
10
10
  export declare function bindMfaTokenSecretsFromFeature(feature: FeatureDefinition): BindMfaTokenSecrets | undefined;
11
- export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => void;
11
+ export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => () => void;
12
12
  export declare function bindRevokeAllPatTokensFromFeature(feature: FeatureDefinition): BindRevokeAllPatTokens | undefined;
@@ -147,9 +147,17 @@ export function createAuthMfaFeature(opts) {
147
147
  const verifyMfaCode = createMfaCodeVerifier();
148
148
  const bindRevokeAllOtherSessions = (revoker) => {
149
149
  revokeAllOtherSessions = revoker;
150
+ return () => {
151
+ if (revokeAllOtherSessions === revoker)
152
+ revokeAllOtherSessions = undefined;
153
+ };
150
154
  };
151
155
  const bindRevokeAllPatTokens = (revoker) => {
152
156
  revokeAllPatTokens = revoker;
157
+ return () => {
158
+ if (revokeAllPatTokens === revoker)
159
+ revokeAllPatTokens = undefined;
160
+ };
153
161
  };
154
162
  return {
155
163
  handlers,
@@ -1,3 +1,4 @@
1
+ import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
1
2
  import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { access, declareEscapeHatch, } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { mfaRequiredConfigHandle, mfaRequiredConfigKey } from "./config.js";
@@ -32,9 +33,10 @@ export function createMfaStatusChecker(opts) {
32
33
  const buildSetupToken = () => signMfaPreauthSetupToken({ userId, tenantId }, MFA_PREAUTH_SETUP_TOKEN_TTL_MINUTES, opts.challengeTokenSecret).token;
33
34
  if (policy === "all")
34
35
  return { setupRequired: true, preauthSetupToken: buildSetupToken() };
35
- // policy === "admins"
36
- const isAdmin = roles.some((role) => access.admin.includes(role));
37
- return isAdmin
36
+ const isEnforced = policy === "systemAdmins"
37
+ ? roles.includes(ROLES.SystemAdmin)
38
+ : roles.some((role) => access.admin.includes(role));
39
+ return isEnforced
38
40
  ? { setupRequired: true, preauthSetupToken: buildSetupToken() }
39
41
  : { required: false };
40
42
  };
@@ -10,6 +10,10 @@ export const defaultTranslations = {
10
10
  "auth.mfa.verify.submit": "Verify",
11
11
  "auth.mfa.verify.submitting": "…",
12
12
  "auth.mfa.verify.backToLogin": "Back to login",
13
+ "auth.mfa.verify.recoveryCode": "Recovery code",
14
+ "auth.mfa.verify.recoverySubtitle": "Enter one of the recovery codes you saved when you set up two-factor authentication.",
15
+ "auth.mfa.verify.useAuthenticatorCode": "Use the code from your app",
16
+ "auth.mfa.verify.useRecoveryCode": "Use a recovery code instead",
13
17
  "auth.mfa.errors.invalidCode": "Invalid code. Please try again.",
14
18
  "auth.mfa.errors.invalidTotpCode": "Invalid code. Please try again.",
15
19
  "auth.mfa.errors.invalidChallengeToken": "Your sign-in has expired. Please sign in again.",
@@ -35,7 +39,7 @@ export const defaultTranslations = {
35
39
  "auth.mfa.setup.confirm": "Complete setup",
36
40
  "auth.mfa.disable.title": "Disable two-factor authentication",
37
41
  "auth.mfa.disable.description": "Confirm with a code from your authenticator app or a recovery code. Your account will then be protected by your password alone.",
38
- "auth.mfa.disable.code": "Code from your authenticator app or a recovery code",
42
+ "auth.mfa.disable.code": "Code from your authenticator app",
39
43
  "auth.mfa.disable.confirm": "Disable",
40
44
  "auth.mfa.disable.cancel": "Cancel",
41
45
  "auth.mfa.disable.trigger": "Disable two-factor authentication",
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from "react";
2
+ export type MfaCodeOrRecoveryFieldProps = {
3
+ readonly id: string;
4
+ readonly codeLabel: string;
5
+ readonly code: string;
6
+ readonly onCodeChange: (code: string) => void;
7
+ readonly useRecovery: boolean;
8
+ readonly onUseRecoveryChange: (useRecovery: boolean) => void;
9
+ readonly disabled?: boolean;
10
+ };
11
+ export declare function MfaCodeOrRecoveryField({ id, codeLabel, code, onCodeChange, useRecovery, onUseRecoveryChange, disabled, }: MfaCodeOrRecoveryFieldProps): ReactNode;
@@ -0,0 +1,12 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
3
+ export function MfaCodeOrRecoveryField({ id, codeLabel, code, onCodeChange, useRecovery, onUseRecoveryChange, disabled, }) {
4
+ const t = useTranslation();
5
+ const { Button, Field, Input } = usePrimitives();
6
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { id: id, label: useRecovery ? t("auth.mfa.verify.recoveryCode") : codeLabel, required: true, children: useRecovery ? (_jsx(Input, { kind: "text", id: id, name: id, value: code, onChange: onCodeChange, disabled: disabled, autoFocus: true, autoComplete: "off" })) : (_jsx(Input, { kind: "code", length: 6, id: id, name: id, value: code, onChange: onCodeChange, disabled: disabled, autoFocus: true })) }), _jsx(Button, { type: "button", variant: "link", disabled: disabled, onClick: () => {
7
+ onUseRecoveryChange(!useRecovery);
8
+ onCodeChange("");
9
+ }, children: useRecovery
10
+ ? t("auth.mfa.verify.useAuthenticatorCode")
11
+ : t("auth.mfa.verify.useRecoveryCode") })] }));
12
+ }
@@ -12,20 +12,31 @@ import { jsx as _jsx } from "react/jsx-runtime";
12
12
  import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
13
13
  import { useState } from "react";
14
14
  import { AuthMfaHandlers } from "./constants.js";
15
+ import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.js";
15
16
  // kumiko-lint-ignore no-custom-primitives Uses usePrimitives().Dialog internally, name is an MFA domain action, not a primitive reimplementation
16
17
  export function MfaDisableDialog({ open, onOpenChange, onDisabled, onError, }) {
17
18
  const t = useTranslation();
18
- const { Dialog, Field, Input } = usePrimitives();
19
+ const { Dialog } = usePrimitives();
19
20
  const dispatcher = useDispatcher();
20
21
  const [code, setCode] = useState("");
22
+ const [useRecovery, setUseRecovery] = useState(false);
23
+ const reset = () => {
24
+ setCode("");
25
+ setUseRecovery(false);
26
+ };
27
+ const handleOpenChange = (nextOpen) => {
28
+ if (!nextOpen)
29
+ reset();
30
+ onOpenChange(nextOpen);
31
+ };
21
32
  const confirm = async () => {
22
33
  const res = await dispatcher.write(AuthMfaHandlers.disable, { code });
23
- setCode("");
34
+ reset();
24
35
  if (!res.isSuccess) {
25
36
  onError(res.error.i18nKey);
26
37
  return;
27
38
  }
28
39
  onDisabled();
29
40
  };
30
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, variant: "danger", title: t("auth.mfa.disable.title"), description: t("auth.mfa.disable.description"), confirmLabel: t("auth.mfa.disable.confirm"), cancelLabel: t("auth.mfa.disable.cancel"), onConfirm: confirm, testId: "mfa-disable-dialog", children: _jsx(Field, { id: "mfa-disable-code", label: t("auth.mfa.disable.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-disable-code", name: "mfa-disable-code", value: code, onChange: setCode, autoComplete: "one-time-code" }) }) }));
41
+ return (_jsx(Dialog, { open: open, onOpenChange: handleOpenChange, variant: "danger", title: t("auth.mfa.disable.title"), description: t("auth.mfa.disable.description"), confirmLabel: t("auth.mfa.disable.confirm"), cancelLabel: t("auth.mfa.disable.cancel"), onConfirm: confirm, testId: "mfa-disable-dialog", children: _jsx(MfaCodeOrRecoveryField, { id: "mfa-disable-code", codeLabel: t("auth.mfa.disable.code"), code: code, onCodeChange: setCode, useRecovery: useRecovery, onUseRecoveryChange: setUseRecovery }) }));
31
42
  }
@@ -9,20 +9,31 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
10
10
  import { useState } from "react";
11
11
  import { AuthMfaHandlers } from "./constants.js";
12
+ import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.js";
12
13
  // kumiko-lint-ignore no-custom-primitives Uses usePrimitives().Dialog internally, name is an MFA domain action, not a primitive reimplementation
13
14
  export function MfaRegenerateRecoveryDialog({ open, onOpenChange, onRegenerated, onError, }) {
14
15
  const t = useTranslation();
15
- const { Dialog, Field, Input } = usePrimitives();
16
+ const { Dialog } = usePrimitives();
16
17
  const dispatcher = useDispatcher();
17
18
  const [code, setCode] = useState("");
19
+ const [useRecovery, setUseRecovery] = useState(false);
20
+ const reset = () => {
21
+ setCode("");
22
+ setUseRecovery(false);
23
+ };
24
+ const handleOpenChange = (nextOpen) => {
25
+ if (!nextOpen)
26
+ reset();
27
+ onOpenChange(nextOpen);
28
+ };
18
29
  const confirm = async () => {
19
30
  const res = await dispatcher.write(AuthMfaHandlers.regenerateRecovery, { code });
20
- setCode("");
31
+ reset();
21
32
  if (!res.isSuccess) {
22
33
  onError(res.error.i18nKey);
23
34
  return;
24
35
  }
25
36
  onRegenerated(res.data.recoveryCodes);
26
37
  };
27
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, title: t("auth.mfa.regenerate.title"), description: t("auth.mfa.regenerate.description"), confirmLabel: t("auth.mfa.regenerate.confirm"), cancelLabel: t("auth.mfa.regenerate.cancel"), onConfirm: confirm, testId: "mfa-regenerate-recovery-dialog", children: _jsx(Field, { id: "mfa-regenerate-code", label: t("auth.mfa.regenerate.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-regenerate-code", name: "mfa-regenerate-code", value: code, onChange: setCode, autoComplete: "one-time-code" }) }) }));
38
+ return (_jsx(Dialog, { open: open, onOpenChange: handleOpenChange, title: t("auth.mfa.regenerate.title"), description: t("auth.mfa.regenerate.description"), confirmLabel: t("auth.mfa.regenerate.confirm"), cancelLabel: t("auth.mfa.regenerate.cancel"), onConfirm: confirm, testId: "mfa-regenerate-recovery-dialog", children: _jsx(MfaCodeOrRecoveryField, { id: "mfa-regenerate-code", codeLabel: t("auth.mfa.regenerate.code"), code: code, onCodeChange: setCode, useRecovery: useRecovery, onUseRecoveryChange: setUseRecovery }) }));
28
39
  }
@@ -48,6 +48,7 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
48
48
  const [setup, setSetup] = useState(null);
49
49
  const [acknowledged, setAcknowledged] = useState(false);
50
50
  const [code, setCode] = useState("");
51
+ const [attempt, setAttempt] = useState(0);
51
52
  const [busy, setBusy] = useState(false);
52
53
  const [error, setError] = useState(null);
53
54
  const [retryAfterSeconds, setRetryAfterSeconds] = useState(null);
@@ -78,14 +79,14 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
78
79
  setBusy(false);
79
80
  }
80
81
  };
81
- const confirmSetup = async () => {
82
- if (!setup)
82
+ const confirmSetup = async (submittedCode) => {
83
+ if (!setup || busy)
83
84
  return;
84
85
  setBusy(true);
85
86
  setError(null);
86
87
  setRetryAfterSeconds(null);
87
88
  try {
88
- const res = await confirmMfaSetupPreauth(setup.setupToken, code);
89
+ const res = await confirmMfaSetupPreauth(setup.setupToken, submittedCode);
89
90
  if (res.kind !== "success") {
90
91
  setError(res.error.reason);
91
92
  setRetryAfterSeconds(res.error.retryAfterSeconds ?? null);
@@ -98,7 +99,11 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
98
99
  setSetup(null);
99
100
  setAcknowledged(false);
100
101
  setCode("");
102
+ return;
101
103
  }
104
+ // Remount: the field was disabled during confirm and lost focus.
105
+ setCode("");
106
+ setAttempt((n) => n + 1);
102
107
  return;
103
108
  }
104
109
  onSuccess?.(res.data.landingPath);
@@ -112,7 +117,7 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
112
117
  };
113
118
  const onSubmit = (e) => {
114
119
  e?.preventDefault();
115
- void confirmSetup();
120
+ void confirmSetup(code);
116
121
  };
117
122
  return (_jsxs(AuthCard, { title: title ?? t("auth.mfa.setup.title"), subtitle: subtitle ?? t("auth.mfa.setup.subtitle"), children: [error !== null ? (_jsx(Banner, { variant: "error", children: error === "too_many_attempts" && retryAfterSeconds !== null
118
123
  ? t("auth.mfa.errors.tooManyAttemptsWithSeconds", { seconds: retryAfterSeconds })
@@ -124,5 +129,5 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
124
129
  copyable: false,
125
130
  multiline: false,
126
131
  },
127
- ], copyLabel: t("kumiko.secretMint.copy"), copiedLabel: t("kumiko.secretMint.copied"), testId: "mfa-setup-preauth-qr" })) : (_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.scanTitle") })), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.manualEntry") }), _jsx("code", { className: "inline-block break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.secretParam })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.recoveryTitle") }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.recoveryHint") }), _jsx("code", { className: "block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.recoveryCodes.join("\n") }), _jsx(Field, { id: "mfa-setup-preauth-ack", label: t("auth.mfa.enable.acknowledge"), children: _jsx(Input, { kind: "boolean", id: "mfa-setup-preauth-ack", name: "mfa-setup-preauth-ack", value: acknowledged, onChange: setAcknowledged }) })] }), _jsx(Field, { id: "mfa-setup-preauth-code", label: t("auth.mfa.enable.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-setup-preauth-code", name: "mfa-setup-preauth-code", value: code, onChange: setCode, disabled: busy || !acknowledged, autoComplete: "one-time-code" }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", variant: "primary", loading: busy, disabled: busy || !acknowledged || code.length !== 6, children: t("auth.mfa.setup.confirm") }) })] }) })), onCancel ? (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: busy, children: t("auth.mfa.verify.backToLogin") }) })) : null] }));
132
+ ], copyLabel: t("kumiko.secretMint.copy"), copiedLabel: t("kumiko.secretMint.copied"), testId: "mfa-setup-preauth-qr" })) : (_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.scanTitle") })), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.manualEntry") }), _jsx("code", { className: "inline-block break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.secretParam })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.recoveryTitle") }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.recoveryHint") }), _jsx("code", { className: "block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.recoveryCodes.join("\n") }), _jsx(Field, { id: "mfa-setup-preauth-ack", label: t("auth.mfa.enable.acknowledge"), children: _jsx(Input, { kind: "boolean", id: "mfa-setup-preauth-ack", name: "mfa-setup-preauth-ack", value: acknowledged, onChange: setAcknowledged }) })] }), _jsx(Field, { id: "mfa-setup-preauth-code", label: t("auth.mfa.enable.code"), required: true, children: _jsx(Input, { kind: "code", length: 6, id: "mfa-setup-preauth-code", name: "mfa-setup-preauth-code", value: code, onChange: setCode, onComplete: (next) => void confirmSetup(next), disabled: busy || !acknowledged, hasError: error !== null, autoFocus: attempt > 0 }, attempt) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", variant: "primary", loading: busy, disabled: busy || !acknowledged || code.length !== 6, children: t("auth.mfa.setup.confirm") }) })] }) })), onCancel ? (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: busy, children: t("auth.mfa.verify.backToLogin") }) })) : null] }));
128
133
  }
@@ -10,6 +10,7 @@ import { useState } from "react";
10
10
  // kumiko-lint-ignore cross-feature-import client-only barrel, the feature's server barrel has no web/ re-export
11
11
  import { AuthCard, useSession } from "../../auth-email-password/web/index.js";
12
12
  import { verifyMfaChallenge } from "./mfa-client.js";
13
+ const MFA_CODE_LENGTH = 6;
13
14
  function reasonToKey(reason) {
14
15
  switch (reason) {
15
16
  case "invalid_totp_code":
@@ -31,13 +32,17 @@ export function MfaVerifyScreen({ challengeToken, title, subtitle, submitLabel,
31
32
  const t = useTranslation();
32
33
  const { Form, Field, Input, Button, Banner } = usePrimitives();
33
34
  const session = useSession();
35
+ const [mode, setMode] = useState("authenticator");
34
36
  const [code, setCode] = useState("");
37
+ const [attempt, setAttempt] = useState(0);
35
38
  const [submitting, setSubmitting] = useState(false);
36
39
  const [error, setError] = useState(null);
37
- const doSubmit = async () => {
40
+ const doSubmit = async (submittedCode) => {
41
+ if (submitting)
42
+ return;
38
43
  setSubmitting(true);
39
44
  setError(null);
40
- const res = await verifyMfaChallenge(challengeToken, code);
45
+ const res = await verifyMfaChallenge(challengeToken, submittedCode);
41
46
  if (res.kind === "success") {
42
47
  await session.refresh();
43
48
  setSubmitting(false);
@@ -46,12 +51,28 @@ export function MfaVerifyScreen({ challengeToken, title, subtitle, submitLabel,
46
51
  }
47
52
  setSubmitting(false);
48
53
  setError(res.error.reason);
54
+ // Remount: the input was disabled during submit and lost focus.
55
+ setAttempt((n) => n + 1);
56
+ if (mode === "authenticator")
57
+ setCode("");
49
58
  };
50
59
  const onSubmit = (e) => {
51
60
  e?.preventDefault();
52
- void doSubmit();
61
+ void doSubmit(code);
62
+ };
63
+ const toggleMode = () => {
64
+ setMode(mode === "authenticator" ? "recovery" : "authenticator");
65
+ setCode("");
66
+ setError(null);
53
67
  };
54
- return (_jsx(AuthCard, { title: title ?? t("auth.mfa.verify.title"), subtitle: subtitle ?? t("auth.mfa.verify.subtitle"), children: _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: "mfa-verify-code", label: t("auth.mfa.verify.code"), required: true, children: _jsx(Input, { kind: "text", id: "mfa-verify-code", name: "mfa-verify-code", value: code, onChange: setCode, disabled: submitting, required: true, autoComplete: "one-time-code" }) }), error !== null ? _jsx(Banner, { variant: "error", children: t(reasonToKey(error)) }) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting
68
+ const isRecovery = mode === "recovery";
69
+ const fieldId = isRecovery ? "mfa-verify-recovery-code" : "mfa-verify-code";
70
+ const defaultSubtitle = isRecovery
71
+ ? t("auth.mfa.verify.recoverySubtitle")
72
+ : t("auth.mfa.verify.subtitle");
73
+ return (_jsx(AuthCard, { title: title ?? t("auth.mfa.verify.title"), subtitle: subtitle ?? defaultSubtitle, children: _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: fieldId, label: isRecovery ? t("auth.mfa.verify.recoveryCode") : t("auth.mfa.verify.code"), required: true, children: isRecovery ? (_jsx(Input, { kind: "text", id: fieldId, name: fieldId, value: code, onChange: setCode, disabled: submitting, required: true, autoFocus: true, autoComplete: "off" }, `${mode}-${attempt}`)) : (_jsx(Input, { kind: "code", length: MFA_CODE_LENGTH, id: fieldId, name: fieldId, value: code, onChange: setCode, onComplete: (next) => void doSubmit(next), disabled: submitting, hasError: error !== null, required: true, autoFocus: true }, `${mode}-${attempt}`)) }), error !== null ? _jsx(Banner, { variant: "error", children: t(reasonToKey(error)) }) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting
55
74
  ? t("auth.mfa.verify.submitting")
56
- : (submitLabel ?? t("auth.mfa.verify.submit")) }), onCancel ? (_jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: submitting, children: t("auth.mfa.verify.backToLogin") })) : null] }) }));
75
+ : (submitLabel ?? t("auth.mfa.verify.submit")) }), _jsx(Button, { type: "button", variant: "link", onClick: toggleMode, disabled: submitting, children: isRecovery
76
+ ? t("auth.mfa.verify.useAuthenticatorCode")
77
+ : t("auth.mfa.verify.useRecoveryCode") }), onCancel ? (_jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: submitting, children: t("auth.mfa.verify.backToLogin") })) : null] }) }));
57
78
  }