@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
2
3
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
4
|
import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
5
|
import { createRegistry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -19,7 +20,7 @@ import { createUserFeature } from "../../user/feature";
|
|
|
19
20
|
import { createSessionsFeature } from "../feature";
|
|
20
21
|
import { userSessionEntity, userSessionTable } from "../schema/user-session";
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
+
// store_user_sessions is a hot-path direct-write store: sessionCreator inserts
|
|
23
24
|
// rows and the revoke handlers update them WITHOUT emitting lifecycle events.
|
|
24
25
|
// If the table is registered as an r.entity, the framework makes it a
|
|
25
26
|
// rebuildable implicit projection whose replay finds zero matching events and
|
|
@@ -27,7 +28,7 @@ import { userSessionEntity, userSessionTable } from "../schema/user-session";
|
|
|
27
28
|
// session on the next projection rebuild (deploy / `schema apply`). #498/#494.
|
|
28
29
|
//
|
|
29
30
|
// Pre-fix both tests are RED: the implicit projection "sessions:projection:
|
|
30
|
-
// user-session-entity" exists and rebuilding it empties
|
|
31
|
+
// user-session-entity" exists and rebuilding it empties store_user_sessions.
|
|
31
32
|
// Post-fix (r.unmanagedTable) the table is no longer a rebuild target.
|
|
32
33
|
|
|
33
34
|
const IMPLICIT_PROJECTION = "sessions:projection:user-session-entity";
|
|
@@ -48,7 +49,7 @@ afterAll(async () => {
|
|
|
48
49
|
|
|
49
50
|
beforeEach(async () => {
|
|
50
51
|
await asRawClient(testDb.db).unsafe(
|
|
51
|
-
"TRUNCATE
|
|
52
|
+
"TRUNCATE store_user_sessions, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE",
|
|
52
53
|
);
|
|
53
54
|
});
|
|
54
55
|
|
|
@@ -56,7 +57,9 @@ beforeEach(async () => {
|
|
|
56
57
|
// written directly on the hot path, then revoked — no events anywhere.
|
|
57
58
|
const SID = "00000000-0000-0000-0000-000000000001";
|
|
58
59
|
|
|
59
|
-
async function insertRevokedSession(
|
|
60
|
+
async function insertRevokedSession(
|
|
61
|
+
db: DbConnection | ReturnType<typeof createTenantDb>,
|
|
62
|
+
): Promise<void> {
|
|
60
63
|
const now = Temporal.Now.instant();
|
|
61
64
|
await seedRow(db, userSessionTable, {
|
|
62
65
|
id: SID,
|
|
@@ -70,16 +73,24 @@ async function insertRevokedSession(db: DbConnection): Promise<void> {
|
|
|
70
73
|
await updateRows(db, userSessionTable, { revokedAt: now }, { id: SID, revokedAt: null });
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
describe("sessions /
|
|
76
|
+
describe("sessions / store_user_sessions survives projection rebuild", () => {
|
|
74
77
|
test("is NOT registered as a rebuildable implicit projection", () => {
|
|
75
|
-
const registry = createRegistry([
|
|
78
|
+
const registry = createRegistry([
|
|
79
|
+
authFoundationFeature,
|
|
80
|
+
createSessionsFeature(),
|
|
81
|
+
createUserFeature(),
|
|
82
|
+
]);
|
|
76
83
|
expect(registry.getAllProjections().has(IMPLICIT_PROJECTION)).toBe(false);
|
|
77
84
|
});
|
|
78
85
|
|
|
79
86
|
test("direct-written rows (incl. revoked state) survive a rebuild", async () => {
|
|
80
87
|
await insertRevokedSession(createTenantDb(testDb.db, TENANT));
|
|
81
88
|
|
|
82
|
-
const registry = createRegistry([
|
|
89
|
+
const registry = createRegistry([
|
|
90
|
+
authFoundationFeature,
|
|
91
|
+
createSessionsFeature(),
|
|
92
|
+
createUserFeature(),
|
|
93
|
+
]);
|
|
83
94
|
// Pre-fix: the implicit projection exists → rebuild swaps an empty shadow
|
|
84
95
|
// → rows wiped. Post-fix: absent → no rebuild → rows untouched. Either way
|
|
85
96
|
// a regression (re-adding r.entity) makes this fail.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// sessions self-registers as auth-foundation's sessionStore provider (#1371)
|
|
2
|
+
// instead of only being wired by hand into buildServer({ auth: { ... } }).
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { EXT_SESSION_STORE } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
6
|
+
import { createSessionsFeature } from "../feature";
|
|
7
|
+
|
|
8
|
+
describe("createSessionsFeature — sessionStore registration (#1371)", () => {
|
|
9
|
+
test('registers via r.useExtension(EXT_SESSION_STORE, "default", ...)', () => {
|
|
10
|
+
const feature = createSessionsFeature();
|
|
11
|
+
const usage = feature.extensionUsages.find((u) => u.extensionName === EXT_SESSION_STORE);
|
|
12
|
+
expect(usage).toBeDefined();
|
|
13
|
+
expect(usage?.entityName).toBe("default");
|
|
14
|
+
expect(typeof (usage?.options as { build?: unknown } | undefined)?.build).toBe("function");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("requires auth-foundation (owner of EXT_SESSION_STORE)", () => {
|
|
18
|
+
const feature = createSessionsFeature();
|
|
19
|
+
expect(feature.requires).toContain("auth-foundation");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
|
|
4
|
+
import { authFoundationFeature } from "../../auth-foundation";
|
|
5
|
+
import { createConfigFeature } from "../../config/feature";
|
|
6
|
+
import { createPersonalAccessTokensFeature } from "../../personal-access-tokens";
|
|
7
|
+
import { createTenantFeature } from "../../tenant";
|
|
8
|
+
import { createUserFeature } from "../../user/feature";
|
|
9
|
+
import { SESSION_DETAIL_SCREEN_ID, SESSION_LIST_SCREEN_ID } from "../constants";
|
|
10
|
+
import { createSessionsFeature } from "../feature";
|
|
11
|
+
|
|
12
|
+
describe("sessions screens + query access alignment (kumiko-framework#255)", () => {
|
|
13
|
+
const features = [
|
|
14
|
+
createConfigFeature(),
|
|
15
|
+
createUserFeature(),
|
|
16
|
+
createTenantFeature(),
|
|
17
|
+
authFoundationFeature,
|
|
18
|
+
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
19
|
+
createSessionsFeature(),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
test("boot-validates with session-list/session-detail screens registered", () => {
|
|
23
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("session-list is projectionList, session-detail is projectionDetail, both admin-gated", () => {
|
|
27
|
+
const sessions = createSessionsFeature();
|
|
28
|
+
const list = sessions.screens[SESSION_LIST_SCREEN_ID];
|
|
29
|
+
expect(list?.type).toBe("projectionList");
|
|
30
|
+
const detail = sessions.screens[SESSION_DETAIL_SCREEN_ID];
|
|
31
|
+
expect(detail?.type).toBe("projectionDetail");
|
|
32
|
+
for (const screen of [list, detail]) {
|
|
33
|
+
if (screen && "access" in screen && screen.access && "roles" in screen.access) {
|
|
34
|
+
expect(screen.access.roles).toEqual(["TenantAdmin", "Admin", "SystemAdmin"]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("session-list row-action navigates to session-detail with entityId 'id'", () => {
|
|
40
|
+
const sessions = createSessionsFeature();
|
|
41
|
+
const list = sessions.screens[SESSION_LIST_SCREEN_ID];
|
|
42
|
+
if (list?.type !== "projectionList") throw new Error("expected projectionList");
|
|
43
|
+
const openAction = list.rowActions?.find((a) => a.id === "open");
|
|
44
|
+
if (openAction?.kind !== "navigate") throw new Error("expected a navigate rowAction");
|
|
45
|
+
expect(openAction.screen).toBe(SESSION_DETAIL_SCREEN_ID);
|
|
46
|
+
expect(openAction.entityId).toBe("id");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("sessions queries share the admin-or-higher access rule", () => {
|
|
50
|
+
const sessions = createSessionsFeature();
|
|
51
|
+
const roles = ["TenantAdmin", "Admin", "SystemAdmin"];
|
|
52
|
+
expect(rolesOf(sessions.queryHandlers["user-session:list"]?.access)).toEqual(roles);
|
|
53
|
+
expect(rolesOf(sessions.queryHandlers["user-session:detail"]?.access)).toEqual(roles);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
3
4
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
configureBlindIndexKey,
|
|
7
|
+
configurePiiSubjectKms,
|
|
8
|
+
InMemoryKmsAdapter,
|
|
9
|
+
isPiiCiphertext,
|
|
10
|
+
resetBlindIndexKeyForTests,
|
|
11
|
+
resetPiiSubjectKmsForTests,
|
|
12
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
5
13
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
+
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
6
15
|
import {
|
|
7
16
|
setupTestStack,
|
|
8
17
|
type TestStack,
|
|
18
|
+
TestUsers,
|
|
9
19
|
testTenantId,
|
|
10
20
|
unsafeCreateEntityTable,
|
|
11
21
|
unsafePushTables,
|
|
12
22
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
13
23
|
import {
|
|
14
24
|
createLateBoundHolder,
|
|
25
|
+
createTestEnvelopeCipher,
|
|
15
26
|
deleteRows,
|
|
16
27
|
resetTestTables,
|
|
17
28
|
updateRows,
|
|
@@ -31,6 +42,7 @@ import { SessionHandlers, SessionQueries } from "../constants";
|
|
|
31
42
|
import { createSessionsFeature } from "../feature";
|
|
32
43
|
import { userSessionEntity, userSessionTable } from "../schema/user-session";
|
|
33
44
|
import { createSessionCallbacks, type SessionCallbacks } from "../session-callbacks";
|
|
45
|
+
import { SESSION_REVOKED_EVENT_QN, type SessionRevokedPayload } from "../session-revoked-event";
|
|
34
46
|
import { sessionCallbacksFromLateBound } from "../testing";
|
|
35
47
|
import { makeSessionHelpers } from "./test-helpers";
|
|
36
48
|
|
|
@@ -47,8 +59,8 @@ const encryptionKey = randomBytes(32).toString("base64");
|
|
|
47
59
|
const TENANT: TenantId = testTenantId(1);
|
|
48
60
|
|
|
49
61
|
beforeAll(async () => {
|
|
50
|
-
const encryption =
|
|
51
|
-
const resolver = createConfigResolver({ encryption });
|
|
62
|
+
const encryption = createTestEnvelopeCipher(encryptionKey);
|
|
63
|
+
const resolver = createConfigResolver({ cipher: encryption });
|
|
52
64
|
const bound = sessionCallbacksFromLateBound(callbacks);
|
|
53
65
|
|
|
54
66
|
stack = await setupTestStack({
|
|
@@ -57,6 +69,7 @@ beforeAll(async () => {
|
|
|
57
69
|
createUserFeature(),
|
|
58
70
|
createTenantFeature(),
|
|
59
71
|
createAuthEmailPasswordFeature(),
|
|
72
|
+
authFoundationFeature,
|
|
60
73
|
createSessionsFeature(),
|
|
61
74
|
],
|
|
62
75
|
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
@@ -67,12 +80,13 @@ beforeAll(async () => {
|
|
|
67
80
|
},
|
|
68
81
|
});
|
|
69
82
|
callbacks.set(createSessionCallbacks({ db: stack.db }));
|
|
70
|
-
h = makeSessionHelpers(stack, TENANT);
|
|
83
|
+
h = makeSessionHelpers(stack, TENANT, bound.asAuthConfig().sessionCreator);
|
|
71
84
|
|
|
72
85
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
73
86
|
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
74
87
|
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
75
88
|
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
89
|
+
await createEventsTable(stack.db);
|
|
76
90
|
});
|
|
77
91
|
|
|
78
92
|
afterAll(async () => {
|
|
@@ -80,7 +94,12 @@ afterAll(async () => {
|
|
|
80
94
|
});
|
|
81
95
|
|
|
82
96
|
beforeEach(async () => {
|
|
83
|
-
await resetTestTables(stack.db, [
|
|
97
|
+
await resetTestTables(stack.db, [
|
|
98
|
+
userTable,
|
|
99
|
+
tenantMembershipsTable,
|
|
100
|
+
userSessionTable,
|
|
101
|
+
eventsTable,
|
|
102
|
+
]);
|
|
84
103
|
});
|
|
85
104
|
|
|
86
105
|
describe("sessions feature — login → check → revoke → rejected", () => {
|
|
@@ -89,9 +108,10 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
89
108
|
const { sid } = await h.login("persist@example.com", "pw-long-enough");
|
|
90
109
|
|
|
91
110
|
const rows = await selectMany(stack.db, userSessionTable);
|
|
92
|
-
|
|
93
|
-
expect(
|
|
94
|
-
expect(
|
|
111
|
+
const userRows = rows.filter((r) => r["userId"] !== TestUsers.systemAdmin.id);
|
|
112
|
+
expect(userRows).toHaveLength(1);
|
|
113
|
+
expect(userRows[0]?.["id"]).toBe(sid);
|
|
114
|
+
expect(userRows[0]?.["revokedAt"]).toBeNull();
|
|
95
115
|
});
|
|
96
116
|
|
|
97
117
|
test("authenticated request passes while session is live, 401s once revoked", async () => {
|
|
@@ -130,8 +150,9 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
130
150
|
expect(logoutRes.status).toBe(200);
|
|
131
151
|
|
|
132
152
|
const rows = await selectMany(stack.db, userSessionTable);
|
|
133
|
-
|
|
134
|
-
expect(
|
|
153
|
+
const row = rows.find((r) => r["id"] === sid);
|
|
154
|
+
expect(row?.["id"]).toBe(sid);
|
|
155
|
+
expect(row?.["revokedAt"]).not.toBeNull();
|
|
135
156
|
|
|
136
157
|
const next = await h.authedPost("/api/query", token, {
|
|
137
158
|
type: "user:query:user:me",
|
|
@@ -178,6 +199,69 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
178
199
|
expect(preservedRevokedAt?.epochMilliseconds).toBe(originalRevokedAt?.epochMilliseconds);
|
|
179
200
|
});
|
|
180
201
|
|
|
202
|
+
// #1559 — store_user_sessions stays a direct-write store (no CRUD
|
|
203
|
+
// lifecycle events, see feature.ts), but a successful revoke must still
|
|
204
|
+
// append a lightweight session-revoked event so a cross-instance
|
|
205
|
+
// consumer (#1560) can react. Asserted directly against kumiko_events —
|
|
206
|
+
// the response body carries no trace of it.
|
|
207
|
+
test("revoke appends a session-revoked event carrying the userId + sid", async () => {
|
|
208
|
+
const { userId } = await h.seedUser("event1@example.com", "pw-long-enough");
|
|
209
|
+
const { token, sid } = await h.login("event1@example.com", "pw-long-enough");
|
|
210
|
+
|
|
211
|
+
const res = await h.authedPost("/api/write", token, {
|
|
212
|
+
type: SessionHandlers.revoke,
|
|
213
|
+
payload: { id: sid },
|
|
214
|
+
});
|
|
215
|
+
expect(res.status).toBe(200);
|
|
216
|
+
|
|
217
|
+
const events = await selectMany(stack.db, eventsTable, { type: SESSION_REVOKED_EVENT_QN });
|
|
218
|
+
expect(events).toHaveLength(1);
|
|
219
|
+
const payload = events[0]?.["payload"] as SessionRevokedPayload;
|
|
220
|
+
expect(payload.userId).toBe(userId);
|
|
221
|
+
expect(payload.sessionIds).toEqual([sid]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Negative case: an already-revoked sid must NOT emit a second event —
|
|
225
|
+
// a spurious invalidation is the failure mode #1559 guards against.
|
|
226
|
+
test("revoking an already-revoked sid emits no additional event", async () => {
|
|
227
|
+
await h.seedUser("event2@example.com", "pw-long-enough");
|
|
228
|
+
const first = await h.login("event2@example.com", "pw-long-enough");
|
|
229
|
+
|
|
230
|
+
await h.authedPost("/api/write", first.token, {
|
|
231
|
+
type: SessionHandlers.revoke,
|
|
232
|
+
payload: { id: first.sid },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const second = await h.login("event2@example.com", "pw-long-enough");
|
|
236
|
+
const retry = await h.authedPost("/api/write", second.token, {
|
|
237
|
+
type: SessionHandlers.revoke,
|
|
238
|
+
payload: { id: first.sid },
|
|
239
|
+
});
|
|
240
|
+
expect(retry.status).toBe(422);
|
|
241
|
+
|
|
242
|
+
const events = await selectMany(stack.db, eventsTable, { type: SESSION_REVOKED_EVENT_QN });
|
|
243
|
+
expect(events).toHaveLength(1); // only the first, successful revoke
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("revoke-all-others appends one event listing every revoked sid", async () => {
|
|
247
|
+
const { userId } = await h.seedUser("event3@example.com", "pw-long-enough");
|
|
248
|
+
const a = await h.login("event3@example.com", "pw-long-enough");
|
|
249
|
+
const b = await h.login("event3@example.com", "pw-long-enough");
|
|
250
|
+
const c = await h.login("event3@example.com", "pw-long-enough");
|
|
251
|
+
|
|
252
|
+
const res = await h.authedPost("/api/write", b.token, {
|
|
253
|
+
type: SessionHandlers.revokeAllOthers,
|
|
254
|
+
payload: {},
|
|
255
|
+
});
|
|
256
|
+
expect(res.status).toBe(200);
|
|
257
|
+
|
|
258
|
+
const events = await selectMany(stack.db, eventsTable, { type: SESSION_REVOKED_EVENT_QN });
|
|
259
|
+
expect(events).toHaveLength(1);
|
|
260
|
+
const payload = events[0]?.["payload"] as SessionRevokedPayload;
|
|
261
|
+
expect(payload.userId).toBe(userId);
|
|
262
|
+
expect(new Set(payload.sessionIds)).toEqual(new Set([a.sid, c.sid]));
|
|
263
|
+
});
|
|
264
|
+
|
|
181
265
|
test("session:mine only returns live sessions, marks the current one", async () => {
|
|
182
266
|
await h.seedUser("mine@example.com", "pw-long-enough");
|
|
183
267
|
// Three logins = three sessions (think: browser, mobile app, tablet)
|
|
@@ -286,12 +370,13 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
286
370
|
expect(body.error?.details?.reason).toBe("ownership_denied");
|
|
287
371
|
});
|
|
288
372
|
|
|
289
|
-
test("revoke-all-others on a sidless JWT
|
|
373
|
+
test("revoke-all-others on a sidless JWT is rejected by the middleware (no_sid)", async () => {
|
|
290
374
|
const { userId } = await h.seedUser("sidless@example.com", "pw-long-enough");
|
|
291
375
|
|
|
292
|
-
// Hand-sign a JWT without jti
|
|
293
|
-
//
|
|
294
|
-
//
|
|
376
|
+
// Hand-sign a JWT without jti. Once a sessionChecker is wired there is
|
|
377
|
+
// no relaxed mode (see session-strict-mode.integration.test.ts) — the
|
|
378
|
+
// middleware rejects it with no_sid before the handler's own
|
|
379
|
+
// session_required guard is ever reached.
|
|
295
380
|
const tokenNoSid = await stack.jwt.sign({ id: userId, tenantId: TENANT, roles: ["User"] });
|
|
296
381
|
|
|
297
382
|
const res = await stack.http.raw(
|
|
@@ -300,9 +385,9 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
300
385
|
{ type: SessionHandlers.revokeAllOthers, payload: {} },
|
|
301
386
|
{ Authorization: `Bearer ${tokenNoSid}` },
|
|
302
387
|
);
|
|
303
|
-
expect(res.status).toBe(
|
|
388
|
+
expect(res.status).toBe(401);
|
|
304
389
|
const body = (await res.json()) as { error?: { details?: { reason?: string } } };
|
|
305
|
-
expect(body.error?.details?.reason).toBe("
|
|
390
|
+
expect(body.error?.details?.reason).toBe("no_sid");
|
|
306
391
|
});
|
|
307
392
|
|
|
308
393
|
test("N concurrent revoke+logout requests on the same sid converge safely", async () => {
|
|
@@ -383,6 +468,41 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
383
468
|
expect(body.error?.details?.reason).toBe("missing");
|
|
384
469
|
});
|
|
385
470
|
|
|
471
|
+
// Cross-user check: a valid JWT for Alice paired with Bob's live sid must
|
|
472
|
+
// look identical to a forged/missing sid — otherwise the checker would be
|
|
473
|
+
// an existence oracle on other users' sessions.
|
|
474
|
+
test("sid belonging to another user → 401 reason=missing (no existence oracle)", async () => {
|
|
475
|
+
const alice = await h.seedUser("alice-xuser@example.com", "pw-long-enough");
|
|
476
|
+
const bob = await h.seedUser("bob-xuser@example.com", "pw-long-enough");
|
|
477
|
+
const bobLogin = await h.login("bob-xuser@example.com", "pw-long-enough");
|
|
478
|
+
|
|
479
|
+
// Direct invariant: Bob's sid + Alice's userId → missing (not live/revoked).
|
|
480
|
+
expect(await callbacks.get().sessionChecker(bobLogin.sid, alice.userId)).toBe("missing");
|
|
481
|
+
expect(await callbacks.get().sessionChecker(bobLogin.sid, bob.userId)).toBe("live");
|
|
482
|
+
|
|
483
|
+
// HTTP path: mint Alice's identity onto Bob's sid (stolen-sid scenario).
|
|
484
|
+
const forged = await stack.jwt.sign({
|
|
485
|
+
id: alice.userId,
|
|
486
|
+
tenantId: TENANT,
|
|
487
|
+
roles: ["User"],
|
|
488
|
+
sid: bobLogin.sid,
|
|
489
|
+
});
|
|
490
|
+
const res = await h.authedPost("/api/query", forged, {
|
|
491
|
+
type: "user:query:user:me",
|
|
492
|
+
payload: {},
|
|
493
|
+
});
|
|
494
|
+
expect(res.status).toBe(401);
|
|
495
|
+
const body = (await res.json()) as { error?: { details?: { reason?: string } } };
|
|
496
|
+
expect(body.error?.details?.reason).toBe("missing");
|
|
497
|
+
|
|
498
|
+
// Control: Bob's own JWT still authenticates — the sid itself is fine.
|
|
499
|
+
const bobOk = await h.authedPost("/api/query", bobLogin.token, {
|
|
500
|
+
type: "user:query:user:me",
|
|
501
|
+
payload: {},
|
|
502
|
+
});
|
|
503
|
+
expect(bobOk.status).toBe(200);
|
|
504
|
+
});
|
|
505
|
+
|
|
386
506
|
test("expired session row → 401 with reason=expired", async () => {
|
|
387
507
|
await h.seedUser("stale@example.com", "pw-long-enough");
|
|
388
508
|
const { token, sid } = await h.login("stale@example.com", "pw-long-enough");
|
|
@@ -448,10 +568,13 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
448
568
|
revokedAt: string | null;
|
|
449
569
|
}>;
|
|
450
570
|
};
|
|
451
|
-
// Three rows total: Alice's pre-promotion session,
|
|
452
|
-
// session, Bob's session.
|
|
453
|
-
|
|
454
|
-
|
|
571
|
+
// Three rows total for the two test users: Alice's pre-promotion session,
|
|
572
|
+
// Alice's post-promotion session, Bob's session. seedUser's bootstrap
|
|
573
|
+
// actor (systemAdmin) also holds live sessions in this tenant — exclude
|
|
574
|
+
// them, they aren't part of what this test pins.
|
|
575
|
+
const nonSystemRows = body.data.filter((r) => r.userId !== TestUsers.systemAdmin.id);
|
|
576
|
+
expect(nonSystemRows).toHaveLength(3);
|
|
577
|
+
const userIds = new Set(nonSystemRows.map((r) => r.userId));
|
|
455
578
|
expect(userIds.size).toBe(2);
|
|
456
579
|
|
|
457
580
|
// Order: most-recently-created first. aliceAsAdmin's session was the
|
|
@@ -459,6 +582,94 @@ describe("sessions feature — login → check → revoke → rejected", () => {
|
|
|
459
582
|
// silent orderBy removal.
|
|
460
583
|
expect(body.data[0]?.id).toBe(aliceAsAdmin.sid);
|
|
461
584
|
});
|
|
585
|
+
|
|
586
|
+
// Single-row inspector backing the session-detail screen (kumiko-framework#255).
|
|
587
|
+
// Same access-gate as session:list (admin-or-higher); verifies field-shape,
|
|
588
|
+
// decryption of ip/userAgent, and the "unknown id" not-found path.
|
|
589
|
+
test("session:detail returns one decrypted row for admins, null for an unknown id", async () => {
|
|
590
|
+
const { userId: aliceId } = await h.seedUser("alice3@example.com", "pw-long-enough");
|
|
591
|
+
|
|
592
|
+
await updateRows(
|
|
593
|
+
stack.db,
|
|
594
|
+
tenantMembershipsTable,
|
|
595
|
+
{ roles: JSON.stringify(["Admin"]) },
|
|
596
|
+
{ userId: aliceId, tenantId: TENANT },
|
|
597
|
+
);
|
|
598
|
+
const aliceAsAdmin = await h.login("alice3@example.com", "pw-long-enough");
|
|
599
|
+
|
|
600
|
+
const res = await h.authedPost("/api/query", aliceAsAdmin.token, {
|
|
601
|
+
type: SessionQueries.detail,
|
|
602
|
+
payload: { id: aliceAsAdmin.sid },
|
|
603
|
+
});
|
|
604
|
+
expect(res.status).toBe(200);
|
|
605
|
+
const body = (await res.json()) as {
|
|
606
|
+
data: { id: string; userId: string; revokedAt: string | null } | null;
|
|
607
|
+
};
|
|
608
|
+
expect(body.data?.id).toBe(aliceAsAdmin.sid);
|
|
609
|
+
expect(body.data?.userId).toBe(aliceId);
|
|
610
|
+
expect(body.data?.revokedAt).toBeNull();
|
|
611
|
+
|
|
612
|
+
const missing = await h.authedPost("/api/query", aliceAsAdmin.token, {
|
|
613
|
+
type: SessionQueries.detail,
|
|
614
|
+
payload: { id: "00000000-0000-4000-8000-0000deadbeef" },
|
|
615
|
+
});
|
|
616
|
+
expect(missing.status).toBe(200);
|
|
617
|
+
const missingBody = (await missing.json()) as { data: unknown };
|
|
618
|
+
expect(missingBody.data).toBeNull();
|
|
619
|
+
|
|
620
|
+
// Plain User gets the same 403 as session:list — same access-gate.
|
|
621
|
+
await h.seedUser("bob3@example.com", "pw-long-enough");
|
|
622
|
+
const bob = await h.login("bob3@example.com", "pw-long-enough");
|
|
623
|
+
const asUser = await h.authedPost("/api/query", bob.token, {
|
|
624
|
+
type: SessionQueries.detail,
|
|
625
|
+
payload: { id: aliceAsAdmin.sid },
|
|
626
|
+
});
|
|
627
|
+
expect(asUser.status).toBe(403);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// IDOR guard: session:detail/session:list are scoped by ctx.db (TenantDb)
|
|
631
|
+
// alone, no explicit tenant predicate in the handler — an admin from a
|
|
632
|
+
// different tenant must not be able to read another tenant's session rows.
|
|
633
|
+
test("session:detail and session:list never leak sessions across tenants", async () => {
|
|
634
|
+
const TENANT_2 = testTenantId(2);
|
|
635
|
+
const h2 = makeSessionHelpers(stack, TENANT_2, (user, meta) =>
|
|
636
|
+
callbacks.get().sessionCreator(user, meta),
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
const { userId: carolId } = await h.seedUser("carol4@example.com", "pw-long-enough");
|
|
640
|
+
await updateRows(
|
|
641
|
+
stack.db,
|
|
642
|
+
tenantMembershipsTable,
|
|
643
|
+
{ roles: JSON.stringify(["Admin"]) },
|
|
644
|
+
{ userId: carolId, tenantId: TENANT },
|
|
645
|
+
);
|
|
646
|
+
const carolAsAdmin = await h.login("carol4@example.com", "pw-long-enough");
|
|
647
|
+
|
|
648
|
+
const { userId: daveId } = await h2.seedUser("dave4@example.com", "pw-long-enough");
|
|
649
|
+
await updateRows(
|
|
650
|
+
stack.db,
|
|
651
|
+
tenantMembershipsTable,
|
|
652
|
+
{ roles: JSON.stringify(["Admin"]) },
|
|
653
|
+
{ userId: daveId, tenantId: TENANT_2 },
|
|
654
|
+
);
|
|
655
|
+
const daveAsAdmin = await h2.login("dave4@example.com", "pw-long-enough");
|
|
656
|
+
|
|
657
|
+
const detailRes = await h2.authedPost("/api/query", daveAsAdmin.token, {
|
|
658
|
+
type: SessionQueries.detail,
|
|
659
|
+
payload: { id: carolAsAdmin.sid },
|
|
660
|
+
});
|
|
661
|
+
expect(detailRes.status).toBe(200);
|
|
662
|
+
const detailBody = (await detailRes.json()) as { data: unknown };
|
|
663
|
+
expect(detailBody.data).toBeNull();
|
|
664
|
+
|
|
665
|
+
const listRes = await h2.authedPost("/api/query", daveAsAdmin.token, {
|
|
666
|
+
type: SessionQueries.list,
|
|
667
|
+
payload: {},
|
|
668
|
+
});
|
|
669
|
+
expect(listRes.status).toBe(200);
|
|
670
|
+
const listBody = (await listRes.json()) as { data: Array<{ id: string }> };
|
|
671
|
+
expect(listBody.data.map((r) => r.id)).not.toContain(carolAsAdmin.sid);
|
|
672
|
+
});
|
|
462
673
|
});
|
|
463
674
|
|
|
464
675
|
// Defense-in-depth: the sessionChecker refuses a live sid once the user it
|
|
@@ -538,3 +749,44 @@ describe("sessions feature — locked accounts blocked on a live session", () =>
|
|
|
538
749
|
expect(res.status).toBe(200);
|
|
539
750
|
});
|
|
540
751
|
});
|
|
752
|
+
|
|
753
|
+
describe("sessions with active KMS (#820): ip/userAgent are userOwned PII", () => {
|
|
754
|
+
test("sessionCreator stores ciphertext at rest, mine returns plaintext", async () => {
|
|
755
|
+
configurePiiSubjectKms(new InMemoryKmsAdapter());
|
|
756
|
+
configureBlindIndexKey(Buffer.alloc(32, 7).toString("base64"));
|
|
757
|
+
try {
|
|
758
|
+
const { userId } = await h.seedUser("kms-session@example.com", "pw-long-enough");
|
|
759
|
+
const cbs = createSessionCallbacks({ db: stack.db });
|
|
760
|
+
const sid = await cbs.sessionCreator(
|
|
761
|
+
{ id: userId, tenantId: TENANT, roles: ["User"] },
|
|
762
|
+
{ ip: "203.0.113.7", userAgent: "TestBrowser/1.0" },
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
const stored = await selectMany<{ ip: string | null; userAgent: string | null }>(
|
|
766
|
+
stack.db,
|
|
767
|
+
userSessionTable,
|
|
768
|
+
{ id: sid },
|
|
769
|
+
);
|
|
770
|
+
expect(isPiiCiphertext(stored[0]?.ip)).toBe(true);
|
|
771
|
+
expect(isPiiCiphertext(stored[0]?.userAgent)).toBe(true);
|
|
772
|
+
|
|
773
|
+
// Login (bidx-Lookup auf verschluesselter user.email) + mine ueber HTTP:
|
|
774
|
+
// die manuell erzeugte Session kommt mit Klartext-ip/userAgent zurueck.
|
|
775
|
+
const login = await h.login("kms-session@example.com", "pw-long-enough");
|
|
776
|
+
const res = await h.authedPost("/api/query", login.token, {
|
|
777
|
+
type: SessionQueries.mine,
|
|
778
|
+
payload: {},
|
|
779
|
+
});
|
|
780
|
+
expect(res.status).toBe(200);
|
|
781
|
+
const body = (await res.json()) as {
|
|
782
|
+
data: Array<{ id: string; ip: string | null; userAgent: string | null }>;
|
|
783
|
+
};
|
|
784
|
+
const manual = body.data.find((s) => s.id === sid);
|
|
785
|
+
expect(manual?.ip).toBe("203.0.113.7");
|
|
786
|
+
expect(manual?.userAgent).toBe("TestBrowser/1.0");
|
|
787
|
+
} finally {
|
|
788
|
+
resetPiiSubjectKmsForTests();
|
|
789
|
+
resetBlindIndexKeyForTests();
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
});
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
// const res = await h.authedPost("/api/query", token, { type, payload });
|
|
10
10
|
|
|
11
11
|
import { expect } from "bun:test";
|
|
12
|
+
import type { SessionCreator } from "@cosmicdrift/kumiko-framework/api";
|
|
12
13
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
14
|
import { type TestStack, TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
14
15
|
import * as jose from "jose";
|
|
15
|
-
import { hashPassword } from "../../
|
|
16
|
+
import { hashPassword } from "../../shared";
|
|
16
17
|
import { seedTenantMembership } from "../../tenant/seeding";
|
|
17
18
|
import { UserHandlers } from "../../user";
|
|
19
|
+
import { withMintedSession } from "../testing";
|
|
18
20
|
|
|
19
21
|
export type LoginResult = {
|
|
20
22
|
readonly token: string;
|
|
@@ -25,7 +27,15 @@ export type LoginResult = {
|
|
|
25
27
|
// `ReturnType<typeof makeSessionHelpers>` — no separate type export to
|
|
26
28
|
// keep in sync with the implementation. Params are typed inline on each
|
|
27
29
|
// method so the inference is sharp.
|
|
28
|
-
|
|
30
|
+
// `sessionCreator` is only needed when the suite wires a real sessionChecker
|
|
31
|
+
// (auth-middleware then rejects the sidless systemAdmin bootstrap actor
|
|
32
|
+
// used by seedUser). Suites without a sessionChecker (e.g. PAT tests) can
|
|
33
|
+
// omit it.
|
|
34
|
+
export function makeSessionHelpers(
|
|
35
|
+
stack: TestStack,
|
|
36
|
+
tenantId: TenantId,
|
|
37
|
+
sessionCreator?: SessionCreator,
|
|
38
|
+
) {
|
|
29
39
|
return {
|
|
30
40
|
async seedUser(
|
|
31
41
|
email: string,
|
|
@@ -33,10 +43,13 @@ export function makeSessionHelpers(stack: TestStack, tenantId: TenantId) {
|
|
|
33
43
|
opts?: { roles?: readonly string[] },
|
|
34
44
|
): Promise<{ userId: string }> {
|
|
35
45
|
const hash = await hashPassword(password);
|
|
46
|
+
const actor = sessionCreator
|
|
47
|
+
? await withMintedSession(sessionCreator, TestUsers.systemAdmin)
|
|
48
|
+
: TestUsers.systemAdmin;
|
|
36
49
|
const created = await stack.http.writeOk<{ id: string }>(
|
|
37
50
|
UserHandlers.create,
|
|
38
51
|
{ email, passwordHash: hash, displayName: email.split("@")[0] ?? "u" },
|
|
39
|
-
|
|
52
|
+
actor,
|
|
40
53
|
);
|
|
41
54
|
await seedTenantMembership(stack.db, {
|
|
42
55
|
userId: created.id,
|
|
@@ -17,6 +17,8 @@ export const SessionQueries = {
|
|
|
17
17
|
// Admin-scoped: all sessions in the caller's tenant (live + revoked).
|
|
18
18
|
// Tenant isolation comes from ctx.db; access-gate is admin-or-higher.
|
|
19
19
|
list: "sessions:query:user-session:list",
|
|
20
|
+
// Admin-scoped single-session inspector, backs the session-detail screen.
|
|
21
|
+
detail: "sessions:query:user-session:detail",
|
|
20
22
|
} as const;
|
|
21
23
|
|
|
22
24
|
export const SessionErrors = {
|
|
@@ -45,3 +47,6 @@ export const DEFAULT_SESSION_CACHE_TTL_MS = 60_000;
|
|
|
45
47
|
// longer so refresh can rotate the token without requiring a new password).
|
|
46
48
|
// MVP ships a single window; per-app overrides can come later.
|
|
47
49
|
export const DEFAULT_SESSION_EXPIRY_MS = 30 * 24 * 60 * 60 * 1000;
|
|
50
|
+
|
|
51
|
+
export const SESSION_LIST_SCREEN_ID = "session-list" as const;
|
|
52
|
+
export const SESSION_DETAIL_SCREEN_ID = "session-detail" as const;
|
|
@@ -7,9 +7,9 @@ export async function deleteStaleSessionsBatch(
|
|
|
7
7
|
batchSize: number,
|
|
8
8
|
): Promise<number> {
|
|
9
9
|
const rows = (await asRawClient(db).unsafe(
|
|
10
|
-
`DELETE FROM "
|
|
10
|
+
`DELETE FROM "store_user_sessions"
|
|
11
11
|
WHERE "id" IN (
|
|
12
|
-
SELECT "id" FROM "
|
|
12
|
+
SELECT "id" FROM "store_user_sessions"
|
|
13
13
|
WHERE "expires_at" < now() - ($1::int * interval '1 day')
|
|
14
14
|
OR "revoked_at" < now() - ($1::int * interval '1 day')
|
|
15
15
|
LIMIT $2
|