@cosmicdrift/kumiko-bundled-features 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +41 -7
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +279 -0
- package/src/__tests__/access-invalidation-stream.integration.test.ts +217 -0
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +200 -0
- package/src/admin-shell/__tests__/admin-shell.boot.test.ts +147 -0
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +43 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +35 -0
- package/src/admin-shell/constants.ts +11 -0
- package/src/admin-shell/feature.ts +135 -0
- package/src/admin-shell/i18n.ts +36 -0
- package/src/admin-shell/index.ts +17 -0
- package/src/admin-shell/overview-allowlist.ts +37 -0
- package/src/admin-shell/web/client-plugin.tsx +26 -0
- package/src/admin-shell/web/i18n.ts +16 -0
- package/src/admin-shell/web/index.ts +2 -0
- package/src/admin-shell/web/overview-layout.tsx +69 -0
- package/src/admin-shell/web/overview-query.ts +15 -0
- package/src/admin-shell/web/platform-overview-screen.tsx +90 -0
- package/src/admin-shell/web/tenant-overview-screen.tsx +90 -0
- package/src/agent-tools/__tests__/tool-catalog.test.ts +179 -0
- package/src/agent-tools/__tests__/tool-dispatch.integration.test.ts +100 -0
- package/src/agent-tools/__tests__/tool-dispatch.test.ts +126 -0
- package/src/agent-tools/index.ts +9 -0
- package/src/agent-tools/tool-catalog.ts +130 -0
- package/src/agent-tools/tool-dispatch.ts +82 -0
- package/src/agent-tools/types.ts +38 -0
- package/src/audit/__tests__/audit-screens.boot.test.ts +51 -0
- package/src/audit/__tests__/audit-security.integration.test.ts +201 -0
- package/src/audit/__tests__/audit.integration.test.ts +50 -25
- package/src/audit/constants.ts +9 -2
- package/src/audit/feature.ts +33 -1
- package/src/audit/handlers/details.query.ts +47 -0
- package/src/audit/handlers/list.query.ts +3 -2
- package/src/audit/i18n.ts +31 -0
- package/src/audit/web/audit-log-detail-screen.tsx +131 -0
- package/src/audit/web/audit-log-screen.tsx +250 -0
- package/src/audit/web/client-plugin.tsx +22 -0
- package/src/audit/web/i18n.ts +12 -0
- package/src/audit/web/index.ts +5 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +4 -4
- package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +4 -4
- package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +10 -11
- package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +33 -6
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +9 -6
- package/src/auth-email-password/__tests__/feature-options.test.ts +33 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +35 -1
- package/src/auth-email-password/__tests__/login-mfa-setup-required.integration.test.ts +129 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +19 -6
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +264 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +20 -4
- package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +91 -0
- package/src/auth-email-password/constants.ts +25 -0
- package/src/auth-email-password/email-templates.ts +32 -0
- package/src/auth-email-password/errors.ts +9 -0
- package/src/auth-email-password/feature.ts +59 -11
- package/src/auth-email-password/handlers/change-password.write.ts +14 -13
- package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +34 -105
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +12 -5
- package/src/auth-email-password/handlers/invite-accept.write.ts +9 -2
- package/src/auth-email-password/handlers/invite-create.write.ts +2 -2
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +12 -4
- package/src/auth-email-password/handlers/login.write.ts +86 -16
- package/src/auth-email-password/handlers/logout.write.ts +4 -2
- package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +3 -2
- package/src/auth-email-password/handlers/self-registration-status.query.ts +23 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +19 -11
- package/src/auth-email-password/handlers/signup-request.write.ts +22 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +4 -2
- package/src/auth-email-password/i18n.ts +52 -0
- package/src/auth-email-password/index.ts +13 -2
- package/src/auth-email-password/lockout-store.ts +10 -5
- package/src/auth-email-password/password-policy.ts +2108 -0
- package/src/auth-email-password/seeding.ts +55 -27
- package/src/auth-email-password/self-registration-toggle.ts +25 -0
- package/src/auth-email-password/signed-token.ts +1 -0
- package/src/auth-email-password/unlock-token.ts +28 -0
- package/src/auth-email-password/web/__tests__/auth-client.test.ts +542 -0
- package/src/auth-email-password/web/__tests__/auth-form-logic.test.ts +44 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +194 -6
- package/src/auth-email-password/web/__tests__/client-plugin.test.tsx +34 -0
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +64 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +129 -2
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +149 -5
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +137 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +134 -0
- package/src/auth-email-password/web/__tests__/signup-screen.test.tsx +93 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +12 -2
- package/src/auth-email-password/web/__tests__/use-url-token.test.tsx +29 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +5 -1
- package/src/auth-email-password/web/auth-client.ts +96 -18
- package/src/auth-email-password/web/auth-form-logic.ts +30 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +22 -20
- package/src/auth-email-password/web/auth-gate.tsx +140 -19
- package/src/auth-email-password/web/client-plugin.ts +24 -2
- package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +6 -6
- package/src/auth-email-password/web/index.ts +16 -4
- package/src/auth-email-password/web/invite-accept-screen.tsx +5 -5
- package/src/auth-email-password/web/login-screen.tsx +78 -22
- package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +15 -18
- package/src/auth-email-password/web/session.tsx +11 -6
- package/src/auth-email-password/web/signup-complete-screen.tsx +13 -15
- package/src/auth-email-password/web/signup-screen.tsx +8 -10
- package/src/auth-email-password/web/use-shell-user.ts +5 -2
- package/src/auth-email-password/web/user-menu.tsx +3 -1
- package/src/auth-email-password/web/verify-email-screen.tsx +10 -14
- package/src/auth-foundation/__tests__/feature.test.ts +46 -0
- package/src/auth-foundation/__tests__/session-store.test.ts +77 -0
- package/src/auth-foundation/__tests__/tenant-providers.test.ts +160 -0
- package/src/auth-foundation/__tests__/token-verifier.test.ts +150 -0
- package/src/auth-foundation/boot-checks.ts +150 -0
- package/src/auth-foundation/feature.ts +157 -0
- package/src/auth-foundation/index.ts +37 -0
- package/src/auth-foundation/resolve-anonymous-access.ts +62 -0
- package/src/auth-foundation/types.ts +170 -0
- package/src/auth-mfa/__tests__/base32.test.ts +28 -0
- package/src/auth-mfa/__tests__/disable-and-regenerate.integration.test.ts +265 -0
- package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +321 -0
- package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +322 -0
- package/src/auth-mfa/__tests__/enable-start-preauth.integration.test.ts +237 -0
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +39 -0
- package/src/auth-mfa/__tests__/mfa-preauth-setup-token.test.ts +113 -0
- package/src/auth-mfa/__tests__/no-redis.integration.test.ts +209 -0
- package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +139 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +247 -0
- package/src/auth-mfa/__tests__/session-auto-revoke.integration.test.ts +169 -0
- package/src/auth-mfa/__tests__/totp.test.ts +131 -0
- package/src/auth-mfa/__tests__/verify.integration.test.ts +319 -0
- package/src/auth-mfa/base32.ts +51 -0
- package/src/auth-mfa/config.ts +26 -0
- package/src/auth-mfa/constants.ts +45 -0
- package/src/auth-mfa/db/queries.ts +45 -0
- package/src/auth-mfa/errors.ts +66 -0
- package/src/auth-mfa/feature.ts +170 -0
- package/src/auth-mfa/handlers/disable.write.ts +67 -0
- package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +200 -0
- package/src/auth-mfa/handlers/enable-confirm.write.ts +106 -0
- package/src/auth-mfa/handlers/enable-start-preauth.write.ts +87 -0
- package/src/auth-mfa/handlers/enable-start.write.ts +66 -0
- package/src/auth-mfa/handlers/reencrypt.job.ts +234 -0
- package/src/auth-mfa/handlers/regenerate-recovery.write.ts +77 -0
- package/src/auth-mfa/handlers/status.query.ts +23 -0
- package/src/auth-mfa/handlers/verify.write.ts +179 -0
- package/src/auth-mfa/i18n.ts +12 -0
- package/src/auth-mfa/index.ts +19 -0
- package/src/auth-mfa/mfa-challenge-token.ts +90 -0
- package/src/auth-mfa/mfa-preauth-setup-token.ts +86 -0
- package/src/auth-mfa/mfa-setup-token.ts +96 -0
- package/src/auth-mfa/mfa-status-checker.ts +84 -0
- package/src/auth-mfa/mfa-verify-attempts.ts +90 -0
- package/src/auth-mfa/otpauth-uri.ts +21 -0
- package/src/auth-mfa/recovery-codes.ts +54 -0
- package/src/auth-mfa/schema/user-mfa.ts +60 -0
- package/src/auth-mfa/testing.ts +5 -0
- package/src/auth-mfa/totp.ts +67 -0
- package/src/auth-mfa/verify-factor.ts +58 -0
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +265 -0
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +182 -0
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +379 -0
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +104 -0
- package/src/auth-mfa/web/client-plugin.ts +37 -0
- package/src/auth-mfa/web/i18n.ts +136 -0
- package/src/auth-mfa/web/index.ts +27 -0
- package/src/auth-mfa/web/mfa-client.ts +184 -0
- package/src/auth-mfa/web/mfa-disable-dialog.tsx +69 -0
- package/src/auth-mfa/web/mfa-enable-screen.tsx +228 -0
- package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +53 -0
- package/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +68 -0
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +267 -0
- package/src/auth-mfa/web/mfa-verify-screen.tsx +117 -0
- package/src/auth-mfa/web/qrcode-browser.d.ts +12 -0
- package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +134 -0
- package/src/auth-mfa-user-data/hooks.ts +53 -0
- package/src/auth-mfa-user-data/index.ts +21 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +206 -2
- package/src/billing-foundation/__tests__/subscription-provider-contract.ts +93 -0
- package/src/billing-foundation/entities.ts +28 -4
- package/src/billing-foundation/events.ts +6 -2
- package/src/billing-foundation/feature.ts +9 -1
- package/src/billing-foundation/get-subscription-for-tenant.ts +16 -5
- package/src/billing-foundation/handlers/create-portal-session.write.ts +17 -1
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +15 -1
- package/src/billing-foundation/handlers/process-event.write.ts +39 -3
- package/src/billing-foundation/tenant-destroy-hook.ts +46 -0
- package/src/cap-counter/__tests__/cap-list.boot.test.ts +27 -0
- package/src/cap-counter/__tests__/cap-list.security.integration.test.ts +53 -0
- package/src/cap-counter/__tests__/entity-ddl.test.ts +15 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +8 -5
- package/src/cap-counter/constants.ts +2 -0
- package/src/cap-counter/enforce-cap.ts +15 -18
- package/src/cap-counter/entity.ts +3 -3
- package/src/cap-counter/feature.ts +28 -1
- package/src/cap-counter/i18n.ts +12 -0
- package/src/cap-counter/with-cap-enforcement.ts +2 -2
- package/src/channel-email/__tests__/pii-guard.test.ts +56 -0
- package/src/channel-email/email-channel.ts +2 -1
- package/src/channel-email/index.ts +1 -0
- package/src/channel-email/pii-guard.ts +39 -0
- package/src/compliance-profiles/__tests__/profile-picker.boot.test.ts +28 -0
- package/src/compliance-profiles/__tests__/profile-picker.security.integration.test.ts +91 -0
- package/src/compliance-profiles/constants.ts +15 -0
- package/src/compliance-profiles/feature.ts +22 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +2 -2
- package/src/compliance-profiles/i18n.ts +6 -0
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/web/client-plugin.tsx +22 -0
- package/src/compliance-profiles/web/compliance-profile-screen.tsx +152 -0
- package/src/compliance-profiles/web/i18n.ts +25 -0
- package/src/compliance-profiles/web/index.ts +6 -0
- package/src/config/__tests__/app-overrides.test.ts +11 -0
- package/src/config/__tests__/backing-secrets.integration.test.ts +22 -0
- package/src/config/__tests__/cascade.integration.test.ts +35 -0
- package/src/config/__tests__/config.integration.test.ts +71 -10
- package/src/config/__tests__/deserialize-value.test.ts +5 -8
- package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
- package/src/config/__tests__/pii-encrypted.integration.test.ts +179 -0
- package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +234 -0
- package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +243 -0
- package/src/config/__tests__/write-helpers.test.ts +23 -0
- package/src/config/db/queries/resolver.ts +3 -3
- package/src/config/feature.ts +15 -8
- package/src/config/handlers/cascade.query.ts +2 -1
- package/src/config/handlers/reencrypt.job.ts +209 -0
- package/src/config/handlers/set.write.ts +55 -3
- package/src/config/handlers/values.query.ts +2 -1
- package/src/config/i18n.ts +9 -0
- package/src/config/index.ts +5 -4
- package/src/config/resolver.ts +75 -61
- package/src/config/table.ts +1 -1
- package/src/config/write-helpers.ts +22 -0
- package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +148 -0
- package/src/crypto-shredding/constants.ts +5 -0
- package/src/crypto-shredding/feature.ts +18 -0
- package/src/crypto-shredding/handlers/forget-subject.write.ts +91 -0
- package/src/crypto-shredding/index.ts +11 -0
- package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +92 -53
- package/src/custom-fields/__tests__/field-access.integration.test.ts +25 -0
- package/src/custom-fields/__tests__/parse-serialized-field.test.ts +11 -3
- package/src/custom-fields/__tests__/retention.integration.test.ts +40 -0
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +26 -79
- package/src/custom-fields/constants.ts +1 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +0 -20
- package/src/custom-fields/entity.ts +1 -1
- package/src/custom-fields/events.ts +4 -3
- package/src/custom-fields/feature.ts +8 -0
- package/src/custom-fields/handlers/delete-system-field.write.ts +2 -1
- package/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -1
- package/src/custom-fields/handlers/set-custom-field.write.ts +3 -44
- package/src/custom-fields/handlers/update-tenant-field.write.ts +0 -17
- package/src/custom-fields/lib/field-access.ts +8 -1
- package/src/custom-fields/lib/parse-serialized-field.ts +12 -4
- package/src/custom-fields/lib/value-schema.ts +1 -1
- package/src/custom-fields/run-retention.ts +9 -1
- package/src/custom-fields/schemas.ts +9 -9
- package/src/custom-fields/wire-for-entity.ts +110 -78
- package/src/custom-fields/wire-user-data-rights.ts +5 -54
- package/src/data-retention/__tests__/parse-override.test.ts +14 -13
- package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +145 -8
- package/src/data-retention/feature.ts +3 -3
- package/src/data-retention/run-retention-cleanup.ts +102 -19
- package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +171 -0
- package/src/delivery/__tests__/delivery-screens.boot.test.ts +27 -0
- package/src/delivery/__tests__/delivery-security.integration.test.ts +106 -0
- package/src/delivery/constants.ts +13 -51
- package/src/delivery/feature.ts +29 -5
- package/src/delivery/handlers/log.query.ts +28 -7
- package/src/delivery/i18n.ts +6 -0
- package/src/delivery/index.ts +6 -1
- package/src/delivery/public-names.ts +36 -0
- package/src/delivery/tables.ts +3 -3
- package/src/delivery/web/client-plugin.tsx +20 -0
- package/src/delivery/web/delivery-log-screen.tsx +103 -0
- package/src/delivery/web/i18n.ts +25 -0
- package/src/delivery/web/index.ts +3 -0
- package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +147 -0
- package/src/document-ingest-foundation/__tests__/feature.test.ts +105 -0
- package/src/document-ingest-foundation/__tests__/pages.test.ts +37 -0
- package/src/document-ingest-foundation/entity.ts +43 -0
- package/src/document-ingest-foundation/events.ts +25 -0
- package/src/document-ingest-foundation/feature.ts +126 -0
- package/src/document-ingest-foundation/index.ts +12 -0
- package/src/document-ingest-foundation/pages.ts +34 -0
- package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +304 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +7 -6
- package/src/feature-toggles/__tests__/toggle-admin.boot.test.ts +31 -0
- package/src/feature-toggles/__tests__/toggle-admin.security.integration.test.ts +69 -0
- package/src/feature-toggles/compose-tier-resolver.ts +134 -0
- package/src/feature-toggles/constants.ts +14 -0
- package/src/feature-toggles/feature.ts +23 -3
- package/src/feature-toggles/global-feature-state-table.ts +20 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +1 -1
- package/src/feature-toggles/i18n.ts +6 -0
- package/src/feature-toggles/index.ts +8 -0
- package/src/feature-toggles/toggle-runtime.ts +14 -0
- package/src/feature-toggles/web/client-plugin.tsx +22 -0
- package/src/feature-toggles/web/i18n.ts +29 -0
- package/src/feature-toggles/web/index.ts +6 -0
- package/src/feature-toggles/web/toggle-admin-screen.tsx +136 -0
- package/src/file-foundation/__tests__/feature.test.ts +5 -5
- package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
- package/src/file-foundation/feature.ts +10 -11
- package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +27 -2
- package/src/files-provider-s3/s3-provider.ts +12 -1
- package/src/folders/__tests__/folders.integration.test.ts +3 -0
- package/src/folders/web/__tests__/folder-manager.test.tsx +43 -2
- package/src/folders/web/__tests__/tree.test.ts +8 -0
- package/src/folders/web/folder-manager.tsx +2 -3
- package/src/folders/web/i18n.ts +0 -2
- package/src/folders/web/tree.ts +13 -2
- package/src/folders-user-data/__tests__/hooks.integration.test.ts +1 -0
- package/src/folders-user-data/index.ts +4 -4
- package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
- package/src/inbound-mail-foundation/__tests__/connect-routes.integration.test.ts +307 -0
- package/src/inbound-mail-foundation/__tests__/feature.test.ts +169 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +644 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +111 -0
- package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +237 -0
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +433 -0
- package/src/inbound-mail-foundation/aggregate-id.ts +40 -0
- package/src/inbound-mail-foundation/connect-routes.ts +230 -0
- package/src/inbound-mail-foundation/constants.ts +80 -0
- package/src/inbound-mail-foundation/db/queries/inbound-projections.ts +38 -0
- package/src/inbound-mail-foundation/entities.ts +172 -0
- package/src/inbound-mail-foundation/events.ts +147 -0
- package/src/inbound-mail-foundation/feature.ts +243 -0
- package/src/inbound-mail-foundation/handlers/account-state.ts +23 -0
- package/src/inbound-mail-foundation/handlers/connect-account.write.ts +112 -0
- package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +69 -0
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +333 -0
- package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +44 -0
- package/src/inbound-mail-foundation/handlers/list-messages.query.ts +58 -0
- package/src/inbound-mail-foundation/handlers/scope-visibility.ts +14 -0
- package/src/inbound-mail-foundation/handlers/update-account.write.ts +78 -0
- package/src/inbound-mail-foundation/index.ts +97 -0
- package/src/inbound-mail-foundation/oauth-state.ts +103 -0
- package/src/inbound-mail-foundation/projection.ts +152 -0
- package/src/inbound-mail-foundation/provider-factory.ts +53 -0
- package/src/inbound-mail-foundation/retention-sweep.ts +168 -0
- package/src/inbound-mail-foundation/tenant-destroy-hook.ts +93 -0
- package/src/inbound-mail-foundation/types.ts +250 -0
- package/src/inbound-mail-foundation/watch-supervisor.ts +482 -0
- package/src/inbound-provider-imap/__tests__/feature.test.ts +115 -0
- package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +302 -0
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +206 -0
- package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +358 -0
- package/src/inbound-provider-imap/credential-document.ts +51 -0
- package/src/inbound-provider-imap/feature.ts +283 -0
- package/src/inbound-provider-imap/imap-client.ts +158 -0
- package/src/inbound-provider-imap/index.ts +17 -0
- package/src/inbound-provider-inmemory/__tests__/feature.test.ts +52 -0
- package/src/inbound-provider-inmemory/feature.ts +154 -0
- package/src/inbound-provider-inmemory/index.ts +12 -0
- package/src/jobs/__tests__/entity-ddl.test.ts +15 -0
- package/src/jobs/__tests__/job-instant.test.ts +15 -0
- package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
- package/src/jobs/__tests__/jobs-events.integration.test.ts +37 -1
- package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +120 -0
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +36 -0
- package/src/jobs/__tests__/jobs-security.integration.test.ts +108 -0
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +169 -0
- package/src/jobs/constants.ts +8 -1
- package/src/jobs/feature.ts +52 -8
- package/src/jobs/handlers/detail.query.ts +6 -0
- package/src/jobs/handlers/list.query.ts +8 -1
- package/src/jobs/handlers/projection-rebuild.job.ts +14 -4
- package/src/jobs/handlers/reindex-entity.job.ts +49 -0
- package/src/jobs/i18n.ts +37 -0
- package/src/jobs/job-instant.ts +9 -0
- package/src/jobs/job-run-table.ts +4 -4
- package/src/jobs/web/client-plugin.tsx +22 -0
- package/src/jobs/web/i18n.ts +12 -0
- package/src/jobs/web/index.ts +4 -0
- package/src/jobs/web/job-run-detail-screen.tsx +180 -0
- package/src/jobs/web/job-runs-screen.tsx +146 -0
- package/src/ledger/__tests__/ledger.integration.test.ts +8 -3
- package/src/ledger/handlers/confirm-schedule-period.write.ts +17 -4
- package/src/ledger/recurring.ts +17 -8
- package/src/ledger/web/index.ts +3 -2
- package/src/legal-pages/__tests__/legal-pages.integration.test.ts +55 -0
- package/src/legal-pages/feature.ts +24 -31
- package/src/legal-pages/index.ts +1 -0
- package/src/mail-foundation/__tests__/feature.test.ts +5 -6
- package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
- package/src/mail-foundation/__tests__/mail-transport-contract.ts +58 -0
- package/src/mail-foundation/feature.ts +26 -24
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +23 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +65 -0
- package/src/managed-pages/__tests__/branding-coerce.test.ts +2 -6
- package/src/managed-pages/__tests__/i18n-surface.boot.test.ts +30 -0
- package/src/managed-pages/__tests__/managed-pages.integration.test.ts +137 -0
- package/src/managed-pages/feature.ts +45 -47
- package/src/managed-pages/handlers/branding.query.ts +5 -1
- package/src/managed-pages/handlers/by-tenant-published.query.ts +52 -0
- package/src/managed-pages/i18n.ts +66 -0
- package/src/managed-pages/screens/page-screens.ts +5 -1
- package/src/managed-pages/web/client-plugin.tsx +22 -0
- package/src/managed-pages/web/i18n.ts +24 -0
- package/src/managed-pages/web/index.ts +3 -0
- package/src/page-render/__tests__/layout.test.ts +63 -0
- package/src/page-render/__tests__/markdown.test.ts +29 -0
- package/src/page-render/branding.ts +4 -3
- package/src/page-render/index.ts +1 -1
- package/src/page-render/layout.ts +18 -3
- package/src/page-render/markdown.ts +1 -9
- package/src/page-render/web.ts +13 -0
- package/src/personal-access-tokens/__tests__/feature.test.ts +42 -0
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +239 -0
- package/src/personal-access-tokens/__tests__/scopes.test.ts +42 -0
- package/src/personal-access-tokens/constants.ts +33 -0
- package/src/personal-access-tokens/feature.ts +105 -0
- package/src/personal-access-tokens/handlers/available-scopes.query.ts +21 -0
- package/src/personal-access-tokens/handlers/create.write.ts +47 -0
- package/src/personal-access-tokens/handlers/list.query.ts +50 -0
- package/src/personal-access-tokens/handlers/revoke.write.ts +37 -0
- package/src/personal-access-tokens/hash.ts +17 -0
- package/src/personal-access-tokens/i18n.ts +5 -0
- package/src/personal-access-tokens/index.ts +31 -0
- package/src/personal-access-tokens/resolver.ts +58 -0
- package/src/personal-access-tokens/roles.ts +29 -0
- package/src/personal-access-tokens/schema/api-token.ts +63 -0
- package/src/personal-access-tokens/scopes.ts +43 -0
- package/src/personal-access-tokens/web/client-plugin.tsx +29 -0
- package/src/personal-access-tokens/web/i18n.ts +69 -0
- package/src/personal-access-tokens/web/index.ts +7 -0
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +265 -0
- package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
- package/src/renderer-simple/__tests__/resolve-variables.test.ts +37 -0
- package/src/renderer-simple/resolve-variables.ts +3 -1
- package/src/secrets/__tests__/rotate.integration.test.ts +115 -18
- package/src/secrets/handlers/rotate.job.ts +77 -98
- package/src/secrets/secrets-context.ts +7 -38
- package/src/secrets/table.ts +7 -8
- package/src/seo/__tests__/llms-txt.test.ts +38 -0
- package/src/seo/__tests__/robots-txt.test.ts +18 -0
- package/src/seo/__tests__/schema-builders.test.ts +57 -0
- package/src/seo/__tests__/seo.integration.test.ts +350 -0
- package/src/seo/__tests__/sitemap.test.ts +38 -0
- package/src/seo/constants.ts +60 -0
- package/src/seo/feature.ts +316 -0
- package/src/seo/handlers/seo-config.query.ts +35 -0
- package/src/seo/index.ts +19 -0
- package/src/seo/llms-txt.ts +33 -0
- package/src/seo/robots-txt.ts +13 -0
- package/src/seo/schema-builders.ts +57 -0
- package/src/seo/sitemap.ts +33 -0
- package/src/sessions/__tests__/auto-revoke-binding.test.ts +71 -0
- package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
- package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +11 -7
- package/src/sessions/__tests__/rebuild-survival.integration.test.ts +18 -7
- package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +55 -0
- package/src/sessions/__tests__/sessions.integration.test.ts +272 -20
- package/src/sessions/__tests__/test-helpers.ts +16 -3
- package/src/sessions/constants.ts +5 -0
- package/src/sessions/db/queries/cleanup.ts +2 -2
- package/src/sessions/feature.ts +175 -26
- package/src/sessions/handlers/detail.query.ts +36 -0
- package/src/sessions/handlers/list.query.ts +14 -9
- package/src/sessions/handlers/mine.query.ts +13 -8
- package/src/sessions/handlers/revoke-all-for-user.write.ts +51 -12
- package/src/sessions/handlers/revoke-all-others.write.ts +19 -1
- package/src/sessions/handlers/revoke.write.ts +13 -0
- package/src/sessions/i18n.ts +23 -0
- package/src/sessions/index.ts +9 -2
- package/src/sessions/schema/user-session.ts +8 -2
- package/src/sessions/session-callbacks.ts +99 -14
- package/src/sessions/session-checker-fail-open.test.ts +77 -0
- package/src/sessions/session-revoked-event.ts +29 -0
- package/src/sessions/testing.ts +14 -1
- package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
- package/src/shared/chunked-entity-migration.ts +89 -0
- package/src/shared/decrypt-stored-pii.ts +26 -0
- package/src/shared/encrypt-for-direct-write.ts +23 -0
- package/src/shared/entities-of.ts +17 -0
- package/src/shared/grace-period.test.ts +26 -0
- package/src/shared/grace-period.ts +20 -0
- package/src/{auth-email-password/__tests__ → shared}/identity-v3-hash.test.ts +2 -2
- package/src/shared/index.ts +16 -0
- package/src/shared/map-with-concurrency.test.ts +104 -0
- package/src/shared/map-with-concurrency.ts +38 -0
- package/src/shared/password-hashing.test.ts +35 -0
- package/src/{auth-email-password → shared}/password-hashing.ts +11 -0
- package/src/shared/system-query.ts +5 -0
- package/src/{auth-email-password → shared}/token-burn-store.ts +1 -1
- package/src/subscription-mollie/__tests__/feature.test.ts +97 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.test.ts +12 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +21 -0
- package/src/subscription-mollie/verify-webhook.ts +1 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +127 -1
- package/src/subscription-stripe/__tests__/runtime.test.ts +14 -24
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +15 -5
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +20 -0
- package/src/subscription-stripe/feature.ts +2 -0
- package/src/subscription-stripe/verify-webhook.ts +3 -2
- package/src/tags/__tests__/schemas.test.ts +34 -0
- package/src/tags/__tests__/tags.integration.test.ts +12 -1
- package/src/tags/entity.ts +6 -1
- package/src/tags/feature.ts +2 -0
- package/src/tags/i18n.ts +5 -0
- package/src/tags/schemas.ts +9 -2
- package/src/tags/web/__tests__/tag-manager.test.tsx +333 -0
- package/src/tags/web/__tests__/tag-section.test.tsx +47 -1
- package/src/tags/web/tag-manager.tsx +1 -1
- package/src/tags/web/tag-section.tsx +6 -3
- package/src/template-resolver/testing.ts +2 -1
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
- package/src/tenant/__tests__/members-screens.boot.test.ts +51 -0
- package/src/tenant/__tests__/multi-tenant.integration.test.ts +1 -1
- package/src/tenant/__tests__/tenant-security.integration.test.ts +310 -0
- package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
- package/src/tenant/constants.ts +5 -0
- package/src/tenant/feature.ts +19 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +8 -2
- package/src/tenant/handlers/cancel-invitation.write.ts +2 -2
- package/src/tenant/handlers/invitations.query.ts +21 -4
- package/src/tenant/handlers/members.query.ts +41 -6
- package/src/tenant/i18n.ts +28 -0
- package/src/tenant/index.ts +7 -2
- package/src/tenant/invitation-table.ts +3 -2
- package/src/tenant/schema/tenant.ts +27 -2
- package/src/tenant/screens.ts +3 -2
- package/src/tenant/seeding.ts +136 -3
- package/src/tenant/web/client-plugin.tsx +20 -0
- package/src/tenant/web/i18n.ts +43 -0
- package/src/tenant/web/index.ts +4 -0
- package/src/tenant/web/members-screen.tsx +270 -0
- package/src/tenant-lifecycle/__tests__/stages.test.ts +22 -0
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +471 -0
- package/src/tenant-lifecycle/boot-checks.ts +45 -0
- package/src/tenant-lifecycle/constants.ts +56 -0
- package/src/tenant-lifecycle/events.ts +46 -0
- package/src/tenant-lifecycle/feature.ts +112 -0
- package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +79 -0
- package/src/tenant-lifecycle/handlers/request-destruction.write.ts +83 -0
- package/src/tenant-lifecycle/index.ts +7 -0
- package/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +16 -0
- package/src/tenant-lifecycle/lifecycle-gate.ts +65 -0
- package/src/tenant-lifecycle/run-tenant-destroy.ts +368 -0
- package/src/tenant-lifecycle/stages.ts +206 -0
- package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +115 -0
- package/src/tenant-settings/config.ts +44 -0
- package/src/tenant-settings/constants.ts +16 -0
- package/src/tenant-settings/feature.ts +49 -0
- package/src/tenant-settings/index.ts +8 -0
- package/src/tenant-settings/tenant-defaults.ts +67 -0
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +33 -6
- package/src/text-content/web/__tests__/group-blocks.test.ts +28 -3
- package/src/text-content/web/client-plugin.tsx +126 -104
- package/src/tier-engine/__tests__/auto-default-tier.integration.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.test.ts +1 -1
- package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
- package/src/tier-engine/__tests__/trial-config-validation.test.ts +30 -0
- package/src/tier-engine/feature.ts +13 -6
- package/src/tier-engine/i18n.ts +8 -0
- package/src/tier-engine/web/tier-admin-screen.tsx +26 -19
- package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +186 -0
- package/src/user/__tests__/user.integration.test.ts +26 -0
- package/src/user/constants.ts +1 -0
- package/src/user/db/queries/stream-tenant-backfill.ts +112 -0
- package/src/user/feature.ts +3 -0
- package/src/user/handlers/find-for-auth.query.ts +8 -6
- package/src/user/i18n.ts +20 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/user.ts +6 -2
- package/src/user/screens.ts +3 -15
- package/src/user/seeding.ts +8 -0
- package/src/user-data-rights/COMPLIANCE.md +21 -1
- package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +145 -0
- package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +67 -0
- package/src/user-data-rights/__tests__/boot-checks.test.ts +170 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +169 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +101 -33
- package/src/user-data-rights/__tests__/email-templates.test.ts +15 -0
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +154 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-retention.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +41 -62
- package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +110 -0
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +14 -0
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +19 -2
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +25 -14
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +172 -19
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +257 -26
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +339 -5
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +77 -1
- package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
- package/src/user-data-rights/boot-checks.ts +70 -0
- package/src/user-data-rights/db/queries/__tests__/forget-cleanup.integration.test.ts +69 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +9 -2
- package/src/user-data-rights/email-templates.ts +6 -6
- package/src/user-data-rights/feature.ts +162 -43
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +8 -11
- package/src/user-data-rights/handlers/deletion-grace-period.ts +4 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +26 -18
- package/src/user-data-rights/handlers/download-by-token.query.ts +23 -15
- package/src/user-data-rights/handlers/lift-restriction.write.ts +44 -19
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -0
- package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +3 -1
- package/src/user-data-rights/handlers/restrict-account.write.ts +58 -7
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +2 -0
- package/src/user-data-rights/i18n.ts +35 -32
- package/src/user-data-rights/lib/is-admin-actor.ts +19 -0
- package/src/user-data-rights/lib/storage-provider-resolver.ts +6 -5
- package/src/user-data-rights/lib/update-user-lifecycle.ts +8 -24
- package/src/user-data-rights/run-export-jobs.ts +55 -19
- package/src/user-data-rights/run-forget-cleanup.ts +112 -7
- package/src/user-data-rights/run-user-export.ts +70 -4
- package/src/user-data-rights/schema/download-token.ts +5 -3
- package/src/user-data-rights/schema/export-job.ts +2 -0
- package/src/user-data-rights/screens.ts +2 -1
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -0
- package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +7 -1
- package/src/user-data-rights/web/client-plugin.tsx +6 -1
- package/src/user-data-rights/web/i18n.ts +30 -10
- package/src/user-data-rights/web/privacy-center-screen.tsx +5 -3
- package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +455 -0
- package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +33 -9
- package/src/user-data-rights-defaults/feature.ts +78 -1
- package/src/user-data-rights-defaults/hooks/api-token.userdata-hook.ts +37 -0
- package/src/user-data-rights-defaults/hooks/config-value.userdata-hook.ts +51 -0
- package/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +37 -0
- package/src/user-data-rights-defaults/hooks/feature-mounted.ts +10 -0
- package/src/user-data-rights-defaults/hooks/in-app-message.userdata-hook.ts +38 -0
- package/src/user-data-rights-defaults/hooks/job-run.userdata-hook.ts +35 -0
- package/src/user-data-rights-defaults/hooks/notification-preference.userdata-hook.ts +54 -0
- package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +112 -0
- package/src/user-data-rights-defaults/hooks/user-session.userdata-hook.ts +42 -0
- package/src/user-data-rights-defaults/index.ts +26 -0
- package/src/user-profile/__tests__/change-email.integration.test.ts +7 -1
- package/src/user-profile/__tests__/profile-screen.test.tsx +5 -0
- package/src/user-profile/feature.ts +4 -1
- package/src/user-profile/handlers/change-email.write.ts +15 -10
- package/src/user-profile/web/profile-screen.tsx +15 -3
- package/src/auth-email-password/stream-tenant.ts +0 -31
- package/src/feature-toggles/db/queries/toggle-state.ts +0 -25
- package/src/legal-pages/security-headers.ts +0 -1
- package/src/sessions/schema/index.ts +0 -5
- package/src/tenant/schema/index.ts +0 -5
- package/src/user/schema/index.ts +0 -5
- /package/src/{auth-email-password → shared}/identity-v3-hash.ts +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Regression guard for kumiko-framework#1188: proves a write interleaved
|
|
2
|
+
// with a running rotation survives — the event-store executor's optimistic
|
|
3
|
+
// concurrency check rejects the rotation job's stale-version update, so the
|
|
4
|
+
// version_conflict branch in reencrypt.job.ts's migrateRow (currently just
|
|
5
|
+
// "a concurrent config:set beat us; the row now holds a fresh envelope —
|
|
6
|
+
// already fine.") is verified, not just an unverified comment. Modeled on
|
|
7
|
+
// reencrypt-job-kek-rotation.integration.test.ts (kumiko-framework#1187)
|
|
8
|
+
// for the stack/rotation setup.
|
|
9
|
+
//
|
|
10
|
+
// The race is simulated deterministically: the job context's
|
|
11
|
+
// configEncryption.encrypt is wrapped so the FIRST call (which migrateRow
|
|
12
|
+
// makes right before its own executor.update) performs a second, unrelated
|
|
13
|
+
// write to the same row first. That write lands on the row version the job
|
|
14
|
+
// captured at batch-read time, so the job's own update — issued a moment
|
|
15
|
+
// later with that now-stale version — is guaranteed to hit version_conflict.
|
|
16
|
+
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
20
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
21
|
+
import {
|
|
22
|
+
access,
|
|
23
|
+
createSystemConfig,
|
|
24
|
+
createSystemUser,
|
|
25
|
+
defineFeature,
|
|
26
|
+
SYSTEM_TENANT_ID,
|
|
27
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
28
|
+
import { rebuildProjection } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
29
|
+
import {
|
|
30
|
+
createEnvelopeCipher,
|
|
31
|
+
createEnvMasterKeyProvider,
|
|
32
|
+
type EnvelopeCipher,
|
|
33
|
+
} from "@cosmicdrift/kumiko-framework/secrets";
|
|
34
|
+
import {
|
|
35
|
+
setupTestStack,
|
|
36
|
+
type TestStack,
|
|
37
|
+
unsafePushTables,
|
|
38
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
39
|
+
import { createMutableMasterKeyProvider } from "@cosmicdrift/kumiko-framework/testing";
|
|
40
|
+
import { createConfigFeature } from "../feature";
|
|
41
|
+
import { reencryptJob } from "../handlers/reencrypt.job";
|
|
42
|
+
import { createConfigResolver } from "../resolver";
|
|
43
|
+
import { configValueEntity, configValuesTable } from "../table";
|
|
44
|
+
|
|
45
|
+
const KEY = "kek-rot-race:config:race-pass";
|
|
46
|
+
const ORIGINAL_PLAINTEXT = "pre-race-value";
|
|
47
|
+
const CONCURRENT_PLAINTEXT = "concurrent-write-value";
|
|
48
|
+
const PROJECTION_NAME = "config:projection:config-value-entity";
|
|
49
|
+
|
|
50
|
+
const v1Key = randomBytes(32).toString("base64");
|
|
51
|
+
const v2Key = randomBytes(32).toString("base64");
|
|
52
|
+
const mutableProvider = createMutableMasterKeyProvider(
|
|
53
|
+
createEnvMasterKeyProvider({
|
|
54
|
+
env: {
|
|
55
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: v1Key,
|
|
56
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
const cipher = createEnvelopeCipher(mutableProvider, {});
|
|
61
|
+
const resolver = createConfigResolver({ cipher });
|
|
62
|
+
|
|
63
|
+
const keyDef = createSystemConfig("text", {
|
|
64
|
+
encrypted: true,
|
|
65
|
+
read: access.systemAdmin,
|
|
66
|
+
write: access.systemAdmin,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const rotFeature = defineFeature("kek-rot-race", (r) => {
|
|
70
|
+
r.requires("config");
|
|
71
|
+
r.config({ keys: { "race-pass": keyDef } });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const executor = createEventStoreExecutor(configValuesTable, configValueEntity, {
|
|
75
|
+
entityName: "config-value",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let stack: TestStack;
|
|
79
|
+
|
|
80
|
+
type ConfigRow = {
|
|
81
|
+
id: string;
|
|
82
|
+
key: string;
|
|
83
|
+
value: string | null;
|
|
84
|
+
tenantId: string;
|
|
85
|
+
version: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
async function readRow(): Promise<ConfigRow> {
|
|
89
|
+
const rows = await selectMany<ConfigRow>(stack.db, configValuesTable, { key: KEY });
|
|
90
|
+
const row = rows[0];
|
|
91
|
+
if (!row) throw new Error("no config row");
|
|
92
|
+
return row;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function seedV1Row(): Promise<void> {
|
|
96
|
+
const envelope = await cipher.encrypt(JSON.stringify(ORIGINAL_PLAINTEXT), {
|
|
97
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
98
|
+
});
|
|
99
|
+
const systemUser = createSystemUser(SYSTEM_TENANT_ID);
|
|
100
|
+
const tdb = createTenantDb(stack.db, SYSTEM_TENANT_ID, "system");
|
|
101
|
+
const result = await executor.create(
|
|
102
|
+
{ key: KEY, value: envelope, tenantId: SYSTEM_TENANT_ID, userId: null },
|
|
103
|
+
systemUser,
|
|
104
|
+
tdb,
|
|
105
|
+
);
|
|
106
|
+
if (!result.isSuccess) throw new Error(`seed failed: ${result.error.code}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Writes CONCURRENT_PLAINTEXT to the row using the version the rotation job
|
|
110
|
+
// itself captured at batch-read time — the same version-N -> N+1 bump a
|
|
111
|
+
// real interleaved handler write would cause.
|
|
112
|
+
async function writeConcurrently(atVersion: number): Promise<void> {
|
|
113
|
+
const envelope = await cipher.encrypt(JSON.stringify(CONCURRENT_PLAINTEXT), {
|
|
114
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
115
|
+
});
|
|
116
|
+
const systemUser = createSystemUser(SYSTEM_TENANT_ID);
|
|
117
|
+
const tdb = createTenantDb(stack.db, SYSTEM_TENANT_ID, "system");
|
|
118
|
+
const row = await readRow();
|
|
119
|
+
const result = await executor.update(
|
|
120
|
+
{ id: row.id, version: atVersion, changes: { value: envelope } },
|
|
121
|
+
systemUser,
|
|
122
|
+
tdb,
|
|
123
|
+
);
|
|
124
|
+
if (!result.isSuccess) throw new Error(`concurrent write setup failed: ${result.error.code}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const noopLog = {
|
|
128
|
+
info: () => {},
|
|
129
|
+
warn: () => {},
|
|
130
|
+
error: () => {},
|
|
131
|
+
debug: () => {},
|
|
132
|
+
child: () => noopLog,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Fires the concurrent write on the first encrypt() call the job makes —
|
|
136
|
+
// that's the point in migrateRow right after decrypt, right before its own
|
|
137
|
+
// executor.update, so the job's captured row.version is guaranteed stale by
|
|
138
|
+
// the time it issues that update.
|
|
139
|
+
function racyJobCtx(
|
|
140
|
+
rowVersionAtBatchRead: number,
|
|
141
|
+
log: typeof noopLog = noopLog,
|
|
142
|
+
): Parameters<typeof reencryptJob>[1] {
|
|
143
|
+
let fired = false;
|
|
144
|
+
const racyCipher: EnvelopeCipher = {
|
|
145
|
+
decrypt: (stored, scope) => cipher.decrypt(stored, scope),
|
|
146
|
+
encrypt: async (plaintext, scope) => {
|
|
147
|
+
if (!fired) {
|
|
148
|
+
fired = true;
|
|
149
|
+
await writeConcurrently(rowVersionAtBatchRead);
|
|
150
|
+
}
|
|
151
|
+
return cipher.encrypt(plaintext, scope);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
db: stack.db,
|
|
156
|
+
registry: stack.registry,
|
|
157
|
+
masterKeyProvider: mutableProvider,
|
|
158
|
+
configEncryption: racyCipher,
|
|
159
|
+
log,
|
|
160
|
+
} as unknown as Parameters<typeof reencryptJob>[1]; // @cast-boundary test-seam — job only reads db/registry/masterKeyProvider/configEncryption/log
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
beforeAll(async () => {
|
|
164
|
+
stack = await setupTestStack({
|
|
165
|
+
features: [createConfigFeature(), rotFeature],
|
|
166
|
+
masterKeyProvider: mutableProvider,
|
|
167
|
+
extraContext: { configResolver: resolver, configEncryption: cipher },
|
|
168
|
+
});
|
|
169
|
+
await unsafePushTables(stack.db, { configValuesTable });
|
|
170
|
+
await seedV1Row();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterAll(async () => {
|
|
174
|
+
await stack.cleanup();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("config KEK-rotation job — concurrent write race (kumiko-framework#1188)", () => {
|
|
178
|
+
test("a write interleaved with a running rotation wins — rotation's stale update is rejected, not silently applied", async () => {
|
|
179
|
+
const seeded = await readRow();
|
|
180
|
+
expect(JSON.parse(seeded.value as string).kekVersion).toBe(1);
|
|
181
|
+
|
|
182
|
+
// "ops flips CURRENT=2 mid-flight" — same rotation trigger as #1187's test.
|
|
183
|
+
mutableProvider.replace(
|
|
184
|
+
createEnvMasterKeyProvider({
|
|
185
|
+
env: {
|
|
186
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: v1Key,
|
|
187
|
+
KUMIKO_SECRETS_MASTER_KEY_V2: v2Key,
|
|
188
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "2",
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Distinguishes "skipped" (version_conflict, row already fine) from
|
|
194
|
+
// "failed" (any other rejection) — both leave the row's end-state
|
|
195
|
+
// identical (reencrypt.job.ts:159-166), so a regression that silently
|
|
196
|
+
// turns this branch into "failed" would pass every assertion below it.
|
|
197
|
+
// The warn log at reencrypt.job.ts:171 is the only observable signature
|
|
198
|
+
// that fires on "failed" but not on "skipped" — assert it stayed silent.
|
|
199
|
+
const warnCalls: unknown[][] = [];
|
|
200
|
+
const spyLog = {
|
|
201
|
+
...noopLog,
|
|
202
|
+
warn: (...args: unknown[]) => {
|
|
203
|
+
warnCalls.push(args);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
await reencryptJob({}, racyJobCtx(seeded.version, spyLog));
|
|
207
|
+
expect(warnCalls).toEqual([]);
|
|
208
|
+
|
|
209
|
+
// The concurrent write already lands under the current key — its
|
|
210
|
+
// envelope must survive untouched; the rotation job's own update lost
|
|
211
|
+
// the version_conflict race and must not have overwritten it.
|
|
212
|
+
const afterJobRow = await readRow();
|
|
213
|
+
expect(JSON.parse(afterJobRow.value as string).kekVersion).toBe(2);
|
|
214
|
+
expect(await resolver.get(KEY, keyDef, SYSTEM_TENANT_ID, "u1", stack.db)).toBe(
|
|
215
|
+
CONCURRENT_PLAINTEXT,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Rebuild guard: a from-scratch replay must land on the concurrent
|
|
219
|
+
// write's event, not resurrect the rotation job's rejected update (it
|
|
220
|
+
// never became an event) nor the original pre-race value.
|
|
221
|
+
expect(stack.registry.getAllProjections().has(PROJECTION_NAME)).toBe(true);
|
|
222
|
+
const rebuildResult = await rebuildProjection(PROJECTION_NAME, {
|
|
223
|
+
db: stack.db,
|
|
224
|
+
registry: stack.registry,
|
|
225
|
+
});
|
|
226
|
+
expect(rebuildResult.eventsProcessed).toBeGreaterThan(0);
|
|
227
|
+
|
|
228
|
+
const afterRebuildRow = await readRow();
|
|
229
|
+
expect(JSON.parse(afterRebuildRow.value as string).kekVersion).toBe(2);
|
|
230
|
+
expect(await resolver.get(KEY, keyDef, SYSTEM_TENANT_ID, "u1", stack.db)).toBe(
|
|
231
|
+
CONCURRENT_PLAINTEXT,
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Regression guard for kumiko-framework#1187: proves the config
|
|
2
|
+
// KEK-rotation job handles a real version-N → version-N+1 rotation. Modeled
|
|
3
|
+
// directly on auth-mfa's
|
|
4
|
+
// reencrypt-job.integration.test.ts, which is the reference regression
|
|
5
|
+
// guard for this exact bug class (kumiko-framework#266 Step 8): rotation
|
|
6
|
+
// must go through executor.update() (a real event), not a raw UPDATE on
|
|
7
|
+
// the projection table — otherwise a full projection rebuild replays the
|
|
8
|
+
// OLD event and resurrects the pre-rotation kekVersion.
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
13
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
14
|
+
import {
|
|
15
|
+
access,
|
|
16
|
+
createSystemConfig,
|
|
17
|
+
createSystemUser,
|
|
18
|
+
defineFeature,
|
|
19
|
+
SYSTEM_TENANT_ID,
|
|
20
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
21
|
+
import { rebuildProjection } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
22
|
+
import {
|
|
23
|
+
createEnvelopeCipher,
|
|
24
|
+
createEnvMasterKeyProvider,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/secrets";
|
|
26
|
+
import {
|
|
27
|
+
setupTestStack,
|
|
28
|
+
type TestStack,
|
|
29
|
+
unsafePushTables,
|
|
30
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
31
|
+
import { createMutableMasterKeyProvider } from "@cosmicdrift/kumiko-framework/testing";
|
|
32
|
+
import { createConfigFeature } from "../feature";
|
|
33
|
+
import { reencryptJob } from "../handlers/reencrypt.job";
|
|
34
|
+
import { createConfigResolver } from "../resolver";
|
|
35
|
+
import { configValueEntity, configValuesTable } from "../table";
|
|
36
|
+
|
|
37
|
+
const KEY = "kek-rot:config:secret-pass";
|
|
38
|
+
const PLAINTEXT = "rotate-me-please";
|
|
39
|
+
const PROJECTION_NAME = "config:projection:config-value-entity";
|
|
40
|
+
|
|
41
|
+
const v1Key = randomBytes(32).toString("base64");
|
|
42
|
+
const v2Key = randomBytes(32).toString("base64");
|
|
43
|
+
const mutableProvider = createMutableMasterKeyProvider(
|
|
44
|
+
createEnvMasterKeyProvider({
|
|
45
|
+
env: {
|
|
46
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: v1Key,
|
|
47
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
const cipher = createEnvelopeCipher(mutableProvider, {});
|
|
52
|
+
const resolver = createConfigResolver({ cipher });
|
|
53
|
+
|
|
54
|
+
const keyDef = createSystemConfig("text", {
|
|
55
|
+
encrypted: true,
|
|
56
|
+
read: access.systemAdmin,
|
|
57
|
+
write: access.systemAdmin,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const GARBAGE_KEY = "kek-rot:config:garbage-value";
|
|
61
|
+
const GARBAGE_VALUE = "not-json-and-not-an-envelope";
|
|
62
|
+
|
|
63
|
+
const rotFeature = defineFeature("kek-rot", (r) => {
|
|
64
|
+
r.requires("config");
|
|
65
|
+
r.config({ keys: { "secret-pass": keyDef, "garbage-value": keyDef } });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const executor = createEventStoreExecutor(configValuesTable, configValueEntity, {
|
|
69
|
+
entityName: "config-value",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let stack: TestStack;
|
|
73
|
+
|
|
74
|
+
async function seedV1Row(): Promise<void> {
|
|
75
|
+
const envelope = await cipher.encrypt(JSON.stringify(PLAINTEXT), { tenantId: SYSTEM_TENANT_ID });
|
|
76
|
+
const systemUser = createSystemUser(SYSTEM_TENANT_ID);
|
|
77
|
+
const tdb = createTenantDb(stack.db, SYSTEM_TENANT_ID, "system");
|
|
78
|
+
const result = await executor.create(
|
|
79
|
+
{ key: KEY, value: envelope, tenantId: SYSTEM_TENANT_ID, userId: null },
|
|
80
|
+
systemUser,
|
|
81
|
+
tdb,
|
|
82
|
+
);
|
|
83
|
+
if (!result.isSuccess) throw new Error(`seed failed: ${result.error.code}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function seedGarbageRow(): Promise<void> {
|
|
87
|
+
const systemUser = createSystemUser(SYSTEM_TENANT_ID);
|
|
88
|
+
const tdb = createTenantDb(stack.db, SYSTEM_TENANT_ID, "system");
|
|
89
|
+
const result = await executor.create(
|
|
90
|
+
{ key: GARBAGE_KEY, value: GARBAGE_VALUE, tenantId: SYSTEM_TENANT_ID, userId: null },
|
|
91
|
+
systemUser,
|
|
92
|
+
tdb,
|
|
93
|
+
);
|
|
94
|
+
if (!result.isSuccess) throw new Error(`seed failed: ${result.error.code}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type RawConfigRow = { value: string };
|
|
98
|
+
|
|
99
|
+
async function readRawValue(key: string = KEY): Promise<RawConfigRow> {
|
|
100
|
+
const rows = await selectMany<RawConfigRow>(stack.db, configValuesTable, { key });
|
|
101
|
+
const row = rows[0];
|
|
102
|
+
if (!row) throw new Error("no config row");
|
|
103
|
+
return row;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const noopLog = {
|
|
107
|
+
info: () => {},
|
|
108
|
+
warn: () => {},
|
|
109
|
+
error: () => {},
|
|
110
|
+
debug: () => {},
|
|
111
|
+
child: () => noopLog,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type CapturedLog = { info: string[]; warn: string[] };
|
|
115
|
+
|
|
116
|
+
type TestJobLog = {
|
|
117
|
+
info: (msg: string) => void;
|
|
118
|
+
warn: (msg: string) => void;
|
|
119
|
+
error: () => void;
|
|
120
|
+
debug: () => void;
|
|
121
|
+
child: () => TestJobLog;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function capturingLog(captured: CapturedLog): TestJobLog {
|
|
125
|
+
const log: TestJobLog = {
|
|
126
|
+
info: (msg) => captured.info.push(msg),
|
|
127
|
+
warn: (msg) => captured.warn.push(msg),
|
|
128
|
+
error: () => {},
|
|
129
|
+
debug: () => {},
|
|
130
|
+
child: () => log,
|
|
131
|
+
};
|
|
132
|
+
return log;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function jobCtx(captured?: CapturedLog): Parameters<typeof reencryptJob>[1] {
|
|
136
|
+
const log = captured ? capturingLog(captured) : noopLog;
|
|
137
|
+
return {
|
|
138
|
+
db: stack.db,
|
|
139
|
+
registry: stack.registry,
|
|
140
|
+
masterKeyProvider: mutableProvider,
|
|
141
|
+
configEncryption: cipher,
|
|
142
|
+
log,
|
|
143
|
+
} as unknown as Parameters<typeof reencryptJob>[1]; // @cast-boundary test-seam — job only reads db/registry/masterKeyProvider/configEncryption/log
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
beforeAll(async () => {
|
|
147
|
+
stack = await setupTestStack({
|
|
148
|
+
features: [createConfigFeature(), rotFeature],
|
|
149
|
+
masterKeyProvider: mutableProvider,
|
|
150
|
+
extraContext: { configResolver: resolver, configEncryption: cipher },
|
|
151
|
+
});
|
|
152
|
+
await unsafePushTables(stack.db, { configValuesTable });
|
|
153
|
+
await seedV1Row();
|
|
154
|
+
await seedGarbageRow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterAll(async () => {
|
|
158
|
+
await stack.cleanup();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("config KEK-rotation job — unrecognized values are not silently skipped (kumiko-framework#1513)", () => {
|
|
162
|
+
test("a non-envelope row is counted failed, never alreadyCurrent, and is left untouched", async () => {
|
|
163
|
+
const beforeRow = await readRawValue(GARBAGE_KEY);
|
|
164
|
+
expect(beforeRow.value).toBe(GARBAGE_VALUE);
|
|
165
|
+
|
|
166
|
+
// Runs before the #1187 rotation test below flips the master-key
|
|
167
|
+
// version, so counts are deterministic: the V1 row is current, the
|
|
168
|
+
// garbage row is unrecognized, nothing needs rotating yet.
|
|
169
|
+
const captured: CapturedLog = { info: [], warn: [] };
|
|
170
|
+
await reencryptJob({}, jobCtx(captured));
|
|
171
|
+
|
|
172
|
+
const completeLine = captured.info.find((line) =>
|
|
173
|
+
line.includes("[config:reencrypt] complete:"),
|
|
174
|
+
);
|
|
175
|
+
if (!completeLine) throw new Error("job did not log a completion summary");
|
|
176
|
+
const result = JSON.parse(completeLine.slice(completeLine.indexOf("{"))) as {
|
|
177
|
+
migrated: number;
|
|
178
|
+
failed: number;
|
|
179
|
+
alreadyCurrent: number;
|
|
180
|
+
};
|
|
181
|
+
expect(result.alreadyCurrent).toBe(1);
|
|
182
|
+
expect(result.migrated).toBe(0);
|
|
183
|
+
expect(result.failed).toBe(1);
|
|
184
|
+
|
|
185
|
+
const rejectedWarning = captured.warn.find((line) =>
|
|
186
|
+
line.includes("not a current-cipher envelope"),
|
|
187
|
+
);
|
|
188
|
+
expect(rejectedWarning).toBeDefined();
|
|
189
|
+
|
|
190
|
+
// Never written: an unreadable row must not be rewritten as if it
|
|
191
|
+
// decrypted cleanly, and must not be reported as already current.
|
|
192
|
+
const afterRow = await readRawValue(GARBAGE_KEY);
|
|
193
|
+
expect(afterRow.value).toBe(GARBAGE_VALUE);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("config KEK-rotation job — version-N to version-N+1 (kumiko-framework#1187)", () => {
|
|
198
|
+
test("rotation rewraps the value onto the current KEK, and a full projection rebuild still lands on it", async () => {
|
|
199
|
+
const beforeRow = await readRawValue();
|
|
200
|
+
expect(JSON.parse(beforeRow.value).kekVersion).toBe(1);
|
|
201
|
+
|
|
202
|
+
// "ops added a new master key version and flipped CURRENT=2" —
|
|
203
|
+
// same simulate-rotation-without-restart shape auth-mfa's test uses.
|
|
204
|
+
mutableProvider.replace(
|
|
205
|
+
createEnvMasterKeyProvider({
|
|
206
|
+
env: {
|
|
207
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: v1Key,
|
|
208
|
+
KUMIKO_SECRETS_MASTER_KEY_V2: v2Key,
|
|
209
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "2",
|
|
210
|
+
},
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await reencryptJob({}, jobCtx());
|
|
215
|
+
|
|
216
|
+
const afterJobRow = await readRawValue();
|
|
217
|
+
expect(JSON.parse(afterJobRow.value).kekVersion).toBe(2);
|
|
218
|
+
|
|
219
|
+
// Vacuous-rebuild guard: confirm the projection name actually exists
|
|
220
|
+
// before trusting a silent no-op rebuild as a pass.
|
|
221
|
+
expect(stack.registry.getAllProjections().has(PROJECTION_NAME)).toBe(true);
|
|
222
|
+
|
|
223
|
+
const rebuildResult = await rebuildProjection(PROJECTION_NAME, {
|
|
224
|
+
db: stack.db,
|
|
225
|
+
registry: stack.registry,
|
|
226
|
+
});
|
|
227
|
+
expect(rebuildResult.eventsProcessed).toBeGreaterThan(0);
|
|
228
|
+
|
|
229
|
+
// The regression guard itself: a from-scratch rebuild replays every
|
|
230
|
+
// event for this row and must land on the V2 envelope from the
|
|
231
|
+
// rotation job's own .updated event — NOT a resurrected V1 wrap from
|
|
232
|
+
// the original seed event. Kept alive with the V1 key still present
|
|
233
|
+
// in the env on purpose: a resurrected V1 row would still decrypt
|
|
234
|
+
// cleanly, so only this version-tag check (not the decrypt below)
|
|
235
|
+
// catches the regression this issue describes.
|
|
236
|
+
const afterRebuildRow = await readRawValue();
|
|
237
|
+
expect(JSON.parse(afterRebuildRow.value).kekVersion).toBe(2);
|
|
238
|
+
|
|
239
|
+
// Decrypt-level proof, not just the version tag: the plaintext
|
|
240
|
+
// survived rotation + rebuild byte-identically.
|
|
241
|
+
expect(await resolver.get(KEY, keyDef, SYSTEM_TENANT_ID, "u1", stack.db)).toBe(PLAINTEXT);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -10,6 +10,7 @@ import type { KumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
|
10
10
|
import {
|
|
11
11
|
checkScopeWriteAccess,
|
|
12
12
|
hasConfigAccess,
|
|
13
|
+
resolvePiiSubject,
|
|
13
14
|
resolveScopeIds,
|
|
14
15
|
validatePattern,
|
|
15
16
|
validateScope,
|
|
@@ -101,6 +102,28 @@ describe("resolveScopeIds", () => {
|
|
|
101
102
|
});
|
|
102
103
|
});
|
|
103
104
|
|
|
105
|
+
describe("resolvePiiSubject (kumiko-platform#231/#459)", () => {
|
|
106
|
+
const tenant = "tenant-9" as TenantId;
|
|
107
|
+
|
|
108
|
+
test("tenant scope resolves to the tenant subject", () => {
|
|
109
|
+
expect(resolvePiiSubject(ConfigScopes.tenant, tenant, null)).toEqual({
|
|
110
|
+
kind: "tenant",
|
|
111
|
+
tenantId: tenant,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("user scope resolves to the user subject — the row's target user, not necessarily the writer", () => {
|
|
116
|
+
expect(resolvePiiSubject(ConfigScopes.user, tenant, "user-1")).toEqual({
|
|
117
|
+
kind: "user",
|
|
118
|
+
userId: "user-1",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("user scope without a userId throws instead of silently falling back to tenant", () => {
|
|
123
|
+
expect(() => resolvePiiSubject(ConfigScopes.user, tenant, null)).toThrow(/userId is null/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
104
127
|
describe("validateType", () => {
|
|
105
128
|
const numberKey = createTenantConfig("number", {});
|
|
106
129
|
const boolKey = createTenantConfig("boolean", {});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
|
|
5
5
|
export type ConfigRow = {
|
|
@@ -11,7 +11,7 @@ export type ConfigRow = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export async function selectConfigRowsForScope(
|
|
14
|
-
db: DbRunner,
|
|
14
|
+
db: DbRunner | TenantDb,
|
|
15
15
|
systemTenantId: TenantId,
|
|
16
16
|
tenantId: TenantId,
|
|
17
17
|
userId: string,
|
|
@@ -27,7 +27,7 @@ export async function selectConfigRowsForScope(
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export async function selectConfigRowsForKeys(
|
|
30
|
-
db: DbRunner,
|
|
30
|
+
db: DbRunner | TenantDb,
|
|
31
31
|
keys: readonly string[],
|
|
32
32
|
systemTenantId: TenantId,
|
|
33
33
|
tenantId: TenantId,
|
package/src/config/feature.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DbConnection,
|
|
1
|
+
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import {
|
|
3
3
|
type ConfigAccessor,
|
|
4
4
|
type ConfigAccessorFactory,
|
|
@@ -13,12 +13,15 @@ import {
|
|
|
13
13
|
type TenantId,
|
|
14
14
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
15
15
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
16
|
+
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
16
17
|
import { cascadeQuery } from "./handlers/cascade.query";
|
|
17
18
|
import { readinessQuery } from "./handlers/readiness.query";
|
|
19
|
+
import { reencryptJob } from "./handlers/reencrypt.job";
|
|
18
20
|
import { resetWrite } from "./handlers/reset.write";
|
|
19
21
|
import { schemaQuery } from "./handlers/schema.query";
|
|
20
22
|
import { setWrite } from "./handlers/set.write";
|
|
21
23
|
import { valuesQuery } from "./handlers/values.query";
|
|
24
|
+
import { CONFIG_FEATURE_I18N } from "./i18n";
|
|
22
25
|
import type { ConfigResolver } from "./resolver";
|
|
23
26
|
import { configValueEntity } from "./table";
|
|
24
27
|
|
|
@@ -27,7 +30,7 @@ export type ConfigContext = { readonly config: ConfigAccessor };
|
|
|
27
30
|
export function createConfigFeature(): FeatureDefinition {
|
|
28
31
|
return defineFeature("config", (r) => {
|
|
29
32
|
r.describe(
|
|
30
|
-
"Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row \u2192 tenant-row \u2192 system-row \u2192 app-override (deploy-time `AppConfigOverrides`) \u2192 computed \u2192 feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through
|
|
33
|
+
"Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row \u2192 tenant-row \u2192 system-row \u2192 app-override (deploy-time `AppConfigOverrides`) \u2192 computed \u2192 feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through the envelope cipher (versioned master key). Use this feature whenever a tenant admin needs to customise behaviour at runtime without a code deploy.",
|
|
31
34
|
);
|
|
32
35
|
r.uiHints({
|
|
33
36
|
displayLabel: "Tenant Config Store",
|
|
@@ -55,6 +58,12 @@ export function createConfigFeature(): FeatureDefinition {
|
|
|
55
58
|
readiness: r.queryHandler(readinessQuery),
|
|
56
59
|
};
|
|
57
60
|
|
|
61
|
+
// KEK-rotation for encrypted values. Manual trigger — ops runs it once
|
|
62
|
+
// after adding a new master key version.
|
|
63
|
+
r.job("reencrypt", { trigger: { manual: true } }, reencryptJob);
|
|
64
|
+
|
|
65
|
+
r.translations({ keys: CONFIG_FEATURE_I18N });
|
|
66
|
+
|
|
58
67
|
return { handlers, queries };
|
|
59
68
|
});
|
|
60
69
|
}
|
|
@@ -116,16 +125,14 @@ export function requireConfigResolver(ctx: HandlerContext, handlerName: string):
|
|
|
116
125
|
// lazily so apps that never wire encryption still boot (and only crash
|
|
117
126
|
// if a handler tries to write an encrypted key without the provider in
|
|
118
127
|
// place, pointing at the exact wiring gap).
|
|
119
|
-
export function requireConfigEncryption(
|
|
120
|
-
ctx: HandlerContext,
|
|
121
|
-
handlerName: string,
|
|
122
|
-
): EncryptionProvider {
|
|
128
|
+
export function requireConfigEncryption(ctx: HandlerContext, handlerName: string): EnvelopeCipher {
|
|
123
129
|
if (!ctx.configEncryption) {
|
|
124
130
|
throw new InternalError({
|
|
125
131
|
message:
|
|
126
132
|
`[${handlerName}] ctx.configEncryption missing — at least one config key declares ` +
|
|
127
|
-
`encrypted: true, so the boot wiring must pass an
|
|
128
|
-
`extraContext.configEncryption (
|
|
133
|
+
`encrypted: true, so the boot wiring must pass an EnvelopeCipher via ` +
|
|
134
|
+
`extraContext.configEncryption (run{Prod,Dev}App wire it automatically from ` +
|
|
135
|
+
`KUMIKO_SECRETS_MASTER_KEY_V<n>; same instance the resolver was built with).`,
|
|
129
136
|
});
|
|
130
137
|
}
|
|
131
138
|
return ctx.configEncryption;
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
type ConfigCascade,
|
|
3
3
|
type ConfigCascadeLevel,
|
|
4
4
|
defineQueryHandler,
|
|
5
|
+
isEncryptedAtRest,
|
|
5
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { requireConfigResolver } from "../feature";
|
|
@@ -59,7 +60,7 @@ export const cascadeQuery = defineQueryHandler({
|
|
|
59
60
|
|
|
60
61
|
// backing="secrets" is masked like encrypted — the resolver revealed the
|
|
61
62
|
// plaintext for internal reads, but it must never reach the cascade UI.
|
|
62
|
-
if (keyDef
|
|
63
|
+
if (isEncryptedAtRest(keyDef)) {
|
|
63
64
|
const maskedLevels: ConfigCascadeLevel[] = cascade.levels.map((l) => ({
|
|
64
65
|
...l,
|
|
65
66
|
value: l.hasValue ? MASKED : l.value,
|