@cosmicdrift/kumiko-bundled-features 1.0.0 → 2.0.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/package.json +41 -7
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +279 -0
- package/src/__tests__/access-invalidation-stream.integration.test.ts +217 -0
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +200 -0
- package/src/admin-shell/__tests__/admin-shell.boot.test.ts +147 -0
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +43 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +35 -0
- package/src/admin-shell/constants.ts +11 -0
- package/src/admin-shell/feature.ts +135 -0
- package/src/admin-shell/i18n.ts +36 -0
- package/src/admin-shell/index.ts +17 -0
- package/src/admin-shell/overview-allowlist.ts +37 -0
- package/src/admin-shell/web/client-plugin.tsx +26 -0
- package/src/admin-shell/web/i18n.ts +16 -0
- package/src/admin-shell/web/index.ts +2 -0
- package/src/admin-shell/web/overview-layout.tsx +69 -0
- package/src/admin-shell/web/overview-query.ts +15 -0
- package/src/admin-shell/web/platform-overview-screen.tsx +90 -0
- package/src/admin-shell/web/tenant-overview-screen.tsx +90 -0
- package/src/agent-tools/__tests__/tool-catalog.test.ts +179 -0
- package/src/agent-tools/__tests__/tool-dispatch.integration.test.ts +100 -0
- package/src/agent-tools/__tests__/tool-dispatch.test.ts +126 -0
- package/src/agent-tools/index.ts +9 -0
- package/src/agent-tools/tool-catalog.ts +130 -0
- package/src/agent-tools/tool-dispatch.ts +82 -0
- package/src/agent-tools/types.ts +38 -0
- package/src/audit/__tests__/audit-screens.boot.test.ts +51 -0
- package/src/audit/__tests__/audit-security.integration.test.ts +201 -0
- package/src/audit/__tests__/audit.integration.test.ts +50 -25
- package/src/audit/constants.ts +9 -2
- package/src/audit/feature.ts +33 -1
- package/src/audit/handlers/details.query.ts +47 -0
- package/src/audit/handlers/list.query.ts +3 -2
- package/src/audit/i18n.ts +31 -0
- package/src/audit/web/audit-log-detail-screen.tsx +131 -0
- package/src/audit/web/audit-log-screen.tsx +250 -0
- package/src/audit/web/client-plugin.tsx +22 -0
- package/src/audit/web/i18n.ts +12 -0
- package/src/audit/web/index.ts +5 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +4 -4
- package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +4 -4
- package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +10 -11
- package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +33 -6
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +9 -6
- package/src/auth-email-password/__tests__/feature-options.test.ts +33 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +35 -1
- package/src/auth-email-password/__tests__/login-mfa-setup-required.integration.test.ts +129 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +19 -6
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +264 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +20 -4
- package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +91 -0
- package/src/auth-email-password/constants.ts +25 -0
- package/src/auth-email-password/email-templates.ts +32 -0
- package/src/auth-email-password/errors.ts +9 -0
- package/src/auth-email-password/feature.ts +59 -11
- package/src/auth-email-password/handlers/change-password.write.ts +14 -13
- package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +34 -105
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +12 -5
- package/src/auth-email-password/handlers/invite-accept.write.ts +9 -2
- package/src/auth-email-password/handlers/invite-create.write.ts +2 -2
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +12 -4
- package/src/auth-email-password/handlers/login.write.ts +86 -16
- package/src/auth-email-password/handlers/logout.write.ts +4 -2
- package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +3 -2
- package/src/auth-email-password/handlers/self-registration-status.query.ts +23 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +19 -11
- package/src/auth-email-password/handlers/signup-request.write.ts +22 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +4 -2
- package/src/auth-email-password/i18n.ts +52 -0
- package/src/auth-email-password/index.ts +13 -2
- package/src/auth-email-password/lockout-store.ts +10 -5
- package/src/auth-email-password/password-policy.ts +2108 -0
- package/src/auth-email-password/seeding.ts +55 -27
- package/src/auth-email-password/self-registration-toggle.ts +25 -0
- package/src/auth-email-password/signed-token.ts +1 -0
- package/src/auth-email-password/unlock-token.ts +28 -0
- package/src/auth-email-password/web/__tests__/auth-client.test.ts +542 -0
- package/src/auth-email-password/web/__tests__/auth-form-logic.test.ts +44 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +194 -6
- package/src/auth-email-password/web/__tests__/client-plugin.test.tsx +34 -0
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +64 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +129 -2
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +149 -5
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +137 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +134 -0
- package/src/auth-email-password/web/__tests__/signup-screen.test.tsx +93 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +12 -2
- package/src/auth-email-password/web/__tests__/use-url-token.test.tsx +29 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +5 -1
- package/src/auth-email-password/web/auth-client.ts +96 -18
- package/src/auth-email-password/web/auth-form-logic.ts +30 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +22 -20
- package/src/auth-email-password/web/auth-gate.tsx +140 -19
- package/src/auth-email-password/web/client-plugin.ts +24 -2
- package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +6 -6
- package/src/auth-email-password/web/index.ts +16 -4
- package/src/auth-email-password/web/invite-accept-screen.tsx +5 -5
- package/src/auth-email-password/web/login-screen.tsx +78 -22
- package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +15 -18
- package/src/auth-email-password/web/session.tsx +11 -6
- package/src/auth-email-password/web/signup-complete-screen.tsx +13 -15
- package/src/auth-email-password/web/signup-screen.tsx +8 -10
- package/src/auth-email-password/web/use-shell-user.ts +5 -2
- package/src/auth-email-password/web/user-menu.tsx +3 -1
- package/src/auth-email-password/web/verify-email-screen.tsx +10 -14
- package/src/auth-foundation/__tests__/feature.test.ts +46 -0
- package/src/auth-foundation/__tests__/session-store.test.ts +77 -0
- package/src/auth-foundation/__tests__/tenant-providers.test.ts +160 -0
- package/src/auth-foundation/__tests__/token-verifier.test.ts +150 -0
- package/src/auth-foundation/boot-checks.ts +150 -0
- package/src/auth-foundation/feature.ts +157 -0
- package/src/auth-foundation/index.ts +37 -0
- package/src/auth-foundation/resolve-anonymous-access.ts +62 -0
- package/src/auth-foundation/types.ts +170 -0
- package/src/auth-mfa/__tests__/base32.test.ts +28 -0
- package/src/auth-mfa/__tests__/disable-and-regenerate.integration.test.ts +265 -0
- package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +321 -0
- package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +322 -0
- package/src/auth-mfa/__tests__/enable-start-preauth.integration.test.ts +237 -0
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +39 -0
- package/src/auth-mfa/__tests__/mfa-preauth-setup-token.test.ts +113 -0
- package/src/auth-mfa/__tests__/no-redis.integration.test.ts +209 -0
- package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +139 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +247 -0
- package/src/auth-mfa/__tests__/session-auto-revoke.integration.test.ts +169 -0
- package/src/auth-mfa/__tests__/totp.test.ts +131 -0
- package/src/auth-mfa/__tests__/verify.integration.test.ts +319 -0
- package/src/auth-mfa/base32.ts +51 -0
- package/src/auth-mfa/config.ts +26 -0
- package/src/auth-mfa/constants.ts +45 -0
- package/src/auth-mfa/db/queries.ts +45 -0
- package/src/auth-mfa/errors.ts +66 -0
- package/src/auth-mfa/feature.ts +170 -0
- package/src/auth-mfa/handlers/disable.write.ts +67 -0
- package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +200 -0
- package/src/auth-mfa/handlers/enable-confirm.write.ts +106 -0
- package/src/auth-mfa/handlers/enable-start-preauth.write.ts +87 -0
- package/src/auth-mfa/handlers/enable-start.write.ts +66 -0
- package/src/auth-mfa/handlers/reencrypt.job.ts +234 -0
- package/src/auth-mfa/handlers/regenerate-recovery.write.ts +77 -0
- package/src/auth-mfa/handlers/status.query.ts +23 -0
- package/src/auth-mfa/handlers/verify.write.ts +179 -0
- package/src/auth-mfa/i18n.ts +12 -0
- package/src/auth-mfa/index.ts +19 -0
- package/src/auth-mfa/mfa-challenge-token.ts +90 -0
- package/src/auth-mfa/mfa-preauth-setup-token.ts +86 -0
- package/src/auth-mfa/mfa-setup-token.ts +96 -0
- package/src/auth-mfa/mfa-status-checker.ts +84 -0
- package/src/auth-mfa/mfa-verify-attempts.ts +90 -0
- package/src/auth-mfa/otpauth-uri.ts +21 -0
- package/src/auth-mfa/recovery-codes.ts +54 -0
- package/src/auth-mfa/schema/user-mfa.ts +60 -0
- package/src/auth-mfa/testing.ts +5 -0
- package/src/auth-mfa/totp.ts +67 -0
- package/src/auth-mfa/verify-factor.ts +58 -0
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +265 -0
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +182 -0
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +379 -0
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +104 -0
- package/src/auth-mfa/web/client-plugin.ts +37 -0
- package/src/auth-mfa/web/i18n.ts +136 -0
- package/src/auth-mfa/web/index.ts +27 -0
- package/src/auth-mfa/web/mfa-client.ts +184 -0
- package/src/auth-mfa/web/mfa-disable-dialog.tsx +69 -0
- package/src/auth-mfa/web/mfa-enable-screen.tsx +228 -0
- package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +53 -0
- package/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +68 -0
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +267 -0
- package/src/auth-mfa/web/mfa-verify-screen.tsx +117 -0
- package/src/auth-mfa/web/qrcode-browser.d.ts +12 -0
- package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +134 -0
- package/src/auth-mfa-user-data/hooks.ts +53 -0
- package/src/auth-mfa-user-data/index.ts +21 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +206 -2
- package/src/billing-foundation/__tests__/subscription-provider-contract.ts +93 -0
- package/src/billing-foundation/entities.ts +28 -4
- package/src/billing-foundation/events.ts +6 -2
- package/src/billing-foundation/feature.ts +9 -1
- package/src/billing-foundation/get-subscription-for-tenant.ts +16 -5
- package/src/billing-foundation/handlers/create-portal-session.write.ts +17 -1
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +15 -1
- package/src/billing-foundation/handlers/process-event.write.ts +39 -3
- package/src/billing-foundation/tenant-destroy-hook.ts +46 -0
- package/src/cap-counter/__tests__/cap-list.boot.test.ts +27 -0
- package/src/cap-counter/__tests__/cap-list.security.integration.test.ts +53 -0
- package/src/cap-counter/__tests__/entity-ddl.test.ts +15 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +8 -5
- package/src/cap-counter/constants.ts +2 -0
- package/src/cap-counter/enforce-cap.ts +15 -18
- package/src/cap-counter/entity.ts +3 -3
- package/src/cap-counter/feature.ts +28 -1
- package/src/cap-counter/i18n.ts +12 -0
- package/src/cap-counter/with-cap-enforcement.ts +2 -2
- package/src/channel-email/__tests__/pii-guard.test.ts +56 -0
- package/src/channel-email/email-channel.ts +2 -1
- package/src/channel-email/index.ts +1 -0
- package/src/channel-email/pii-guard.ts +39 -0
- package/src/compliance-profiles/__tests__/profile-picker.boot.test.ts +28 -0
- package/src/compliance-profiles/__tests__/profile-picker.security.integration.test.ts +91 -0
- package/src/compliance-profiles/constants.ts +15 -0
- package/src/compliance-profiles/feature.ts +22 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +2 -2
- package/src/compliance-profiles/i18n.ts +6 -0
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/web/client-plugin.tsx +22 -0
- package/src/compliance-profiles/web/compliance-profile-screen.tsx +152 -0
- package/src/compliance-profiles/web/i18n.ts +25 -0
- package/src/compliance-profiles/web/index.ts +6 -0
- package/src/config/__tests__/app-overrides.test.ts +11 -0
- package/src/config/__tests__/backing-secrets.integration.test.ts +22 -0
- package/src/config/__tests__/cascade.integration.test.ts +35 -0
- package/src/config/__tests__/config.integration.test.ts +71 -10
- package/src/config/__tests__/deserialize-value.test.ts +5 -8
- package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
- package/src/config/__tests__/pii-encrypted.integration.test.ts +179 -0
- package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +234 -0
- package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +243 -0
- package/src/config/__tests__/write-helpers.test.ts +23 -0
- package/src/config/db/queries/resolver.ts +3 -3
- package/src/config/feature.ts +15 -8
- package/src/config/handlers/cascade.query.ts +2 -1
- package/src/config/handlers/reencrypt.job.ts +209 -0
- package/src/config/handlers/set.write.ts +55 -3
- package/src/config/handlers/values.query.ts +2 -1
- package/src/config/i18n.ts +9 -0
- package/src/config/index.ts +5 -4
- package/src/config/resolver.ts +75 -61
- package/src/config/table.ts +1 -1
- package/src/config/write-helpers.ts +22 -0
- package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +148 -0
- package/src/crypto-shredding/constants.ts +5 -0
- package/src/crypto-shredding/feature.ts +18 -0
- package/src/crypto-shredding/handlers/forget-subject.write.ts +91 -0
- package/src/crypto-shredding/index.ts +11 -0
- package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +92 -53
- package/src/custom-fields/__tests__/field-access.integration.test.ts +25 -0
- package/src/custom-fields/__tests__/parse-serialized-field.test.ts +11 -3
- package/src/custom-fields/__tests__/retention.integration.test.ts +40 -0
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +26 -79
- package/src/custom-fields/constants.ts +1 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +0 -20
- package/src/custom-fields/entity.ts +1 -1
- package/src/custom-fields/events.ts +4 -3
- package/src/custom-fields/feature.ts +8 -0
- package/src/custom-fields/handlers/delete-system-field.write.ts +2 -1
- package/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -1
- package/src/custom-fields/handlers/set-custom-field.write.ts +3 -44
- package/src/custom-fields/handlers/update-tenant-field.write.ts +0 -17
- package/src/custom-fields/lib/field-access.ts +8 -1
- package/src/custom-fields/lib/parse-serialized-field.ts +12 -4
- package/src/custom-fields/lib/value-schema.ts +1 -1
- package/src/custom-fields/run-retention.ts +9 -1
- package/src/custom-fields/schemas.ts +9 -9
- package/src/custom-fields/wire-for-entity.ts +110 -78
- package/src/custom-fields/wire-user-data-rights.ts +5 -54
- package/src/data-retention/__tests__/parse-override.test.ts +14 -13
- package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +145 -8
- package/src/data-retention/feature.ts +3 -3
- package/src/data-retention/run-retention-cleanup.ts +102 -19
- package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +171 -0
- package/src/delivery/__tests__/delivery-screens.boot.test.ts +27 -0
- package/src/delivery/__tests__/delivery-security.integration.test.ts +106 -0
- package/src/delivery/constants.ts +13 -51
- package/src/delivery/feature.ts +29 -5
- package/src/delivery/handlers/log.query.ts +28 -7
- package/src/delivery/i18n.ts +6 -0
- package/src/delivery/index.ts +6 -1
- package/src/delivery/public-names.ts +36 -0
- package/src/delivery/tables.ts +3 -3
- package/src/delivery/web/client-plugin.tsx +20 -0
- package/src/delivery/web/delivery-log-screen.tsx +103 -0
- package/src/delivery/web/i18n.ts +25 -0
- package/src/delivery/web/index.ts +3 -0
- package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +147 -0
- package/src/document-ingest-foundation/__tests__/feature.test.ts +105 -0
- package/src/document-ingest-foundation/__tests__/pages.test.ts +37 -0
- package/src/document-ingest-foundation/entity.ts +43 -0
- package/src/document-ingest-foundation/events.ts +25 -0
- package/src/document-ingest-foundation/feature.ts +126 -0
- package/src/document-ingest-foundation/index.ts +12 -0
- package/src/document-ingest-foundation/pages.ts +34 -0
- package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +304 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +7 -6
- package/src/feature-toggles/__tests__/toggle-admin.boot.test.ts +31 -0
- package/src/feature-toggles/__tests__/toggle-admin.security.integration.test.ts +69 -0
- package/src/feature-toggles/compose-tier-resolver.ts +134 -0
- package/src/feature-toggles/constants.ts +14 -0
- package/src/feature-toggles/feature.ts +23 -3
- package/src/feature-toggles/global-feature-state-table.ts +20 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +1 -1
- package/src/feature-toggles/i18n.ts +6 -0
- package/src/feature-toggles/index.ts +8 -0
- package/src/feature-toggles/toggle-runtime.ts +14 -0
- package/src/feature-toggles/web/client-plugin.tsx +22 -0
- package/src/feature-toggles/web/i18n.ts +29 -0
- package/src/feature-toggles/web/index.ts +6 -0
- package/src/feature-toggles/web/toggle-admin-screen.tsx +136 -0
- package/src/file-foundation/__tests__/feature.test.ts +5 -5
- package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
- package/src/file-foundation/feature.ts +10 -11
- package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +27 -2
- package/src/files-provider-s3/s3-provider.ts +12 -1
- package/src/folders/__tests__/folders.integration.test.ts +3 -0
- package/src/folders/web/__tests__/folder-manager.test.tsx +43 -2
- package/src/folders/web/__tests__/tree.test.ts +8 -0
- package/src/folders/web/folder-manager.tsx +2 -3
- package/src/folders/web/i18n.ts +0 -2
- package/src/folders/web/tree.ts +13 -2
- package/src/folders-user-data/__tests__/hooks.integration.test.ts +1 -0
- package/src/folders-user-data/index.ts +4 -4
- package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
- package/src/inbound-mail-foundation/__tests__/connect-routes.integration.test.ts +307 -0
- package/src/inbound-mail-foundation/__tests__/feature.test.ts +169 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +644 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +111 -0
- package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +237 -0
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +433 -0
- package/src/inbound-mail-foundation/aggregate-id.ts +40 -0
- package/src/inbound-mail-foundation/connect-routes.ts +230 -0
- package/src/inbound-mail-foundation/constants.ts +80 -0
- package/src/inbound-mail-foundation/db/queries/inbound-projections.ts +38 -0
- package/src/inbound-mail-foundation/entities.ts +172 -0
- package/src/inbound-mail-foundation/events.ts +147 -0
- package/src/inbound-mail-foundation/feature.ts +243 -0
- package/src/inbound-mail-foundation/handlers/account-state.ts +23 -0
- package/src/inbound-mail-foundation/handlers/connect-account.write.ts +112 -0
- package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +69 -0
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +333 -0
- package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +44 -0
- package/src/inbound-mail-foundation/handlers/list-messages.query.ts +58 -0
- package/src/inbound-mail-foundation/handlers/scope-visibility.ts +14 -0
- package/src/inbound-mail-foundation/handlers/update-account.write.ts +78 -0
- package/src/inbound-mail-foundation/index.ts +97 -0
- package/src/inbound-mail-foundation/oauth-state.ts +103 -0
- package/src/inbound-mail-foundation/projection.ts +152 -0
- package/src/inbound-mail-foundation/provider-factory.ts +53 -0
- package/src/inbound-mail-foundation/retention-sweep.ts +168 -0
- package/src/inbound-mail-foundation/tenant-destroy-hook.ts +93 -0
- package/src/inbound-mail-foundation/types.ts +250 -0
- package/src/inbound-mail-foundation/watch-supervisor.ts +482 -0
- package/src/inbound-provider-imap/__tests__/feature.test.ts +115 -0
- package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +302 -0
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +206 -0
- package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +358 -0
- package/src/inbound-provider-imap/credential-document.ts +51 -0
- package/src/inbound-provider-imap/feature.ts +283 -0
- package/src/inbound-provider-imap/imap-client.ts +158 -0
- package/src/inbound-provider-imap/index.ts +17 -0
- package/src/inbound-provider-inmemory/__tests__/feature.test.ts +52 -0
- package/src/inbound-provider-inmemory/feature.ts +154 -0
- package/src/inbound-provider-inmemory/index.ts +12 -0
- package/src/jobs/__tests__/entity-ddl.test.ts +15 -0
- package/src/jobs/__tests__/job-instant.test.ts +15 -0
- package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
- package/src/jobs/__tests__/jobs-events.integration.test.ts +37 -1
- package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +120 -0
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +36 -0
- package/src/jobs/__tests__/jobs-security.integration.test.ts +108 -0
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +169 -0
- package/src/jobs/constants.ts +8 -1
- package/src/jobs/feature.ts +52 -8
- package/src/jobs/handlers/detail.query.ts +6 -0
- package/src/jobs/handlers/list.query.ts +8 -1
- package/src/jobs/handlers/projection-rebuild.job.ts +14 -4
- package/src/jobs/handlers/reindex-entity.job.ts +49 -0
- package/src/jobs/i18n.ts +37 -0
- package/src/jobs/job-instant.ts +9 -0
- package/src/jobs/job-run-table.ts +4 -4
- package/src/jobs/web/client-plugin.tsx +22 -0
- package/src/jobs/web/i18n.ts +12 -0
- package/src/jobs/web/index.ts +4 -0
- package/src/jobs/web/job-run-detail-screen.tsx +180 -0
- package/src/jobs/web/job-runs-screen.tsx +146 -0
- package/src/ledger/__tests__/ledger.integration.test.ts +8 -3
- package/src/ledger/handlers/confirm-schedule-period.write.ts +17 -4
- package/src/ledger/recurring.ts +17 -8
- package/src/ledger/web/index.ts +3 -2
- package/src/legal-pages/__tests__/legal-pages.integration.test.ts +55 -0
- package/src/legal-pages/feature.ts +24 -31
- package/src/legal-pages/index.ts +1 -0
- package/src/mail-foundation/__tests__/feature.test.ts +5 -6
- package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
- package/src/mail-foundation/__tests__/mail-transport-contract.ts +58 -0
- package/src/mail-foundation/feature.ts +26 -24
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +23 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +65 -0
- package/src/managed-pages/__tests__/branding-coerce.test.ts +2 -6
- package/src/managed-pages/__tests__/i18n-surface.boot.test.ts +30 -0
- package/src/managed-pages/__tests__/managed-pages.integration.test.ts +137 -0
- package/src/managed-pages/feature.ts +45 -47
- package/src/managed-pages/handlers/branding.query.ts +5 -1
- package/src/managed-pages/handlers/by-tenant-published.query.ts +52 -0
- package/src/managed-pages/i18n.ts +66 -0
- package/src/managed-pages/screens/page-screens.ts +5 -1
- package/src/managed-pages/web/client-plugin.tsx +22 -0
- package/src/managed-pages/web/i18n.ts +24 -0
- package/src/managed-pages/web/index.ts +3 -0
- package/src/page-render/__tests__/layout.test.ts +63 -0
- package/src/page-render/__tests__/markdown.test.ts +29 -0
- package/src/page-render/branding.ts +4 -3
- package/src/page-render/index.ts +1 -1
- package/src/page-render/layout.ts +18 -3
- package/src/page-render/markdown.ts +1 -9
- package/src/page-render/web.ts +13 -0
- package/src/personal-access-tokens/__tests__/feature.test.ts +42 -0
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +239 -0
- package/src/personal-access-tokens/__tests__/scopes.test.ts +42 -0
- package/src/personal-access-tokens/constants.ts +33 -0
- package/src/personal-access-tokens/feature.ts +105 -0
- package/src/personal-access-tokens/handlers/available-scopes.query.ts +21 -0
- package/src/personal-access-tokens/handlers/create.write.ts +47 -0
- package/src/personal-access-tokens/handlers/list.query.ts +50 -0
- package/src/personal-access-tokens/handlers/revoke.write.ts +37 -0
- package/src/personal-access-tokens/hash.ts +17 -0
- package/src/personal-access-tokens/i18n.ts +5 -0
- package/src/personal-access-tokens/index.ts +31 -0
- package/src/personal-access-tokens/resolver.ts +58 -0
- package/src/personal-access-tokens/roles.ts +29 -0
- package/src/personal-access-tokens/schema/api-token.ts +63 -0
- package/src/personal-access-tokens/scopes.ts +43 -0
- package/src/personal-access-tokens/web/client-plugin.tsx +29 -0
- package/src/personal-access-tokens/web/i18n.ts +69 -0
- package/src/personal-access-tokens/web/index.ts +7 -0
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +265 -0
- package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
- package/src/renderer-simple/__tests__/resolve-variables.test.ts +37 -0
- package/src/renderer-simple/resolve-variables.ts +3 -1
- package/src/secrets/__tests__/rotate.integration.test.ts +115 -18
- package/src/secrets/handlers/rotate.job.ts +77 -98
- package/src/secrets/secrets-context.ts +7 -38
- package/src/secrets/table.ts +7 -8
- package/src/seo/__tests__/llms-txt.test.ts +38 -0
- package/src/seo/__tests__/robots-txt.test.ts +18 -0
- package/src/seo/__tests__/schema-builders.test.ts +57 -0
- package/src/seo/__tests__/seo.integration.test.ts +350 -0
- package/src/seo/__tests__/sitemap.test.ts +38 -0
- package/src/seo/constants.ts +60 -0
- package/src/seo/feature.ts +316 -0
- package/src/seo/handlers/seo-config.query.ts +35 -0
- package/src/seo/index.ts +19 -0
- package/src/seo/llms-txt.ts +33 -0
- package/src/seo/robots-txt.ts +13 -0
- package/src/seo/schema-builders.ts +57 -0
- package/src/seo/sitemap.ts +33 -0
- package/src/sessions/__tests__/auto-revoke-binding.test.ts +71 -0
- package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
- package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +11 -7
- package/src/sessions/__tests__/rebuild-survival.integration.test.ts +18 -7
- package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +55 -0
- package/src/sessions/__tests__/sessions.integration.test.ts +272 -20
- package/src/sessions/__tests__/test-helpers.ts +16 -3
- package/src/sessions/constants.ts +5 -0
- package/src/sessions/db/queries/cleanup.ts +2 -2
- package/src/sessions/feature.ts +175 -26
- package/src/sessions/handlers/detail.query.ts +36 -0
- package/src/sessions/handlers/list.query.ts +14 -9
- package/src/sessions/handlers/mine.query.ts +13 -8
- package/src/sessions/handlers/revoke-all-for-user.write.ts +51 -12
- package/src/sessions/handlers/revoke-all-others.write.ts +19 -1
- package/src/sessions/handlers/revoke.write.ts +13 -0
- package/src/sessions/i18n.ts +23 -0
- package/src/sessions/index.ts +9 -2
- package/src/sessions/schema/user-session.ts +8 -2
- package/src/sessions/session-callbacks.ts +99 -14
- package/src/sessions/session-checker-fail-open.test.ts +77 -0
- package/src/sessions/session-revoked-event.ts +29 -0
- package/src/sessions/testing.ts +14 -1
- package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
- package/src/shared/chunked-entity-migration.ts +89 -0
- package/src/shared/decrypt-stored-pii.ts +26 -0
- package/src/shared/encrypt-for-direct-write.ts +23 -0
- package/src/shared/entities-of.ts +17 -0
- package/src/shared/grace-period.test.ts +26 -0
- package/src/shared/grace-period.ts +20 -0
- package/src/{auth-email-password/__tests__ → shared}/identity-v3-hash.test.ts +2 -2
- package/src/shared/index.ts +16 -0
- package/src/shared/map-with-concurrency.test.ts +104 -0
- package/src/shared/map-with-concurrency.ts +38 -0
- package/src/shared/password-hashing.test.ts +35 -0
- package/src/{auth-email-password → shared}/password-hashing.ts +11 -0
- package/src/shared/system-query.ts +5 -0
- package/src/{auth-email-password → shared}/token-burn-store.ts +1 -1
- package/src/subscription-mollie/__tests__/feature.test.ts +97 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.test.ts +12 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +21 -0
- package/src/subscription-mollie/verify-webhook.ts +1 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +127 -1
- package/src/subscription-stripe/__tests__/runtime.test.ts +14 -24
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +15 -5
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +20 -0
- package/src/subscription-stripe/feature.ts +2 -0
- package/src/subscription-stripe/verify-webhook.ts +3 -2
- package/src/tags/__tests__/schemas.test.ts +34 -0
- package/src/tags/__tests__/tags.integration.test.ts +12 -1
- package/src/tags/entity.ts +6 -1
- package/src/tags/feature.ts +2 -0
- package/src/tags/i18n.ts +5 -0
- package/src/tags/schemas.ts +9 -2
- package/src/tags/web/__tests__/tag-manager.test.tsx +333 -0
- package/src/tags/web/__tests__/tag-section.test.tsx +47 -1
- package/src/tags/web/tag-manager.tsx +1 -1
- package/src/tags/web/tag-section.tsx +6 -3
- package/src/template-resolver/testing.ts +2 -1
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
- package/src/tenant/__tests__/members-screens.boot.test.ts +51 -0
- package/src/tenant/__tests__/multi-tenant.integration.test.ts +1 -1
- package/src/tenant/__tests__/tenant-security.integration.test.ts +310 -0
- package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
- package/src/tenant/constants.ts +5 -0
- package/src/tenant/feature.ts +19 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +8 -2
- package/src/tenant/handlers/cancel-invitation.write.ts +2 -2
- package/src/tenant/handlers/invitations.query.ts +21 -4
- package/src/tenant/handlers/members.query.ts +41 -6
- package/src/tenant/i18n.ts +28 -0
- package/src/tenant/index.ts +7 -2
- package/src/tenant/invitation-table.ts +3 -2
- package/src/tenant/schema/tenant.ts +27 -2
- package/src/tenant/screens.ts +3 -2
- package/src/tenant/seeding.ts +136 -3
- package/src/tenant/web/client-plugin.tsx +20 -0
- package/src/tenant/web/i18n.ts +43 -0
- package/src/tenant/web/index.ts +4 -0
- package/src/tenant/web/members-screen.tsx +270 -0
- package/src/tenant-lifecycle/__tests__/stages.test.ts +22 -0
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +471 -0
- package/src/tenant-lifecycle/boot-checks.ts +45 -0
- package/src/tenant-lifecycle/constants.ts +56 -0
- package/src/tenant-lifecycle/events.ts +46 -0
- package/src/tenant-lifecycle/feature.ts +112 -0
- package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +79 -0
- package/src/tenant-lifecycle/handlers/request-destruction.write.ts +83 -0
- package/src/tenant-lifecycle/index.ts +7 -0
- package/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +16 -0
- package/src/tenant-lifecycle/lifecycle-gate.ts +65 -0
- package/src/tenant-lifecycle/run-tenant-destroy.ts +368 -0
- package/src/tenant-lifecycle/stages.ts +206 -0
- package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +115 -0
- package/src/tenant-settings/config.ts +44 -0
- package/src/tenant-settings/constants.ts +16 -0
- package/src/tenant-settings/feature.ts +49 -0
- package/src/tenant-settings/index.ts +8 -0
- package/src/tenant-settings/tenant-defaults.ts +67 -0
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +33 -6
- package/src/text-content/web/__tests__/group-blocks.test.ts +28 -3
- package/src/text-content/web/client-plugin.tsx +126 -104
- package/src/tier-engine/__tests__/auto-default-tier.integration.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.test.ts +1 -1
- package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
- package/src/tier-engine/__tests__/trial-config-validation.test.ts +30 -0
- package/src/tier-engine/feature.ts +13 -6
- package/src/tier-engine/i18n.ts +8 -0
- package/src/tier-engine/web/tier-admin-screen.tsx +26 -19
- package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +186 -0
- package/src/user/__tests__/user.integration.test.ts +26 -0
- package/src/user/constants.ts +1 -0
- package/src/user/db/queries/stream-tenant-backfill.ts +112 -0
- package/src/user/feature.ts +3 -0
- package/src/user/handlers/find-for-auth.query.ts +8 -6
- package/src/user/i18n.ts +20 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/user.ts +6 -2
- package/src/user/screens.ts +3 -15
- package/src/user/seeding.ts +8 -0
- package/src/user-data-rights/COMPLIANCE.md +21 -1
- package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +145 -0
- package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +67 -0
- package/src/user-data-rights/__tests__/boot-checks.test.ts +170 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +169 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +101 -33
- package/src/user-data-rights/__tests__/email-templates.test.ts +15 -0
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +154 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-retention.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +41 -62
- package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +110 -0
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +14 -0
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +19 -2
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +25 -14
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +172 -19
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +257 -26
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +339 -5
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +77 -1
- package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
- package/src/user-data-rights/boot-checks.ts +70 -0
- package/src/user-data-rights/db/queries/__tests__/forget-cleanup.integration.test.ts +69 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +9 -2
- package/src/user-data-rights/email-templates.ts +6 -6
- package/src/user-data-rights/feature.ts +162 -43
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +8 -11
- package/src/user-data-rights/handlers/deletion-grace-period.ts +4 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +26 -18
- package/src/user-data-rights/handlers/download-by-token.query.ts +23 -15
- package/src/user-data-rights/handlers/lift-restriction.write.ts +44 -19
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -0
- package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +3 -1
- package/src/user-data-rights/handlers/restrict-account.write.ts +58 -7
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +2 -0
- package/src/user-data-rights/i18n.ts +35 -32
- package/src/user-data-rights/lib/is-admin-actor.ts +19 -0
- package/src/user-data-rights/lib/storage-provider-resolver.ts +6 -5
- package/src/user-data-rights/lib/update-user-lifecycle.ts +8 -24
- package/src/user-data-rights/run-export-jobs.ts +55 -19
- package/src/user-data-rights/run-forget-cleanup.ts +112 -7
- package/src/user-data-rights/run-user-export.ts +70 -4
- package/src/user-data-rights/schema/download-token.ts +5 -3
- package/src/user-data-rights/schema/export-job.ts +2 -0
- package/src/user-data-rights/screens.ts +2 -1
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -0
- package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +7 -1
- package/src/user-data-rights/web/client-plugin.tsx +6 -1
- package/src/user-data-rights/web/i18n.ts +30 -10
- package/src/user-data-rights/web/privacy-center-screen.tsx +5 -3
- package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +455 -0
- package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +33 -9
- package/src/user-data-rights-defaults/feature.ts +78 -1
- package/src/user-data-rights-defaults/hooks/api-token.userdata-hook.ts +37 -0
- package/src/user-data-rights-defaults/hooks/config-value.userdata-hook.ts +51 -0
- package/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +37 -0
- package/src/user-data-rights-defaults/hooks/feature-mounted.ts +10 -0
- package/src/user-data-rights-defaults/hooks/in-app-message.userdata-hook.ts +38 -0
- package/src/user-data-rights-defaults/hooks/job-run.userdata-hook.ts +35 -0
- package/src/user-data-rights-defaults/hooks/notification-preference.userdata-hook.ts +54 -0
- package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +112 -0
- package/src/user-data-rights-defaults/hooks/user-session.userdata-hook.ts +42 -0
- package/src/user-data-rights-defaults/index.ts +26 -0
- package/src/user-profile/__tests__/change-email.integration.test.ts +7 -1
- package/src/user-profile/__tests__/profile-screen.test.tsx +5 -0
- package/src/user-profile/feature.ts +4 -1
- package/src/user-profile/handlers/change-email.write.ts +15 -10
- package/src/user-profile/web/profile-screen.tsx +15 -3
- package/src/auth-email-password/stream-tenant.ts +0 -31
- package/src/feature-toggles/db/queries/toggle-state.ts +0 -25
- package/src/legal-pages/security-headers.ts +0 -1
- package/src/sessions/schema/index.ts +0 -5
- package/src/tenant/schema/index.ts +0 -5
- package/src/user/schema/index.ts +0 -5
- /package/src/{auth-email-password → shared}/identity-v3-hash.ts +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { mfaRequiredConfigKey } from "./config";
|
|
3
|
+
import { MFA_ENABLE_SCREEN_ID } from "./constants";
|
|
4
|
+
import { createDisableHandler } from "./handlers/disable.write";
|
|
5
|
+
import { createEnableConfirmHandler } from "./handlers/enable-confirm.write";
|
|
6
|
+
import { createEnableConfirmPreauthHandler } from "./handlers/enable-confirm-preauth.write";
|
|
7
|
+
import { createEnableStartHandler } from "./handlers/enable-start.write";
|
|
8
|
+
import { createEnableStartPreauthHandler } from "./handlers/enable-start-preauth.write";
|
|
9
|
+
import { mfaReencryptJob } from "./handlers/reencrypt.job";
|
|
10
|
+
import { createRegenerateRecoveryHandler } from "./handlers/regenerate-recovery.write";
|
|
11
|
+
import { mfaStatusQuery } from "./handlers/status.query";
|
|
12
|
+
import { createMfaVerifyHandler } from "./handlers/verify.write";
|
|
13
|
+
import { AUTH_MFA_FEATURE_I18N } from "./i18n";
|
|
14
|
+
import { createMfaStatusChecker, type MfaStatusChecker } from "./mfa-status-checker";
|
|
15
|
+
import { userMfaEntity } from "./schema/user-mfa";
|
|
16
|
+
|
|
17
|
+
export type AuthMfaFeatureOptions = {
|
|
18
|
+
// HMAC secret for the stateless enable-flow token (carries the generated
|
|
19
|
+
// TOTP secret + recovery-code hashes between enable.start and
|
|
20
|
+
// enable.confirm). Distinct secret from any other token flow in the app —
|
|
21
|
+
// do not reuse passwordReset.hmacSecret.
|
|
22
|
+
readonly setupTokenSecret: string;
|
|
23
|
+
// otpauth:// URI issuer — shown in the user's authenticator app next to
|
|
24
|
+
// the account label ("Kumiko: jane@example.com").
|
|
25
|
+
readonly issuer: string;
|
|
26
|
+
// HMAC secret for the login-flow challenge token (carries {userId,
|
|
27
|
+
// tenantId} between the password-check and /auth/mfa/verify). Distinct
|
|
28
|
+
// secret from setupTokenSecret — a compromised setup-token secret must
|
|
29
|
+
// not also forge login challenges.
|
|
30
|
+
readonly challengeTokenSecret: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type BindMfaRevokeAllOtherSessions = (
|
|
34
|
+
revoker: (userId: string, currentSid: string | undefined) => Promise<number>,
|
|
35
|
+
) => void;
|
|
36
|
+
|
|
37
|
+
// Reads the late-bind setter off a mounted auth-mfa feature's exports —
|
|
38
|
+
// mirrors sessions' own bindAutoRevokeFromFeature. run{Prod,Dev}App call
|
|
39
|
+
// this once the sessions feature (if mounted) has produced a concrete
|
|
40
|
+
// sessionRevokeAllOthers callback.
|
|
41
|
+
export function bindMfaRevokeAllOtherSessionsFromFeature(
|
|
42
|
+
feature: FeatureDefinition,
|
|
43
|
+
): BindMfaRevokeAllOtherSessions | undefined {
|
|
44
|
+
const exports = feature.exports;
|
|
45
|
+
if (exports && typeof exports === "object" && "bindRevokeAllOtherSessions" in exports) {
|
|
46
|
+
const { bindRevokeAllOtherSessions } = exports as {
|
|
47
|
+
bindRevokeAllOtherSessions: unknown;
|
|
48
|
+
};
|
|
49
|
+
if (typeof bindRevokeAllOtherSessions === "function") {
|
|
50
|
+
// @cast-boundary exports-walk — feature.exports is untyped by design
|
|
51
|
+
return bindRevokeAllOtherSessions as BindMfaRevokeAllOtherSessions;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Reads the eagerly-built `checkMfaStatus` off a mounted auth-mfa feature's
|
|
58
|
+
// exports — no bind-setter needed (see the comment where it's built).
|
|
59
|
+
export function mfaStatusCheckerFromFeature(
|
|
60
|
+
feature: FeatureDefinition,
|
|
61
|
+
): MfaStatusChecker | undefined {
|
|
62
|
+
const exports = feature.exports;
|
|
63
|
+
if (exports && typeof exports === "object" && "checkMfaStatus" in exports) {
|
|
64
|
+
const { checkMfaStatus } = exports as { checkMfaStatus: unknown };
|
|
65
|
+
if (typeof checkMfaStatus === "function") {
|
|
66
|
+
// @cast-boundary exports-walk — feature.exports is untyped by design
|
|
67
|
+
return checkMfaStatus as MfaStatusChecker;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createAuthMfaFeature(opts: AuthMfaFeatureOptions): FeatureDefinition {
|
|
74
|
+
return defineFeature("auth-mfa", (r) => {
|
|
75
|
+
r.describe(
|
|
76
|
+
"TOTP-based two-factor authentication: enable/disable flow with QR-code setup, 8 single-use recovery codes, and (once wired into a login flow) a second login step after password verification. Secrets are envelope-encrypted at rest via the same MasterKeyProvider as `secrets`/`config`.",
|
|
77
|
+
);
|
|
78
|
+
r.uiHints({
|
|
79
|
+
displayLabel: "2FA / TOTP",
|
|
80
|
+
category: "identity",
|
|
81
|
+
recommended: true,
|
|
82
|
+
});
|
|
83
|
+
r.requires("user");
|
|
84
|
+
r.requires("config");
|
|
85
|
+
// verify.write.ts dispatches "tenant:query:memberships" — without this
|
|
86
|
+
// the app boots clean and the first MFA login dies at runtime on an
|
|
87
|
+
// unknown handler instead of failing at mount time.
|
|
88
|
+
r.requires("tenant");
|
|
89
|
+
r.config("required", mfaRequiredConfigKey());
|
|
90
|
+
|
|
91
|
+
// Dormant custom-screen — the client maps MFA_ENABLE_SCREEN_ID to
|
|
92
|
+
// MfaEnableScreen (see personal-access-tokens/feature.ts for the same
|
|
93
|
+
// convention). App places it via r.nav in its logged-in settings area.
|
|
94
|
+
r.screen({
|
|
95
|
+
id: MFA_ENABLE_SCREEN_ID,
|
|
96
|
+
type: "custom",
|
|
97
|
+
renderer: { react: { __component: "MfaEnableScreen" } },
|
|
98
|
+
access: { openToAll: true },
|
|
99
|
+
});
|
|
100
|
+
r.translations({ keys: AUTH_MFA_FEATURE_I18N });
|
|
101
|
+
|
|
102
|
+
// KEK-rotation for totpSecret (entity-field encryption). Manual
|
|
103
|
+
// trigger — ops runs it once after adding a new master key version,
|
|
104
|
+
// same operator workflow as config's own reencrypt job.
|
|
105
|
+
r.job("reencrypt", { trigger: { manual: true } }, mfaReencryptJob);
|
|
106
|
+
|
|
107
|
+
r.entity("user-mfa", userMfaEntity);
|
|
108
|
+
|
|
109
|
+
// Late-bound by run-prod-app once the sessions feature (if mounted) has
|
|
110
|
+
// produced a concrete `sessionRevokeAllOthers` callback — mirrors
|
|
111
|
+
// sessions' own bindAutoRevokeOnPasswordChange. Shared by every handler
|
|
112
|
+
// that changes MFA state (enable/disable/regenerate), so each one's
|
|
113
|
+
// "log out every other session" behavior tracks the same wiring.
|
|
114
|
+
let revokeAllOtherSessions:
|
|
115
|
+
| ((userId: string, currentSid: string | undefined) => Promise<number>)
|
|
116
|
+
| undefined;
|
|
117
|
+
const sharedRevoker = (userId: string, currentSid: string | undefined): Promise<number> =>
|
|
118
|
+
revokeAllOtherSessions?.(userId, currentSid) ?? Promise.resolve(0);
|
|
119
|
+
|
|
120
|
+
const handlers = {
|
|
121
|
+
enableStart: r.writeHandler(
|
|
122
|
+
createEnableStartHandler({ setupTokenSecret: opts.setupTokenSecret, issuer: opts.issuer }),
|
|
123
|
+
),
|
|
124
|
+
enableStartPreauth: r.writeHandler(
|
|
125
|
+
createEnableStartPreauthHandler({
|
|
126
|
+
challengeTokenSecret: opts.challengeTokenSecret,
|
|
127
|
+
setupTokenSecret: opts.setupTokenSecret,
|
|
128
|
+
issuer: opts.issuer,
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
enableConfirm: r.writeHandler(
|
|
132
|
+
createEnableConfirmHandler({
|
|
133
|
+
setupTokenSecret: opts.setupTokenSecret,
|
|
134
|
+
revokeAllOtherSessions: sharedRevoker,
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
enableConfirmPreauth: r.writeHandler(
|
|
138
|
+
createEnableConfirmPreauthHandler({
|
|
139
|
+
setupTokenSecret: opts.setupTokenSecret,
|
|
140
|
+
revokeAllOtherSessions: sharedRevoker,
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
disable: r.writeHandler(createDisableHandler({ revokeAllOtherSessions: sharedRevoker })),
|
|
144
|
+
regenerateRecovery: r.writeHandler(
|
|
145
|
+
createRegenerateRecoveryHandler({ revokeAllOtherSessions: sharedRevoker }),
|
|
146
|
+
),
|
|
147
|
+
verify: r.writeHandler(
|
|
148
|
+
createMfaVerifyHandler({ challengeTokenSecret: opts.challengeTokenSecret }),
|
|
149
|
+
),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const queries = {
|
|
153
|
+
status: r.queryHandler(mfaStatusQuery),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// No late-bind needed (unlike sharedRevoker) — this checker only needs
|
|
157
|
+
// the HandlerContext the CALLER already has (login.write.ts runs it
|
|
158
|
+
// from inside its own dispatcher call), not a raw db handle assembled
|
|
159
|
+
// at app-boot time.
|
|
160
|
+
const checkMfaStatus = createMfaStatusChecker({
|
|
161
|
+
challengeTokenSecret: opts.challengeTokenSecret,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const bindRevokeAllOtherSessions: BindMfaRevokeAllOtherSessions = (revoker) => {
|
|
165
|
+
revokeAllOtherSessions = revoker;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return { handlers, queries, bindRevokeAllOtherSessions, checkMfaStatus };
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { findUserMfaRow } from "../db/queries";
|
|
6
|
+
import { invalidTotpCode, mfaNotEnabled } from "../errors";
|
|
7
|
+
import { userMfaEntity, userMfaTable } from "../schema/user-mfa";
|
|
8
|
+
import { verifyMfaFactor } from "../verify-factor";
|
|
9
|
+
|
|
10
|
+
export type DisableOptions = {
|
|
11
|
+
readonly revokeAllOtherSessions?: (
|
|
12
|
+
userId: string,
|
|
13
|
+
currentSid: string | undefined,
|
|
14
|
+
) => Promise<number>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
|
|
18
|
+
entityName: "user-mfa",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// A TOTP code or a recovery code both prove possession of the second
|
|
22
|
+
// factor — either is accepted to turn MFA back off. Password alone is NOT
|
|
23
|
+
// enough: that would make MFA worthless against exactly the "stolen
|
|
24
|
+
// password" scenario it exists to defend.
|
|
25
|
+
export function createDisableHandler(opts: DisableOptions) {
|
|
26
|
+
return defineWriteHandler({
|
|
27
|
+
name: "disable",
|
|
28
|
+
schema: z.object({ code: z.string().min(6).max(9) }),
|
|
29
|
+
access: { openToAll: true },
|
|
30
|
+
handler: async (event, ctx) => {
|
|
31
|
+
const row = await findUserMfaRow(ctx.db, event.user);
|
|
32
|
+
if (!row) return mfaNotEnabled();
|
|
33
|
+
|
|
34
|
+
// Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
|
|
35
|
+
// (#1467): without ctx.redis, verifyMfaFactor's TOTP-replay burn is
|
|
36
|
+
// skipped, and a code observed once (phishing proxy/shoulder-surfing)
|
|
37
|
+
// could be replayed within the ±1-step window to disable MFA outright.
|
|
38
|
+
// "no ctx.redis means no one gets through MFA login" doesn't cover
|
|
39
|
+
// this route — it's session-authed, and JWTs issued before Redis was
|
|
40
|
+
// unwired stay valid until they expire.
|
|
41
|
+
if (!ctx.redis) {
|
|
42
|
+
return writeFailure(
|
|
43
|
+
new InternalError({
|
|
44
|
+
message: "disable (mfa) requires ctx.redis for TOTP replay protection",
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const replay = { redis: ctx.redis, userId: event.user.id };
|
|
49
|
+
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
50
|
+
if (!verify.ok) return invalidTotpCode();
|
|
51
|
+
|
|
52
|
+
const result = await executor.delete({ id: row.id }, event.user, ctx.db);
|
|
53
|
+
if (!result.isSuccess) return result;
|
|
54
|
+
|
|
55
|
+
// Disabling MFA is a security-relevant state change — same
|
|
56
|
+
// auto-revoke as enable, in case the person doing this isn't the
|
|
57
|
+
// legitimate account owner (attacker with a stolen-but-not-yet-
|
|
58
|
+
// MFA-locked session, or a recovery code obtained via social
|
|
59
|
+
// engineering).
|
|
60
|
+
if (opts.revokeAllOtherSessions) {
|
|
61
|
+
await opts.revokeAllOtherSessions(event.user.id, event.user.sid);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { isSuccess: true, data: { disabled: true } };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
buildSessionRoles,
|
|
4
|
+
createSystemUser,
|
|
5
|
+
defineWriteHandler,
|
|
6
|
+
type SessionUser,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
9
|
+
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
10
|
+
import { Temporal } from "temporal-polyfill";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { burnToken } from "../../shared";
|
|
13
|
+
import { USER_STATUS, UserQueries } from "../../user";
|
|
14
|
+
import { base32Decode } from "../base32";
|
|
15
|
+
import { MFA_VERIFY_LOCKOUT_MINUTES, MFA_VERIFY_MAX_ATTEMPTS } from "../constants";
|
|
16
|
+
import { findUserMfaRow } from "../db/queries";
|
|
17
|
+
import { invalidSetupToken, invalidTotpCode, mfaAlreadyEnabled, tooManyAttempts } from "../errors";
|
|
18
|
+
import { verifyMfaSetupToken } from "../mfa-setup-token";
|
|
19
|
+
import {
|
|
20
|
+
clearMfaVerifyAttempts,
|
|
21
|
+
getMfaVerifyLockoutState,
|
|
22
|
+
recordFailedMfaVerifyAttempt,
|
|
23
|
+
} from "../mfa-verify-attempts";
|
|
24
|
+
import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
|
|
25
|
+
import { verifyTotp } from "../totp";
|
|
26
|
+
|
|
27
|
+
export type EnableConfirmPreauthOptions = {
|
|
28
|
+
// Must match the secret enable-start-preauth.write.ts signs setupTokens
|
|
29
|
+
// with — same secret + format as the session-authed enable-confirm.
|
|
30
|
+
readonly setupTokenSecret: string;
|
|
31
|
+
readonly maxAttempts?: number;
|
|
32
|
+
readonly lockoutMinutes?: number;
|
|
33
|
+
// Wired late by run-prod-app, same shared callback as enable-confirm and
|
|
34
|
+
// enable-start's session-authed siblings.
|
|
35
|
+
readonly revokeAllOtherSessions?: (
|
|
36
|
+
userId: string,
|
|
37
|
+
currentSid: string | undefined,
|
|
38
|
+
) => Promise<number>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
|
|
42
|
+
entityName: "user-mfa",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Pre-auth twin of enable-confirm.write.ts: identity comes from the
|
|
46
|
+
// verified setupToken (minted by enable-start-preauth.write.ts), not from
|
|
47
|
+
// event.user — there is no session yet. Combines enable-confirm's
|
|
48
|
+
// enrollment-completion logic with verify.write.ts's session-derivation
|
|
49
|
+
// (membership lookup, status re-check, buildSessionRoles) since this IS
|
|
50
|
+
// the completion of the blocked login, not just an enrollment step — a
|
|
51
|
+
// successful call here logs the user in.
|
|
52
|
+
export function createEnableConfirmPreauthHandler(opts: EnableConfirmPreauthOptions) {
|
|
53
|
+
const maxAttempts = opts.maxAttempts ?? MFA_VERIFY_MAX_ATTEMPTS;
|
|
54
|
+
const lockoutMinutes = opts.lockoutMinutes ?? MFA_VERIFY_LOCKOUT_MINUTES;
|
|
55
|
+
|
|
56
|
+
return defineWriteHandler({
|
|
57
|
+
name: "enable-confirm-preauth",
|
|
58
|
+
schema: z.object({
|
|
59
|
+
setupToken: z.string().min(1),
|
|
60
|
+
code: z.string().length(6),
|
|
61
|
+
}),
|
|
62
|
+
access: { roles: ["all"] },
|
|
63
|
+
handler: async (event, ctx) => {
|
|
64
|
+
const verify = verifyMfaSetupToken(event.payload.setupToken, opts.setupTokenSecret);
|
|
65
|
+
if (!verify.ok) return invalidSetupToken();
|
|
66
|
+
// A setupToken minted for the session-authed enable-start never
|
|
67
|
+
// carries tenantId (see mfa-setup-token.ts) — reject it here rather
|
|
68
|
+
// than falling back to a guest tenant, that token was never meant to
|
|
69
|
+
// reach this pre-auth endpoint.
|
|
70
|
+
const { userId, tenantId } = verify.payload;
|
|
71
|
+
if (tenantId === undefined) return invalidSetupToken();
|
|
72
|
+
|
|
73
|
+
// Fail closed: unlike verify.write.ts/enable-confirm.write.ts, this
|
|
74
|
+
// endpoint has NO gate before it (access: { roles: ["all"] }, no JWT,
|
|
75
|
+
// no prior challenge-token from a successful login) — its only secret
|
|
76
|
+
// is a 6-digit TOTP code. Silently skipping the brute-force cap and
|
|
77
|
+
// single-use burn when ctx.redis is absent would let the setup token
|
|
78
|
+
// be guessed against unboundedly until its TTL expires. The route's
|
|
79
|
+
// IP rate limiter is not a substitute: it's IP-scoped and resets on
|
|
80
|
+
// every success.
|
|
81
|
+
if (!ctx.redis) {
|
|
82
|
+
return writeFailure(
|
|
83
|
+
new InternalError({
|
|
84
|
+
message:
|
|
85
|
+
"enable-confirm-preauth requires ctx.redis for the brute-force cap and single-use burn",
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Same guessing surface as /auth/mfa/verify (a short TOTP code) and
|
|
91
|
+
// the same per-userId cap — deliberately shared counter, see
|
|
92
|
+
// mfa-verify-attempts.ts.
|
|
93
|
+
const state = await getMfaVerifyLockoutState(ctx.redis, userId);
|
|
94
|
+
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
if (state.lockedUntil > now) {
|
|
97
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((state.lockedUntil - now) / 1000));
|
|
98
|
+
return tooManyAttempts(retryAfterSeconds);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const secret = base32Decode(verify.payload.totpSecretBase32);
|
|
103
|
+
if (verifyTotp(secret, event.payload.code) === false) {
|
|
104
|
+
await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
|
|
105
|
+
return invalidTotpCode();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Burn the setup token on the first successful confirm — same
|
|
109
|
+
// single-use guarantee as enable-confirm.write.ts.
|
|
110
|
+
const burnResult = await burnToken(ctx.redis, "mfa-setup", userId, verify.expiresAtMs);
|
|
111
|
+
if (burnResult === "already-used") return invalidSetupToken();
|
|
112
|
+
|
|
113
|
+
// "system" mode: no session exists yet, tenantId comes from the
|
|
114
|
+
// verified token, mirroring enable-start-preauth.write.ts.
|
|
115
|
+
const scopedDb = createTenantDb(ctx.db.raw, tenantId, "system");
|
|
116
|
+
const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
|
|
117
|
+
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
118
|
+
if (existing) return mfaAlreadyEnabled();
|
|
119
|
+
|
|
120
|
+
// Status + membership must be verified BEFORE the entity-write below —
|
|
121
|
+
// an admin can restrict/delete the account or revoke its tenant
|
|
122
|
+
// membership in the window between preauth-enable-start and
|
|
123
|
+
// preauth-confirm (up to MFA_SETUP_TOKEN_TTL_MINUTES). Checking after
|
|
124
|
+
// the write left two irreversible side effects behind an
|
|
125
|
+
// already-signed, non-forgeable setupToken that then just returns
|
|
126
|
+
// invalidSetupToken(): the MFA row persisted and (formerly) all other
|
|
127
|
+
// sessions revoked — with mfaAlreadyEnabled() blocking any
|
|
128
|
+
// re-enrollment until an operator manually disables MFA. NOT fixed by
|
|
129
|
+
// this ordering: burnToken() above still runs before this check, so a
|
|
130
|
+
// mid-flight restricted/deleted user still burns their setup token
|
|
131
|
+
// (framework#1523/5) — moving the burn later would need care around
|
|
132
|
+
// the existing userId-unique-constraint backstop between the
|
|
133
|
+
// `existing` check above and executor.create() below.
|
|
134
|
+
const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
|
|
135
|
+
const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
136
|
+
id: userId,
|
|
137
|
+
})) as { roles?: string | null; status?: string } | null; // @cast-boundary engine-payload
|
|
138
|
+
|
|
139
|
+
if (!userRow) return invalidSetupToken();
|
|
140
|
+
if (
|
|
141
|
+
userRow?.status === USER_STATUS.Restricted ||
|
|
142
|
+
userRow?.status === USER_STATUS.DeletionRequested ||
|
|
143
|
+
userRow?.status === USER_STATUS.Deleted
|
|
144
|
+
) {
|
|
145
|
+
return invalidSetupToken();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const globalRoles = parseRoles(userRow?.roles ?? null);
|
|
149
|
+
|
|
150
|
+
const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
|
|
151
|
+
userId,
|
|
152
|
+
})) as ReadonlyArray<{ tenantId: string; roles: readonly string[] }>; // @cast-boundary engine-payload
|
|
153
|
+
const membership = memberships.find((m) => m.tenantId === tenantId);
|
|
154
|
+
if (!membership) return invalidSetupToken();
|
|
155
|
+
|
|
156
|
+
const result = await executor.create(
|
|
157
|
+
{
|
|
158
|
+
userId,
|
|
159
|
+
totpSecret: verify.payload.totpSecretBase32,
|
|
160
|
+
recoveryCodes: encodeRecoveryCodes(verify.payload.recoveryCodeHashes),
|
|
161
|
+
enabledAt: Temporal.Now.instant(),
|
|
162
|
+
lastUsedAt: null,
|
|
163
|
+
},
|
|
164
|
+
scopedUser,
|
|
165
|
+
scopedDb,
|
|
166
|
+
);
|
|
167
|
+
if (!result.isSuccess) return result;
|
|
168
|
+
|
|
169
|
+
// ctx.redis is guaranteed by the fail-closed check above.
|
|
170
|
+
await clearMfaVerifyAttempts(ctx.redis, userId);
|
|
171
|
+
|
|
172
|
+
// Re-derive the full session the way verify.write.ts does — the
|
|
173
|
+
// setupToken only proves "password + new TOTP secret", not roles;
|
|
174
|
+
// roles come from the membership lookup above, same as every other
|
|
175
|
+
// login-completing handler.
|
|
176
|
+
// buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
|
|
177
|
+
// roles from the membership portion (globalRoles keeps SystemAdmin) —
|
|
178
|
+
// read-time backstop against a rebuild-resurrected role, same as
|
|
179
|
+
// verify.write.ts.
|
|
180
|
+
const mergedRoles = buildSessionRoles(globalRoles, membership.roles);
|
|
181
|
+
|
|
182
|
+
const baseSession: SessionUser = { id: userId, tenantId, roles: mergedRoles };
|
|
183
|
+
const claims = await ctx.resolveAuthClaims(baseSession);
|
|
184
|
+
const session: SessionUser =
|
|
185
|
+
Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
186
|
+
|
|
187
|
+
// Last step before returning success: every gate above (status,
|
|
188
|
+
// membership, entity-write) has passed, so this is the point of no
|
|
189
|
+
// return anyway — revoking here instead of right after the write
|
|
190
|
+
// means a rejected confirm never touches the user's other sessions.
|
|
191
|
+
// No currentSid to exclude — this is the first session for this
|
|
192
|
+
// login, not a follow-up on an already-running one.
|
|
193
|
+
if (opts.revokeAllOtherSessions) {
|
|
194
|
+
await opts.revokeAllOtherSessions(userId, undefined);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { isSuccess: true, data: { kind: "mfa-preauth-confirm-success", session } };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { Temporal } from "temporal-polyfill";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { burnToken } from "../../shared";
|
|
7
|
+
import { base32Decode } from "../base32";
|
|
8
|
+
import { findUserMfaRow } from "../db/queries";
|
|
9
|
+
import { invalidSetupToken, invalidTotpCode, mfaAlreadyEnabled } from "../errors";
|
|
10
|
+
import { verifyMfaSetupToken } from "../mfa-setup-token";
|
|
11
|
+
import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
|
|
12
|
+
import { verifyTotp } from "../totp";
|
|
13
|
+
|
|
14
|
+
export type EnableConfirmOptions = {
|
|
15
|
+
readonly setupTokenSecret: string;
|
|
16
|
+
// Wired late by run-prod-app once the sessions feature (if mounted) is
|
|
17
|
+
// concrete — see sessions/session-callbacks.ts. Absent when sessions
|
|
18
|
+
// isn't mounted: enabling MFA just doesn't revoke other sessions.
|
|
19
|
+
readonly revokeAllOtherSessions?: (
|
|
20
|
+
userId: string,
|
|
21
|
+
currentSid: string | undefined,
|
|
22
|
+
) => Promise<number>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
|
|
26
|
+
entityName: "user-mfa",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function createEnableConfirmHandler(opts: EnableConfirmOptions) {
|
|
30
|
+
return defineWriteHandler({
|
|
31
|
+
name: "enable-confirm",
|
|
32
|
+
schema: z.object({
|
|
33
|
+
setupToken: z.string().min(1),
|
|
34
|
+
code: z.string().length(6),
|
|
35
|
+
}),
|
|
36
|
+
access: { openToAll: true },
|
|
37
|
+
handler: async (event, ctx) => {
|
|
38
|
+
const verify = verifyMfaSetupToken(event.payload.setupToken, opts.setupTokenSecret);
|
|
39
|
+
if (!verify.ok) return invalidSetupToken();
|
|
40
|
+
// A setup token minted for one user can't be redeemed by another —
|
|
41
|
+
// guards against a leaked/shared token, not just tampering (the HMAC
|
|
42
|
+
// already rules that out).
|
|
43
|
+
if (verify.payload.userId !== event.user.id) return invalidSetupToken();
|
|
44
|
+
|
|
45
|
+
// Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
|
|
46
|
+
// (#1467): session-authed (lower risk — the caller already has a
|
|
47
|
+
// valid JWT), but silently skipping the single-use burn here would
|
|
48
|
+
// still let a double-confirm race complete twice instead of the
|
|
49
|
+
// second seeing invalidSetupToken(). Checked before the TOTP verify
|
|
50
|
+
// for the same reason the other two handlers check it early: fail
|
|
51
|
+
// closed before doing any of the guarded work, not partway through.
|
|
52
|
+
if (!ctx.redis) {
|
|
53
|
+
return writeFailure(
|
|
54
|
+
new InternalError({
|
|
55
|
+
message: "enable-confirm requires ctx.redis for the single-use setup-token burn",
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const secret = base32Decode(verify.payload.totpSecretBase32);
|
|
61
|
+
if (verifyTotp(secret, event.payload.code) === false) return invalidTotpCode();
|
|
62
|
+
|
|
63
|
+
// Burn the setup token on the first successful confirm — without this
|
|
64
|
+
// a `disable` doesn't retire it (re-enabling with the secret the user
|
|
65
|
+
// thinks they discarded), and two parallel confirms both proceed to
|
|
66
|
+
// executor.create instead of the second seeing mfa_already_enabled().
|
|
67
|
+
// ponytail: burned here, not unburned on a later executor.create
|
|
68
|
+
// failure (unlike login.write.ts's unburnToken pattern) — self-service
|
|
69
|
+
// enable-start always mints a fresh setup token on demand (no mailed
|
|
70
|
+
// link to go stale), so the realistic remaining failure path (a
|
|
71
|
+
// double-confirm race where executor.create itself fails, e.g. a
|
|
72
|
+
// concurrent mfa_already_enabled() write) is meant to leave the burn
|
|
73
|
+
// standing. Add unburnToken here if create-failure retries turn out
|
|
74
|
+
// to matter.
|
|
75
|
+
const burnResult = await burnToken(ctx.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
|
|
76
|
+
if (burnResult === "already-used") return invalidSetupToken();
|
|
77
|
+
|
|
78
|
+
const existing = await findUserMfaRow(ctx.db, event.user);
|
|
79
|
+
if (existing) return mfaAlreadyEnabled();
|
|
80
|
+
|
|
81
|
+
const result = await executor.create(
|
|
82
|
+
{
|
|
83
|
+
userId: event.user.id,
|
|
84
|
+
totpSecret: verify.payload.totpSecretBase32,
|
|
85
|
+
// Stored as a JSON string — see schema/user-mfa.ts (recoveryCodes
|
|
86
|
+
// is an encrypted+userOwned text field, both layers need a string).
|
|
87
|
+
recoveryCodes: encodeRecoveryCodes(verify.payload.recoveryCodeHashes),
|
|
88
|
+
enabledAt: Temporal.Now.instant(),
|
|
89
|
+
lastUsedAt: null,
|
|
90
|
+
},
|
|
91
|
+
event.user,
|
|
92
|
+
ctx.db,
|
|
93
|
+
);
|
|
94
|
+
if (!result.isSuccess) return result;
|
|
95
|
+
|
|
96
|
+
// Confirming enable proves possession of a second factor — every
|
|
97
|
+
// other session (a stolen-cookie attacker included) gets logged out.
|
|
98
|
+
// The session that just did this confirm keeps running.
|
|
99
|
+
if (opts.revokeAllOtherSessions) {
|
|
100
|
+
await opts.revokeAllOtherSessions(event.user.id, event.user.sid);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { isSuccess: true, data: { enabled: true } };
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { defineWriteHandler, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { base32Encode } from "../base32";
|
|
5
|
+
import { MFA_SETUP_TOKEN_TTL_MINUTES } from "../constants";
|
|
6
|
+
import { findUserMfaRow } from "../db/queries";
|
|
7
|
+
import { invalidChallengeToken, mfaAlreadyEnabled } from "../errors";
|
|
8
|
+
import { verifyMfaPreauthSetupToken } from "../mfa-preauth-setup-token";
|
|
9
|
+
import { signMfaSetupToken } from "../mfa-setup-token";
|
|
10
|
+
import { buildOtpauthUri } from "../otpauth-uri";
|
|
11
|
+
import { generateRecoveryCodes, hashRecoveryCodes } from "../recovery-codes";
|
|
12
|
+
import { generateTotpSecret } from "../totp";
|
|
13
|
+
|
|
14
|
+
export type EnableStartPreauthOptions = {
|
|
15
|
+
// Verifies the incoming preauthSetupToken — must match login.write.ts's
|
|
16
|
+
// mfaStatusChecker (challengeTokenSecret), NOT setupTokenSecret.
|
|
17
|
+
readonly challengeTokenSecret: string;
|
|
18
|
+
// Signs the outgoing secret-carrying setupToken — same secret + token
|
|
19
|
+
// format as enable-start.write.ts, so the later pre-auth confirm step can
|
|
20
|
+
// verify it with the existing verifyMfaSetupToken.
|
|
21
|
+
readonly setupTokenSecret: string;
|
|
22
|
+
readonly issuer: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Pre-auth twin of enable-start.write.ts: identity comes from a verified
|
|
26
|
+
// preauthSetupToken (minted by login.write.ts when enforcement policy
|
|
27
|
+
// blocks an unenrolled user), not from event.user — there is no session at
|
|
28
|
+
// this point. Runs pre-session the same way verify.write.ts does (dispatched
|
|
29
|
+
// by the framework's /auth/mfa/preauth-enable-start route with GUEST_USER).
|
|
30
|
+
export function createEnableStartPreauthHandler(opts: EnableStartPreauthOptions) {
|
|
31
|
+
return defineWriteHandler({
|
|
32
|
+
name: "enable-start-preauth",
|
|
33
|
+
schema: z.object({
|
|
34
|
+
preauthSetupToken: z.string().min(1),
|
|
35
|
+
accountLabel: z.string().min(1).max(200),
|
|
36
|
+
}),
|
|
37
|
+
access: { roles: ["all"] },
|
|
38
|
+
handler: async (event, ctx) => {
|
|
39
|
+
const verified = verifyMfaPreauthSetupToken(
|
|
40
|
+
event.payload.preauthSetupToken,
|
|
41
|
+
opts.challengeTokenSecret,
|
|
42
|
+
);
|
|
43
|
+
// Same generic "sign in again" error as an expired/invalid MFA
|
|
44
|
+
// challenge token — both mean the login-flow token expired, no
|
|
45
|
+
// distinct i18n needed (see #1465's scoping comment).
|
|
46
|
+
if (!verified.ok) return invalidChallengeToken();
|
|
47
|
+
const { userId, tenantId } = verified.payload;
|
|
48
|
+
|
|
49
|
+
// "system" mode: the guest dispatch identity's own tenantId is
|
|
50
|
+
// meaningless here — the preauthSetupToken is the source of truth
|
|
51
|
+
// for which tenant's row to read, mirroring verify.write.ts.
|
|
52
|
+
const scopedDb = createTenantDb(ctx.db.raw, tenantId, "system");
|
|
53
|
+
const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
|
|
54
|
+
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
55
|
+
if (existing) return mfaAlreadyEnabled();
|
|
56
|
+
|
|
57
|
+
const secret = generateTotpSecret();
|
|
58
|
+
const recoveryCodes = generateRecoveryCodes();
|
|
59
|
+
const recoveryCodeHashes = await hashRecoveryCodes(recoveryCodes);
|
|
60
|
+
|
|
61
|
+
const { token: setupToken } = signMfaSetupToken(
|
|
62
|
+
{
|
|
63
|
+
userId,
|
|
64
|
+
totpSecretBase32: base32Encode(secret),
|
|
65
|
+
recoveryCodeHashes,
|
|
66
|
+
tenantId,
|
|
67
|
+
},
|
|
68
|
+
MFA_SETUP_TOKEN_TTL_MINUTES,
|
|
69
|
+
opts.setupTokenSecret,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
isSuccess: true,
|
|
74
|
+
data: {
|
|
75
|
+
setupToken,
|
|
76
|
+
otpauthUri: buildOtpauthUri({
|
|
77
|
+
issuer: opts.issuer,
|
|
78
|
+
accountLabel: event.payload.accountLabel,
|
|
79
|
+
secret,
|
|
80
|
+
}),
|
|
81
|
+
// Plaintext — this is the one and only time these are shown.
|
|
82
|
+
recoveryCodes,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|