@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,66 @@
|
|
|
1
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { base32Encode } from "../base32";
|
|
4
|
+
import { MFA_SETUP_TOKEN_TTL_MINUTES } from "../constants";
|
|
5
|
+
import { findUserMfaRow } from "../db/queries";
|
|
6
|
+
import { mfaAlreadyEnabled } from "../errors";
|
|
7
|
+
import { signMfaSetupToken } from "../mfa-setup-token";
|
|
8
|
+
import { buildOtpauthUri } from "../otpauth-uri";
|
|
9
|
+
import { generateRecoveryCodes, hashRecoveryCodes } from "../recovery-codes";
|
|
10
|
+
import { generateTotpSecret } from "../totp";
|
|
11
|
+
|
|
12
|
+
export type EnableStartOptions = {
|
|
13
|
+
readonly setupTokenSecret: string;
|
|
14
|
+
readonly issuer: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Stateless setup: no `userMfa` row is created here. The generated secret +
|
|
18
|
+
// recovery-code hashes are signed into a short-lived `setupToken` (see
|
|
19
|
+
// mfa-setup-token.ts) that `enable-confirm` verifies. An abandoned setup
|
|
20
|
+
// (user closes the tab without entering a code) leaves zero trace — no
|
|
21
|
+
// cleanup job needed for orphaned "pending MFA" rows.
|
|
22
|
+
export function createEnableStartHandler(opts: EnableStartOptions) {
|
|
23
|
+
return defineWriteHandler({
|
|
24
|
+
name: "enable-start",
|
|
25
|
+
schema: z.object({
|
|
26
|
+
// Client-supplied label for the otpauth:// URI / authenticator-app
|
|
27
|
+
// entry (typically the user's own email) — avoids an extra DB lookup
|
|
28
|
+
// for something the already-authenticated client already knows about
|
|
29
|
+
// itself.
|
|
30
|
+
accountLabel: z.string().min(1).max(200),
|
|
31
|
+
}),
|
|
32
|
+
access: { openToAll: true },
|
|
33
|
+
handler: async (event, ctx) => {
|
|
34
|
+
const existing = await findUserMfaRow(ctx.db, event.user);
|
|
35
|
+
if (existing) return mfaAlreadyEnabled();
|
|
36
|
+
|
|
37
|
+
const secret = generateTotpSecret();
|
|
38
|
+
const recoveryCodes = generateRecoveryCodes();
|
|
39
|
+
const recoveryCodeHashes = await hashRecoveryCodes(recoveryCodes);
|
|
40
|
+
|
|
41
|
+
const { token: setupToken } = signMfaSetupToken(
|
|
42
|
+
{
|
|
43
|
+
userId: event.user.id,
|
|
44
|
+
totpSecretBase32: base32Encode(secret),
|
|
45
|
+
recoveryCodeHashes,
|
|
46
|
+
},
|
|
47
|
+
MFA_SETUP_TOKEN_TTL_MINUTES,
|
|
48
|
+
opts.setupTokenSecret,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isSuccess: true,
|
|
53
|
+
data: {
|
|
54
|
+
setupToken,
|
|
55
|
+
otpauthUri: buildOtpauthUri({
|
|
56
|
+
issuer: opts.issuer,
|
|
57
|
+
accountLabel: event.payload.accountLabel,
|
|
58
|
+
secret,
|
|
59
|
+
}),
|
|
60
|
+
// Plaintext — this is the one and only time these are shown.
|
|
61
|
+
recoveryCodes,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// KEK-rotation job for userMfa.totpSecret + recoveryCodes (entity-field
|
|
2
|
+
// encryption, same MasterKeyProvider as secrets/config — see
|
|
3
|
+
// schema/user-mfa.ts). Modeled directly on config/handlers/reencrypt.job.ts:
|
|
4
|
+
// entity-field encryption stores a StoredEnvelope JSON string per value (no
|
|
5
|
+
// separate kekVersion column, unlike secrets' DEK-wrap model), so rotation
|
|
6
|
+
// means decrypt under the OLD key, then let the executor's own write-path
|
|
7
|
+
// re-encrypt under provider.currentVersion() — the same auto-encrypt every
|
|
8
|
+
// enable-confirm write already goes through (see enable-confirm.write.ts: it
|
|
9
|
+
// passes PLAINTEXT to executor.create, never a manually-built envelope).
|
|
10
|
+
//
|
|
11
|
+
// Both fields also carry `userOwned` — the executor's own encryptForStorage
|
|
12
|
+
// wraps them PII(envelope(plaintext)) (see event-store-executor-context.ts).
|
|
13
|
+
// This job reads raw rows via selectMany (bypassing the executor's
|
|
14
|
+
// decryptForRead), so it must peel that PII layer itself before the envelope
|
|
15
|
+
// cipher ever sees the value, mirroring decryptForRead's PII-then-envelope
|
|
16
|
+
// order — an envelope decrypt on a still-PII-wrapped string throws
|
|
17
|
+
// "[envelope-cipher] stored value is not valid JSON" instead of rotating anything.
|
|
18
|
+
//
|
|
19
|
+
// Idempotent: re-running skips rows already on the current kekVersion.
|
|
20
|
+
// Every write is a normal executor.update — after a full run even a
|
|
21
|
+
// from-scratch projection rebuild only ever sees the current-KEK envelope
|
|
22
|
+
// (the very risk this job's plan entry called out: a rebuild resurrecting
|
|
23
|
+
// an old-KEK wrap would happen if rotation only touched the read-side
|
|
24
|
+
// projection instead of appending a real `.updated` event).
|
|
25
|
+
|
|
26
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
27
|
+
import {
|
|
28
|
+
collectPiiSubjectFields,
|
|
29
|
+
configuredPiiSubjectKms,
|
|
30
|
+
decryptPiiFieldValues,
|
|
31
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
32
|
+
import {
|
|
33
|
+
configuredEntityFieldEncryption,
|
|
34
|
+
createEventStoreExecutor,
|
|
35
|
+
createTenantDb,
|
|
36
|
+
type DbConnection,
|
|
37
|
+
type TenantDb,
|
|
38
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
39
|
+
import type { JobHandlerFn, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
40
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
41
|
+
import { type EnvelopeCipher, isStoredEnvelope } from "@cosmicdrift/kumiko-framework/secrets";
|
|
42
|
+
import { type ChunkedMigrationStopReason, runChunkedMigration } from "../../shared";
|
|
43
|
+
import { userMfaEntity, userMfaTable } from "../schema/user-mfa";
|
|
44
|
+
|
|
45
|
+
const DEFAULT_BATCH_SIZE = 100;
|
|
46
|
+
const DEFAULT_MAX_FAILURES = 10;
|
|
47
|
+
const SYSTEM_ROLES = ["system"] as const;
|
|
48
|
+
|
|
49
|
+
const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
|
|
50
|
+
entityName: "user-mfa",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const piiFieldNames = collectPiiSubjectFields(userMfaEntity);
|
|
54
|
+
|
|
55
|
+
export type MfaReencryptJobPayload = {
|
|
56
|
+
readonly batchSize?: number;
|
|
57
|
+
readonly maxDurationMs?: number;
|
|
58
|
+
readonly maxFailures?: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type MfaReencryptJobResult = {
|
|
62
|
+
readonly migrated: number;
|
|
63
|
+
readonly failed: number;
|
|
64
|
+
readonly alreadyCurrent: number;
|
|
65
|
+
readonly batchesProcessed: number;
|
|
66
|
+
readonly stoppedReason: ChunkedMigrationStopReason;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type EnvelopeClassification = "rotate" | "current" | "unrecognized";
|
|
70
|
+
|
|
71
|
+
// After PII peel (when configured), values must be current-cipher envelopes.
|
|
72
|
+
// Non-JSON / non-envelope is not a supported legacy decrypt path — fail the
|
|
73
|
+
// row instead of treating it as "needs rotate" (#1541, mirrors config #1513).
|
|
74
|
+
function classifyEnvelope(value: string, targetVersion: number): EnvelopeClassification {
|
|
75
|
+
let parsed: unknown;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(value);
|
|
78
|
+
} catch {
|
|
79
|
+
return "unrecognized";
|
|
80
|
+
}
|
|
81
|
+
if (!isStoredEnvelope(parsed)) return "unrecognized";
|
|
82
|
+
return parsed.kekVersion === targetVersion ? "current" : "rotate";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const mfaReencryptJob: JobHandlerFn = async (rawPayload, ctx): Promise<void> => {
|
|
86
|
+
const payload = rawPayload as MfaReencryptJobPayload; // @cast-boundary engine-payload
|
|
87
|
+
const maybeCipher = configuredEntityFieldEncryption();
|
|
88
|
+
if (!maybeCipher) {
|
|
89
|
+
throw new InternalError({
|
|
90
|
+
message:
|
|
91
|
+
"[auth-mfa:reencrypt] entity-field encryption is not configured — provide a master key " +
|
|
92
|
+
"(KUMIKO_SECRETS_MASTER_KEY_V<n>); run{Prod,Dev}App wire this automatically, custom boots " +
|
|
93
|
+
"call configureEntityFieldEncryption(cipher).",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// hoisted function declarations below capture this — pin the narrowed
|
|
97
|
+
// type explicitly so TS keeps it inside the closures.
|
|
98
|
+
const cipher: EnvelopeCipher = maybeCipher;
|
|
99
|
+
if (!ctx.masterKeyProvider) {
|
|
100
|
+
throw new InternalError({
|
|
101
|
+
message:
|
|
102
|
+
"[auth-mfa:reencrypt] ctx.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const provider = ctx.masterKeyProvider;
|
|
106
|
+
if (!ctx.db) {
|
|
107
|
+
throw new InternalError({
|
|
108
|
+
message: "[auth-mfa:reencrypt] ctx.db missing — job context requires a database connection.",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const db = ctx.db as DbConnection; // @cast-boundary db-operator
|
|
112
|
+
// Undefined = per-subject crypto-shredding isn't configured for this app —
|
|
113
|
+
// both fields are then plain envelope strings, no PII layer to peel.
|
|
114
|
+
const piiKms = configuredPiiSubjectKms();
|
|
115
|
+
|
|
116
|
+
const batchSize = payload.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
117
|
+
const maxFailures = payload.maxFailures ?? DEFAULT_MAX_FAILURES;
|
|
118
|
+
const deadline = payload.maxDurationMs
|
|
119
|
+
? Date.now() + payload.maxDurationMs
|
|
120
|
+
: Number.POSITIVE_INFINITY;
|
|
121
|
+
|
|
122
|
+
const tdbCache = new Map<TenantId, TenantDb>();
|
|
123
|
+
function tdbFor(tenantId: TenantId): TenantDb {
|
|
124
|
+
let existing = tdbCache.get(tenantId);
|
|
125
|
+
if (!existing) {
|
|
126
|
+
existing = createTenantDb(db, tenantId, "system");
|
|
127
|
+
tdbCache.set(tenantId, existing);
|
|
128
|
+
}
|
|
129
|
+
return existing;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type UserMfaRow = {
|
|
133
|
+
id: string;
|
|
134
|
+
tenantId: string;
|
|
135
|
+
version: number;
|
|
136
|
+
totpSecret: string;
|
|
137
|
+
recoveryCodes: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let alreadyCurrent = 0;
|
|
141
|
+
const targetVersion = provider.currentVersion();
|
|
142
|
+
|
|
143
|
+
// ponytail: one full candidate scan — MFA rows are user-scale, not
|
|
144
|
+
// tenant-scale like config; cursor pagination when that ever matters.
|
|
145
|
+
// Served in slices so runChunkedMigration's deadline/signal/failure
|
|
146
|
+
// checks run between chunks, not only once.
|
|
147
|
+
let pending: UserMfaRow[] | undefined;
|
|
148
|
+
async function nextBatch(): Promise<readonly UserMfaRow[]> {
|
|
149
|
+
if (pending === undefined) {
|
|
150
|
+
pending = [...(await selectMany<UserMfaRow>(db, userMfaTable, {}))];
|
|
151
|
+
}
|
|
152
|
+
return pending.splice(0, batchSize);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function migrateRow(row: UserMfaRow): Promise<"migrated" | "skipped" | "failed"> {
|
|
156
|
+
const tenantId = row.tenantId as TenantId; // @cast-boundary db-row
|
|
157
|
+
|
|
158
|
+
// Peel the PII layer first (if active) so classifyEnvelope/cipher.decrypt
|
|
159
|
+
// below only ever see envelope strings, same order as decryptForRead.
|
|
160
|
+
let envelopeValues: { totpSecret: string; recoveryCodes: string } = row;
|
|
161
|
+
if (piiKms) {
|
|
162
|
+
const unwrapped = await decryptPiiFieldValues(row, piiFieldNames, piiKms, {
|
|
163
|
+
requestId: "auth-mfa-reencrypt-job",
|
|
164
|
+
tenantId,
|
|
165
|
+
});
|
|
166
|
+
envelopeValues = unwrapped as { totpSecret: string; recoveryCodes: string }; // @cast-boundary engine-payload
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const totpClass = classifyEnvelope(envelopeValues.totpSecret, targetVersion);
|
|
170
|
+
const recoveryClass = classifyEnvelope(envelopeValues.recoveryCodes, targetVersion);
|
|
171
|
+
if (totpClass === "unrecognized" || recoveryClass === "unrecognized") {
|
|
172
|
+
ctx.log?.warn?.(
|
|
173
|
+
`[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`,
|
|
174
|
+
);
|
|
175
|
+
return "failed";
|
|
176
|
+
}
|
|
177
|
+
if (totpClass === "current" && recoveryClass === "current") {
|
|
178
|
+
alreadyCurrent++;
|
|
179
|
+
return "skipped";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// decrypt failure (unknown kekVersion, tamper) throws → counted as
|
|
183
|
+
// failed via onRowError; the row stays untouched.
|
|
184
|
+
const changes: Record<string, string> = {};
|
|
185
|
+
if (totpClass === "rotate") {
|
|
186
|
+
changes["totpSecret"] = await cipher.decrypt(envelopeValues.totpSecret, { tenantId });
|
|
187
|
+
}
|
|
188
|
+
if (recoveryClass === "rotate") {
|
|
189
|
+
changes["recoveryCodes"] = await cipher.decrypt(envelopeValues.recoveryCodes, { tenantId });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const actor: SessionUser = { id: "system", tenantId, roles: SYSTEM_ROLES };
|
|
193
|
+
// PLAINTEXT here, not a manually-built envelope — the executor's own
|
|
194
|
+
// write-path (encryptForStorage) re-encrypts under the CURRENT
|
|
195
|
+
// injected cipher/KEK (and re-wraps the PII layer under the same,
|
|
196
|
+
// unrotated subject DEK), exactly like every other write to this field.
|
|
197
|
+
const result = await executor.update(
|
|
198
|
+
{ id: row.id, version: row.version, changes },
|
|
199
|
+
actor,
|
|
200
|
+
tdbFor(tenantId),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// version_conflict == a concurrent enable/disable/regenerate beat us;
|
|
204
|
+
// the row now holds a fresh write, already fine.
|
|
205
|
+
if (!result.isSuccess) {
|
|
206
|
+
if (result.error.code === "version_conflict") return "skipped";
|
|
207
|
+
ctx.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
|
|
208
|
+
code: result.error.code,
|
|
209
|
+
});
|
|
210
|
+
return "failed";
|
|
211
|
+
}
|
|
212
|
+
return "migrated";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const outcome = await runChunkedMigration<UserMfaRow>({
|
|
216
|
+
nextBatch,
|
|
217
|
+
migrateRow,
|
|
218
|
+
maxFailures,
|
|
219
|
+
deadlineAt: deadline,
|
|
220
|
+
signal: ctx.signal,
|
|
221
|
+
onRowError: (row, err) => {
|
|
222
|
+
ctx.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result: MfaReencryptJobResult = {
|
|
227
|
+
migrated: outcome.migrated,
|
|
228
|
+
failed: outcome.failed,
|
|
229
|
+
alreadyCurrent,
|
|
230
|
+
batchesProcessed: outcome.batchesProcessed,
|
|
231
|
+
stoppedReason: outcome.stoppedReason,
|
|
232
|
+
};
|
|
233
|
+
ctx.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
|
|
234
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
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 { generateRecoveryCodes, hashRecoveryCodes } from "../recovery-codes";
|
|
8
|
+
import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
|
|
9
|
+
import { verifyMfaFactor } from "../verify-factor";
|
|
10
|
+
|
|
11
|
+
export type RegenerateRecoveryOptions = {
|
|
12
|
+
readonly revokeAllOtherSessions?: (
|
|
13
|
+
userId: string,
|
|
14
|
+
currentSid: string | undefined,
|
|
15
|
+
) => Promise<number>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
|
|
19
|
+
entityName: "user-mfa",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Invalidates ALL existing recovery codes (even unused ones) and issues a
|
|
23
|
+
// fresh set of 8 — the standard response to "I think my recovery codes
|
|
24
|
+
// leaked" without having to disable+re-enable TOTP itself.
|
|
25
|
+
export function createRegenerateRecoveryHandler(opts: RegenerateRecoveryOptions) {
|
|
26
|
+
return defineWriteHandler({
|
|
27
|
+
name: "regenerate-recovery",
|
|
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
|
+
// Proof of possession before wiping the old codes — a TOTP code or
|
|
35
|
+
// (deliberately) one of the very codes about to be replaced both
|
|
36
|
+
// count, since either way the presented code is consumed/irrelevant
|
|
37
|
+
// afterward.
|
|
38
|
+
//
|
|
39
|
+
// Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
|
|
40
|
+
// (#1467): without ctx.redis, verifyMfaFactor's TOTP-replay burn is
|
|
41
|
+
// skipped, letting an observed code be replayed within the ±1-step
|
|
42
|
+
// window to regenerate (and thus invalidate) recovery codes — a
|
|
43
|
+
// session-authed route, so pre-unwiring JWTs stay a valid attack path.
|
|
44
|
+
if (!ctx.redis) {
|
|
45
|
+
return writeFailure(
|
|
46
|
+
new InternalError({
|
|
47
|
+
message: "regenerate-recovery (mfa) requires ctx.redis for TOTP replay protection",
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const replay = { redis: ctx.redis, userId: event.user.id };
|
|
52
|
+
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
53
|
+
if (!verify.ok) return invalidTotpCode();
|
|
54
|
+
|
|
55
|
+
const newCodes = generateRecoveryCodes();
|
|
56
|
+
const newHashes = await hashRecoveryCodes(newCodes);
|
|
57
|
+
|
|
58
|
+
const result = await executor.update(
|
|
59
|
+
{
|
|
60
|
+
id: row.id,
|
|
61
|
+
version: row.version,
|
|
62
|
+
changes: { recoveryCodes: encodeRecoveryCodes(newHashes) },
|
|
63
|
+
},
|
|
64
|
+
event.user,
|
|
65
|
+
ctx.db,
|
|
66
|
+
);
|
|
67
|
+
if (!result.isSuccess) return result;
|
|
68
|
+
|
|
69
|
+
if (opts.revokeAllOtherSessions) {
|
|
70
|
+
await opts.revokeAllOtherSessions(event.user.id, event.user.sid);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Plaintext — shown once, same as the initial enable-flow codes.
|
|
74
|
+
return { isSuccess: true, data: { recoveryCodes: newCodes } };
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { userMfaTable } from "../schema/user-mfa";
|
|
5
|
+
|
|
6
|
+
// "Is MFA enabled for me" — the one thing a settings screen needs before it
|
|
7
|
+
// can decide whether to show the enrollment flow or the disable/regenerate
|
|
8
|
+
// actions. No client-side signal (session/JWT) carries this today, so
|
|
9
|
+
// without this query every app builds its own row-existence check via a
|
|
10
|
+
// side channel. Plain fetchOne — no need for the entity's decrypting
|
|
11
|
+
// executor.detail() (see db/queries.ts's findUserMfaRow), existence is all
|
|
12
|
+
// the caller wants.
|
|
13
|
+
export const mfaStatusQuery = defineQueryHandler({
|
|
14
|
+
name: "user-mfa:status",
|
|
15
|
+
schema: z.object({}),
|
|
16
|
+
access: { openToAll: true },
|
|
17
|
+
handler: async (query, ctx) => {
|
|
18
|
+
const row = await fetchOne<{ id: string }>(ctx.db, userMfaTable, {
|
|
19
|
+
userId: query.user.id,
|
|
20
|
+
});
|
|
21
|
+
return { enabled: row !== undefined };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
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 { z } from "zod";
|
|
11
|
+
import { burnToken } from "../../shared";
|
|
12
|
+
import { USER_STATUS, UserQueries } from "../../user";
|
|
13
|
+
import { MFA_VERIFY_LOCKOUT_MINUTES, MFA_VERIFY_MAX_ATTEMPTS } from "../constants";
|
|
14
|
+
import { findUserMfaRow } from "../db/queries";
|
|
15
|
+
import { invalidChallengeToken, invalidTotpCode, tooManyAttempts } from "../errors";
|
|
16
|
+
import { verifyMfaChallengeToken } from "../mfa-challenge-token";
|
|
17
|
+
import {
|
|
18
|
+
clearMfaVerifyAttempts,
|
|
19
|
+
getMfaVerifyLockoutState,
|
|
20
|
+
recordFailedMfaVerifyAttempt,
|
|
21
|
+
} from "../mfa-verify-attempts";
|
|
22
|
+
import { encodeRecoveryCodes, userMfaEntity, userMfaTable } from "../schema/user-mfa";
|
|
23
|
+
import { verifyMfaFactor } from "../verify-factor";
|
|
24
|
+
|
|
25
|
+
export type MfaVerifyOptions = {
|
|
26
|
+
// Must match the secret the login handler signs mfa-challenge tokens
|
|
27
|
+
// with — distinct from setupTokenSecret (different token purpose).
|
|
28
|
+
readonly challengeTokenSecret: string;
|
|
29
|
+
readonly maxAttempts?: number;
|
|
30
|
+
readonly lockoutMinutes?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
|
|
34
|
+
entityName: "user-mfa",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Completes the two-step login. Runs pre-session (dispatched by the
|
|
38
|
+
// framework's /auth/mfa/verify route with a guest identity, same as
|
|
39
|
+
// login.write.ts) — everything it needs (which user, which tenant) comes
|
|
40
|
+
// from the challenge token, not from an authenticated caller.
|
|
41
|
+
export function createMfaVerifyHandler(opts: MfaVerifyOptions) {
|
|
42
|
+
const maxAttempts = opts.maxAttempts ?? MFA_VERIFY_MAX_ATTEMPTS;
|
|
43
|
+
const lockoutMinutes = opts.lockoutMinutes ?? MFA_VERIFY_LOCKOUT_MINUTES;
|
|
44
|
+
|
|
45
|
+
return defineWriteHandler({
|
|
46
|
+
name: "verify",
|
|
47
|
+
schema: z.object({ challengeToken: z.string().min(1), code: z.string().min(6).max(9) }),
|
|
48
|
+
access: { roles: ["all"] },
|
|
49
|
+
handler: async (event, ctx) => {
|
|
50
|
+
const verified = verifyMfaChallengeToken(
|
|
51
|
+
event.payload.challengeToken,
|
|
52
|
+
opts.challengeTokenSecret,
|
|
53
|
+
);
|
|
54
|
+
if (!verified.ok) return invalidChallengeToken();
|
|
55
|
+
const { userId, tenantId } = verified.payload;
|
|
56
|
+
|
|
57
|
+
// Fail closed, matching enable-confirm-preauth.write.ts (#1467):
|
|
58
|
+
// this route sits behind a challenge token from a successful login,
|
|
59
|
+
// but its own secret is still just a short TOTP/recovery code —
|
|
60
|
+
// silently skipping the brute-force cap and single-use burn when
|
|
61
|
+
// ctx.redis is absent would let a stolen challenge token be guessed
|
|
62
|
+
// against unboundedly, and let a recovery code be replayed via a
|
|
63
|
+
// reissued challenge (mfa-verify-attempts.ts's counter is deliberately
|
|
64
|
+
// NOT reset by reissuance, precisely to survive this).
|
|
65
|
+
if (!ctx.redis) {
|
|
66
|
+
return writeFailure(
|
|
67
|
+
new InternalError({
|
|
68
|
+
message: "verify (mfa) requires ctx.redis for the brute-force cap and single-use burn",
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Per-account brute-force cap — survives challenge-token reissuance
|
|
74
|
+
// on purpose (see mfa-verify-attempts.ts header). Checked BEFORE the
|
|
75
|
+
// TOTP/recovery verify so a locked account can't be guessed against.
|
|
76
|
+
const state = await getMfaVerifyLockoutState(ctx.redis, userId);
|
|
77
|
+
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (state.lockedUntil > now) {
|
|
80
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((state.lockedUntil - now) / 1000));
|
|
81
|
+
return tooManyAttempts(retryAfterSeconds);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// "system" mode: this handler runs with a guest identity whose own
|
|
86
|
+
// tenantId is meaningless here — the challenge token is the source
|
|
87
|
+
// of truth for which tenant's row to read.
|
|
88
|
+
const scopedDb = createTenantDb(ctx.db.raw, tenantId, "system");
|
|
89
|
+
const scopedUser: SessionUser = { id: userId, tenantId, roles: ["User"] };
|
|
90
|
+
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
91
|
+
// MFA got disabled between login and verify (race, or a stale
|
|
92
|
+
// challenge token from before a disable) — same generic error as a
|
|
93
|
+
// bad code, no detail leak about account state.
|
|
94
|
+
if (!row) return invalidChallengeToken();
|
|
95
|
+
|
|
96
|
+
const replay = { redis: ctx.redis, userId };
|
|
97
|
+
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
98
|
+
if (!verify.ok) {
|
|
99
|
+
await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
|
|
100
|
+
return invalidTotpCode();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Burn the challenge token only after a successful code check — a
|
|
104
|
+
// still-failing attempt (wrong code) must not consume it, so a
|
|
105
|
+
// fat-fingered retry on the same token still works. The TOTP-counter
|
|
106
|
+
// burn inside verifyMfaFactor already rejects an exact-duplicate
|
|
107
|
+
// replay (same token + same code) with invalid_totp_code before this
|
|
108
|
+
// is reached, so this burn only needs to guard a DIFFERENT code
|
|
109
|
+
// (recovery, or a still-valid-but-not-yet-burned TOTP step) reused
|
|
110
|
+
// against the same challenge token.
|
|
111
|
+
const burnResult = await burnToken(ctx.redis, "mfa-challenge", userId, verified.expiresAtMs);
|
|
112
|
+
if (burnResult === "already-used") return invalidChallengeToken();
|
|
113
|
+
|
|
114
|
+
// Recovery-code use consumes the code — persist the reduced hash-list
|
|
115
|
+
// immediately so it can't be replayed. Without this a recovery code
|
|
116
|
+
// would work forever instead of being single-use.
|
|
117
|
+
if (verify.method === "recovery") {
|
|
118
|
+
const updateResult = await executor.update(
|
|
119
|
+
{
|
|
120
|
+
id: row.id,
|
|
121
|
+
version: row.version,
|
|
122
|
+
changes: { recoveryCodes: encodeRecoveryCodes(verify.remainingHashes) },
|
|
123
|
+
},
|
|
124
|
+
scopedUser,
|
|
125
|
+
scopedDb,
|
|
126
|
+
);
|
|
127
|
+
if (!updateResult.isSuccess) return updateResult;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await clearMfaVerifyAttempts(ctx.redis, userId);
|
|
131
|
+
|
|
132
|
+
// Re-derive the full session the way login.write.ts would have, for
|
|
133
|
+
// the SAME tenant the challenge token already committed to (no
|
|
134
|
+
// "pick a tenant" step here — that already happened at login time).
|
|
135
|
+
const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
|
|
136
|
+
const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
137
|
+
id: userId,
|
|
138
|
+
})) as { roles?: string | null; status?: string } | null; // @cast-boundary engine-payload
|
|
139
|
+
|
|
140
|
+
// Re-check status + membership the way login.write.ts does after its
|
|
141
|
+
// password check — the challenge token only proves "password was
|
|
142
|
+
// correct 10 minutes ago", not that the account is still loginable
|
|
143
|
+
// now. Deliberately NOT re-checking strictEmailVerification here:
|
|
144
|
+
// MfaVerifyOptions has no such flag, and email verification can't
|
|
145
|
+
// regress between login and verify within the 10-minute window.
|
|
146
|
+
if (!userRow) return invalidChallengeToken();
|
|
147
|
+
if (
|
|
148
|
+
userRow?.status === USER_STATUS.Restricted ||
|
|
149
|
+
userRow?.status === USER_STATUS.DeletionRequested ||
|
|
150
|
+
userRow?.status === USER_STATUS.Deleted
|
|
151
|
+
) {
|
|
152
|
+
return invalidChallengeToken();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const globalRoles = parseRoles(userRow?.roles ?? null);
|
|
156
|
+
|
|
157
|
+
const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
|
|
158
|
+
userId,
|
|
159
|
+
})) as ReadonlyArray<{ tenantId: string; roles: readonly string[] }>; // @cast-boundary engine-payload
|
|
160
|
+
const membership = memberships.find((m) => m.tenantId === tenantId);
|
|
161
|
+
// Membership revoked between login and verify (race, or a stale
|
|
162
|
+
// challenge token from before removal) — login.write.ts would have
|
|
163
|
+
// refused with noMembership() at the same juncture; mirror it here
|
|
164
|
+
// instead of silently falling back to globalRoles only.
|
|
165
|
+
if (!membership) return invalidChallengeToken();
|
|
166
|
+
// buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
|
|
167
|
+
// roles from the membership portion (globalRoles keeps SystemAdmin) —
|
|
168
|
+
// read-time backstop against a rebuild-resurrected role.
|
|
169
|
+
const mergedRoles = buildSessionRoles(globalRoles, membership.roles);
|
|
170
|
+
|
|
171
|
+
const baseSession: SessionUser = { id: userId, tenantId, roles: mergedRoles };
|
|
172
|
+
const claims = await ctx.resolveAuthClaims(baseSession);
|
|
173
|
+
const session: SessionUser =
|
|
174
|
+
Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
175
|
+
|
|
176
|
+
return { isSuccess: true, data: { kind: "mfa-verify-success", session } };
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Server-side, boot-validated i18n keys — screen titles/descriptions the
|
|
2
|
+
// engine requires every registered r.screen to declare. Distinct from
|
|
3
|
+
// web/i18n.ts (the client's free-form UI-string bundle) — see
|
|
4
|
+
// personal-access-tokens/i18n.ts for the same split.
|
|
5
|
+
type LocalizedString = { readonly de: string; readonly en: string };
|
|
6
|
+
|
|
7
|
+
export const AUTH_MFA_FEATURE_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
8
|
+
"screen:auth-mfa-enable.title": {
|
|
9
|
+
de: "Zwei-Faktor-Authentifizierung",
|
|
10
|
+
en: "Two-factor authentication",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { base32Decode } from "./base32";
|
|
2
|
+
export { type MfaRequiredPolicy, mfaRequiredConfigHandle } from "./config";
|
|
3
|
+
export {
|
|
4
|
+
AUTH_MFA_FEATURE,
|
|
5
|
+
AuthMfaHandlers,
|
|
6
|
+
AuthMfaQueries,
|
|
7
|
+
MFA_ENABLE_SCREEN_ID,
|
|
8
|
+
} from "./constants";
|
|
9
|
+
export type {
|
|
10
|
+
AuthMfaFeatureOptions,
|
|
11
|
+
BindMfaRevokeAllOtherSessions,
|
|
12
|
+
} from "./feature";
|
|
13
|
+
export {
|
|
14
|
+
bindMfaRevokeAllOtherSessionsFromFeature,
|
|
15
|
+
createAuthMfaFeature,
|
|
16
|
+
mfaStatusCheckerFromFeature,
|
|
17
|
+
} from "./feature";
|
|
18
|
+
export type { MfaStatusChecker, MfaStatusCheckResult } from "./mfa-status-checker";
|
|
19
|
+
export { userMfaEntity, userMfaTable } from "./schema/user-mfa";
|