@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.
- 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 +12 -7
- package/dist/agent-tools/tool-dispatch.js +42 -31
- package/dist/audit/feature.js +1 -0
- package/dist/audit/handlers/audit-events.js +1 -1
- 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/signup-request.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.js +4 -0
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +5 -0
- 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/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-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +3 -7
- package/dist/auth-mfa/feature-exports.d.ts +2 -2
- package/dist/auth-mfa/feature.js +8 -0
- package/dist/auth-mfa/mfa-status-checker.js +5 -3
- package/dist/auth-mfa/web/i18n.js +5 -1
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
- package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
- package/dist/billing-foundation/checkout-core.d.ts +9 -14
- package/dist/billing-foundation/checkout-core.js +24 -39
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
- package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
- package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +1 -0
- package/dist/billing-foundation/feature.js +3 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
- package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
- package/dist/billing-foundation/handlers/process-event.write.js +8 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
- package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
- package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
- package/dist/billing-foundation/i18n.js +22 -0
- package/dist/billing-foundation/index.d.ts +1 -1
- package/dist/billing-foundation/plan-catalog.d.ts +21 -5
- package/dist/billing-foundation/plan-catalog.js +25 -16
- package/dist/billing-foundation/plan-switch.d.ts +12 -0
- package/dist/billing-foundation/plan-switch.js +60 -0
- package/dist/billing-foundation/types.d.ts +57 -1
- package/dist/billing-foundation/types.js +9 -0
- package/dist/billing-foundation/validate-options.js +17 -0
- package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
- package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
- package/dist/billing-foundation/web/constants.d.ts +4 -0
- package/dist/billing-foundation/web/constants.js +4 -0
- package/dist/billing-foundation/web/plan-price.d.ts +15 -0
- package/dist/billing-foundation/web/plan-price.js +32 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
- package/dist/cap-counter/cap-reservation.js +20 -7
- package/dist/cap-counter/enforce-cap.js +2 -1
- package/dist/cap-counter/feature.js +1 -0
- package/dist/cap-counter/stock-cap-guard.js +7 -1
- package/dist/cap-overview/feature.js +1 -0
- package/dist/channel-email/feature.js +1 -1
- package/dist/config/handlers/cascade.query.d.ts +5 -0
- package/dist/config/handlers/cascade.query.js +4 -3
- package/dist/config/handlers/set.write.js +12 -2
- package/dist/config/handlers/values.query.d.ts +5 -0
- package/dist/config/handlers/values.query.js +4 -3
- package/dist/config/write-helpers.d.ts +1 -0
- package/dist/config/write-helpers.js +18 -9
- package/dist/crypto-shredding/handlers/forget-subject.write.js +14 -11
- package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
- package/dist/delivery/channel-context.js +6 -3
- package/dist/delivery/db/queries/preferences.js +2 -2
- package/dist/delivery/delivery-service.js +22 -7
- package/dist/delivery/feature.js +1 -0
- package/dist/delivery/handlers/preferences.query.d.ts +1 -1
- package/dist/delivery/handlers/preferences.query.js +20 -1
- package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/system-channel-context.d.ts +15 -0
- package/dist/delivery/system-channel-context.js +5 -0
- package/dist/delivery/unsubscribe-link.d.ts +3 -1
- package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
- package/dist/delivery/unsubscribe-page-strings.js +1 -0
- package/dist/delivery/unsubscribe-page.d.ts +2 -0
- package/dist/delivery/unsubscribe-page.js +5 -0
- package/dist/delivery/unsubscribe.js +25 -7
- 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 +11 -0
- 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/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
- package/dist/file-foundation/read-file-ref-content.js +6 -1
- package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
- package/dist/form-draft/db/queries/cleanup.js +2 -2
- package/dist/form-draft/decrypt-draft.js +5 -1
- package/dist/host-routing/dispatch.js +11 -20
- package/dist/host-routing/validate-options.js +3 -0
- package/dist/inbound-mail-foundation/feature.js +1 -0
- package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
- package/dist/inbound-mail-foundation/i18n.js +0 -1
- package/dist/inbound-mail-foundation/screens.js +0 -3
- package/dist/inbound-mail-foundation/types.d.ts +4 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
- package/dist/jobs/feature.d.ts +1 -0
- package/dist/jobs/feature.js +9 -4
- package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +45 -41
- package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +54 -50
- package/dist/jobs/job-run-logger.js +5 -4
- package/dist/ledger/web/index.js +1 -1
- package/dist/legal-pages/seed.d.ts +3 -1
- package/dist/legal-pages/seed.js +2 -2
- package/dist/mail-foundation/feature.d.ts +3 -3
- package/dist/mail-foundation/feature.js +3 -0
- package/dist/mail-transport-smtp/config-keys.js +1 -1
- package/dist/mail-transport-smtp/feature.js +6 -5
- 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/notes-history/handlers/add-note.write.js +1 -1
- package/dist/personal-access-tokens/feature.d.ts +1 -1
- package/dist/personal-access-tokens/feature.js +5 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
- package/dist/renderer-pdf/render-table-pdf.js +22 -10
- package/dist/secrets/write-gate.js +8 -2
- package/dist/sessions/feature.d.ts +2 -2
- package/dist/sessions/feature.js +5 -0
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.js +1 -1
- package/dist/sessions/session-callbacks.d.ts +0 -2
- package/dist/sessions/session-callbacks.js +1 -3
- package/dist/shared/decrypt-stored-pii.d.ts +1 -0
- package/dist/shared/decrypt-stored-pii.js +6 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/mail-template-body.d.ts +6 -2
- package/dist/shared/mail-template-body.js +7 -4
- package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
- package/dist/shared/purge-files-reaching-subject.js +19 -2
- package/dist/shared/run-in-sub-transaction.js +5 -5
- package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
- package/dist/step-dispatcher/feature.js +1 -0
- package/dist/step-dispatcher/mail-runner.d.ts +1 -0
- package/dist/subscription-dodo/client.d.ts +17 -0
- package/dist/subscription-dodo/client.js +31 -0
- package/dist/subscription-dodo/constants.d.ts +12 -0
- package/dist/subscription-dodo/constants.js +19 -0
- package/dist/subscription-dodo/feature.d.ts +37 -0
- package/dist/subscription-dodo/feature.js +144 -0
- package/dist/subscription-dodo/index.d.ts +2 -0
- package/dist/subscription-dodo/index.js +4 -0
- package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
- package/dist/subscription-dodo/plugin-methods.js +260 -0
- package/dist/subscription-dodo/runtime.d.ts +27 -0
- package/dist/subscription-dodo/runtime.js +89 -0
- package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
- package/dist/subscription-dodo/verify-webhook.js +189 -0
- package/dist/subscription-stripe/feature.js +1 -0
- package/dist/subscription-stripe/runtime.js +7 -6
- package/dist/tags/feature.js +1 -0
- package/dist/template-resolver/api.d.ts +3 -2
- package/dist/template-resolver/api.js +24 -2
- package/dist/template-resolver/compile-document-body.js +1 -0
- 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 +4 -4
- package/dist/template-resolver/handlers/bundle-import.js +3 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
- package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
- package/dist/template-resolver/handlers/document-base.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
- 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/list.query.js +5 -6
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +21 -10
- package/dist/template-resolver/handlers/patch-draft.write.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/tenant-override-counts.query.js +1 -3
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
- package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
- package/dist/template-resolver/handlers/usage.query.js +26 -16
- package/dist/template-resolver/handlers/validate.query.js +7 -2
- package/dist/template-resolver/seeding.d.ts +1 -0
- package/dist/template-resolver/seeding.js +31 -12
- package/dist/template-resolver/store-template-resource.d.ts +1 -0
- package/dist/template-resolver/store-template-resource.js +65 -10
- package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
- package/dist/template-resolver/template-delivery-policy.js +0 -6
- package/dist/template-resolver/template-meta.js +7 -4
- package/dist/template-resolver/template-preview.js +4 -3
- package/dist/template-resolver/web/constants.d.ts +2 -2
- package/dist/template-resolver/web/constants.js +3 -3
- package/dist/template-resolver/web/i18n.js +3 -0
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
- package/dist/template-resolver/web/mail-layout-constants.js +7 -0
- package/dist/template-resolver/web/qualified-names.d.ts +1 -0
- package/dist/template-resolver/web/qualified-names.js +1 -0
- package/dist/tenant/feature.js +1 -0
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +10 -3
- package/dist/tenant/handlers/team-list.query.js +3 -3
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +8 -4
- package/dist/tenant-lifecycle/constants.d.ts +1 -0
- package/dist/tenant-lifecycle/constants.js +4 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tenant-lifecycle/stages.js +22 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
- package/dist/tier-engine/web/constants.js +2 -2
- package/dist/user/feature.js +1 -0
- package/dist/user-data-rights/feature.js +1 -0
- package/dist/user-data-rights/index.d.ts +1 -1
- package/dist/user-data-rights/index.js +1 -1
- package/dist/user-data-rights/run-export-jobs.js +3 -4
- package/dist/user-data-rights/run-forget-cleanup.js +2 -1
- package/dist/user-data-rights/web/constants.d.ts +6 -0
- package/dist/user-data-rights/web/constants.js +6 -0
- package/dist/user-data-rights/web/index.d.ts +1 -0
- package/dist/user-data-rights/web/index.js +1 -0
- package/dist/waitlist/feature.js +1 -0
- package/dist/waitlist/handlers/export-purge.write.js +2 -1
- package/dist/waitlist/handlers/submit.write.js +14 -10
- package/dist/waitlist/import.d.ts +3 -1
- package/dist/waitlist/import.js +26 -2
- package/dist/waitlist/legacy-migrate.js +18 -7
- package/dist/waitlist/link-on-signup.js +18 -6
- package/dist/waitlist/options.d.ts +5 -1
- package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
- package/package.json +19 -9
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +12 -0
- package/src/auth-email-password/changes.json +12 -0
- package/src/auth-mfa/changes.json +12 -0
- package/src/billing-foundation/changes.json +55 -0
- package/src/cap-counter/changes.json +30 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +18 -0
- package/src/crypto-shredding/changes.json +18 -0
- package/src/delivery/changes.json +42 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/form-draft/changes.json +6 -0
- package/src/host-routing/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +18 -0
- package/src/jobs/changes.json +13 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/mail-transport-smtp/changes.json +6 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-pdf/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/sessions/changes.json +7 -0
- package/src/step-dispatcher/changes.json +12 -0
- package/src/subscription-dodo/changes.json +14 -0
- package/src/subscription-stripe/changes.json +12 -0
- package/src/template-resolver/changes.json +84 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +12 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/user-data-rights/changes.json +12 -0
- package/src/waitlist/changes.json +36 -0
- package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
- 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:
|
|
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);
|
|
@@ -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",
|
|
@@ -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;
|
package/dist/auth-mfa/feature.js
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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: "
|
|
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,
|
|
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
|
-
|
|
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")) }),
|
|
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
|
}
|