@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,319 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
|
+
import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
4
|
+
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import {
|
|
6
|
+
createTestUser,
|
|
7
|
+
setupTestStack,
|
|
8
|
+
type TestStack,
|
|
9
|
+
unsafeCreateEntityTable,
|
|
10
|
+
unsafePushTables,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
12
|
+
import {
|
|
13
|
+
createTestEnvelopeCipher,
|
|
14
|
+
expectErrorIncludes,
|
|
15
|
+
seedRow,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
17
|
+
import { createConfigFeature } from "../../config";
|
|
18
|
+
import { createConfigResolver } from "../../config/resolver";
|
|
19
|
+
import { configValuesTable } from "../../config/table";
|
|
20
|
+
import { createTenantFeature } from "../../tenant";
|
|
21
|
+
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
22
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
23
|
+
import { USER_STATUS } from "../../user";
|
|
24
|
+
import { createUserFeature } from "../../user/feature";
|
|
25
|
+
import { userEntity, userTable } from "../../user/schema/user";
|
|
26
|
+
import { base32Decode } from "../base32";
|
|
27
|
+
import { AuthMfaHandlers } from "../constants";
|
|
28
|
+
import { createAuthMfaFeature } from "../feature";
|
|
29
|
+
import { signMfaChallengeToken } from "../mfa-challenge-token";
|
|
30
|
+
import { userMfaEntity } from "../schema/user-mfa";
|
|
31
|
+
import { currentTotpCode } from "../totp";
|
|
32
|
+
|
|
33
|
+
let stack: TestStack;
|
|
34
|
+
|
|
35
|
+
const SETUP_TOKEN_SECRET = "test-mfa-setup-secret-at-least-32-bytes-long!!";
|
|
36
|
+
const CHALLENGE_TOKEN_SECRET = "test-mfa-challenge-secret-at-least-32-bytes!!";
|
|
37
|
+
|
|
38
|
+
// Handler access is `{ roles: ["all"] }`, matching how the framework route
|
|
39
|
+
// dispatches with a guest identity — a literal here is enough since the
|
|
40
|
+
// handler derives everything from the challenge-token payload, not from
|
|
41
|
+
// event.user.
|
|
42
|
+
const GUEST: SessionUser = {
|
|
43
|
+
id: "00000000-0000-0000-0000-000000000000",
|
|
44
|
+
tenantId: "00000000-0000-4000-8000-000000000001" as TenantId,
|
|
45
|
+
roles: ["all"],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
beforeAll(async () => {
|
|
49
|
+
const encryption = createTestEnvelopeCipher();
|
|
50
|
+
configureEntityFieldEncryption(encryption);
|
|
51
|
+
const resolver = createConfigResolver({ cipher: encryption });
|
|
52
|
+
stack = await setupTestStack({
|
|
53
|
+
features: [
|
|
54
|
+
createConfigFeature(),
|
|
55
|
+
createUserFeature(),
|
|
56
|
+
createTenantFeature(),
|
|
57
|
+
createAuthMfaFeature({
|
|
58
|
+
setupTokenSecret: SETUP_TOKEN_SECRET,
|
|
59
|
+
issuer: "Kumiko Test",
|
|
60
|
+
challengeTokenSecret: CHALLENGE_TOKEN_SECRET,
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
64
|
+
});
|
|
65
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
66
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
67
|
+
await unsafeCreateEntityTable(stack.db, userMfaEntity);
|
|
68
|
+
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterAll(async () => {
|
|
72
|
+
await stack.cleanup();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
async function enableMfaFor(idSeed: number): Promise<{
|
|
76
|
+
user: ReturnType<typeof createTestUser>;
|
|
77
|
+
secret: Buffer;
|
|
78
|
+
recoveryCodes: string[];
|
|
79
|
+
}> {
|
|
80
|
+
const user = createTestUser({ id: idSeed, roles: ["User"] });
|
|
81
|
+
const start = await stack.http.writeOk<{
|
|
82
|
+
setupToken: string;
|
|
83
|
+
otpauthUri: string;
|
|
84
|
+
recoveryCodes: string[];
|
|
85
|
+
}>(AuthMfaHandlers.enableStart, { accountLabel: `user-${idSeed}@example.com` }, user);
|
|
86
|
+
const secretParam = new URLSearchParams(start.otpauthUri.split("?")[1]).get("secret") ?? "";
|
|
87
|
+
const secret = base32Decode(secretParam);
|
|
88
|
+
await stack.http.writeOk(
|
|
89
|
+
AuthMfaHandlers.enableConfirm,
|
|
90
|
+
{ setupToken: start.setupToken, code: currentTotpCode(secret) },
|
|
91
|
+
user,
|
|
92
|
+
);
|
|
93
|
+
// verify.write.ts re-checks tenant membership AND the user's own row
|
|
94
|
+
// (status gate) the way login.write.ts does — auth-mfa never writes a
|
|
95
|
+
// read_users row itself (enableStart/enableConfirm only touch user-mfa),
|
|
96
|
+
// so both need seeding here, not just per-test where a scenario cares.
|
|
97
|
+
await seedRow(stack.db, tenantMembershipsTable, {
|
|
98
|
+
userId: user.id,
|
|
99
|
+
tenantId: user.tenantId,
|
|
100
|
+
roles: JSON.stringify(["User"]),
|
|
101
|
+
});
|
|
102
|
+
await seedRow(stack.db, userTable, {
|
|
103
|
+
id: user.id,
|
|
104
|
+
tenantId: user.tenantId,
|
|
105
|
+
email: `user-${user.id}@example.com`,
|
|
106
|
+
passwordHash: "h",
|
|
107
|
+
displayName: `Test User ${idSeed}`,
|
|
108
|
+
locale: "de",
|
|
109
|
+
emailVerified: true,
|
|
110
|
+
roles: "[]",
|
|
111
|
+
status: USER_STATUS.Active,
|
|
112
|
+
});
|
|
113
|
+
return { user, secret, recoveryCodes: start.recoveryCodes };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function challengeFor(userId: string, tenantId: TenantId): string {
|
|
117
|
+
return signMfaChallengeToken({ userId, tenantId }, 10, CHALLENGE_TOKEN_SECRET).token;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe("mfa verify — completes a two-step login", () => {
|
|
121
|
+
test("a valid TOTP code succeeds and returns the full session", async () => {
|
|
122
|
+
const { user, secret } = await enableMfaFor(1);
|
|
123
|
+
const challengeToken = challengeFor(user.id, user.tenantId);
|
|
124
|
+
|
|
125
|
+
const res = await stack.http.writeOk<{ session: SessionUser }>(
|
|
126
|
+
AuthMfaHandlers.verify,
|
|
127
|
+
{ challengeToken, code: currentTotpCode(secret) },
|
|
128
|
+
GUEST,
|
|
129
|
+
);
|
|
130
|
+
expect(res.session.id).toBe(user.id);
|
|
131
|
+
expect(res.session.tenantId).toBe(user.tenantId);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a wrong TOTP code is rejected", async () => {
|
|
135
|
+
const { user } = await enableMfaFor(2);
|
|
136
|
+
const challengeToken = challengeFor(user.id, user.tenantId);
|
|
137
|
+
const err = await stack.http.writeErr(
|
|
138
|
+
AuthMfaHandlers.verify,
|
|
139
|
+
{ challengeToken, code: "000000" },
|
|
140
|
+
GUEST,
|
|
141
|
+
);
|
|
142
|
+
expectErrorIncludes(err, "invalid_totp_code");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a valid recovery code succeeds, and the SAME code is rejected on replay", async () => {
|
|
146
|
+
const { user, recoveryCodes } = await enableMfaFor(3);
|
|
147
|
+
const code = recoveryCodes[0];
|
|
148
|
+
if (code === undefined) throw new Error("no recovery code minted");
|
|
149
|
+
|
|
150
|
+
const challengeA = challengeFor(user.id, user.tenantId);
|
|
151
|
+
const res = await stack.http.writeOk<{ session: SessionUser }>(
|
|
152
|
+
AuthMfaHandlers.verify,
|
|
153
|
+
{ challengeToken: challengeA, code },
|
|
154
|
+
GUEST,
|
|
155
|
+
);
|
|
156
|
+
expect(res.session.id).toBe(user.id);
|
|
157
|
+
|
|
158
|
+
// Advisor-flagged trap: a recovery code must be single-use. Re-verify
|
|
159
|
+
// with a FRESH challenge token (a real attacker would just re-login)
|
|
160
|
+
// but the SAME recovery code — it must now be rejected because
|
|
161
|
+
// verify.write.ts persists remainingHashes on the row.
|
|
162
|
+
const challengeB = challengeFor(user.id, user.tenantId);
|
|
163
|
+
const err = await stack.http.writeErr(
|
|
164
|
+
AuthMfaHandlers.verify,
|
|
165
|
+
{ challengeToken: challengeB, code },
|
|
166
|
+
GUEST,
|
|
167
|
+
);
|
|
168
|
+
expectErrorIncludes(err, "invalid_totp_code");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("an exact-duplicate verify (same challenge token, same code) is rejected", async () => {
|
|
172
|
+
const { user, secret } = await enableMfaFor(4);
|
|
173
|
+
const challengeToken = challengeFor(user.id, user.tenantId);
|
|
174
|
+
|
|
175
|
+
await stack.http.writeOk<{ session: SessionUser }>(
|
|
176
|
+
AuthMfaHandlers.verify,
|
|
177
|
+
{ challengeToken, code: currentTotpCode(secret) },
|
|
178
|
+
GUEST,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Same challenge token, same (still time-window-valid) code — rejected
|
|
182
|
+
// by the TOTP-counter burn inside verifyMfaFactor before the challenge-
|
|
183
|
+
// token burn is even reached (that burn now happens after success).
|
|
184
|
+
const err = await stack.http.writeErr(
|
|
185
|
+
AuthMfaHandlers.verify,
|
|
186
|
+
{ challengeToken, code: currentTotpCode(secret) },
|
|
187
|
+
GUEST,
|
|
188
|
+
);
|
|
189
|
+
expectErrorIncludes(err, "invalid_totp_code");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a TOTP code cannot be replayed across a FRESH challenge token (RFC 6238 §5.2)", async () => {
|
|
193
|
+
const { user, secret } = await enableMfaFor(105);
|
|
194
|
+
const code = currentTotpCode(secret);
|
|
195
|
+
|
|
196
|
+
const challengeA = challengeFor(user.id, user.tenantId);
|
|
197
|
+
await stack.http.writeOk<{ session: SessionUser }>(
|
|
198
|
+
AuthMfaHandlers.verify,
|
|
199
|
+
{ challengeToken: challengeA, code },
|
|
200
|
+
GUEST,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// A DIFFERENT challenge token (as a real re-login would mint) but the
|
|
204
|
+
// SAME still-time-window-valid code — this is the actual attack this
|
|
205
|
+
// finding is about: a phished/shoulder-surfed code used for a parallel
|
|
206
|
+
// login, not a naive replay of the whole prior request.
|
|
207
|
+
const challengeB = challengeFor(user.id, user.tenantId);
|
|
208
|
+
const err = await stack.http.writeErr(
|
|
209
|
+
AuthMfaHandlers.verify,
|
|
210
|
+
{ challengeToken: challengeB, code },
|
|
211
|
+
GUEST,
|
|
212
|
+
);
|
|
213
|
+
expectErrorIncludes(err, "invalid_totp_code");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("a malformed challenge token is rejected without leaking account state", async () => {
|
|
217
|
+
const err = await stack.http.writeErr(
|
|
218
|
+
AuthMfaHandlers.verify,
|
|
219
|
+
{ challengeToken: "not-a-real-token", code: "123456" },
|
|
220
|
+
GUEST,
|
|
221
|
+
);
|
|
222
|
+
expectErrorIncludes(err, "invalid_challenge_token");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("a challenge token for a user without MFA enabled is rejected", async () => {
|
|
226
|
+
const challengeToken = challengeFor("verify-never-enabled-1", GUEST.tenantId);
|
|
227
|
+
const err = await stack.http.writeErr(
|
|
228
|
+
AuthMfaHandlers.verify,
|
|
229
|
+
{ challengeToken, code: "123456" },
|
|
230
|
+
GUEST,
|
|
231
|
+
);
|
|
232
|
+
expectErrorIncludes(err, "invalid_challenge_token");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("mfa verify — re-checks account state the way login.write.ts does", () => {
|
|
237
|
+
test("membership revoked between login and verify → challenge rejected, no fallback to global roles", async () => {
|
|
238
|
+
const { user, secret } = await enableMfaFor(5);
|
|
239
|
+
const challengeToken = challengeFor(user.id, user.tenantId);
|
|
240
|
+
|
|
241
|
+
// Simulate the membership being pulled after the challenge token was
|
|
242
|
+
// issued but before verify runs — the exact gap login.write.ts's
|
|
243
|
+
// noMembership() guards against.
|
|
244
|
+
await asRawClient(stack.db).unsafe(
|
|
245
|
+
`DELETE FROM "${tenantMembershipsTable.tableName}" WHERE user_id = $1 AND tenant_id = $2`,
|
|
246
|
+
[user.id, user.tenantId],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const err = await stack.http.writeErr(
|
|
250
|
+
AuthMfaHandlers.verify,
|
|
251
|
+
{ challengeToken, code: currentTotpCode(secret) },
|
|
252
|
+
GUEST,
|
|
253
|
+
);
|
|
254
|
+
expectErrorIncludes(err, "invalid_challenge_token");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("account restricted between login and verify → challenge rejected", async () => {
|
|
258
|
+
const { user, secret } = await enableMfaFor(6);
|
|
259
|
+
const challengeToken = challengeFor(user.id, user.tenantId);
|
|
260
|
+
|
|
261
|
+
// enableMfaFor already seeded the user row Active — flip it to simulate
|
|
262
|
+
// a restriction landing between login and verify.
|
|
263
|
+
await asRawClient(stack.db).unsafe(
|
|
264
|
+
`UPDATE "${userTable.tableName}" SET status = $1 WHERE id = $2`,
|
|
265
|
+
[USER_STATUS.Restricted, user.id],
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const err = await stack.http.writeErr(
|
|
269
|
+
AuthMfaHandlers.verify,
|
|
270
|
+
{ challengeToken, code: currentTotpCode(secret) },
|
|
271
|
+
GUEST,
|
|
272
|
+
);
|
|
273
|
+
expectErrorIncludes(err, "invalid_challenge_token");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("user row deleted between login and verify → challenge rejected (the !userRow gate)", async () => {
|
|
277
|
+
const { user, secret } = await enableMfaFor(7);
|
|
278
|
+
const challengeToken = challengeFor(user.id, user.tenantId);
|
|
279
|
+
|
|
280
|
+
// Simulate the read_users row disappearing after the challenge token
|
|
281
|
+
// was issued but before verify runs — hits verify.write.ts's
|
|
282
|
+
// `if (!userRow) return invalidChallengeToken()` gate directly, distinct
|
|
283
|
+
// from a revoked membership or a restricted-but-present user row.
|
|
284
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}" WHERE id = $1`, [
|
|
285
|
+
user.id,
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
const err = await stack.http.writeErr(
|
|
289
|
+
AuthMfaHandlers.verify,
|
|
290
|
+
{ challengeToken, code: currentTotpCode(secret) },
|
|
291
|
+
GUEST,
|
|
292
|
+
);
|
|
293
|
+
expectErrorIncludes(err, "invalid_challenge_token");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("user row homed in a DIFFERENT tenant than the challenge → verify succeeds (#1235)", async () => {
|
|
297
|
+
const { user, secret } = await enableMfaFor(8);
|
|
298
|
+
|
|
299
|
+
// Re-home the read_users row into another tenant — the #1235 layout:
|
|
300
|
+
// the user entity is tenant-agnostic (systemStream), so its row's
|
|
301
|
+
// tenant_id need not match the tenant being logged into. The old
|
|
302
|
+
// tenant-scoped UserQueries.detail lookup returned null here and every
|
|
303
|
+
// verify died with invalid_challenge_token; the unscoped findForAuth
|
|
304
|
+
// must resolve the row regardless.
|
|
305
|
+
await asRawClient(stack.db).unsafe(
|
|
306
|
+
`UPDATE "${userTable.tableName}" SET tenant_id = $1 WHERE id = $2`,
|
|
307
|
+
["00000000-0000-4000-8000-0000000000fe", user.id],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const challengeToken = challengeFor(user.id, user.tenantId);
|
|
311
|
+
const res = await stack.http.writeOk<{ session: SessionUser }>(
|
|
312
|
+
AuthMfaHandlers.verify,
|
|
313
|
+
{ challengeToken, code: currentTotpCode(secret) },
|
|
314
|
+
GUEST,
|
|
315
|
+
);
|
|
316
|
+
expect(res.session.id).toBe(user.id);
|
|
317
|
+
expect(res.session.tenantId).toBe(user.tenantId);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// RFC 4648 Base32 (no padding) — the encoding TOTP secrets and otpauth://
|
|
2
|
+
// URIs use. No external dependency: the alphabet lookup is ~20 lines and every
|
|
3
|
+
// authenticator app (Google/Microsoft/Authy) expects exactly this variant.
|
|
4
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
5
|
+
|
|
6
|
+
export function base32Encode(bytes: Buffer): string {
|
|
7
|
+
let bits = 0;
|
|
8
|
+
let value = 0;
|
|
9
|
+
let output = "";
|
|
10
|
+
for (const byte of bytes) {
|
|
11
|
+
value = (value << 8) | byte;
|
|
12
|
+
bits += 8;
|
|
13
|
+
while (bits >= 5) {
|
|
14
|
+
output += ALPHABET[(value >>> (bits - 5)) & 0x1f];
|
|
15
|
+
bits -= 5;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (bits > 0) {
|
|
19
|
+
output += ALPHABET[(value << (5 - bits)) & 0x1f];
|
|
20
|
+
}
|
|
21
|
+
return output;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Accepts lowercase and stray whitespace (authenticator apps and users
|
|
25
|
+
// copy-pasting secrets are inconsistent about casing) — normalizes case and
|
|
26
|
+
// strips whitespace before decoding, but throws on a genuinely invalid
|
|
27
|
+
// character so a corrupted stored secret fails loud at read time instead of
|
|
28
|
+
// silently producing wrong codes.
|
|
29
|
+
export function base32Decode(encoded: string): Buffer {
|
|
30
|
+
const clean = encoded.trim().toUpperCase().replace(/\s+/g, "").replace(/=+$/, "");
|
|
31
|
+
let bits = 0;
|
|
32
|
+
let value = 0;
|
|
33
|
+
const bytes: number[] = [];
|
|
34
|
+
for (const char of clean) {
|
|
35
|
+
const index = ALPHABET.indexOf(char);
|
|
36
|
+
if (index === -1) throw new Error(`base32Decode: invalid character "${char}"`);
|
|
37
|
+
value = (value << 5) | index;
|
|
38
|
+
bits += 5;
|
|
39
|
+
if (bits >= 8) {
|
|
40
|
+
bytes.push((value >>> (bits - 8)) & 0xff);
|
|
41
|
+
bits -= 8;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Leftover bits must be padding zeros (a canonical-length encoding never
|
|
45
|
+
// leaves a full byte's worth, i.e. >=5 residual bits) — anything else means
|
|
46
|
+
// truncated/non-canonical input decoded to the wrong byte count.
|
|
47
|
+
if (bits >= 5 || (value & ((1 << bits) - 1)) !== 0) {
|
|
48
|
+
throw new Error("base32Decode: invalid length or non-zero padding bits");
|
|
49
|
+
}
|
|
50
|
+
return Buffer.from(bytes);
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Tenant-scoped enforcement policy: does a user need MFA to log in even if
|
|
2
|
+
// they haven't opted in themselves? Default "optional" means the feature
|
|
3
|
+
// is fully opt-in (existing tenants/users unaffected — no migration step).
|
|
4
|
+
//
|
|
5
|
+
// ponytail: "admins"/"all" HARD-BLOCK login for an unenrolled user (see
|
|
6
|
+
// mfa-status-checker.ts's setupRequired branch) — there is no in-band way
|
|
7
|
+
// for a blocked user to complete enrollment yet (that flow ships in PR3's
|
|
8
|
+
// UI). Flipping this away from "optional" before PR3 locks out every
|
|
9
|
+
// unenrolled matching user with no recovery path. Fine for a tenant that
|
|
10
|
+
// enrolls its admins out-of-band first; a footgun otherwise.
|
|
11
|
+
import { type ConfigKeyHandle, createTenantConfig } from "@cosmicdrift/kumiko-framework/engine";
|
|
12
|
+
|
|
13
|
+
export const MFA_REQUIRED_POLICIES = ["optional", "admins", "all"] as const;
|
|
14
|
+
export type MfaRequiredPolicy = (typeof MFA_REQUIRED_POLICIES)[number];
|
|
15
|
+
|
|
16
|
+
export const mfaRequiredConfigHandle: ConfigKeyHandle<"select"> = {
|
|
17
|
+
name: "auth-mfa:config:required",
|
|
18
|
+
type: "select",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function mfaRequiredConfigKey() {
|
|
22
|
+
return createTenantConfig("select", {
|
|
23
|
+
default: "optional" satisfies MfaRequiredPolicy,
|
|
24
|
+
options: [...MFA_REQUIRED_POLICIES],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Pure constants — client-marked so auth-mfa/web/ may import handler QNs
|
|
3
|
+
// and the screen id without pulling the feature's server runtime barrel.
|
|
4
|
+
// Qualified write-handler names — feature is registered as "auth-mfa", each
|
|
5
|
+
// short `name` below gets auto-prefixed to "auth-mfa:write:<name>" by
|
|
6
|
+
// r.writeHandler. Exported so cross-feature wiring (login.write.ts,
|
|
7
|
+
// run-prod-app.ts) can reference them without hardcoding the string.
|
|
8
|
+
export const AUTH_MFA_FEATURE = "auth-mfa" as const;
|
|
9
|
+
|
|
10
|
+
export const AuthMfaHandlers = {
|
|
11
|
+
enableStart: "auth-mfa:write:enable-start",
|
|
12
|
+
enableStartPreauth: "auth-mfa:write:enable-start-preauth",
|
|
13
|
+
enableConfirm: "auth-mfa:write:enable-confirm",
|
|
14
|
+
enableConfirmPreauth: "auth-mfa:write:enable-confirm-preauth",
|
|
15
|
+
disable: "auth-mfa:write:disable",
|
|
16
|
+
regenerateRecovery: "auth-mfa:write:regenerate-recovery",
|
|
17
|
+
verify: "auth-mfa:write:verify",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export const AuthMfaQueries = {
|
|
21
|
+
status: "auth-mfa:query:user-mfa:status",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
// Write-failure codes minted server-side into UnprocessableError (errors.ts).
|
|
25
|
+
// The client reads the i18n key straight off the write result
|
|
26
|
+
// (res.error.i18nKey) instead of re-deriving it from these codes.
|
|
27
|
+
export const AuthMfaErrorCodes = {
|
|
28
|
+
mfaAlreadyEnabled: "mfa_already_enabled",
|
|
29
|
+
mfaNotEnabled: "mfa_not_enabled",
|
|
30
|
+
invalidSetupToken: "invalid_setup_token",
|
|
31
|
+
invalidTotpCode: "invalid_totp_code",
|
|
32
|
+
invalidRecoveryCode: "invalid_recovery_code",
|
|
33
|
+
invalidChallengeToken: "invalid_challenge_token",
|
|
34
|
+
tooManyAttempts: "too_many_attempts",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export const MFA_SETUP_TOKEN_TTL_MINUTES = 10;
|
|
38
|
+
export const MFA_CHALLENGE_TOKEN_TTL_MINUTES = 10;
|
|
39
|
+
export const MFA_PREAUTH_SETUP_TOKEN_TTL_MINUTES = 10;
|
|
40
|
+
export const MFA_VERIFY_MAX_ATTEMPTS = 5;
|
|
41
|
+
export const MFA_VERIFY_LOCKOUT_MINUTES = 5;
|
|
42
|
+
|
|
43
|
+
// Dormant custom-screen id — see personal-access-tokens/feature.ts for the
|
|
44
|
+
// same convention. App places it via r.nav in its logged-in settings area.
|
|
45
|
+
export const MFA_ENABLE_SCREEN_ID = "auth-mfa-enable";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
|
+
import { userMfaEntity, userMfaTable } from "../schema/user-mfa";
|
|
6
|
+
|
|
7
|
+
export type UserMfaRow = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly version: number;
|
|
10
|
+
readonly userId: string;
|
|
11
|
+
readonly totpSecret: string;
|
|
12
|
+
readonly recoveryCodes: { readonly hashes: readonly string[] };
|
|
13
|
+
readonly enabledAt: string;
|
|
14
|
+
readonly lastUsedAt: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const executor = createEventStoreExecutor(userMfaTable, userMfaEntity, {
|
|
18
|
+
entityName: "user-mfa",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// `totpSecret`/`recoveryCodes` are `encrypted: true` — a raw fetchOne would
|
|
22
|
+
// return ciphertext, not plaintext. fetchOne only locates the row's id (no
|
|
23
|
+
// decryption needed for that); executor.detail() does the real, decrypting
|
|
24
|
+
// read the same way every other entity handler does. `recoveryCodes` is
|
|
25
|
+
// stored as a JSON string (schema/user-mfa.ts — both encryption layers need
|
|
26
|
+
// a string value) — parsed back into `{ hashes }` here, once, for every
|
|
27
|
+
// caller instead of each handler re-parsing it. A parse failure here means
|
|
28
|
+
// stored data is corrupt (post-decrypt, not user input) — fail loud.
|
|
29
|
+
export async function findUserMfaRow(db: TenantDb, user: SessionUser): Promise<UserMfaRow | null> {
|
|
30
|
+
const idLookup = await fetchOne<{ id: string }>(db, userMfaTable, {
|
|
31
|
+
userId: user.id,
|
|
32
|
+
tenantId: user.tenantId,
|
|
33
|
+
});
|
|
34
|
+
if (!idLookup) return null;
|
|
35
|
+
const detail = await executor.detail({ id: idLookup.id }, user, db);
|
|
36
|
+
if (!detail) return null;
|
|
37
|
+
// @cast-boundary engine-payload — executor.detail returns a decrypted,
|
|
38
|
+
// untyped Record; the userMfa entity's own field shape narrows it.
|
|
39
|
+
const row = detail as Omit<UserMfaRow, "recoveryCodes"> & { recoveryCodes: string };
|
|
40
|
+
const recoveryCodes = parseJsonOrThrow<{ hashes: readonly string[] }>(
|
|
41
|
+
row.recoveryCodes,
|
|
42
|
+
"user-mfa.recoveryCodes",
|
|
43
|
+
);
|
|
44
|
+
return { ...row, recoveryCodes };
|
|
45
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UnprocessableError,
|
|
3
|
+
type WriteFailure,
|
|
4
|
+
writeFailure,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
6
|
+
import { AuthMfaErrorCodes } from "./constants";
|
|
7
|
+
|
|
8
|
+
export { AuthMfaErrorCodes as AuthMfaErrors } from "./constants";
|
|
9
|
+
|
|
10
|
+
export function mfaAlreadyEnabled(): WriteFailure {
|
|
11
|
+
return writeFailure(
|
|
12
|
+
new UnprocessableError(AuthMfaErrorCodes.mfaAlreadyEnabled, {
|
|
13
|
+
i18nKey: "auth.mfa.errors.mfaAlreadyEnabled",
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mfaNotEnabled(): WriteFailure {
|
|
19
|
+
return writeFailure(
|
|
20
|
+
new UnprocessableError(AuthMfaErrorCodes.mfaNotEnabled, {
|
|
21
|
+
i18nKey: "auth.mfa.errors.mfaNotEnabled",
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function invalidSetupToken(): WriteFailure {
|
|
27
|
+
return writeFailure(
|
|
28
|
+
new UnprocessableError(AuthMfaErrorCodes.invalidSetupToken, {
|
|
29
|
+
i18nKey: "auth.mfa.errors.invalidSetupToken",
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function invalidTotpCode(): WriteFailure {
|
|
35
|
+
return writeFailure(
|
|
36
|
+
new UnprocessableError(AuthMfaErrorCodes.invalidTotpCode, {
|
|
37
|
+
i18nKey: "auth.mfa.errors.invalidTotpCode",
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function invalidRecoveryCode(): WriteFailure {
|
|
43
|
+
return writeFailure(
|
|
44
|
+
new UnprocessableError(AuthMfaErrorCodes.invalidRecoveryCode, {
|
|
45
|
+
i18nKey: "auth.mfa.errors.invalidRecoveryCode",
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function invalidChallengeToken(): WriteFailure {
|
|
51
|
+
return writeFailure(
|
|
52
|
+
new UnprocessableError(AuthMfaErrorCodes.invalidChallengeToken, {
|
|
53
|
+
i18nKey: "auth.mfa.errors.invalidChallengeToken",
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// retryAfterSeconds drives the login UI countdown — must stay > 0.
|
|
59
|
+
export function tooManyAttempts(retryAfterSeconds: number): WriteFailure {
|
|
60
|
+
return writeFailure(
|
|
61
|
+
new UnprocessableError(AuthMfaErrorCodes.tooManyAttempts, {
|
|
62
|
+
i18nKey: "auth.mfa.errors.tooManyAttempts",
|
|
63
|
+
details: { retryAfterSeconds },
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
}
|