@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,209 @@
|
|
|
1
|
+
// KEK-rotation job for `encrypted: true` config values: envelope values
|
|
2
|
+
// wrapped under an older kekVersion get re-encrypted under
|
|
3
|
+
// provider.currentVersion(). Config has no kek_version column (values live
|
|
4
|
+
// in a TEXT column), so unlike the secrets rotate job the version check
|
|
5
|
+
// parses the stored JSON.
|
|
6
|
+
//
|
|
7
|
+
// A row whose value isn't a current-cipher envelope (malformed JSON, or
|
|
8
|
+
// any pre-envelope format) isn't a supported input here — cipher.decrypt
|
|
9
|
+
// rejects it, so it's counted `failed` rather than silently treated as
|
|
10
|
+
// already current. There is no legacy single-key decrypt path anymore.
|
|
11
|
+
//
|
|
12
|
+
// Idempotent: a re-run skips rows already on the current version. Every
|
|
13
|
+
// write goes through the event-store executor (config values are
|
|
14
|
+
// entity-backed — raw UPDATEs would be wiped by a projection rebuild),
|
|
15
|
+
// so each rotation appends a normal `.updated` event whose payload carries
|
|
16
|
+
// the NEW envelope: after a full run even a from-scratch rebuild only
|
|
17
|
+
// ever sees the current-KEK envelope.
|
|
18
|
+
|
|
19
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
20
|
+
import {
|
|
21
|
+
createEventStoreExecutor,
|
|
22
|
+
createTenantDb,
|
|
23
|
+
type DbConnection,
|
|
24
|
+
type TenantDb,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
26
|
+
import type { JobHandlerFn, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
27
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
28
|
+
import { type EnvelopeCipher, isStoredEnvelope } from "@cosmicdrift/kumiko-framework/secrets";
|
|
29
|
+
import { type ChunkedMigrationStopReason, runChunkedMigration } from "../../shared";
|
|
30
|
+
import { configValueEntity, configValuesTable } from "../table";
|
|
31
|
+
|
|
32
|
+
const DEFAULT_MAX_FAILURES = 10;
|
|
33
|
+
const SCAN_SLICE_SIZE = 100;
|
|
34
|
+
const SYSTEM_ROLES = ["system"] as const;
|
|
35
|
+
|
|
36
|
+
const executor = createEventStoreExecutor(configValuesTable, configValueEntity, {
|
|
37
|
+
entityName: "config-value",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type ReencryptJobPayload = {
|
|
41
|
+
readonly maxDurationMs?: number;
|
|
42
|
+
readonly maxFailures?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ReencryptJobResult = {
|
|
46
|
+
readonly migrated: number;
|
|
47
|
+
readonly failed: number;
|
|
48
|
+
readonly alreadyCurrent: number;
|
|
49
|
+
readonly stoppedReason: ChunkedMigrationStopReason;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type RowClassification = "rotate" | "current" | "unrecognized";
|
|
53
|
+
|
|
54
|
+
function classifyRow(value: string, targetVersion: number): RowClassification {
|
|
55
|
+
let parsed: unknown;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(value);
|
|
58
|
+
} catch {
|
|
59
|
+
// not JSON at all — no supported format decrypts this; let
|
|
60
|
+
// cipher.decrypt reject it and count the row as failed.
|
|
61
|
+
return "unrecognized";
|
|
62
|
+
}
|
|
63
|
+
if (!isStoredEnvelope(parsed)) return "unrecognized";
|
|
64
|
+
return parsed.kekVersion === targetVersion ? "current" : "rotate";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const reencryptJob: JobHandlerFn = async (rawPayload, ctx): Promise<void> => {
|
|
68
|
+
const payload = rawPayload as ReencryptJobPayload; // @cast-boundary engine-payload
|
|
69
|
+
const maybeCipher = ctx.configEncryption;
|
|
70
|
+
if (!maybeCipher) {
|
|
71
|
+
throw new InternalError({
|
|
72
|
+
message:
|
|
73
|
+
"[config:reencrypt] ctx.configEncryption missing — provide a master key " +
|
|
74
|
+
"(KUMIKO_SECRETS_MASTER_KEY_V<n>) so the boot wires the envelope cipher.",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// hoisted function declarations below capture these — pin the narrowed
|
|
78
|
+
// type explicitly so TS keeps it inside the closures
|
|
79
|
+
const cipher: EnvelopeCipher = maybeCipher;
|
|
80
|
+
const provider = ctx.masterKeyProvider;
|
|
81
|
+
if (!provider) {
|
|
82
|
+
throw new InternalError({
|
|
83
|
+
message:
|
|
84
|
+
"[config:reencrypt] ctx.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!ctx.db) {
|
|
88
|
+
throw new InternalError({
|
|
89
|
+
message: "[config:reencrypt] ctx.db missing — job context requires a database connection.",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (!ctx.registry) {
|
|
93
|
+
throw new InternalError({
|
|
94
|
+
message: "[config:reencrypt] ctx.registry missing — job context requires the registry.",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const db = ctx.db as DbConnection; // @cast-boundary db-operator
|
|
98
|
+
|
|
99
|
+
const encryptedKeys = [...ctx.registry.getAllConfigKeys()]
|
|
100
|
+
.filter(([, def]) => def.encrypted === true)
|
|
101
|
+
.map(([key]) => key);
|
|
102
|
+
|
|
103
|
+
const maxFailures = payload.maxFailures ?? DEFAULT_MAX_FAILURES;
|
|
104
|
+
const deadline = payload.maxDurationMs
|
|
105
|
+
? Date.now() + payload.maxDurationMs
|
|
106
|
+
: Number.POSITIVE_INFINITY;
|
|
107
|
+
|
|
108
|
+
const tdbCache = new Map<TenantId, TenantDb>();
|
|
109
|
+
function tdbFor(tenantId: TenantId): TenantDb {
|
|
110
|
+
let existing = tdbCache.get(tenantId);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
existing = createTenantDb(db, tenantId, "system");
|
|
113
|
+
tdbCache.set(tenantId, existing);
|
|
114
|
+
}
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type ConfigRow = {
|
|
119
|
+
id: string;
|
|
120
|
+
key: string;
|
|
121
|
+
value: string | null;
|
|
122
|
+
tenantId: string;
|
|
123
|
+
version: number;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let alreadyCurrent = 0;
|
|
127
|
+
const targetVersion = provider.currentVersion();
|
|
128
|
+
|
|
129
|
+
// ponytail: one full candidate scan — config rows are operator-scale
|
|
130
|
+
// (tenants × encrypted keys), cursor pagination when that ever changes.
|
|
131
|
+
// Served to the shared loop in slices so its deadline/signal/failure
|
|
132
|
+
// checks run between chunks, not only once.
|
|
133
|
+
let pending: ConfigRow[] | undefined;
|
|
134
|
+
async function nextBatch(): Promise<readonly ConfigRow[]> {
|
|
135
|
+
if (pending === undefined) {
|
|
136
|
+
pending =
|
|
137
|
+
encryptedKeys.length === 0
|
|
138
|
+
? []
|
|
139
|
+
: [
|
|
140
|
+
...(await selectMany<ConfigRow>(db, configValuesTable, {
|
|
141
|
+
key: { in: encryptedKeys },
|
|
142
|
+
})),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
const slice = pending;
|
|
146
|
+
return slice.splice(0, SCAN_SLICE_SIZE);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function migrateRow(row: ConfigRow): Promise<"migrated" | "skipped" | "failed"> {
|
|
150
|
+
if (row.value === null || row.value === undefined) return "skipped";
|
|
151
|
+
const classification = classifyRow(row.value, targetVersion);
|
|
152
|
+
if (classification === "current") {
|
|
153
|
+
alreadyCurrent++;
|
|
154
|
+
return "skipped";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (classification === "unrecognized") {
|
|
158
|
+
// Not a current-cipher envelope (malformed JSON, or any pre-envelope
|
|
159
|
+
// format) — never a supported re-encrypt input, so fail loudly
|
|
160
|
+
// instead of relying on cipher.decrypt to reject it. Log only the
|
|
161
|
+
// row id, never the value (ciphertext-adjacent).
|
|
162
|
+
ctx.log?.warn?.(
|
|
163
|
+
`[config:reencrypt] row ${row.id} is not a current-cipher envelope, not re-encryptable`,
|
|
164
|
+
);
|
|
165
|
+
return "failed";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const tenantId = row.tenantId as TenantId; // @cast-boundary db-row
|
|
169
|
+
const plaintext = await cipher.decrypt(row.value, { tenantId });
|
|
170
|
+
const reencrypted = await cipher.encrypt(plaintext, { tenantId });
|
|
171
|
+
|
|
172
|
+
const actor: SessionUser = { id: "system", tenantId, roles: SYSTEM_ROLES };
|
|
173
|
+
const result = await executor.update(
|
|
174
|
+
{ id: row.id, version: row.version, changes: { value: reencrypted } },
|
|
175
|
+
actor,
|
|
176
|
+
tdbFor(tenantId),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// version_conflict == a concurrent config:set beat us; the row now
|
|
180
|
+
// holds a fresh envelope written by the set handler — already fine.
|
|
181
|
+
if (!result.isSuccess) {
|
|
182
|
+
if (result.error.code === "version_conflict") return "skipped";
|
|
183
|
+
ctx.log?.warn?.(`[config:reencrypt] executor rejected row ${row.id}`, {
|
|
184
|
+
code: result.error.code,
|
|
185
|
+
});
|
|
186
|
+
return "failed";
|
|
187
|
+
}
|
|
188
|
+
return "migrated";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const outcome = await runChunkedMigration<ConfigRow>({
|
|
192
|
+
nextBatch,
|
|
193
|
+
migrateRow,
|
|
194
|
+
maxFailures,
|
|
195
|
+
deadlineAt: deadline,
|
|
196
|
+
signal: ctx.signal,
|
|
197
|
+
onRowError: (row, err) => {
|
|
198
|
+
ctx.log?.warn?.(`[config:reencrypt] failed to re-encrypt row ${row.id}`, { err });
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result: ReencryptJobResult = {
|
|
203
|
+
migrated: outcome.migrated,
|
|
204
|
+
failed: outcome.failed,
|
|
205
|
+
alreadyCurrent,
|
|
206
|
+
stoppedReason: outcome.stoppedReason,
|
|
207
|
+
};
|
|
208
|
+
ctx.log?.info?.(`[config:reencrypt] complete: ${JSON.stringify(result)}`);
|
|
209
|
+
};
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import {
|
|
3
|
+
configuredPiiSubjectKms,
|
|
4
|
+
encryptPiiValueForSubject,
|
|
5
|
+
type KmsContext,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
1
7
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
8
|
import {
|
|
3
9
|
ConfigScopes,
|
|
4
10
|
defineWriteHandler,
|
|
5
11
|
SYSTEM_TENANT_ID,
|
|
6
12
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
InternalError,
|
|
15
|
+
UnprocessableError,
|
|
16
|
+
writeFailure,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
8
18
|
import { z } from "zod";
|
|
9
19
|
import { requireConfigEncryption } from "../feature";
|
|
10
20
|
import { configValueEntity, configValuesTable } from "../table";
|
|
11
21
|
import {
|
|
12
22
|
findConfigRow,
|
|
13
23
|
prepareConfigWrite,
|
|
24
|
+
resolvePiiSubject,
|
|
14
25
|
validateBounds,
|
|
15
26
|
validatePattern,
|
|
16
27
|
validateScope,
|
|
@@ -47,6 +58,19 @@ export const setWrite = defineWriteHandler({
|
|
|
47
58
|
const scopeError = validateScope(scope, keyDef.scope, event.payload.key);
|
|
48
59
|
if (scopeError) return writeFailure(scopeError);
|
|
49
60
|
|
|
61
|
+
// A piiEncrypted key can never be defined with scope="system" (boot-
|
|
62
|
+
// rejected), but an admin override write can still target scope=
|
|
63
|
+
// "system" for a tenant/user-scoped key (validateScope allows any
|
|
64
|
+
// requestedScope <= definedScope) — system-row has no subject.
|
|
65
|
+
if (keyDef.piiEncrypted && scope === ConfigScopes.system) {
|
|
66
|
+
return writeFailure(
|
|
67
|
+
new UnprocessableError("invalid_scope", {
|
|
68
|
+
i18nKey: "config.errors.invalidScope",
|
|
69
|
+
details: { key: event.payload.key, definedScope: keyDef.scope, requestedScope: scope },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
const typeError = validateType(event.payload.value, keyDef);
|
|
51
75
|
if (typeError) return writeFailure(typeError);
|
|
52
76
|
|
|
@@ -88,8 +112,36 @@ export const setWrite = defineWriteHandler({
|
|
|
88
112
|
|
|
89
113
|
let serialized = JSON.stringify(event.payload.value);
|
|
90
114
|
if (keyDef.encrypted) {
|
|
91
|
-
const
|
|
92
|
-
serialized =
|
|
115
|
+
const cipher = requireConfigEncryption(ctx, "config:write:set");
|
|
116
|
+
serialized = await cipher.encrypt(serialized, { tenantId });
|
|
117
|
+
} else if (keyDef.piiEncrypted) {
|
|
118
|
+
const kms = configuredPiiSubjectKms();
|
|
119
|
+
if (!kms) {
|
|
120
|
+
throw new InternalError({
|
|
121
|
+
message:
|
|
122
|
+
`[config:write:set] key "${event.payload.key}" declares piiEncrypted=true but no ` +
|
|
123
|
+
`PII subject-KMS is configured — pass one via runProdApp({ kms }) / ` +
|
|
124
|
+
`configurePiiSubjectKms(adapter) at boot.`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// scope="system" was already rejected above — narrow accordingly.
|
|
128
|
+
const subject = resolvePiiSubject(
|
|
129
|
+
scope as Exclude<typeof scope, "system">, // @cast-boundary runtime-checked above
|
|
130
|
+
tenantId,
|
|
131
|
+
userId,
|
|
132
|
+
);
|
|
133
|
+
const kmsCtx: KmsContext = {
|
|
134
|
+
requestId: requestContext.get()?.requestId ?? "config:write:set",
|
|
135
|
+
tenantId,
|
|
136
|
+
userId: String(event.user.id),
|
|
137
|
+
};
|
|
138
|
+
serialized = await encryptPiiValueForSubject(
|
|
139
|
+
kms,
|
|
140
|
+
subject,
|
|
141
|
+
serialized,
|
|
142
|
+
kmsCtx,
|
|
143
|
+
event.payload.key,
|
|
144
|
+
);
|
|
93
145
|
}
|
|
94
146
|
|
|
95
147
|
const existing = await findConfigRow(db, event.payload.key, tenantId, userId);
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ConfigScope,
|
|
4
4
|
type ConfigValueSource,
|
|
5
5
|
defineQueryHandler,
|
|
6
|
+
isEncryptedAtRest,
|
|
6
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { requireConfigResolver } from "../feature";
|
|
@@ -63,7 +64,7 @@ export const valuesQuery = defineQueryHandler({
|
|
|
63
64
|
// key so the plaintext (which the resolver revealed for internal reads)
|
|
64
65
|
// never reaches the UI response.
|
|
65
66
|
let value: string | number | boolean | undefined;
|
|
66
|
-
if (keyDef
|
|
67
|
+
if (isEncryptedAtRest(keyDef)) {
|
|
67
68
|
value = cascade.value !== undefined ? MASKED : undefined;
|
|
68
69
|
} else {
|
|
69
70
|
value = cascade.value;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type LocalizedString = { readonly de: string; readonly en: string };
|
|
2
|
+
|
|
3
|
+
/** Server boot keys for the auto-generated Settings-Hub (mirrors web/i18n.ts). */
|
|
4
|
+
export const CONFIG_FEATURE_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
5
|
+
"config.settings.title": { de: "Einstellungen", en: "Settings" },
|
|
6
|
+
"config.settings.system": { de: "Plattform", en: "Platform" },
|
|
7
|
+
"config.settings.tenant": { de: "Organisation", en: "Organization" },
|
|
8
|
+
"config.settings.user": { de: "Persönlich", en: "Personal" },
|
|
9
|
+
};
|
package/src/config/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { DbConnection
|
|
1
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { seedConfigValues } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
4
5
|
import { configValueEntity, configValuesTable } from "./table";
|
|
5
6
|
|
|
6
7
|
export {
|
|
@@ -22,7 +23,7 @@ export {
|
|
|
22
23
|
} from "./handlers/readiness.query";
|
|
23
24
|
export type { AppConfigOverrides, ConfigResolver } from "./resolver";
|
|
24
25
|
export { buildEnvConfigOverrides, createConfigResolver, validateAppOverrides } from "./resolver";
|
|
25
|
-
export { configValuesTable } from "./table";
|
|
26
|
+
export { configValueEntity, configValuesTable } from "./table";
|
|
26
27
|
|
|
27
28
|
// Boot helper for runDevApp / runProdApp: pulls every ConfigSeedDef from
|
|
28
29
|
// the registry and writes the matching system/tenant/user rows via the
|
|
@@ -30,8 +31,8 @@ export { configValuesTable } from "./table";
|
|
|
30
31
|
export function seedAllConfigValues(
|
|
31
32
|
registry: Registry,
|
|
32
33
|
db: DbConnection,
|
|
33
|
-
|
|
34
|
+
cipher?: EnvelopeCipher,
|
|
34
35
|
): Promise<{ created: number; skipped: number }> {
|
|
35
36
|
const seeds = registry.getAllConfigSeeds();
|
|
36
|
-
return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db,
|
|
37
|
+
return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db, cipher);
|
|
37
38
|
}
|
package/src/config/resolver.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
1
2
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
configuredPiiSubjectKms,
|
|
5
|
+
decryptPiiValueForSubject,
|
|
6
|
+
type KmsContext,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
8
|
+
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
9
|
import type {
|
|
4
10
|
ConfigCascade,
|
|
5
11
|
ConfigCascadeLevel,
|
|
6
12
|
ConfigKeyDefinition,
|
|
7
13
|
ConfigResolver,
|
|
8
14
|
ConfigSecretsReader,
|
|
9
|
-
ConfigStoredRowWithSource,
|
|
10
15
|
ConfigValueSource,
|
|
11
16
|
ConfigValueWithSource,
|
|
12
17
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
18
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
19
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
20
|
+
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
15
21
|
import { assertUnreachable, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
16
22
|
import { selectConfigRowsForKeys, selectConfigRowsForScope } from "./db/queries/resolver";
|
|
17
23
|
import { configValuesTable } from "./table";
|
|
@@ -35,8 +41,13 @@ export function deserializeValue(
|
|
|
35
41
|
if (raw === null || raw === undefined) return undefined;
|
|
36
42
|
const parsed = parseJsonOrThrow<unknown>(raw, `config value (type=${type})`);
|
|
37
43
|
switch (type) {
|
|
38
|
-
case "number":
|
|
39
|
-
|
|
44
|
+
case "number": {
|
|
45
|
+
const n = typeof parsed === "number" ? parsed : Number(parsed);
|
|
46
|
+
if (!Number.isFinite(n)) {
|
|
47
|
+
throw new Error(`config value (type=number): "${raw}" does not deserialize to a number.`);
|
|
48
|
+
}
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
40
51
|
case "boolean":
|
|
41
52
|
return typeof parsed === "boolean" ? parsed : parsed === "true";
|
|
42
53
|
case "text":
|
|
@@ -63,10 +74,51 @@ export function deserializeValue(
|
|
|
63
74
|
export type AppConfigOverrides = ReadonlyMap<string, string | number | boolean>;
|
|
64
75
|
|
|
65
76
|
export type ConfigResolverOptions = {
|
|
66
|
-
|
|
77
|
+
// Envelope cipher for `encrypted: true` keys. run{Prod,Dev}App wire it
|
|
78
|
+
// automatically from the secrets master key — apps only pass their own
|
|
79
|
+
// to override (tests, custom providers).
|
|
80
|
+
cipher?: EnvelopeCipher;
|
|
67
81
|
appOverrides?: AppConfigOverrides;
|
|
68
82
|
};
|
|
69
83
|
|
|
84
|
+
// Decrypt gate for encrypted keys: an encrypted row without a cipher MUST
|
|
85
|
+
// throw — passing the ciphertext through as the "value" (the pre-envelope
|
|
86
|
+
// behavior) silently feeds base64 garbage to whatever consumes the key
|
|
87
|
+
// (e.g. an SMTP password) and is a debugging nightmare.
|
|
88
|
+
async function decryptEncrypted(
|
|
89
|
+
cipher: EnvelopeCipher | undefined,
|
|
90
|
+
raw: string,
|
|
91
|
+
qualifiedKey: string,
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
if (!cipher) {
|
|
94
|
+
throw new InternalError({
|
|
95
|
+
message:
|
|
96
|
+
`[config] key "${qualifiedKey}" is encrypted but no cipher is wired — the boot must ` +
|
|
97
|
+
`provide a master key (KUMIKO_SECRETS_MASTER_KEY_V<n>) or pass ConfigResolverOptions.cipher.`,
|
|
98
|
+
i18nKey: "config.errors.cipher_missing",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return cipher.decrypt(raw);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Decrypt gate for piiEncrypted keys, mirroring decryptEncrypted above.
|
|
105
|
+
// The subject lives inside the ciphertext (kumiko-pii:v1:<subject>:...),
|
|
106
|
+
// so unlike the write side, no scope/subject resolution is needed here —
|
|
107
|
+
// same call works for a user-row or a tenant-row value.
|
|
108
|
+
async function decryptPiiEncrypted(raw: string, qualifiedKey: string): Promise<string> {
|
|
109
|
+
const kms = configuredPiiSubjectKms();
|
|
110
|
+
if (!kms) {
|
|
111
|
+
throw new InternalError({
|
|
112
|
+
message:
|
|
113
|
+
`[config] key "${qualifiedKey}" is piiEncrypted but no PII subject-KMS is wired — the ` +
|
|
114
|
+
`boot must configure one via runProdApp({ kms }) / configurePiiSubjectKms(adapter).`,
|
|
115
|
+
i18nKey: "config.errors.cipher_missing",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const kmsCtx: KmsContext = { requestId: requestContext.get()?.requestId ?? "config:resolver" };
|
|
119
|
+
return decryptPiiValueForSubject(kms, raw, kmsCtx, qualifiedKey);
|
|
120
|
+
}
|
|
121
|
+
|
|
70
122
|
// backing="secrets" keys store their value in the secrets store (flat per
|
|
71
123
|
// (tenant,key) at SYSTEM_TENANT_ID), not in config_values. Both read paths
|
|
72
124
|
// (getWithSource + buildCascade) route the system rung here. Missing reader =
|
|
@@ -101,7 +153,7 @@ async function buildCascade(
|
|
|
101
153
|
userId: string | null,
|
|
102
154
|
) => Promise<ConfigRow | null> | ConfigRow | null,
|
|
103
155
|
appOverrides: AppConfigOverrides | undefined,
|
|
104
|
-
|
|
156
|
+
cipher: EnvelopeCipher | undefined,
|
|
105
157
|
secretsReader: ConfigSecretsReader | undefined,
|
|
106
158
|
): Promise<ConfigCascade> {
|
|
107
159
|
type Lookup = {
|
|
@@ -179,8 +231,10 @@ async function buildCascade(
|
|
|
179
231
|
const row = await fetchRow(lookup.tenantId, lookup.userId);
|
|
180
232
|
if (row?.value !== null && row?.value !== undefined) {
|
|
181
233
|
let raw = row.value;
|
|
182
|
-
if (keyDef.encrypted
|
|
183
|
-
raw =
|
|
234
|
+
if (keyDef.encrypted) {
|
|
235
|
+
raw = await decryptEncrypted(cipher, raw, qualifiedKey);
|
|
236
|
+
} else if (keyDef.piiEncrypted) {
|
|
237
|
+
raw = await decryptPiiEncrypted(raw, qualifiedKey);
|
|
184
238
|
}
|
|
185
239
|
if (activeIndex === -1) activeIndex = levels.length;
|
|
186
240
|
levels.push({
|
|
@@ -265,7 +319,7 @@ async function buildCascade(
|
|
|
265
319
|
}
|
|
266
320
|
|
|
267
321
|
export function createConfigResolver(options: ConfigResolverOptions = {}): ConfigResolver {
|
|
268
|
-
const {
|
|
322
|
+
const { cipher, appOverrides } = options;
|
|
269
323
|
async function findRow(
|
|
270
324
|
key: string,
|
|
271
325
|
tenantId: string,
|
|
@@ -356,8 +410,10 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
356
410
|
const row = await findRow(qualifiedKey, lookup.tenantId, lookup.userId, db);
|
|
357
411
|
if (row?.value !== null && row?.value !== undefined) {
|
|
358
412
|
let raw = row.value;
|
|
359
|
-
if (keyDef.encrypted
|
|
360
|
-
raw =
|
|
413
|
+
if (keyDef.encrypted) {
|
|
414
|
+
raw = await decryptEncrypted(cipher, raw, qualifiedKey);
|
|
415
|
+
} else if (keyDef.piiEncrypted) {
|
|
416
|
+
raw = await decryptPiiEncrypted(raw, qualifiedKey);
|
|
361
417
|
}
|
|
362
418
|
return { value: deserializeValue(raw, keyDef.type), source: lookup.source };
|
|
363
419
|
}
|
|
@@ -384,12 +440,12 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
384
440
|
return { value: undefined, source: "missing" };
|
|
385
441
|
},
|
|
386
442
|
|
|
387
|
-
// NOTE: getAll
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
443
|
+
// NOTE: getAll reads only config_values rows and does NOT dispatch to the
|
|
444
|
+
// secrets store (unlike get/getCascade/getWithSource). A backing="secrets"
|
|
445
|
+
// key has no config_values row, so it is simply ABSENT from the result map
|
|
446
|
+
// — never a stale/undefined value. Callers needing a secrets-backed value
|
|
447
|
+
// must use get/getCascade; this bulk reader is for config_values-backed
|
|
448
|
+
// keys only.
|
|
393
449
|
async getAll(tenantId, userId, db) {
|
|
394
450
|
// Only load rows relevant to this user/tenant (system + tenant + user scope)
|
|
395
451
|
const rows = await selectConfigRowsForScope(db, SYSTEM_TENANT_ID, tenantId, userId);
|
|
@@ -410,48 +466,6 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
410
466
|
return result;
|
|
411
467
|
},
|
|
412
468
|
|
|
413
|
-
async getAllWithSource(tenantId, userId, db) {
|
|
414
|
-
// Load ALL potentially relevant rows (user + tenant + system)
|
|
415
|
-
const rows = await selectConfigRowsForScope(db, SYSTEM_TENANT_ID, tenantId, userId);
|
|
416
|
-
|
|
417
|
-
const result = new Map<string, ConfigStoredRowWithSource>();
|
|
418
|
-
|
|
419
|
-
// Group rows by key so we can determine the winner and its source
|
|
420
|
-
const groups = new Map<string, ConfigRow[]>();
|
|
421
|
-
for (const r of rows) {
|
|
422
|
-
const g = groups.get(r.key) ?? [];
|
|
423
|
-
g.push(r);
|
|
424
|
-
groups.set(r.key, g);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
for (const [key, keyRows] of groups) {
|
|
428
|
-
const specificityOf = (candidate: ConfigRow) =>
|
|
429
|
-
(candidate.userId !== null ? 2 : 0) + (candidate.tenantId !== SYSTEM_TENANT_ID ? 1 : 0);
|
|
430
|
-
|
|
431
|
-
const first = keyRows[0];
|
|
432
|
-
if (!first) continue;
|
|
433
|
-
let winner: ConfigRow = first;
|
|
434
|
-
for (const r of keyRows) {
|
|
435
|
-
if (specificityOf(r) > specificityOf(winner)) {
|
|
436
|
-
winner = r;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
let source: ConfigValueSource;
|
|
441
|
-
if (winner.userId !== null) {
|
|
442
|
-
source = "user-row";
|
|
443
|
-
} else if (winner.tenantId !== SYSTEM_TENANT_ID) {
|
|
444
|
-
source = "tenant-row";
|
|
445
|
-
} else {
|
|
446
|
-
source = "system-row";
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
result.set(key, { ...winner, source });
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return result;
|
|
453
|
-
},
|
|
454
|
-
|
|
455
469
|
async getCascade(
|
|
456
470
|
qualifiedKey,
|
|
457
471
|
keyDef,
|
|
@@ -470,7 +484,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
470
484
|
db,
|
|
471
485
|
(tid, uid) => findRow(qualifiedKey, tid, uid, db),
|
|
472
486
|
appOverrides,
|
|
473
|
-
|
|
487
|
+
cipher,
|
|
474
488
|
secretsReader,
|
|
475
489
|
);
|
|
476
490
|
},
|
|
@@ -512,7 +526,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
512
526
|
(tid, uid) =>
|
|
513
527
|
keyRows.find((r) => r.tenantId === tid && (r.userId ?? null) === uid) ?? null,
|
|
514
528
|
appOverrides,
|
|
515
|
-
|
|
529
|
+
cipher,
|
|
516
530
|
secretsReader,
|
|
517
531
|
);
|
|
518
532
|
result.set(key, cascade);
|
package/src/config/table.ts
CHANGED
|
@@ -25,7 +25,7 @@ export const configValueEntity = createEntity({
|
|
|
25
25
|
// breaking the null-vs-missing distinction the resolver already draws.
|
|
26
26
|
value: createTextField({}),
|
|
27
27
|
// user-scope row: userId populated. tenant- / system-scope: null.
|
|
28
|
-
userId: createTextField({}),
|
|
28
|
+
userId: createTextField({ allowPlaintext: "pseudonymous-fk" }),
|
|
29
29
|
},
|
|
30
30
|
indexes: [
|
|
31
31
|
{ unique: true, columns: ["key", "tenantId", "userId"], name: "read_config_values_unique" },
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// have to cross-import from another handler file.
|
|
4
4
|
|
|
5
5
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
6
|
+
import type { SubjectId } from "@cosmicdrift/kumiko-framework/crypto";
|
|
6
7
|
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
7
8
|
import {
|
|
8
9
|
type ConfigKeyDefinition,
|
|
@@ -198,6 +199,27 @@ export function resolveScopeIds(
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
// The subject a piiEncrypted config value is encrypted under, for the scope
|
|
203
|
+
// actually written to (kumiko-platform#231/#459). Pure — no I/O — so a bug
|
|
204
|
+
// here (e.g. picking the writer's identity instead of the row's target,
|
|
205
|
+
// see entity fields' explicit userOwned.ownerField for why that
|
|
206
|
+
// distinction matters) is caught by a fast unit test, not only the DB-
|
|
207
|
+
// backed integration test. Caller has already rejected scope="system"
|
|
208
|
+
// (no subject exists for it).
|
|
209
|
+
export function resolvePiiSubject(
|
|
210
|
+
scope: Exclude<ConfigScope, "system">,
|
|
211
|
+
tenantId: TenantId,
|
|
212
|
+
userId: string | null,
|
|
213
|
+
): SubjectId {
|
|
214
|
+
if (scope === ConfigScopes.user) {
|
|
215
|
+
if (userId === null) {
|
|
216
|
+
throw new Error('resolvePiiSubject: scope="user" but userId is null');
|
|
217
|
+
}
|
|
218
|
+
return { kind: "user", userId };
|
|
219
|
+
}
|
|
220
|
+
return { kind: "tenant", tenantId };
|
|
221
|
+
}
|
|
222
|
+
|
|
201
223
|
export function validateType(
|
|
202
224
|
value: string | number | boolean,
|
|
203
225
|
keyDef: ConfigKeyDefinition,
|