@cosmicdrift/kumiko-bundled-features 0.388.0 → 0.390.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/tool-dispatch.d.ts +8 -3
- package/dist/agent-tools/tool-dispatch.js +36 -25
- package/dist/audit/feature.js +2 -1
- package/dist/auth-email-password/auth-user-row.js +1 -1
- package/dist/auth-email-password/bootstrap.js +6 -1
- package/dist/auth-email-password/feature.js +1 -1
- package/dist/auth-email-password/handlers/change-password.write.js +7 -7
- package/dist/auth-email-password/handlers/confirm-account-unlock.write.js +3 -3
- package/dist/auth-email-password/handlers/confirm-token-flow.js +4 -4
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +8 -7
- package/dist/auth-email-password/handlers/invite-accept.write.js +10 -7
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +2 -2
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +8 -7
- package/dist/auth-email-password/handlers/login.write.js +17 -15
- package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
- package/dist/auth-email-password/handlers/reset-password.write.js +2 -2
- package/dist/auth-email-password/handlers/signup-confirm.write.js +12 -12
- package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.js +16 -8
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +4 -2
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +7 -2
- package/dist/auth-email-password/handlers/verify-email.write.js +2 -2
- package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
- package/dist/auth-email-password/magic-link-mail.js +7 -3
- package/dist/auth-email-password/seeding.d.ts +1 -1
- package/dist/auth-email-password/seeding.js +9 -8
- package/dist/auth-email-password/signup-activation.d.ts +1 -0
- package/dist/auth-email-password/signup-activation.js +1 -0
- package/dist/auth-email-password/web/auth-client.d.ts +2 -2
- package/dist/auth-email-password/web/auth-client.js +8 -4
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
- package/dist/auth-email-password/web/auth-redirect.js +20 -25
- package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
- package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
- package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
- package/dist/auth-email-password/web/index.d.ts +1 -0
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/login-screen.js +11 -6
- package/dist/auth-email-password/web/reset-password-screen.js +7 -1
- package/dist/auth-email-password/web/return-path.d.ts +4 -0
- package/dist/auth-email-password/web/return-path.js +34 -0
- package/dist/auth-email-password/web/session.js +6 -1
- package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
- package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
- package/dist/auth-email-password/web/signup-screen.js +8 -3
- package/dist/auth-foundation/feature.js +12 -4
- package/dist/auth-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +3 -7
- package/dist/auth-mfa/handlers/disable.write.js +5 -5
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +19 -14
- package/dist/auth-mfa/handlers/enable-confirm.write.js +3 -3
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +6 -1
- package/dist/auth-mfa/handlers/enable-start.write.js +1 -1
- package/dist/auth-mfa/handlers/reencrypt.job.js +14 -9
- package/dist/auth-mfa/handlers/regenerate-recovery.write.js +4 -4
- package/dist/auth-mfa/handlers/verify.write.js +19 -14
- package/dist/auth-mfa/mfa-code-verifier.js +9 -4
- package/dist/auth-mfa/mfa-status-checker.js +13 -6
- package/dist/auth-mfa/verify-factor.js +1 -1
- package/dist/billing-foundation/billing-info-query.js +2 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -1
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +5 -5
- package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-open-cancels.d.ts +4 -0
- package/dist/billing-foundation/consumer-protection/termination-open-cancels.js +14 -0
- package/dist/billing-foundation/consumer-protection/termination-provider-lock.d.ts +2 -0
- package/dist/billing-foundation/consumer-protection/termination-provider-lock.js +3 -0
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +4 -4
- package/dist/billing-foundation/consumer-protection/termination-record.js +11 -48
- package/dist/billing-foundation/consumer-protection/termination-request.js +10 -8
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +10 -12
- package/dist/billing-foundation/events.js +1 -1
- package/dist/billing-foundation/feature.js +3 -3
- package/dist/billing-foundation/get-subscription-for-tenant.d.ts +1 -1
- package/dist/billing-foundation/handlers/process-event.write.js +5 -5
- package/dist/billing-foundation/handlers/process-payment-event.write.js +3 -3
- package/dist/billing-foundation/plan-catalog.js +2 -2
- package/dist/billing-foundation/projection.js +1 -1
- package/dist/billing-foundation/types.js +1 -1
- package/dist/cap-counter/aggregate-id.d.ts +1 -1
- package/dist/cap-counter/aggregate-id.js +1 -1
- package/dist/cap-counter/book-cap-usage.d.ts +2 -2
- package/dist/cap-counter/book-cap-usage.js +3 -3
- package/dist/cap-counter/cap-reservation.js +16 -7
- package/dist/cap-counter/constants.js +1 -1
- package/dist/cap-counter/feature.js +3 -2
- package/dist/cap-counter/handlers/increment-rolling.write.js +1 -1
- package/dist/cap-counter/reservation-chain.js +1 -1
- package/dist/cap-counter/with-cap-enforcement.d.ts +3 -3
- package/dist/cap-counter/with-cap-enforcement.js +1 -1
- package/dist/cap-overview/feature.js +1 -0
- package/dist/cap-overview/handlers/caps-usage.query.js +1 -1
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +1 -1
- package/dist/compliance-profiles/seeding.d.ts +1 -1
- package/dist/compliance-profiles/seeding.js +9 -3
- package/dist/config/feature.js +6 -6
- package/dist/config/handlers/cascade.query.js +1 -1
- package/dist/config/handlers/readiness.query.js +1 -1
- package/dist/config/handlers/reencrypt.job.js +23 -13
- package/dist/config/handlers/reset.write.js +3 -3
- package/dist/config/handlers/selected-extensions.query.js +1 -1
- package/dist/config/handlers/set.write.js +15 -5
- package/dist/config/handlers/values.query.js +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +9 -9
- package/dist/crypto-shredding/resolve-file-bytes-deleter.js +3 -3
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
- package/dist/custom-fields/handlers/delete-system-field.write.js +1 -1
- package/dist/custom-fields/handlers/delete-tenant-field.write.js +1 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
- package/dist/custom-fields/index.d.ts +1 -0
- package/dist/custom-fields/index.js +1 -0
- package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/data-retention/run-retention-cleanup.js +12 -2
- package/dist/delivery/attempt-log.js +1 -1
- package/dist/delivery/delivery-service.js +5 -5
- package/dist/delivery/feature.js +3 -2
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/jobs.js +11 -11
- package/dist/delivery/system-channel-context.d.ts +15 -0
- package/dist/delivery/system-channel-context.js +5 -0
- package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
- package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
- package/dist/document-ingest-foundation/events.d.ts +11 -0
- package/dist/document-ingest-foundation/events.js +12 -1
- package/dist/document-ingest-foundation/index.d.ts +2 -1
- package/dist/document-ingest-foundation/index.js +2 -1
- package/dist/feature-toggles/feature.js +1 -0
- package/dist/feature-toggles/handlers/registered.query.js +1 -1
- package/dist/feature-toggles/handlers/set.write.js +1 -1
- package/dist/feature-toggles/toggle-sync-signal.js +1 -1
- package/dist/file-derivatives/feature.d.ts +1 -1
- package/dist/file-derivatives/feature.js +1 -1
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/file-derivatives/handlers/public-variant.query.js +5 -5
- package/dist/file-foundation/feature.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +4 -2
- package/dist/file-foundation/read-file-ref-content.js +3 -3
- package/dist/file-provider-s3/feature.js +1 -1
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +8 -8
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/cleanup.job.js +22 -12
- package/dist/form-draft/handlers/discard.write.js +3 -3
- package/dist/form-draft/handlers/get.query.js +1 -1
- package/dist/foundation-shared/config-helpers.d.ts +1 -1
- package/dist/foundation-shared/config-helpers.js +1 -1
- package/dist/host-routing/validate-options.js +3 -0
- package/dist/inbound-mail-foundation/events.js +1 -1
- package/dist/inbound-mail-foundation/feature.js +2 -1
- package/dist/inbound-mail-foundation/handlers/account-state.d.ts +3 -1
- package/dist/inbound-mail-foundation/handlers/account-state.js +1 -1
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/disconnect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +10 -10
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +1 -1
- package/dist/inbound-mail-foundation/projection.js +1 -1
- package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
- package/dist/inbound-provider-imap/feature.js +1 -1
- package/dist/jobs/events.js +1 -1
- package/dist/jobs/feature.js +1 -0
- package/dist/jobs/handlers/consumer.write.js +1 -1
- package/dist/jobs/handlers/projection-rebuild.job.js +1 -1
- package/dist/jobs/handlers/reindex-entity.job.js +5 -5
- package/dist/jobs/handlers/retention-cleanup.job.js +1 -1
- package/dist/jobs/handlers/retry.write.js +1 -1
- package/dist/jobs/handlers/stale-run-sweep.job.js +1 -1
- package/dist/jobs/handlers/trigger.write.js +2 -2
- package/dist/jobs/job-run-logger.d.ts +1 -0
- package/dist/jobs/job-run-logger.js +38 -18
- package/dist/ledger/web/index.js +1 -1
- package/dist/legal-pages/lib/boot-check.d.ts +10 -4
- package/dist/legal-pages/lib/boot-check.js +2 -2
- package/dist/legal-pages/seed.d.ts +3 -1
- package/dist/legal-pages/seed.js +2 -2
- package/dist/mail-foundation/feature.d.ts +2 -2
- package/dist/mail-transport-smtp/config-keys.js +1 -1
- package/dist/mail-transport-smtp/feature.js +7 -6
- package/dist/managed-pages/constants.d.ts +1 -0
- package/dist/managed-pages/constants.js +1 -0
- package/dist/managed-pages/feature.js +3 -1
- package/dist/managed-pages/index.d.ts +1 -0
- package/dist/managed-pages/index.js +1 -0
- package/dist/managed-pages/seeding.d.ts +1 -1
- package/dist/managed-pages/seeding.js +10 -4
- package/dist/notes-history/handlers/add-note.write.js +2 -2
- package/dist/personal-access-tokens/feature.js +2 -1
- package/dist/personal-access-tokens/handlers/create.write.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
- package/dist/rate-limiting/handlers/status.query.js +2 -2
- package/dist/readiness/handlers/status.query.js +1 -1
- package/dist/renderer-foundation/api.d.ts +4 -2
- package/dist/renderer-foundation/api.js +2 -2
- package/dist/secrets/announce-secret-change.d.ts +6 -2
- package/dist/secrets/announce-secret-change.js +3 -3
- package/dist/secrets/feature.d.ts +1 -1
- package/dist/secrets/feature.js +8 -8
- package/dist/secrets/handlers/delete.write.js +1 -1
- package/dist/secrets/handlers/rotate.job.js +13 -8
- package/dist/secrets/handlers/set.write.js +1 -1
- package/dist/secrets/secrets-context.js +16 -6
- package/dist/secrets/write-gate.js +8 -2
- package/dist/seo/feature.js +1 -1
- package/dist/sessions/feature.js +2 -1
- package/dist/sessions/handlers/cleanup.job.js +2 -2
- package/dist/sessions/handlers/detail.query.d.ts +1 -0
- package/dist/sessions/handlers/detail.query.js +5 -2
- package/dist/sessions/handlers/list.query.d.ts +1 -0
- package/dist/sessions/handlers/list.query.js +5 -2
- package/dist/sessions/handlers/mine.query.d.ts +1 -0
- package/dist/sessions/handlers/mine.query.js +5 -2
- package/dist/sessions/handlers/revoke-all-for-user.write.js +3 -3
- package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
- package/dist/sessions/handlers/revoke.write.js +1 -1
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.js +1 -1
- package/dist/sessions/screens.js +8 -4
- package/dist/sessions/session-callbacks.d.ts +0 -2
- package/dist/sessions/session-callbacks.js +1 -3
- package/dist/sessions/session-revoked-event.js +1 -1
- package/dist/sessions/web/session-device-cell.js +10 -12
- package/dist/sessions/web/user-agent.d.ts +1 -0
- package/dist/sessions/web/user-agent.js +11 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
- package/dist/shared/purge-files-reaching-subject.js +31 -4
- package/dist/shared/require-for-tenant.d.ts +1 -1
- package/dist/shared/require-for-tenant.js +6 -4
- package/dist/shared/run-in-sub-transaction.js +5 -5
- package/dist/shared/stream-tenant-backfill.d.ts +28 -0
- package/dist/shared/stream-tenant-backfill.js +119 -0
- package/dist/shared/stream-tenant-derived-state.d.ts +25 -0
- package/dist/shared/stream-tenant-derived-state.js +68 -0
- package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
- package/dist/step-dispatcher/feature.js +1 -1
- package/dist/step-dispatcher/mail-runner.d.ts +1 -0
- package/dist/subscription-dodo/plugin-methods.js +1 -1
- package/dist/subscription-dodo/runtime.js +4 -4
- package/dist/subscription-stripe/plugin-methods.js +3 -3
- package/dist/subscription-stripe/runtime.js +4 -4
- package/dist/tags/feature.js +1 -0
- package/dist/template-resolver/api.d.ts +4 -2
- package/dist/template-resolver/api.js +2 -2
- package/dist/template-resolver/document-ops.d.ts +5 -0
- package/dist/template-resolver/document-ops.js +18 -2
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.d.ts +4 -3
- package/dist/template-resolver/handlers/export-bundle.query.js +1 -1
- package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
- package/dist/template-resolver/handlers/layout-source.query.js +26 -6
- package/dist/template-resolver/handlers/overview.query.js +17 -9
- package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
- package/dist/template-resolver/handlers/send-test-shared.js +1 -1
- package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
- package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
- package/dist/template-resolver/handlers/send-test.write.js +2 -2
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +3 -1
- package/dist/template-resolver/handlers/tenant-override-selection.js +1 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +1 -1
- package/dist/template-resolver/handlers/usage.query.js +1 -1
- package/dist/template-resolver/seeding.d.ts +1 -0
- package/dist/template-resolver/seeding.js +17 -4
- package/dist/template-resolver/store-template-resource.d.ts +7 -6
- package/dist/template-resolver/store-template-resource.js +48 -8
- package/dist/template-resolver/template-resources.d.ts +2 -2
- package/dist/template-resolver/template-resources.js +2 -2
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/web/document-insert-fields.d.ts +4 -0
- package/dist/template-resolver/web/document-insert-fields.js +69 -0
- package/dist/template-resolver/web/document-insert-model.d.ts +43 -0
- package/dist/template-resolver/web/document-insert-model.js +106 -0
- package/dist/template-resolver/web/document-insert-panel.d.ts +13 -0
- package/dist/template-resolver/web/document-insert-panel.js +49 -0
- package/dist/template-resolver/web/document-tiptap-bubble.d.ts +4 -0
- package/dist/template-resolver/web/document-tiptap-bubble.js +85 -0
- package/dist/template-resolver/web/document-tiptap-content.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-content.js +12 -0
- package/dist/template-resolver/web/document-tiptap-context.d.ts +41 -0
- package/dist/template-resolver/web/document-tiptap-context.js +43 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +50 -495
- package/dist/template-resolver/web/document-tiptap-helpers.d.ts +44 -0
- package/dist/template-resolver/web/document-tiptap-helpers.js +129 -0
- package/dist/template-resolver/web/document-tiptap-layout.d.ts +4 -0
- package/dist/template-resolver/web/document-tiptap-layout.js +24 -0
- package/dist/template-resolver/web/document-tiptap-toolbar-groups.d.ts +6 -0
- package/dist/template-resolver/web/document-tiptap-toolbar-groups.js +33 -0
- package/dist/template-resolver/web/document-tiptap-toolbar.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-toolbar.js +91 -0
- package/dist/template-resolver/web/editor-handle.d.ts +1 -0
- package/dist/template-resolver/web/editor-handle.js +15 -0
- package/dist/template-resolver/web/i18n.js +3 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +16 -5
- package/dist/template-resolver/web/qualified-names.d.ts +1 -0
- package/dist/template-resolver/web/qualified-names.js +1 -0
- package/dist/template-resolver/web/use-document-tiptap-model.d.ts +54 -0
- package/dist/template-resolver/web/use-document-tiptap-model.js +96 -0
- package/dist/tenant/feature.js +1 -0
- package/dist/tenant/handlers/cancel-invitation.write.js +1 -1
- package/dist/tenant/handlers/memberships.query.js +1 -1
- package/dist/tenant/handlers/remove-member.write.js +2 -2
- package/dist/tenant/handlers/team-list.query.js +3 -3
- package/dist/tenant/handlers/update-member-roles.write.js +1 -1
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invited-membership.js +6 -1
- package/dist/tenant/schema/tenant.d.ts +2 -2
- package/dist/tenant/schema/tenant.js +4 -1
- package/dist/tenant/seeding.d.ts +2 -2
- package/dist/tenant/seeding.js +52 -52
- package/dist/tenant/stream-tenant-backfill.d.ts +21 -0
- package/dist/tenant/stream-tenant-backfill.js +63 -0
- package/dist/tenant/testing.d.ts +12 -0
- package/dist/tenant/testing.js +33 -0
- package/dist/tenant-handover/events.js +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -1
- package/dist/tenant-lifecycle/feature.js +5 -5
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +14 -5
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +12 -4
- package/dist/tenant-lifecycle/run-tenant-destroy.js +23 -13
- package/dist/tenant-lifecycle/stages.js +28 -2
- package/dist/tenant-lifecycle/testing.d.ts +1 -1
- package/dist/tenant-lifecycle/testing.js +6 -5
- package/dist/tier-engine/entity.js +1 -1
- package/dist/tier-engine/feature.js +8 -3
- package/dist/tier-engine/handlers/active-tier.query.js +3 -3
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +6 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -4
- package/dist/user/db/queries/stream-tenant-backfill.d.ts +20 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +40 -77
- package/dist/user/feature.js +1 -0
- package/dist/user/handlers/find-for-auth.query.js +2 -2
- package/dist/user/handlers/update-roles.js +2 -2
- package/dist/user/index.d.ts +1 -1
- package/dist/user/index.js +1 -1
- package/dist/user/invited-global-roles.js +6 -1
- package/dist/user/principal-status.js +1 -1
- package/dist/user/seeding.d.ts +1 -1
- package/dist/user/seeding.js +20 -5
- package/dist/user/testing.d.ts +3 -0
- package/dist/user/testing.js +10 -0
- package/dist/user-data-rights/audit-download.js +12 -2
- package/dist/user-data-rights/feature.js +30 -26
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/deletion-grace-period.js +1 -1
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -2
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +4 -4
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +8 -6
- package/dist/user-data-rights/index.d.ts +1 -1
- package/dist/user-data-rights/index.js +1 -1
- package/dist/user-data-rights/lib/mail-transport-resolver.js +1 -1
- package/dist/user-data-rights/lib/storage-provider-resolver.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -1
- package/dist/user-data-rights/run-export-jobs.js +7 -2
- package/dist/user-data-rights/run-forget-cleanup.d.ts +1 -0
- package/dist/user-data-rights/run-forget-cleanup.js +9 -1
- package/dist/user-data-rights/run-user-export.js +7 -1
- package/dist/user-data-rights/web/constants.d.ts +14 -0
- package/dist/user-data-rights/web/constants.js +14 -0
- package/dist/user-data-rights/web/index.d.ts +1 -0
- package/dist/user-data-rights/web/index.js +1 -0
- package/dist/user-profile/handlers/change-email.write.js +8 -6
- package/dist/waitlist/feature.js +1 -0
- package/dist/waitlist/handlers/export-purge.write.js +1 -1
- package/dist/waitlist/handlers/invite.write.js +2 -2
- package/dist/waitlist/handlers/reject.write.js +1 -1
- package/dist/waitlist/handlers/submit.write.js +20 -11
- package/dist/waitlist/import.d.ts +2 -1
- package/dist/waitlist/import.js +22 -2
- package/dist/waitlist/legacy-migrate.js +8 -3
- package/dist/waitlist/link-on-signup.js +6 -1
- package/dist/waitlist/options.d.ts +2 -1
- package/dist/workflow-runner/db/queries/due-runs.js +1 -1
- package/dist/workflow-runner/event-subscriber.js +4 -3
- package/dist/workflow-runner/event-trigger.js +2 -1
- package/dist/workflow-runner/feature.js +2 -2
- package/dist/workflow-runner/handlers/resume-run.write.js +6 -6
- package/dist/workflow-runner/runner.d.ts +3 -1
- package/dist/workflow-runner/runner.js +3 -3
- package/package.json +41 -23
- package/src/agent-tools/changes.json +6 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/cap-counter/changes.json +12 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +12 -0
- package/src/custom-fields/changes.json +6 -0
- package/src/delivery/changes.json +12 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/host-routing/changes.json +6 -0
- package/src/jobs/changes.json +6 -0
- package/src/mail-transport-smtp/changes.json +12 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/sessions/changes.json +19 -0
- package/src/step-dispatcher/changes.json +6 -0
- package/src/template-resolver/changes.json +54 -0
- package/src/tenant/changes.json +19 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/user-data-rights/changes.json +36 -0
- package/src/waitlist/changes.json +18 -0
- package/src/workflow-runner/changes.json +6 -0
|
@@ -1,36 +1,31 @@
|
|
|
1
1
|
// Redirect targets for external login pages. `next` travels through the query
|
|
2
2
|
// string, so anything read back from it is untrusted: only same-origin
|
|
3
3
|
// root-relative paths pass (no "//host", no "/\host", no scheme).
|
|
4
|
+
import { isSafeReturnPath, RETURN_TO_QUERY_PARAM, readReturnToFromSearch } from "./return-path.js";
|
|
4
5
|
export const NEXT_QUERY_PARAM = "next";
|
|
5
|
-
|
|
6
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
7
|
-
const code = candidate.charCodeAt(i);
|
|
8
|
-
if (code <= 0x20 || code === 0x7f)
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
// The URL parse is a backstop: browsers strip tabs/newlines, so "/\t/evil.com"
|
|
14
|
-
// would otherwise normalize to "//evil.com" past the leading-slash check.
|
|
15
|
-
export function isSafeNextPath(candidate) {
|
|
16
|
-
if (typeof candidate !== "string" || !candidate.startsWith("/"))
|
|
17
|
-
return false;
|
|
18
|
-
if (candidate[1] === "/" || candidate.includes("\\"))
|
|
19
|
-
return false;
|
|
20
|
-
if (hasControlOrWhitespace(candidate))
|
|
21
|
-
return false;
|
|
22
|
-
try {
|
|
23
|
-
const parsed = new URL(candidate, "https://next.invalid");
|
|
24
|
-
return parsed.origin === "https://next.invalid" && !parsed.pathname.startsWith("//");
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
6
|
+
export const isSafeNextPath = isSafeReturnPath;
|
|
30
7
|
export function readNextFromSearch(search) {
|
|
31
8
|
const candidate = new URLSearchParams(search).get(NEXT_QUERY_PARAM);
|
|
32
9
|
return isSafeNextPath(candidate) ? candidate : null;
|
|
33
10
|
}
|
|
11
|
+
// The returnTo a mail link or sibling auth page carried in this page's own URL; null on native, where there is no window.
|
|
12
|
+
export function readReturnToFromWindow() {
|
|
13
|
+
return typeof window === "undefined" ? null : readReturnToFromSearch(window.location.search);
|
|
14
|
+
}
|
|
15
|
+
// Adds returnTo to an href of a sibling auth page; a missing value leaves the href untouched.
|
|
16
|
+
export function appendReturnTo(href, returnTo) {
|
|
17
|
+
if (returnTo === null)
|
|
18
|
+
return href;
|
|
19
|
+
const hashAt = href.indexOf("#");
|
|
20
|
+
const base = hashAt === -1 ? href : href.slice(0, hashAt);
|
|
21
|
+
const hash = hashAt === -1 ? "" : href.slice(hashAt);
|
|
22
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
23
|
+
return `${base}${separator}${RETURN_TO_QUERY_PARAM}=${encodeURIComponent(returnTo)}${hash}`;
|
|
24
|
+
}
|
|
25
|
+
// The safe `next` of this page's URL; null on native, where there is no window.
|
|
26
|
+
export function readNextFromWindow() {
|
|
27
|
+
return typeof window === "undefined" ? null : readNextFromSearch(window.location.search);
|
|
28
|
+
}
|
|
34
29
|
// loginUrl/postLogoutUrl come from app code (trusted), but a typo like
|
|
35
30
|
// "javascript:" must fail at startup instead of becoming a navigation target.
|
|
36
31
|
export function assertNavigableUrl(url, optionName) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { stripBasePath, useBasePath } from "@cosmicdrift/kumiko-renderer";
|
|
2
3
|
import { DEFAULT_AUTH_PATHS, DEFAULT_FORGOT_PASSWORD_PATH } from "./auth-paths.js";
|
|
3
4
|
import { ConfirmAccountUnlockScreen } from "./confirm-account-unlock-screen.js";
|
|
4
5
|
import { ForgotPasswordScreen } from "./forgot-password-screen.js";
|
|
@@ -7,8 +8,9 @@ import { ResetPasswordScreen } from "./reset-password-screen.js";
|
|
|
7
8
|
function hasToken(search) {
|
|
8
9
|
return (new URLSearchParams(search).get("token") ?? "") !== "";
|
|
9
10
|
}
|
|
10
|
-
function pickDefaultAuthPage(pathname, search) {
|
|
11
|
-
const
|
|
11
|
+
function pickDefaultAuthPage(pathname, search, basePath) {
|
|
12
|
+
const trimmed = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
13
|
+
const path = stripBasePath(trimmed, basePath);
|
|
12
14
|
switch (path) {
|
|
13
15
|
case DEFAULT_FORGOT_PASSWORD_PATH:
|
|
14
16
|
return _jsx(ForgotPasswordScreen, {});
|
|
@@ -24,5 +26,6 @@ function pickDefaultAuthPage(pathname, search) {
|
|
|
24
26
|
// (outer) still win on the same path. The pages are public: no session needed.
|
|
25
27
|
export function DefaultAuthPagesGate({ children }) {
|
|
26
28
|
const { pathname, search } = window.location;
|
|
27
|
-
|
|
29
|
+
const basePath = useBasePath();
|
|
30
|
+
return pickDefaultAuthPage(pathname, search, basePath) ?? _jsx(_Fragment, { children: children });
|
|
28
31
|
}
|
|
@@ -5,5 +5,8 @@ export type ForgotPasswordScreenProps = {
|
|
|
5
5
|
/** href for the "Back to login" link in the success and form states. App-
|
|
6
6
|
* specific — defaults to "/login". */
|
|
7
7
|
readonly loginHref?: string;
|
|
8
|
+
/** Page the reset mail should lead back to after the new password is set; defaults to the
|
|
9
|
+
* `returnTo` query parameter of this page. Unsafe values are dropped. */
|
|
10
|
+
readonly returnTo?: string;
|
|
8
11
|
};
|
|
9
|
-
export declare function ForgotPasswordScreen({ title, subtitle, loginHref, }: ForgotPasswordScreenProps): ReactNode;
|
|
12
|
+
export declare function ForgotPasswordScreen({ title, subtitle, loginHref, returnTo, }: ForgotPasswordScreenProps): ReactNode;
|
|
@@ -12,7 +12,12 @@ import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
|
12
12
|
import { useState } from "react";
|
|
13
13
|
import { requestPasswordReset } from "./auth-client.js";
|
|
14
14
|
import { AuthCard } from "./auth-form-primitives.js";
|
|
15
|
-
|
|
15
|
+
import { readReturnToFromWindow } from "./auth-redirect.js";
|
|
16
|
+
import { isSafeReturnPath } from "./return-path.js";
|
|
17
|
+
function resolveReturnTo(prop) {
|
|
18
|
+
return (isSafeReturnPath(prop) ? prop : readReturnToFromWindow()) ?? undefined;
|
|
19
|
+
}
|
|
20
|
+
export function ForgotPasswordScreen({ title, subtitle, loginHref = "/login", returnTo, }) {
|
|
16
21
|
const t = useTranslation();
|
|
17
22
|
const { Form, Field, Input, Button, Banner, Link } = usePrimitives();
|
|
18
23
|
const [email, setEmail] = useState("");
|
|
@@ -24,7 +29,7 @@ export function ForgotPasswordScreen({ title, subtitle, loginHref = "/login", })
|
|
|
24
29
|
setSubmitting(true);
|
|
25
30
|
setError(null);
|
|
26
31
|
try {
|
|
27
|
-
const res = await requestPasswordReset(email);
|
|
32
|
+
const res = await requestPasswordReset(email, resolveReturnTo(returnTo));
|
|
28
33
|
if (res.ok) {
|
|
29
34
|
setDone(true);
|
|
30
35
|
}
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
const next = returnTo ?? res.data.landingPath;
|
|
55
|
+
setContinueHref(next === undefined
|
|
53
56
|
? loginHref
|
|
54
|
-
: buildLoginRedirectUrl(loginHref,
|
|
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
|
-
|
|
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);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Middleware / runProdApp resolve providers from the registry at boot
|
|
11
11
|
// (tokenVerifier/sessionStore) or request-time (tokenVerifier shape match).
|
|
12
12
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
+
import { systemScopedRunner } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
13
14
|
import { validateSessionStoreMultiplicity, validateTenantExistenceMultiplicity, validateTenantResolverMultiplicity, validateTokenVerifierMultiplicity, } from "./boot-checks.js";
|
|
14
15
|
import { EXT_SESSION_STORE, EXT_TENANT_EXISTENCE, EXT_TENANT_RESOLVER, EXT_TOKEN_VERIFIER, isAuthProviderPlugin, isSessionStoreProvider, isTenantExistenceProvider, isTenantResolverProvider, tokenShapeMatches, } from "./types.js";
|
|
15
16
|
export { EXT_SESSION_STORE, EXT_TENANT_EXISTENCE, EXT_TENANT_RESOLVER, EXT_TOKEN_VERIFIER, } from "./types.js";
|
|
@@ -49,6 +50,13 @@ export function createAuthFoundationFeature() {
|
|
|
49
50
|
r.bootCheck(({ features }) => validateTenantExistenceMultiplicity(features));
|
|
50
51
|
}, { dedupeOptions: {} });
|
|
51
52
|
}
|
|
53
|
+
// Providers run before any tenant or session exists (token verify, host to tenant), so their reads are cross-tenant by nature.
|
|
54
|
+
function systemScopedDeps(deps) {
|
|
55
|
+
return {
|
|
56
|
+
...deps,
|
|
57
|
+
db: systemScopedRunner(deps.db, "auth lookups run before a tenant is established"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
52
60
|
/**
|
|
53
61
|
* Tries every registered tokenVerifier provider's shape against `rawToken`
|
|
54
62
|
* in registration order and calls the first match's verifier. Returns null
|
|
@@ -62,7 +70,7 @@ export async function resolveTokenVerifier(deps, rawToken) {
|
|
|
62
70
|
continue;
|
|
63
71
|
if (!tokenShapeMatches(usage.options.shape, rawToken))
|
|
64
72
|
continue;
|
|
65
|
-
const verify = await usage.options.build(deps);
|
|
73
|
+
const verify = await usage.options.build(systemScopedDeps(deps));
|
|
66
74
|
return verify(rawToken);
|
|
67
75
|
}
|
|
68
76
|
return null;
|
|
@@ -78,7 +86,7 @@ export async function resolveSessionStore(deps) {
|
|
|
78
86
|
if (!usage || !isSessionStoreProvider(usage.options)) {
|
|
79
87
|
throw new Error("[auth-foundation] no sessionStore provider registered — did the boot validator run?");
|
|
80
88
|
}
|
|
81
|
-
return usage.options.build(deps);
|
|
89
|
+
return usage.options.build(systemScopedDeps(deps));
|
|
82
90
|
}
|
|
83
91
|
/** Optional tenantResolver — null when no provider registered. */
|
|
84
92
|
export async function resolveTenantResolver(deps) {
|
|
@@ -88,7 +96,7 @@ export async function resolveTenantResolver(deps) {
|
|
|
88
96
|
if (!isTenantResolverProvider(usage.options)) {
|
|
89
97
|
throw new Error("[auth-foundation] tenantResolver provider registered without a valid TenantResolverProvider");
|
|
90
98
|
}
|
|
91
|
-
const resolve = await usage.options.build(deps);
|
|
99
|
+
const resolve = await usage.options.build(systemScopedDeps(deps));
|
|
92
100
|
return { resolve, trust: usage.options.trust };
|
|
93
101
|
}
|
|
94
102
|
/** Optional tenantExistence — null when no provider registered. */
|
|
@@ -99,5 +107,5 @@ export async function resolveTenantExistence(deps) {
|
|
|
99
107
|
if (!isTenantExistenceProvider(usage.options)) {
|
|
100
108
|
throw new Error("[auth-foundation] tenantExistence provider registered without a valid TenantExistenceProvider");
|
|
101
109
|
}
|
|
102
|
-
return usage.options.build(deps);
|
|
110
|
+
return usage.options.build(systemScopedDeps(deps));
|
|
103
111
|
}
|
|
@@ -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">;
|
package/dist/auth-mfa/config.js
CHANGED
|
@@ -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
|
-
//
|
|
6
|
-
// mfa-status-checker.ts
|
|
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",
|
|
@@ -30,18 +30,18 @@ export function createDisableHandler(opts) {
|
|
|
30
30
|
if (!row)
|
|
31
31
|
return mfaNotEnabled();
|
|
32
32
|
// Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
|
|
33
|
-
// without ctx.redis, verifyMfaFactor's TOTP-replay burn is skipped, and
|
|
33
|
+
// without ctx.services.redis, verifyMfaFactor's TOTP-replay burn is skipped, and
|
|
34
34
|
// a code observed once (phishing proxy/shoulder-surfing) could be
|
|
35
35
|
// replayed within the ±1-step window to disable MFA outright.
|
|
36
|
-
// "no ctx.redis means no one gets through MFA login" doesn't cover
|
|
36
|
+
// "no ctx.services.redis means no one gets through MFA login" doesn't cover
|
|
37
37
|
// this route — it's session-authed, and JWTs issued before Redis was
|
|
38
38
|
// unwired stay valid until they expire.
|
|
39
|
-
if (!ctx.redis) {
|
|
39
|
+
if (!ctx.services.redis) {
|
|
40
40
|
return writeFailure(new InternalError({
|
|
41
|
-
message: "disable (mfa) requires ctx.redis for TOTP replay protection",
|
|
41
|
+
message: "disable (mfa) requires ctx.services.redis for TOTP replay protection",
|
|
42
42
|
}));
|
|
43
43
|
}
|
|
44
|
-
const replay = { redis: ctx.redis, userId: event.user.id };
|
|
44
|
+
const replay = { redis: ctx.services.redis, userId: event.user.id };
|
|
45
45
|
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
46
46
|
if (!verify.ok)
|
|
47
47
|
return invalidTotpCode();
|
|
@@ -37,8 +37,8 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
37
37
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
38
38
|
escapeHatch: {
|
|
39
39
|
grants: ["systemIdentity", "unsafeRaw"],
|
|
40
|
-
reason: "Pre-auth MFA enrollment step has no session yet — re-checks status via ctx.queryAs(SYSTEM, " +
|
|
41
|
-
"user:findForAuth) and membership via ctx.resolveActiveMembership for the setup token's user. " +
|
|
40
|
+
reason: "Pre-auth MFA enrollment step has no session yet — re-checks status via ctx.dispatch.queryAs(SYSTEM, " +
|
|
41
|
+
"user:findForAuth) and membership via ctx.auth.resolveActiveMembership for the setup token's user. " +
|
|
42
42
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
43
43
|
"guest dispatch tenant.",
|
|
44
44
|
},
|
|
@@ -60,19 +60,19 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
60
60
|
// endpoint has NO gate before it (access: { roles: ["anonymous"] }, no JWT,
|
|
61
61
|
// no prior challenge-token from a successful login) — its only secret
|
|
62
62
|
// is a 6-digit TOTP code. Silently skipping the brute-force cap and
|
|
63
|
-
// single-use burn when ctx.redis is absent would let the setup token
|
|
63
|
+
// single-use burn when ctx.services.redis is absent would let the setup token
|
|
64
64
|
// be guessed against unboundedly until its TTL expires. The route's
|
|
65
65
|
// IP rate limiter is not a substitute: it's IP-scoped and resets on
|
|
66
66
|
// every success.
|
|
67
|
-
if (!ctx.redis) {
|
|
67
|
+
if (!ctx.services.redis) {
|
|
68
68
|
return writeFailure(new InternalError({
|
|
69
|
-
message: "enable-confirm-preauth requires ctx.redis for the brute-force cap and single-use burn",
|
|
69
|
+
message: "enable-confirm-preauth requires ctx.services.redis for the brute-force cap and single-use burn",
|
|
70
70
|
}));
|
|
71
71
|
}
|
|
72
72
|
// Same guessing surface as /auth/mfa/verify (a short TOTP code) and
|
|
73
73
|
// the same per-userId cap — deliberately shared counter, see
|
|
74
74
|
// mfa-verify-attempts.ts.
|
|
75
|
-
const state = await getMfaVerifyLockoutState(ctx.redis, userId);
|
|
75
|
+
const state = await getMfaVerifyLockoutState(ctx.services.redis, userId);
|
|
76
76
|
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
77
77
|
const now = Date.now();
|
|
78
78
|
if (state.lockedUntil > now) {
|
|
@@ -82,17 +82,22 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
82
82
|
}
|
|
83
83
|
const secret = base32Decode(verify.payload.totpSecretBase32);
|
|
84
84
|
if (verifyTotp(secret, event.payload.code) === false) {
|
|
85
|
-
await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
|
|
85
|
+
await recordFailedMfaVerifyAttempt(ctx.services.redis, userId, maxAttempts, lockoutMinutes);
|
|
86
86
|
return invalidTotpCode();
|
|
87
87
|
}
|
|
88
88
|
// Burn the setup token on the first successful confirm — same
|
|
89
89
|
// single-use guarantee as enable-confirm.write.ts.
|
|
90
|
-
const burnResult = await burnToken(ctx.redis, "mfa-setup", userId, verify.expiresAtMs);
|
|
90
|
+
const burnResult = await burnToken(ctx.services.redis, "mfa-setup", userId, verify.expiresAtMs);
|
|
91
91
|
if (burnResult === "already-used")
|
|
92
92
|
return invalidSetupToken();
|
|
93
93
|
// "system" mode: no session exists yet, tenantId comes from the
|
|
94
94
|
// verified token, mirroring enable-start-preauth.write.ts.
|
|
95
|
-
const scopedDb = createTenantDb({
|
|
95
|
+
const scopedDb = createTenantDb({
|
|
96
|
+
db: ctx.db.unsafeRaw(),
|
|
97
|
+
tenantId,
|
|
98
|
+
mode: "system",
|
|
99
|
+
reason: "pre-auth MFA setup has no session, the tenant comes from the verified setup token",
|
|
100
|
+
});
|
|
96
101
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
97
102
|
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
98
103
|
if (existing)
|
|
@@ -112,7 +117,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
112
117
|
// existing userId-unique-constraint backstop between the
|
|
113
118
|
// `existing` check above and executor.create() below.
|
|
114
119
|
const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
|
|
115
|
-
const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
120
|
+
const userRow = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
116
121
|
id: userId,
|
|
117
122
|
})); // @cast-boundary engine-payload
|
|
118
123
|
if (!userRow)
|
|
@@ -123,7 +128,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
123
128
|
return invalidSetupToken();
|
|
124
129
|
}
|
|
125
130
|
const globalRoles = parseRoles(userRow?.roles ?? null);
|
|
126
|
-
const active = await ctx.resolveActiveMembership(userId, tenantId);
|
|
131
|
+
const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
|
|
127
132
|
if (active.kind === "rejected")
|
|
128
133
|
return invalidSetupToken();
|
|
129
134
|
const result = await executor.create({
|
|
@@ -135,8 +140,8 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
135
140
|
}, scopedUser, scopedDb);
|
|
136
141
|
if (!result.isSuccess)
|
|
137
142
|
return result;
|
|
138
|
-
// ctx.redis is guaranteed by the fail-closed check above.
|
|
139
|
-
await clearMfaVerifyAttempts(ctx.redis, userId);
|
|
143
|
+
// ctx.services.redis is guaranteed by the fail-closed check above.
|
|
144
|
+
await clearMfaVerifyAttempts(ctx.services.redis, userId);
|
|
140
145
|
// Re-derive the full session the way verify.write.ts does — the
|
|
141
146
|
// setupToken only proves "password + new TOTP secret", not roles;
|
|
142
147
|
// roles come from the membership lookup above, same as every other
|
|
@@ -157,7 +162,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
157
162
|
...sessionTimezoneField(userRow?.timezone),
|
|
158
163
|
...sessionLocaleField(userRow?.locale),
|
|
159
164
|
};
|
|
160
|
-
const claims = await ctx.
|
|
165
|
+
const claims = await ctx.auth.resolveClaims(baseSession);
|
|
161
166
|
const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
162
167
|
// Last step before returning success: every gate above (status,
|
|
163
168
|
// membership, entity-write) has passed, so this is the point of no
|
|
@@ -45,9 +45,9 @@ export function createEnableConfirmHandler(opts) {
|
|
|
45
45
|
// second seeing invalidSetupToken(). Checked before the TOTP verify
|
|
46
46
|
// for the same reason the other two handlers check it early: fail
|
|
47
47
|
// closed before doing any of the guarded work, not partway through.
|
|
48
|
-
if (!ctx.redis) {
|
|
48
|
+
if (!ctx.services.redis) {
|
|
49
49
|
return writeFailure(new InternalError({
|
|
50
|
-
message: "enable-confirm requires ctx.redis for the single-use setup-token burn",
|
|
50
|
+
message: "enable-confirm requires ctx.services.redis for the single-use setup-token burn",
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
53
|
const secret = base32Decode(verify.payload.totpSecretBase32);
|
|
@@ -65,7 +65,7 @@ export function createEnableConfirmHandler(opts) {
|
|
|
65
65
|
// concurrent mfa_already_enabled() write) is meant to leave the burn
|
|
66
66
|
// standing. Add unburnToken here if create-failure retries turn out
|
|
67
67
|
// to matter.
|
|
68
|
-
const burnResult = await burnToken(ctx.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
|
|
68
|
+
const burnResult = await burnToken(ctx.services.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
|
|
69
69
|
if (burnResult === "already-used")
|
|
70
70
|
return invalidSetupToken();
|
|
71
71
|
const existing = await findUserMfaRow(ctx.db, event.user);
|
|
@@ -44,7 +44,12 @@ export function createEnableStartPreauthHandler(opts) {
|
|
|
44
44
|
// "system" mode: the guest dispatch identity's own tenantId is
|
|
45
45
|
// meaningless here — the preauthSetupToken is the source of truth
|
|
46
46
|
// for which tenant's row to read, mirroring verify.write.ts.
|
|
47
|
-
const scopedDb = createTenantDb({
|
|
47
|
+
const scopedDb = createTenantDb({
|
|
48
|
+
db: ctx.db.unsafeRaw(),
|
|
49
|
+
tenantId,
|
|
50
|
+
mode: "system",
|
|
51
|
+
reason: "pre-auth MFA setup has no session, the tenant comes from the verified setup token",
|
|
52
|
+
});
|
|
48
53
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
49
54
|
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
50
55
|
if (existing)
|