@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
|
@@ -2,17 +2,29 @@ import type {
|
|
|
2
2
|
AuthSessionStatus,
|
|
3
3
|
SessionChecker,
|
|
4
4
|
SessionCreator,
|
|
5
|
+
SessionMassRevoker,
|
|
5
6
|
SessionMetadata,
|
|
6
7
|
SessionRevoker,
|
|
7
8
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
9
|
+
|
|
10
|
+
export type { SessionMassRevoker } from "@cosmicdrift/kumiko-framework/api";
|
|
11
|
+
|
|
8
12
|
import { fetchOne, insertOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
13
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
10
14
|
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
|
+
import { append } from "@cosmicdrift/kumiko-framework/event-store";
|
|
11
17
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
12
18
|
import { Temporal } from "temporal-polyfill";
|
|
19
|
+
import { encryptForDirectWrite } from "../shared";
|
|
13
20
|
import { USER_STATUS, type UserStatus, userTable } from "../user";
|
|
14
21
|
import { DEFAULT_SESSION_EXPIRY_MS } from "./constants";
|
|
15
|
-
import { userSessionTable } from "./schema/user-session";
|
|
22
|
+
import { userSessionEntity, userSessionTable } from "./schema/user-session";
|
|
23
|
+
import {
|
|
24
|
+
SESSION_REVOKED_AGGREGATE_TYPE,
|
|
25
|
+
SESSION_REVOKED_EVENT_QN,
|
|
26
|
+
sessionRevokedSchema,
|
|
27
|
+
} from "./session-revoked-event";
|
|
16
28
|
|
|
17
29
|
// Locked accounts whose live sessions must be refused. deletionRequested is
|
|
18
30
|
// intentionally absent — it's a reversible grace period and the user needs
|
|
@@ -27,10 +39,15 @@ const BLOCKED_STATUSES: ReadonlySet<UserStatus> = new Set([
|
|
|
27
39
|
// login and every request. The (createdAt/revokedAt/ip/userAgent) columns
|
|
28
40
|
// already are the audit trail — a dispatcher roundtrip buys nothing.
|
|
29
41
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
42
|
+
// "Sign out everywhere else, keep this one" — used both by the user-facing
|
|
43
|
+
// revoke-all-others handler and by other features (auth-mfa) that need the
|
|
44
|
+
// same effect from a raw callback instead of a dispatcher round-trip.
|
|
45
|
+
// currentSid undefined (stateless-JWT / no sid claim) revokes everything —
|
|
46
|
+
// there is no "current" row to spare.
|
|
47
|
+
export type SessionAllOthersRevoker = (
|
|
48
|
+
userId: string,
|
|
49
|
+
currentSid: string | undefined,
|
|
50
|
+
) => Promise<number>;
|
|
34
51
|
|
|
35
52
|
export type SessionCallbacksOptions = {
|
|
36
53
|
readonly db: DbConnection;
|
|
@@ -44,6 +61,7 @@ export type SessionCallbacks = {
|
|
|
44
61
|
sessionRevoker: SessionRevoker;
|
|
45
62
|
sessionChecker: SessionChecker;
|
|
46
63
|
sessionMassRevoker: SessionMassRevoker;
|
|
64
|
+
sessionRevokeAllOthers: SessionAllOthersRevoker;
|
|
47
65
|
};
|
|
48
66
|
|
|
49
67
|
export function createSessionCallbacks(opts: SessionCallbacksOptions): SessionCallbacks {
|
|
@@ -55,15 +73,23 @@ export function createSessionCallbacks(opts: SessionCallbacksOptions): SessionCa
|
|
|
55
73
|
const sid = generateId();
|
|
56
74
|
const now = Temporal.Now.instant();
|
|
57
75
|
const expiresAt = now.add({ milliseconds: ttlMs });
|
|
58
|
-
await insertOne(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
await insertOne(
|
|
77
|
+
db,
|
|
78
|
+
userSessionTable,
|
|
79
|
+
await encryptForDirectWrite(
|
|
80
|
+
userSessionEntity,
|
|
81
|
+
{
|
|
82
|
+
id: sid,
|
|
83
|
+
tenantId: user.tenantId,
|
|
84
|
+
userId: user.id,
|
|
85
|
+
createdAt: now,
|
|
86
|
+
expiresAt,
|
|
87
|
+
ip: meta.ip,
|
|
88
|
+
userAgent: meta.userAgent,
|
|
89
|
+
},
|
|
90
|
+
"sessions:create",
|
|
91
|
+
),
|
|
92
|
+
);
|
|
67
93
|
return sid;
|
|
68
94
|
},
|
|
69
95
|
|
|
@@ -124,6 +150,65 @@ export function createSessionCallbacks(opts: SessionCallbacksOptions): SessionCa
|
|
|
124
150
|
{ revokedAt: Temporal.Now.instant() },
|
|
125
151
|
{ userId, revokedAt: null },
|
|
126
152
|
);
|
|
153
|
+
|
|
154
|
+
// Lightweight append alongside the direct-write above, same pattern
|
|
155
|
+
// as revoke.write.ts (#1559) — this callback is the password-change
|
|
156
|
+
// auto-revoke path (sessions/feature.ts postSave hook) and has no
|
|
157
|
+
// dispatcher ctx to call unsafeAppendEvent from, so it uses the raw
|
|
158
|
+
// append() like revoke-all-for-user.write.ts does. Without this, the
|
|
159
|
+
// access-invalidation consumer (#1560) never hears about a password
|
|
160
|
+
// change and an already-open SSE stream survives it — the exact
|
|
161
|
+
// "stale JWT mid-stream" gap this feature exists to close.
|
|
162
|
+
if (result.length > 0) {
|
|
163
|
+
const payload = sessionRevokedSchema.parse({
|
|
164
|
+
userId,
|
|
165
|
+
sessionIds: result.map((row: { id: string }) => row.id),
|
|
166
|
+
});
|
|
167
|
+
await append(db, {
|
|
168
|
+
aggregateId: generateId(),
|
|
169
|
+
aggregateType: SESSION_REVOKED_AGGREGATE_TYPE,
|
|
170
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
171
|
+
expectedVersion: 0,
|
|
172
|
+
type: SESSION_REVOKED_EVENT_QN,
|
|
173
|
+
payload,
|
|
174
|
+
metadata: { userId },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result.length;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async sessionRevokeAllOthers(userId: string, currentSid: string | undefined): Promise<number> {
|
|
182
|
+
const result = await updateMany(
|
|
183
|
+
db,
|
|
184
|
+
userSessionTable,
|
|
185
|
+
{ revokedAt: Temporal.Now.instant() },
|
|
186
|
+
currentSid
|
|
187
|
+
? { userId, revokedAt: null, id: { ne: currentSid } }
|
|
188
|
+
: { userId, revokedAt: null },
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Same reasoning as sessionMassRevoker above — this raw callback
|
|
192
|
+
// (used by auth-mfa and other internal callers, distinct from the
|
|
193
|
+
// user-facing revoke-all-others.write.ts handler which already
|
|
194
|
+
// appends this event) needs its own append so callers reached
|
|
195
|
+
// through here also cut open SSE streams.
|
|
196
|
+
if (result.length > 0) {
|
|
197
|
+
const payload = sessionRevokedSchema.parse({
|
|
198
|
+
userId,
|
|
199
|
+
sessionIds: result.map((row: { id: string }) => row.id),
|
|
200
|
+
});
|
|
201
|
+
await append(db, {
|
|
202
|
+
aggregateId: generateId(),
|
|
203
|
+
aggregateType: SESSION_REVOKED_AGGREGATE_TYPE,
|
|
204
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
205
|
+
expectedVersion: 0,
|
|
206
|
+
type: SESSION_REVOKED_EVENT_QN,
|
|
207
|
+
payload,
|
|
208
|
+
metadata: { userId },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
127
212
|
return result.length;
|
|
128
213
|
},
|
|
129
214
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import * as bunDb from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
|
+
import { Temporal } from "temporal-polyfill";
|
|
5
|
+
import { USER_STATUS, userTable } from "../user/schema/user";
|
|
6
|
+
import { userSessionTable } from "./schema/user-session";
|
|
7
|
+
import { createSessionCallbacks } from "./session-callbacks";
|
|
8
|
+
|
|
9
|
+
// Unit-only: the fail-open-on-throw branch cannot be provoked through real
|
|
10
|
+
// Postgres without faking infrastructure failure. Integration tests forbid
|
|
11
|
+
// mocks, so this lives next to the source as a plain *.test.ts.
|
|
12
|
+
|
|
13
|
+
type FetchOne = typeof bunDb.fetchOne;
|
|
14
|
+
|
|
15
|
+
describe("sessionChecker fail-open on user-lookup throw", () => {
|
|
16
|
+
test("read_users THROW → live (not 500 / not blocked)", async () => {
|
|
17
|
+
const db = {} as DbConnection;
|
|
18
|
+
const cbs = createSessionCallbacks({ db });
|
|
19
|
+
const sid = "00000000-0000-4000-8000-00000000sid1";
|
|
20
|
+
const userId = "00000000-0000-4000-8000-00000000user";
|
|
21
|
+
const farFutureMs = Temporal.Now.instant().add({ hours: 1 }).epochMilliseconds;
|
|
22
|
+
|
|
23
|
+
const originalFetchOne = bunDb.fetchOne;
|
|
24
|
+
const spy = spyOn(bunDb, "fetchOne").mockImplementation((async (_db, table) => {
|
|
25
|
+
if (table === userSessionTable) {
|
|
26
|
+
return {
|
|
27
|
+
userId,
|
|
28
|
+
revokedAt: null,
|
|
29
|
+
expiresAt: { epochMilliseconds: farFutureMs },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (table === userTable) {
|
|
33
|
+
throw new Error("simulated pool exhaustion");
|
|
34
|
+
}
|
|
35
|
+
throw new Error("unexpected table in sessionChecker spy");
|
|
36
|
+
}) as FetchOne);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
expect(await cbs.sessionChecker(sid, userId)).toBe("live");
|
|
40
|
+
} finally {
|
|
41
|
+
spy.mockRestore();
|
|
42
|
+
// Sanity: restore must put the real export back (guards against spy leak).
|
|
43
|
+
expect(bunDb.fetchOne).toBe(originalFetchOne);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("control: Restricted user without throw → blocked (spy must hit userTable)", async () => {
|
|
48
|
+
// If the spy somehow skipped the userTable branch, Restricted would still
|
|
49
|
+
// be "live" via fail-open-on-null — this control pins that the throw path
|
|
50
|
+
// is what we exercise above, not an accidental miss.
|
|
51
|
+
const db = {} as DbConnection;
|
|
52
|
+
const cbs = createSessionCallbacks({ db });
|
|
53
|
+
const sid = "00000000-0000-4000-8000-00000000sid2";
|
|
54
|
+
const userId = "00000000-0000-4000-8000-00000000usr2";
|
|
55
|
+
const farFutureMs = Temporal.Now.instant().add({ hours: 1 }).epochMilliseconds;
|
|
56
|
+
|
|
57
|
+
const spy = spyOn(bunDb, "fetchOne").mockImplementation((async (_db, table) => {
|
|
58
|
+
if (table === userSessionTable) {
|
|
59
|
+
return {
|
|
60
|
+
userId,
|
|
61
|
+
revokedAt: null,
|
|
62
|
+
expiresAt: { epochMilliseconds: farFutureMs },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (table === userTable) {
|
|
66
|
+
return { status: USER_STATUS.Restricted };
|
|
67
|
+
}
|
|
68
|
+
throw new Error("unexpected table in sessionChecker spy");
|
|
69
|
+
}) as FetchOne);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
expect(await cbs.sessionChecker(sid, userId)).toBe("blocked");
|
|
73
|
+
} finally {
|
|
74
|
+
spy.mockRestore();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Lightweight custom event appended alongside the direct-write
|
|
4
|
+
// updateMany() in revoke.write.ts / revoke-all-others.write.ts /
|
|
5
|
+
// revoke-all-for-user.write.ts (#1559). store_user_sessions itself stays
|
|
6
|
+
// an unmanaged direct-write store (see feature.ts) — the row columns are
|
|
7
|
+
// the audit trail, this event is NOT a CRUD/lifecycle event for that
|
|
8
|
+
// table and carries no projection. Its only job is to give cross-instance
|
|
9
|
+
// consumers (access-invalidation, #1560) something to LISTEN for via the
|
|
10
|
+
// existing event-store NOTIFY, keyed by the affected userId.
|
|
11
|
+
//
|
|
12
|
+
// _SHORT/_QN pair mirrors cap-counter's rolling-incremented event:
|
|
13
|
+
// _SHORT — r.defineEvent(short, schema) in feature.ts, framework prefixes
|
|
14
|
+
// it to the QN below.
|
|
15
|
+
// _QN — ctx.unsafeAppendEvent({ type }) / raw append()'s `type` field.
|
|
16
|
+
export const SESSION_REVOKED_EVENT_SHORT = "session-revoked" as const;
|
|
17
|
+
export const SESSION_REVOKED_EVENT_QN = "sessions:event:session-revoked" as const;
|
|
18
|
+
|
|
19
|
+
// Own aggregate type, decoupled from userSessionEntity's implicit
|
|
20
|
+
// "user-session" — each revoke action mints a fresh aggregate id (no
|
|
21
|
+
// predecessor to satisfy, no version_conflict against concurrent revokes).
|
|
22
|
+
export const SESSION_REVOKED_AGGREGATE_TYPE = "user-session-revocation" as const;
|
|
23
|
+
|
|
24
|
+
export const sessionRevokedSchema = z.object({
|
|
25
|
+
userId: z.string().min(1),
|
|
26
|
+
sessionIds: z.array(z.uuid()).min(1),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type SessionRevokedPayload = z.infer<typeof sessionRevokedSchema>;
|
package/src/sessions/testing.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// sessions feature owns. framework/testing only provides the generic
|
|
18
18
|
// `createLateBoundHolder<T>` — shape-independent.
|
|
19
19
|
|
|
20
|
-
import type { AuthRoutesConfig } from "@cosmicdrift/kumiko-framework/api";
|
|
20
|
+
import type { AuthRoutesConfig, SessionCreator } from "@cosmicdrift/kumiko-framework/api";
|
|
21
|
+
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
22
|
import type { LateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
|
|
22
23
|
import type { SessionCallbacks, SessionMassRevoker } from "./session-callbacks";
|
|
23
24
|
|
|
@@ -40,3 +41,15 @@ export function sessionCallbacksFromLateBound(
|
|
|
40
41
|
asMassRevoker: () => (userId) => holder.get().sessionMassRevoker(userId),
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
// Bootstrap actors (system-admin seed writes) need a real sid once a
|
|
46
|
+
// sessionChecker is wired — auth-middleware rejects sidless JWTs as
|
|
47
|
+
// forged/pre-session-tracking. Mints a live session row and returns the
|
|
48
|
+
// actor with `sid` attached so `stack.http.writeOk(..., actor)` passes.
|
|
49
|
+
export async function withMintedSession(
|
|
50
|
+
sessionCreator: SessionCreator,
|
|
51
|
+
user: SessionUser,
|
|
52
|
+
): Promise<SessionUser> {
|
|
53
|
+
const sid = await sessionCreator(user, { ip: "127.0.0.1", userAgent: "test" });
|
|
54
|
+
return { ...user, sid };
|
|
55
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { runChunkedMigration } from "../chunked-entity-migration";
|
|
3
|
+
|
|
4
|
+
describe("runChunkedMigration", () => {
|
|
5
|
+
test("processes all batches and stops with 'done' when nextBatch returns empty", async () => {
|
|
6
|
+
const batches: readonly number[][] = [[1, 2], [3], []];
|
|
7
|
+
let call = 0;
|
|
8
|
+
const migrated: number[] = [];
|
|
9
|
+
|
|
10
|
+
const result = await runChunkedMigration<number>({
|
|
11
|
+
nextBatch: async () => batches[call++] ?? [],
|
|
12
|
+
migrateRow: async (row) => {
|
|
13
|
+
migrated.push(row);
|
|
14
|
+
return "migrated";
|
|
15
|
+
},
|
|
16
|
+
maxFailures: 10,
|
|
17
|
+
deadlineAt: Number.POSITIVE_INFINITY,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(migrated).toEqual([1, 2, 3]);
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
migrated: 3,
|
|
23
|
+
skipped: 0,
|
|
24
|
+
failed: 0,
|
|
25
|
+
batchesProcessed: 2,
|
|
26
|
+
stoppedReason: "done",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("counts skipped and failed outcomes separately from migrated", async () => {
|
|
31
|
+
let call = 0;
|
|
32
|
+
const rows = [
|
|
33
|
+
{ id: 1, outcome: "migrated" as const },
|
|
34
|
+
{ id: 2, outcome: "skipped" as const },
|
|
35
|
+
{ id: 3, outcome: "failed" as const },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const result = await runChunkedMigration({
|
|
39
|
+
nextBatch: async () => (call++ === 0 ? rows : []),
|
|
40
|
+
migrateRow: async (row) => row.outcome,
|
|
41
|
+
maxFailures: 10,
|
|
42
|
+
deadlineAt: Number.POSITIVE_INFINITY,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.migrated).toBe(1);
|
|
46
|
+
expect(result.skipped).toBe(1);
|
|
47
|
+
expect(result.failed).toBe(1);
|
|
48
|
+
expect(result.stoppedReason).toBe("done");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a thrown migrateRow counts as failed and invokes onRowError", async () => {
|
|
52
|
+
let call = 0;
|
|
53
|
+
const errors: unknown[] = [];
|
|
54
|
+
const boom = new Error("boom");
|
|
55
|
+
|
|
56
|
+
const result = await runChunkedMigration<number>({
|
|
57
|
+
nextBatch: async () => (call++ === 0 ? [1] : []),
|
|
58
|
+
migrateRow: async () => {
|
|
59
|
+
throw boom;
|
|
60
|
+
},
|
|
61
|
+
maxFailures: 10,
|
|
62
|
+
deadlineAt: Number.POSITIVE_INFINITY,
|
|
63
|
+
onRowError: (row, err) => errors.push([row, err]),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(result.failed).toBe(1);
|
|
67
|
+
expect(errors).toEqual([[1, boom]]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("stops with 'too_many_failures' once failed >= maxFailures, mid-batch", async () => {
|
|
71
|
+
let call = 0;
|
|
72
|
+
const seen: number[] = [];
|
|
73
|
+
|
|
74
|
+
const result = await runChunkedMigration<number>({
|
|
75
|
+
nextBatch: async () => (call++ === 0 ? [1, 2, 3] : []),
|
|
76
|
+
migrateRow: async (row) => {
|
|
77
|
+
seen.push(row);
|
|
78
|
+
return "failed";
|
|
79
|
+
},
|
|
80
|
+
maxFailures: 2,
|
|
81
|
+
deadlineAt: Number.POSITIVE_INFINITY,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// The circuit-breaker checks before each row — row 3 is never attempted.
|
|
85
|
+
expect(seen).toEqual([1, 2]);
|
|
86
|
+
expect(result.failed).toBe(2);
|
|
87
|
+
expect(result.stoppedReason).toBe("too_many_failures");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("stops with 'too_many_failures' checked between batches too", async () => {
|
|
91
|
+
let call = 0;
|
|
92
|
+
const batches = [["a"], ["b"], ["c"]];
|
|
93
|
+
|
|
94
|
+
const result = await runChunkedMigration<string>({
|
|
95
|
+
nextBatch: async () => batches[call++] ?? [],
|
|
96
|
+
migrateRow: async () => "failed",
|
|
97
|
+
maxFailures: 1,
|
|
98
|
+
deadlineAt: Number.POSITIVE_INFINITY,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Batch 1 processes "a" and pushes failed to 1; the breaker is checked
|
|
102
|
+
// before batchesProcessed is incremented for the next fetched batch, so
|
|
103
|
+
// "b" is fetched but never touched and doesn't count as processed.
|
|
104
|
+
expect(result.batchesProcessed).toBe(1);
|
|
105
|
+
expect(result.stoppedReason).toBe("too_many_failures");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("stops with 'timeout' once the deadline has passed, without calling nextBatch again", async () => {
|
|
109
|
+
let nextBatchCalls = 0;
|
|
110
|
+
|
|
111
|
+
const result = await runChunkedMigration<number>({
|
|
112
|
+
nextBatch: async () => {
|
|
113
|
+
nextBatchCalls++;
|
|
114
|
+
return [];
|
|
115
|
+
},
|
|
116
|
+
migrateRow: async () => "migrated",
|
|
117
|
+
maxFailures: 10,
|
|
118
|
+
deadlineAt: Date.now() - 1,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(nextBatchCalls).toBe(0);
|
|
122
|
+
expect(result.batchesProcessed).toBe(0);
|
|
123
|
+
expect(result.stoppedReason).toBe("timeout");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("stops with 'signal' when the AbortSignal is already aborted", async () => {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
controller.abort();
|
|
129
|
+
let nextBatchCalls = 0;
|
|
130
|
+
|
|
131
|
+
const result = await runChunkedMigration<number>({
|
|
132
|
+
nextBatch: async () => {
|
|
133
|
+
nextBatchCalls++;
|
|
134
|
+
return [];
|
|
135
|
+
},
|
|
136
|
+
migrateRow: async () => "migrated",
|
|
137
|
+
maxFailures: 10,
|
|
138
|
+
deadlineAt: Number.POSITIVE_INFINITY,
|
|
139
|
+
signal: controller.signal,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(nextBatchCalls).toBe(0);
|
|
143
|
+
expect(result.stoppedReason).toBe("signal");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Shared loop for chunked, idempotent row migrations (secrets KEK rotation,
|
|
2
|
+
// config re-encrypt, future entity-field re-encrypts). Owns the operational
|
|
3
|
+
// mechanics — abort signal, deadline, failure circuit-breaker, batch
|
|
4
|
+
// accounting — while the caller owns the domain: what a batch is and how a
|
|
5
|
+
// single row migrates.
|
|
6
|
+
//
|
|
7
|
+
// Contract notes:
|
|
8
|
+
// - nextBatch() returning an empty array ends the run ("done"). Callers
|
|
9
|
+
// whose batch query re-evaluates (WHERE kek_version != current) converge
|
|
10
|
+
// naturally; full-scan callers return their one batch, then [].
|
|
11
|
+
// - migrateRow returns "migrated" | "skipped" (already current, lost a
|
|
12
|
+
// version_conflict race) | "failed" (counted against maxFailures);
|
|
13
|
+
// throws count as "failed" too and go through onRowError.
|
|
14
|
+
// - Rows that keep failing MAY be re-served by a re-evaluating nextBatch —
|
|
15
|
+
// the circuit-breaker (maxFailures) is what terminates that loop, same
|
|
16
|
+
// semantics the secrets rotate job always had.
|
|
17
|
+
|
|
18
|
+
export type MigrationRowOutcome = "migrated" | "skipped" | "failed";
|
|
19
|
+
|
|
20
|
+
export type ChunkedMigrationStopReason = "done" | "timeout" | "signal" | "too_many_failures";
|
|
21
|
+
|
|
22
|
+
export type ChunkedMigrationResult = {
|
|
23
|
+
readonly migrated: number;
|
|
24
|
+
readonly skipped: number;
|
|
25
|
+
readonly failed: number;
|
|
26
|
+
readonly batchesProcessed: number;
|
|
27
|
+
readonly stoppedReason: ChunkedMigrationStopReason;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ChunkedMigrationOptions<Row> = {
|
|
31
|
+
readonly nextBatch: () => Promise<readonly Row[]>;
|
|
32
|
+
readonly migrateRow: (row: Row) => Promise<MigrationRowOutcome>;
|
|
33
|
+
readonly maxFailures: number;
|
|
34
|
+
// Epoch ms; Number.POSITIVE_INFINITY for no time bound.
|
|
35
|
+
readonly deadlineAt: number;
|
|
36
|
+
readonly signal?: AbortSignal | undefined;
|
|
37
|
+
readonly onRowError?: (row: Row, err: unknown) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export async function runChunkedMigration<Row>(
|
|
41
|
+
opts: ChunkedMigrationOptions<Row>,
|
|
42
|
+
): Promise<ChunkedMigrationResult> {
|
|
43
|
+
let migrated = 0;
|
|
44
|
+
let skipped = 0;
|
|
45
|
+
let failed = 0;
|
|
46
|
+
let batchesProcessed = 0;
|
|
47
|
+
let stoppedReason: ChunkedMigrationStopReason = "done";
|
|
48
|
+
|
|
49
|
+
outer: while (true) {
|
|
50
|
+
if (opts.signal?.aborted) {
|
|
51
|
+
stoppedReason = "signal";
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (Date.now() >= opts.deadlineAt) {
|
|
55
|
+
stoppedReason = "timeout";
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const batch = await opts.nextBatch();
|
|
60
|
+
if (batch.length === 0) break;
|
|
61
|
+
|
|
62
|
+
if (failed >= opts.maxFailures) {
|
|
63
|
+
stoppedReason = "too_many_failures";
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
batchesProcessed++;
|
|
68
|
+
|
|
69
|
+
for (const row of batch) {
|
|
70
|
+
if (failed >= opts.maxFailures) {
|
|
71
|
+
stoppedReason = "too_many_failures";
|
|
72
|
+
break outer;
|
|
73
|
+
}
|
|
74
|
+
let outcome: MigrationRowOutcome;
|
|
75
|
+
try {
|
|
76
|
+
outcome = await opts.migrateRow(row);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
opts.onRowError?.(row, err);
|
|
79
|
+
failed++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (outcome === "migrated") migrated++;
|
|
83
|
+
else if (outcome === "skipped") skipped++;
|
|
84
|
+
else failed++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { migrated, skipped, failed, batchesProcessed, stoppedReason };
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import {
|
|
3
|
+
configuredPiiSubjectKms,
|
|
4
|
+
decryptPiiFieldValues,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
6
|
+
|
|
7
|
+
// Raw reads (fetchOne/selectMany) return the stored column value — with an
|
|
8
|
+
// active KMS that is ciphertext (self-describing: it names its own subject,
|
|
9
|
+
// so no entity/field context is needed for key selection). `field` must
|
|
10
|
+
// still match the field name used at encrypt time — it is part of the
|
|
11
|
+
// AAD (#1263), so a mismatch fails decrypt loud rather than silently
|
|
12
|
+
// letting ciphertext cut-and-pasted from another field decrypt. An erased
|
|
13
|
+
// subject yields the sentinel, which matches no real value.
|
|
14
|
+
export async function decryptStoredPii(
|
|
15
|
+
value: string,
|
|
16
|
+
field: string,
|
|
17
|
+
fallbackRequestId: string,
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
const kms = configuredPiiSubjectKms();
|
|
20
|
+
if (!kms) return value;
|
|
21
|
+
const out = await decryptPiiFieldValues({ [field]: value }, [field], kms, {
|
|
22
|
+
requestId: requestContext.get()?.requestId ?? fallbackRequestId,
|
|
23
|
+
});
|
|
24
|
+
const decrypted = out[field];
|
|
25
|
+
return typeof decrypted === "string" ? decrypted : value;
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import {
|
|
3
|
+
collectPiiSubjectFields,
|
|
4
|
+
configuredPiiSubjectKms,
|
|
5
|
+
encryptPiiFieldValues,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
7
|
+
import type { EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
|
|
9
|
+
// Unmanaged direct-write stores (r.unmanagedTable) skip the executor, so its
|
|
10
|
+
// PII encryption never runs — every insert of subject-annotated fields must
|
|
11
|
+
// go through this instead, and the feature declares { piiEncryptedOnWrite:
|
|
12
|
+
// true } at the registration site (enforced by the registry, #820).
|
|
13
|
+
export async function encryptForDirectWrite(
|
|
14
|
+
entity: EntityDefinition,
|
|
15
|
+
row: Record<string, unknown>,
|
|
16
|
+
fallbackRequestId: string,
|
|
17
|
+
): Promise<Record<string, unknown>> {
|
|
18
|
+
const kms = configuredPiiSubjectKms();
|
|
19
|
+
if (!kms) return row;
|
|
20
|
+
return encryptPiiFieldValues(row, entity, collectPiiSubjectFields(entity), kms, {
|
|
21
|
+
requestId: requestContext.get()?.requestId ?? fallbackRequestId,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EntityDefinition, FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
|
|
3
|
+
// r.entity(...) is not the only way a feature exposes an entity shape:
|
|
4
|
+
// r.projection(...) can carry an optional `entity` too (raw read-models with
|
|
5
|
+
// no executor, e.g. billing-foundation's subscription table). Boot-check
|
|
6
|
+
// gates need every entity a feature declares, not just the r.entity ones, or
|
|
7
|
+
// a pii/tenantOwned/userOwned field on a projection-only entity is invisible
|
|
8
|
+
// to the guard it was annotated for.
|
|
9
|
+
export function entitiesOf(
|
|
10
|
+
feature: FeatureDefinition,
|
|
11
|
+
): ReadonlyArray<readonly [string, EntityDefinition]> {
|
|
12
|
+
const fromEntities = Object.entries(feature.entities ?? {});
|
|
13
|
+
const fromProjections = Object.values(feature.projections ?? {})
|
|
14
|
+
.filter((p): p is typeof p & { entity: EntityDefinition } => p.entity !== undefined)
|
|
15
|
+
.map((p) => [p.name, p.entity] as const);
|
|
16
|
+
return [...fromEntities, ...fromProjections];
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { withoutAmbientTemporal } from "@cosmicdrift/kumiko-framework/testing";
|
|
3
|
+
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
4
|
+
import { isWithinGracePeriod } from "./grace-period";
|
|
5
|
+
|
|
6
|
+
describe("isWithinGracePeriod — kumiko-framework#1525/#1550", () => {
|
|
7
|
+
test("null gracePeriodEnd → false without ambient Temporal", async () => {
|
|
8
|
+
await withoutAmbientTemporal(() => {
|
|
9
|
+
expect(isWithinGracePeriod(null)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("future vs past without ambient Temporal", async () => {
|
|
14
|
+
const future = TemporalPolyfill.Now.instant().add({
|
|
15
|
+
hours: 1,
|
|
16
|
+
}) as unknown as Temporal.Instant;
|
|
17
|
+
const past = TemporalPolyfill.Now.instant().subtract({
|
|
18
|
+
hours: 1,
|
|
19
|
+
}) as unknown as Temporal.Instant;
|
|
20
|
+
|
|
21
|
+
await withoutAmbientTemporal(() => {
|
|
22
|
+
expect(isWithinGracePeriod(future)).toBe(true);
|
|
23
|
+
expect(isWithinGracePeriod(past)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Shared grace-period check for user-data-rights + tenant-lifecycle cancel flows. */
|
|
2
|
+
|
|
3
|
+
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* True when `gracePeriodEnd` is still in the future.
|
|
7
|
+
* Uses the polyfill Instant API so callers don't depend on `globalThis.Temporal`.
|
|
8
|
+
*/
|
|
9
|
+
export function isWithinGracePeriod(gracePeriodEnd: Temporal.Instant | null): boolean {
|
|
10
|
+
// @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
|
|
11
|
+
// at runtime — row types resolve against ambient Temporal; polyfill Instant
|
|
12
|
+
// is a separate nominal type across the two .d.ts sources.
|
|
13
|
+
return (
|
|
14
|
+
gracePeriodEnd != null &&
|
|
15
|
+
TemporalPolyfill.Instant.compare(
|
|
16
|
+
gracePeriodEnd as unknown as InstanceType<typeof TemporalPolyfill.Instant>,
|
|
17
|
+
TemporalPolyfill.Now.instant(),
|
|
18
|
+
) > 0
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
import { describe, expect, test } from "bun:test";
|
|
17
17
|
import { pbkdf2Sync } from "node:crypto";
|
|
18
|
-
import { isIdentityV3Hash, verifyIdentityV3Hash } from "
|
|
19
|
-
import { verifyPassword } from "
|
|
18
|
+
import { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash";
|
|
19
|
+
import { verifyPassword } from "./password-hashing";
|
|
20
20
|
|
|
21
21
|
// --- Test helpers ---
|
|
22
22
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type ChunkedMigrationOptions,
|
|
3
|
+
type ChunkedMigrationResult,
|
|
4
|
+
type ChunkedMigrationStopReason,
|
|
5
|
+
type MigrationRowOutcome,
|
|
6
|
+
runChunkedMigration,
|
|
7
|
+
} from "./chunked-entity-migration";
|
|
8
|
+
export { decryptStoredPii } from "./decrypt-stored-pii";
|
|
9
|
+
export { encryptForDirectWrite } from "./encrypt-for-direct-write";
|
|
10
|
+
export { entitiesOf } from "./entities-of";
|
|
11
|
+
export { isWithinGracePeriod } from "./grace-period";
|
|
12
|
+
export { isIdentityV3Hash, verifyIdentityV3Hash } from "./identity-v3-hash";
|
|
13
|
+
export { mapWithConcurrency } from "./map-with-concurrency";
|
|
14
|
+
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing";
|
|
15
|
+
export type { SystemQueryFn } from "./system-query";
|
|
16
|
+
export { type BurnResult, burnToken, unburnToken } from "./token-burn-store";
|