@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
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
// der end-to-end-Default-Pfad (delete).
|
|
17
17
|
|
|
18
18
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
19
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
19
20
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
21
|
+
import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
20
22
|
import type { JobContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
23
|
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
22
24
|
import {
|
|
@@ -30,7 +32,7 @@ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
|
30
32
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
31
33
|
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
|
|
32
34
|
import { createFilesFeature } from "../../files";
|
|
33
|
-
import { createSessionsFeature } from "../../sessions";
|
|
35
|
+
import { createSessionsFeature, userSessionEntity } from "../../sessions";
|
|
34
36
|
import {
|
|
35
37
|
createUserFeature,
|
|
36
38
|
USER_ANONYMIZED_DISPLAY_NAME,
|
|
@@ -66,6 +68,7 @@ beforeAll(async () => {
|
|
|
66
68
|
createFilesFeature(),
|
|
67
69
|
createDataRetentionFeature(),
|
|
68
70
|
createComplianceProfilesFeature(),
|
|
71
|
+
authFoundationFeature,
|
|
69
72
|
createSessionsFeature(),
|
|
70
73
|
|
|
71
74
|
createUserDataRightsFeature(),
|
|
@@ -74,6 +77,9 @@ beforeAll(async () => {
|
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
80
|
+
// sessions ist gemountet → der user-session-Hook der defaults läuft im Forget
|
|
81
|
+
// und braucht die Tabelle (#797).
|
|
82
|
+
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
77
83
|
await unsafeCreateEntityTable(stack.db, tenantRetentionOverrideEntity);
|
|
78
84
|
// tenant-membership-Tabelle (von tenant-feature) manuell anlegen weil
|
|
79
85
|
// wir ohne tenant-feature im stack arbeiten — minimaler Setup.
|
|
@@ -276,6 +282,22 @@ describe("run-forget-cleanup :: registered cron (autonomous Art.17)", () => {
|
|
|
276
282
|
// getJob undefined und die Loeschung laeuft nach Grace-Ablauf NIE. Wir
|
|
277
283
|
// treiben den ECHTEN registrierten Job durch den echten JobContext (db +
|
|
278
284
|
// registry, kein config) — nicht den inneren runForgetCleanup-Helper.
|
|
285
|
+
function capturingJobLog() {
|
|
286
|
+
const warns: string[] = [];
|
|
287
|
+
const log: JobContext["log"] = {
|
|
288
|
+
info() {},
|
|
289
|
+
warn(message: string) {
|
|
290
|
+
warns.push(message);
|
|
291
|
+
},
|
|
292
|
+
error() {},
|
|
293
|
+
debug() {},
|
|
294
|
+
child(): JobContext["log"] {
|
|
295
|
+
return this;
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
return { log, warns };
|
|
299
|
+
}
|
|
300
|
+
|
|
279
301
|
test("registered job exists + erases through the real JobContext", async () => {
|
|
280
302
|
const job = stack.registry.getJob("user-data-rights:job:run-forget-cleanup");
|
|
281
303
|
expect(job).toBeTruthy();
|
|
@@ -289,10 +311,8 @@ describe("run-forget-cleanup :: registered cron (autonomous Art.17)", () => {
|
|
|
289
311
|
});
|
|
290
312
|
await seedMembership(CRON_USER, TENANT_A);
|
|
291
313
|
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
// systemUser/log/triggeredBy) — double-cast at this test boundary since
|
|
295
|
-
// the handler under test never touches those fields.
|
|
314
|
+
const { log } = capturingJobLog();
|
|
315
|
+
const jobCtx = { db: stack.db, registry: stack.registry, log };
|
|
296
316
|
await job?.handler({}, jobCtx as unknown as JobContext);
|
|
297
317
|
|
|
298
318
|
const row = await fetchUser(CRON_USER);
|
|
@@ -300,6 +320,52 @@ describe("run-forget-cleanup :: registered cron (autonomous Art.17)", () => {
|
|
|
300
320
|
expect(row?.email).not.toContain("cron-delete@example.com");
|
|
301
321
|
expect(row?.email).toContain("anonymized.invalid");
|
|
302
322
|
});
|
|
323
|
+
|
|
324
|
+
test("incomplete hooks are written to ctx.log (job-run audit trail, #1572)", async () => {
|
|
325
|
+
const job = stack.registry.getJob("user-data-rights:job:run-forget-cleanup");
|
|
326
|
+
expect(job).toBeTruthy();
|
|
327
|
+
|
|
328
|
+
await seedUser(ALICE_ID, {
|
|
329
|
+
status: USER_STATUS.DeletionRequested,
|
|
330
|
+
gracePeriodEnd: instantFromOffsetMs(-60 * 1000),
|
|
331
|
+
});
|
|
332
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
333
|
+
|
|
334
|
+
const usages = stack.registry.getExtensionUsages("userData");
|
|
335
|
+
const userUsage = usages.find((u) => u.entityName === "user");
|
|
336
|
+
if (!userUsage?.options) throw new Error("user usage not found");
|
|
337
|
+
type DeleteHook = (
|
|
338
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
339
|
+
strategy: string,
|
|
340
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
341
|
+
const originalUserDelete = (userUsage.options as { delete: DeleteHook }).delete;
|
|
342
|
+
(userUsage.options as { delete: DeleteHook }).delete = async (ctx, strategy) => {
|
|
343
|
+
if (ctx.userId === ALICE_ID) {
|
|
344
|
+
return { status: "incomplete", reason: "synthetic partial cleanup for alice" };
|
|
345
|
+
}
|
|
346
|
+
return originalUserDelete(ctx, strategy);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const { log, warns } = capturingJobLog();
|
|
350
|
+
try {
|
|
351
|
+
await job?.handler({}, {
|
|
352
|
+
db: stack.db,
|
|
353
|
+
registry: stack.registry,
|
|
354
|
+
log,
|
|
355
|
+
} as unknown as JobContext);
|
|
356
|
+
} finally {
|
|
357
|
+
(userUsage.options as { delete: DeleteHook }).delete = originalUserDelete;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const hit = warns.find((w) => w.includes("incomplete hook") && w.includes(ALICE_ID));
|
|
361
|
+
expect(hit).toBeDefined();
|
|
362
|
+
expect(hit).toContain("entityName=user");
|
|
363
|
+
expect(hit).toContain("rolledBack=false");
|
|
364
|
+
expect(hit).toContain("reason=synthetic partial cleanup for alice");
|
|
365
|
+
|
|
366
|
+
const aliceRow = await fetchUser(ALICE_ID);
|
|
367
|
+
expect(aliceRow?.status).toBe(USER_STATUS.Deleted);
|
|
368
|
+
});
|
|
303
369
|
});
|
|
304
370
|
|
|
305
371
|
describe("runForgetCleanup :: time-window guards", () => {
|
|
@@ -743,4 +809,272 @@ describe("runForgetCleanup :: per-User-Sub-Tx-Isolation (advisor-pinned Architek
|
|
|
743
809
|
).delete = originalUserDelete;
|
|
744
810
|
}
|
|
745
811
|
});
|
|
812
|
+
|
|
813
|
+
test("Hook meldet {status:'incomplete'} → kein Rollback, User trotzdem Deleted, im result.incomplete gesammelt", async () => {
|
|
814
|
+
await seedUser(ALICE_ID, {
|
|
815
|
+
status: USER_STATUS.DeletionRequested,
|
|
816
|
+
gracePeriodEnd: instantFromOffsetMs(-60 * 1000),
|
|
817
|
+
});
|
|
818
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
819
|
+
|
|
820
|
+
const usages = stack.registry.getExtensionUsages("userData");
|
|
821
|
+
const userUsage = usages.find((u) => u.entityName === "user");
|
|
822
|
+
if (!userUsage?.options) throw new Error("user usage not found");
|
|
823
|
+
const originalUserDelete = (
|
|
824
|
+
userUsage.options as {
|
|
825
|
+
delete: (
|
|
826
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
827
|
+
strategy: string,
|
|
828
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
829
|
+
}
|
|
830
|
+
).delete;
|
|
831
|
+
(
|
|
832
|
+
userUsage.options as {
|
|
833
|
+
delete: (
|
|
834
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
835
|
+
strategy: string,
|
|
836
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
837
|
+
}
|
|
838
|
+
).delete = async (ctx: { userId: string; tenantId: string; db: unknown }, strategy: string) => {
|
|
839
|
+
if (ctx.userId === ALICE_ID) {
|
|
840
|
+
return { status: "incomplete", reason: "synthetic partial cleanup for alice" };
|
|
841
|
+
}
|
|
842
|
+
return originalUserDelete(ctx, strategy);
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
const result = await runForgetCleanup({
|
|
847
|
+
db: stack.db,
|
|
848
|
+
registry: stack.registry,
|
|
849
|
+
now: NOW(),
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// No hard failure: user flipped, no errors entry.
|
|
853
|
+
expect(result.processedUserIds).toContain(ALICE_ID);
|
|
854
|
+
expect(result.errors.some((e) => e.userId === ALICE_ID)).toBe(false);
|
|
855
|
+
|
|
856
|
+
// But collected in the incomplete set with reason + tenant + entity.
|
|
857
|
+
const aliceIncomplete = result.incomplete.find((i) => i.userId === ALICE_ID);
|
|
858
|
+
expect(aliceIncomplete?.reason).toBe("synthetic partial cleanup for alice");
|
|
859
|
+
expect(aliceIncomplete?.tenantId).toBe(TENANT_A);
|
|
860
|
+
expect(aliceIncomplete?.entityName).toBe("user");
|
|
861
|
+
expect(aliceIncomplete?.rolledBack).toBe(false);
|
|
862
|
+
|
|
863
|
+
// Status flip proceeded despite the incomplete report.
|
|
864
|
+
const aliceRow = await fetchUser(ALICE_ID);
|
|
865
|
+
expect(aliceRow?.status).toBe(USER_STATUS.Deleted);
|
|
866
|
+
} finally {
|
|
867
|
+
(
|
|
868
|
+
userUsage.options as {
|
|
869
|
+
delete: (
|
|
870
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
871
|
+
strategy: string,
|
|
872
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
873
|
+
}
|
|
874
|
+
).delete = originalUserDelete;
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("hook incomplete on one tenant, then throws on a later tenant → the incomplete report survives, flagged rolledBack (#1489)", async () => {
|
|
879
|
+
await seedUser(ALICE_ID, {
|
|
880
|
+
status: USER_STATUS.DeletionRequested,
|
|
881
|
+
gracePeriodEnd: instantFromOffsetMs(-60 * 1000),
|
|
882
|
+
});
|
|
883
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
884
|
+
await seedMembership(ALICE_ID, TENANT_B);
|
|
885
|
+
|
|
886
|
+
const usages = stack.registry.getExtensionUsages("userData");
|
|
887
|
+
const userUsage = usages.find((u) => u.entityName === "user");
|
|
888
|
+
if (!userUsage?.options) throw new Error("user usage not found");
|
|
889
|
+
const originalUserDelete = (
|
|
890
|
+
userUsage.options as {
|
|
891
|
+
delete: (
|
|
892
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
893
|
+
strategy: string,
|
|
894
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
895
|
+
}
|
|
896
|
+
).delete;
|
|
897
|
+
(
|
|
898
|
+
userUsage.options as {
|
|
899
|
+
delete: (
|
|
900
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
901
|
+
strategy: string,
|
|
902
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
903
|
+
}
|
|
904
|
+
).delete = async (ctx: { userId: string; tenantId: string; db: unknown }, strategy: string) => {
|
|
905
|
+
if (ctx.userId === ALICE_ID && ctx.tenantId === TENANT_A) {
|
|
906
|
+
return { status: "incomplete", reason: "synthetic partial cleanup for alice (tenant A)" };
|
|
907
|
+
}
|
|
908
|
+
if (ctx.userId === ALICE_ID && ctx.tenantId === TENANT_B) {
|
|
909
|
+
throw new Error("synthetic hook failure for alice (tenant B)");
|
|
910
|
+
}
|
|
911
|
+
return originalUserDelete(ctx, strategy);
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
const result = await runForgetCleanup({
|
|
916
|
+
db: stack.db,
|
|
917
|
+
registry: stack.registry,
|
|
918
|
+
now: NOW(),
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// The whole per-user sub-tx rolled back — the DB rows (including the
|
|
922
|
+
// status flip) are undone, and the failure surfaces as a hard error
|
|
923
|
+
// too. But tenant A's "incomplete" report describes an external,
|
|
924
|
+
// non-transactional side effect (already ran, not rolled back by a
|
|
925
|
+
// DB rollback) — it must still reach the operator, just marked
|
|
926
|
+
// rolledBack so they know the user itself is still DeletionRequested.
|
|
927
|
+
const aliceIncomplete = result.incomplete.find((i) => i.userId === ALICE_ID);
|
|
928
|
+
expect(aliceIncomplete?.rolledBack).toBe(true);
|
|
929
|
+
expect(result.errors.some((e) => e.userId === ALICE_ID)).toBe(true);
|
|
930
|
+
expect(result.processedUserIds).not.toContain(ALICE_ID);
|
|
931
|
+
|
|
932
|
+
const aliceRow = await fetchUser(ALICE_ID);
|
|
933
|
+
expect(aliceRow?.status).toBe(USER_STATUS.DeletionRequested);
|
|
934
|
+
} finally {
|
|
935
|
+
(
|
|
936
|
+
userUsage.options as {
|
|
937
|
+
delete: (
|
|
938
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
939
|
+
strategy: string,
|
|
940
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
941
|
+
}
|
|
942
|
+
).delete = originalUserDelete;
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("hook returns {status:'ok'} → treated as a clean completion, not incomplete", async () => {
|
|
947
|
+
await seedUser(ALICE_ID, {
|
|
948
|
+
status: USER_STATUS.DeletionRequested,
|
|
949
|
+
gracePeriodEnd: instantFromOffsetMs(-60 * 1000),
|
|
950
|
+
});
|
|
951
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
952
|
+
|
|
953
|
+
const usages = stack.registry.getExtensionUsages("userData");
|
|
954
|
+
const userUsage = usages.find((u) => u.entityName === "user");
|
|
955
|
+
if (!userUsage?.options) throw new Error("user usage not found");
|
|
956
|
+
const originalUserDelete = (
|
|
957
|
+
userUsage.options as {
|
|
958
|
+
delete: (
|
|
959
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
960
|
+
strategy: string,
|
|
961
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
962
|
+
}
|
|
963
|
+
).delete;
|
|
964
|
+
(
|
|
965
|
+
userUsage.options as {
|
|
966
|
+
delete: (
|
|
967
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
968
|
+
strategy: string,
|
|
969
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
970
|
+
}
|
|
971
|
+
).delete = async (ctx: { userId: string; tenantId: string; db: unknown }, strategy: string) => {
|
|
972
|
+
if (ctx.userId === ALICE_ID) {
|
|
973
|
+
return { status: "ok" };
|
|
974
|
+
}
|
|
975
|
+
return originalUserDelete(ctx, strategy);
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
const result = await runForgetCleanup({
|
|
980
|
+
db: stack.db,
|
|
981
|
+
registry: stack.registry,
|
|
982
|
+
now: NOW(),
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
expect(result.processedUserIds).toContain(ALICE_ID);
|
|
986
|
+
expect(result.errors.some((e) => e.userId === ALICE_ID)).toBe(false);
|
|
987
|
+
expect(result.incomplete.some((i) => i.userId === ALICE_ID)).toBe(false);
|
|
988
|
+
|
|
989
|
+
const aliceRow = await fetchUser(ALICE_ID);
|
|
990
|
+
expect(aliceRow?.status).toBe(USER_STATUS.Deleted);
|
|
991
|
+
} finally {
|
|
992
|
+
(
|
|
993
|
+
userUsage.options as {
|
|
994
|
+
delete: (
|
|
995
|
+
ctx: { userId: string; tenantId: string; db: unknown },
|
|
996
|
+
strategy: string,
|
|
997
|
+
) => Promise<undefined | { status: "ok" } | { status: "incomplete"; reason: string }>;
|
|
998
|
+
}
|
|
999
|
+
).delete = originalUserDelete;
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// eraseKey never fails in the in-memory adapter, so the rollback test
|
|
1005
|
+
// overrides it to simulate a KMS outage.
|
|
1006
|
+
class FailingEraseKms extends InMemoryKmsAdapter {
|
|
1007
|
+
override async eraseKey(): Promise<void> {
|
|
1008
|
+
throw new Error("kms unavailable");
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
describe("runForgetCleanup :: crypto-shredding (subject key erase)", () => {
|
|
1013
|
+
test("successful forget erases the user's subject key", async () => {
|
|
1014
|
+
await seedUser(ALICE_ID, {
|
|
1015
|
+
status: USER_STATUS.DeletionRequested,
|
|
1016
|
+
gracePeriodEnd: instantFromOffsetMs(-60 * 1000),
|
|
1017
|
+
});
|
|
1018
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
1019
|
+
|
|
1020
|
+
const kms = new InMemoryKmsAdapter();
|
|
1021
|
+
await kms.createKey({ kind: "user", userId: ALICE_ID });
|
|
1022
|
+
|
|
1023
|
+
const result = await runForgetCleanup({
|
|
1024
|
+
db: stack.db,
|
|
1025
|
+
registry: stack.registry,
|
|
1026
|
+
now: NOW(),
|
|
1027
|
+
kms,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
expect(result.processedUserIds).toEqual([ALICE_ID]);
|
|
1031
|
+
expect(result.errors).toHaveLength(0);
|
|
1032
|
+
await expect(kms.getKey({ kind: "user", userId: ALICE_ID })).rejects.toThrow(
|
|
1033
|
+
"Subject key erased",
|
|
1034
|
+
);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test("user without a subject key: erase is a no-op, forget still completes", async () => {
|
|
1038
|
+
await seedUser(ALICE_ID, {
|
|
1039
|
+
status: USER_STATUS.DeletionRequested,
|
|
1040
|
+
gracePeriodEnd: instantFromOffsetMs(-60 * 1000),
|
|
1041
|
+
});
|
|
1042
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
1043
|
+
|
|
1044
|
+
const result = await runForgetCleanup({
|
|
1045
|
+
db: stack.db,
|
|
1046
|
+
registry: stack.registry,
|
|
1047
|
+
now: NOW(),
|
|
1048
|
+
kms: new InMemoryKmsAdapter(),
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
expect(result.processedUserIds).toEqual([ALICE_ID]);
|
|
1052
|
+
expect(result.errors).toHaveLength(0);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
test("eraseKey failure rolls back the user's sub-tx (stays DeletionRequested, PII intact)", async () => {
|
|
1056
|
+
await seedUser(ALICE_ID, {
|
|
1057
|
+
status: USER_STATUS.DeletionRequested,
|
|
1058
|
+
gracePeriodEnd: instantFromOffsetMs(-60 * 1000),
|
|
1059
|
+
email: "alice.erase-fail@example.com",
|
|
1060
|
+
});
|
|
1061
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
1062
|
+
|
|
1063
|
+
const result = await runForgetCleanup({
|
|
1064
|
+
db: stack.db,
|
|
1065
|
+
registry: stack.registry,
|
|
1066
|
+
now: NOW(),
|
|
1067
|
+
kms: new FailingEraseKms(),
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
expect(result.processedUserIds).toHaveLength(0);
|
|
1071
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(1);
|
|
1072
|
+
expect(result.errors[0]?.message).toContain("kms unavailable");
|
|
1073
|
+
|
|
1074
|
+
// Sub-tx rollback: the anonymize hooks ran but must be undone — a user
|
|
1075
|
+
// whose key survives must keep retrying, not sit half-forgotten.
|
|
1076
|
+
const alice = await fetchUser(ALICE_ID);
|
|
1077
|
+
expect(alice?.status).toBe(USER_STATUS.DeletionRequested);
|
|
1078
|
+
expect(alice?.email).toBe("alice.erase-fail@example.com");
|
|
1079
|
+
});
|
|
746
1080
|
});
|
|
@@ -15,7 +15,14 @@
|
|
|
15
15
|
// ueber Pseudo-Tenant.
|
|
16
16
|
|
|
17
17
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
18
19
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
20
|
+
import {
|
|
21
|
+
configurePiiSubjectKms,
|
|
22
|
+
encryptPiiFieldValues,
|
|
23
|
+
InMemoryKmsAdapter,
|
|
24
|
+
resetPiiSubjectKmsForTests,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
19
26
|
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
20
27
|
import {
|
|
21
28
|
setupTestStack,
|
|
@@ -28,7 +35,7 @@ import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
|
28
35
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
29
36
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
30
37
|
import { createFilesFeature } from "../../files";
|
|
31
|
-
import { createSessionsFeature } from "../../sessions";
|
|
38
|
+
import { createSessionsFeature, userSessionEntity } from "../../sessions";
|
|
32
39
|
import { createUserFeature, USER_STATUS, userEntity, userTable } from "../../user";
|
|
33
40
|
import { createUserDataRightsDefaultsFeature } from "../../user-data-rights-defaults";
|
|
34
41
|
import { createUserDataRightsFeature } from "../feature";
|
|
@@ -55,6 +62,7 @@ beforeAll(async () => {
|
|
|
55
62
|
createFilesFeature(),
|
|
56
63
|
createDataRetentionFeature(),
|
|
57
64
|
createComplianceProfilesFeature(),
|
|
65
|
+
authFoundationFeature,
|
|
58
66
|
createSessionsFeature(),
|
|
59
67
|
|
|
60
68
|
createUserDataRightsFeature(),
|
|
@@ -63,6 +71,7 @@ beforeAll(async () => {
|
|
|
63
71
|
});
|
|
64
72
|
|
|
65
73
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
74
|
+
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
66
75
|
await asRawClient(stack.db).unsafe(`
|
|
67
76
|
CREATE TABLE IF NOT EXISTS read_tenant_memberships (
|
|
68
77
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -287,3 +296,70 @@ describe("runUserExport :: Empty-State", () => {
|
|
|
287
296
|
expect(allUserSnippets).toEqual([]);
|
|
288
297
|
});
|
|
289
298
|
});
|
|
299
|
+
|
|
300
|
+
describe("runUserExport :: PII-Subject-Ciphertexte (#820)", () => {
|
|
301
|
+
test("Art. 20: bundle carries plaintext, never kumiko-pii: blobs", async () => {
|
|
302
|
+
const kms = new InMemoryKmsAdapter();
|
|
303
|
+
configurePiiSubjectKms(kms);
|
|
304
|
+
try {
|
|
305
|
+
await seedUser(ALICE_ID, { email: "alice.kms@example.com", displayName: "Alice KMS" });
|
|
306
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
307
|
+
// Read-Pfad-Test: die Spalten tragen echten Subject-Ciphertext, wie
|
|
308
|
+
// sie ein Executor-Write hinterlassen haette (Write-Pfad: PR 1/2).
|
|
309
|
+
const encrypted = await encryptPiiFieldValues(
|
|
310
|
+
{ id: ALICE_ID, email: "alice.kms@example.com", displayName: "Alice KMS" },
|
|
311
|
+
userEntity,
|
|
312
|
+
["email", "displayName"],
|
|
313
|
+
kms,
|
|
314
|
+
{ requestId: "test" },
|
|
315
|
+
);
|
|
316
|
+
await asRawClient(stack.db).unsafe(
|
|
317
|
+
`UPDATE read_users SET email = $1, display_name = $2 WHERE id = $3`,
|
|
318
|
+
[String(encrypted["email"]), String(encrypted["displayName"]), ALICE_ID],
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const bundle = await runUserExport({
|
|
322
|
+
db: stack.db,
|
|
323
|
+
registry: stack.registry,
|
|
324
|
+
userId: ALICE_ID,
|
|
325
|
+
now: NOW(),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const json = JSON.stringify(bundle);
|
|
329
|
+
expect(json).not.toContain("kumiko-pii:");
|
|
330
|
+
expect(json).toContain("alice.kms@example.com");
|
|
331
|
+
expect(json).toContain("Alice KMS");
|
|
332
|
+
} finally {
|
|
333
|
+
resetPiiSubjectKmsForTests();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("ciphertext without a configured KMS exports an explicit marker, not the blob", async () => {
|
|
338
|
+
const kms = new InMemoryKmsAdapter();
|
|
339
|
+
configurePiiSubjectKms(kms);
|
|
340
|
+
await seedUser(ALICE_ID, { email: "alice.nokms@example.com" });
|
|
341
|
+
await seedMembership(ALICE_ID, TENANT_A);
|
|
342
|
+
const encrypted = await encryptPiiFieldValues(
|
|
343
|
+
{ id: ALICE_ID, email: "alice.nokms@example.com" },
|
|
344
|
+
userEntity,
|
|
345
|
+
["email"],
|
|
346
|
+
kms,
|
|
347
|
+
{ requestId: "test" },
|
|
348
|
+
);
|
|
349
|
+
await asRawClient(stack.db).unsafe(`UPDATE read_users SET email = $1 WHERE id = $2`, [
|
|
350
|
+
String(encrypted["email"]),
|
|
351
|
+
ALICE_ID,
|
|
352
|
+
]);
|
|
353
|
+
resetPiiSubjectKmsForTests();
|
|
354
|
+
|
|
355
|
+
const bundle = await runUserExport({
|
|
356
|
+
db: stack.db,
|
|
357
|
+
registry: stack.registry,
|
|
358
|
+
userId: ALICE_ID,
|
|
359
|
+
now: NOW(),
|
|
360
|
+
});
|
|
361
|
+
const json = JSON.stringify(bundle);
|
|
362
|
+
expect(json).not.toContain("kumiko-pii:");
|
|
363
|
+
expect(json).toContain("[encrypted:unavailable]");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// value (the failure mode that shipped the ctx.config export bug).
|
|
14
14
|
|
|
15
15
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
16
17
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
18
|
import {
|
|
18
19
|
createEntity,
|
|
@@ -82,6 +83,7 @@ beforeAll(async () => {
|
|
|
82
83
|
stack = await setupTestStack({
|
|
83
84
|
features: [
|
|
84
85
|
createUserFeature(),
|
|
86
|
+
authFoundationFeature,
|
|
85
87
|
createSessionsFeature(),
|
|
86
88
|
createDataRetentionFeature(),
|
|
87
89
|
createComplianceProfilesFeature(),
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Sprint-2-H1/H2-Hooks) kommt in S2.T1 (Cross-Data-Matrix).
|
|
8
8
|
|
|
9
9
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
10
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
10
11
|
import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
11
12
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
12
13
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
@@ -24,7 +25,14 @@ const userDataRights = createUserDataRightsFeature();
|
|
|
24
25
|
|
|
25
26
|
beforeAll(async () => {
|
|
26
27
|
stack = await setupTestStack({
|
|
27
|
-
features: [
|
|
28
|
+
features: [
|
|
29
|
+
authFoundationFeature,
|
|
30
|
+
userFeature,
|
|
31
|
+
sessionsFeature,
|
|
32
|
+
dataRetention,
|
|
33
|
+
complianceProfiles,
|
|
34
|
+
userDataRights,
|
|
35
|
+
],
|
|
28
36
|
});
|
|
29
37
|
});
|
|
30
38
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EXT_USER_DATA,
|
|
3
|
+
type FeatureDefinition,
|
|
4
|
+
type PiiAnnotations,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { entitiesOf } from "../shared";
|
|
7
|
+
|
|
8
|
+
// V2: export-without-erase gate. A feature that registers an EXT_USER_DATA
|
|
9
|
+
// export hook without a matching delete hook exports data under Art.20 but
|
|
10
|
+
// never erases it on forget — an Art.17 violation. Hard boot failure: no app
|
|
11
|
+
// should ship a GDPR export path with no erase path. Registry-level signal
|
|
12
|
+
// only; runtime no-ops (a delete hook that silently skips) are not detectable
|
|
13
|
+
// here — those are covered by the export/forget integration tests.
|
|
14
|
+
//
|
|
15
|
+
// Registered as an `r.bootCheck()` by this feature (#1314) — EXT_USER_DATA is
|
|
16
|
+
// owned by user-data-rights (r.extendsRegistrar(EXT_USER_DATA, {})), so any
|
|
17
|
+
// r.useExtension(EXT_USER_DATA, ...) usage across the app already requires
|
|
18
|
+
// user-data-rights to be mounted (boot-validator's extension-provider check
|
|
19
|
+
// fails otherwise); running this from user-data-rights' own bootCheck is
|
|
20
|
+
// equivalent to the framework-internal call it replaces.
|
|
21
|
+
export function validateGdprHookCompleteness(features: readonly FeatureDefinition[]): void {
|
|
22
|
+
for (const feature of features) {
|
|
23
|
+
for (const usage of feature.extensionUsages) {
|
|
24
|
+
if (usage.extensionName !== EXT_USER_DATA) continue;
|
|
25
|
+
const hasExport = typeof usage.options?.["export"] === "function";
|
|
26
|
+
const hasDelete = typeof usage.options?.["delete"] === "function";
|
|
27
|
+
if (hasExport && !hasDelete) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[kumiko:boot] Feature "${feature.name}" exports entity "${usage.entityName}" via EXT_USER_DATA but registers no delete hook — data is included in Art.20 exports but never erased on forget (Art.17 violation). Add a delete hook. If erasure is intentionally handled elsewhere (e.g. crypto-shredding key-erase, parent cascade), register a no-op delete: async () => {} with a comment explaining why.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// V3: PII-entity-without-hook gate. V2 checks registered hooks for
|
|
37
|
+
// completeness; V3 catches the entity nobody registered at all — fields
|
|
38
|
+
// annotated as user-subject data (pii / userOwned) yet invisible to the
|
|
39
|
+
// Art.15/20 export and Art.17 forget pipeline. Hard boot failure: a
|
|
40
|
+
// subject-data entity that skips the pipeline is exactly the "feature built
|
|
41
|
+
// past GDPR" leak this gate exists to stop. No "is user-data-rights mounted"
|
|
42
|
+
// guard needed here (unlike the framework-internal predecessor) — this runs
|
|
43
|
+
// as user-data-rights' own bootCheck, so it's only reachable when mounted.
|
|
44
|
+
// Matching is by entity name across all features (usage.entityName is
|
|
45
|
+
// unqualified); a same-named entity in another feature can mask a gap —
|
|
46
|
+
// accepted, as the common case is a distinct entity name.
|
|
47
|
+
export function validateGdprPiiHookCoverage(features: readonly FeatureDefinition[]): void {
|
|
48
|
+
const hookedEntities = new Set<string>();
|
|
49
|
+
for (const f of features) {
|
|
50
|
+
for (const usage of f.extensionUsages) {
|
|
51
|
+
if (usage.extensionName === EXT_USER_DATA) hookedEntities.add(usage.entityName);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const feature of features) {
|
|
56
|
+
for (const [entityName, entity] of entitiesOf(feature)) {
|
|
57
|
+
if (hookedEntities.has(entityName)) continue;
|
|
58
|
+
const subjectFields = Object.entries(entity.fields)
|
|
59
|
+
.filter(([, field]) => {
|
|
60
|
+
const annot = field as PiiAnnotations; // @cast-boundary schema-walk
|
|
61
|
+
return Boolean(annot.pii) || Boolean(annot.userOwned);
|
|
62
|
+
})
|
|
63
|
+
.map(([name]) => name);
|
|
64
|
+
if (subjectFields.length === 0) continue;
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[kumiko:boot] Entity "${entityName}" (feature "${feature.name}") has user-subject fields (${subjectFields.join(", ")}) but no feature registers an EXT_USER_DATA hook for it — the data never appears in Art.15/20 exports and is never erased on forget (Art.17 gap). Register r.useExtension(EXT_USER_DATA, "${entityName}", { export, delete }) in the owning feature or a defaults feature. If this entity is intentionally out of the pipeline (e.g. crypto-shredding key-erase covers it), register a no-op hook { export: async () => null, delete: async () => {} } with a comment explaining why.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// kumiko-framework#1525: selectUsersDueForForgetCleanup must not rely on
|
|
2
|
+
// globalThis.Temporal when the caller passes a string cutoff (the
|
|
3
|
+
// run-forget-cleanup.integration.test.ts suite exercises the full
|
|
4
|
+
// pipeline; this is the narrow seam for the ambient-global regression).
|
|
5
|
+
|
|
6
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import {
|
|
8
|
+
setupTestStack,
|
|
9
|
+
type TestStack,
|
|
10
|
+
unsafeCreateEntityTable,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
12
|
+
import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
13
|
+
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
14
|
+
import { createUserFeature, USER_STATUS, userEntity, userTable } from "../../../../user";
|
|
15
|
+
import { selectUsersDueForForgetCleanup } from "../forget-cleanup";
|
|
16
|
+
|
|
17
|
+
let stack: TestStack;
|
|
18
|
+
|
|
19
|
+
const TENANT = "00000000-0000-4000-8000-00000000000a";
|
|
20
|
+
const USER_ID = "aaaaaaaa-aaaa-4aaa-8aaa-000000000001";
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
stack = await setupTestStack({ features: [createUserFeature()] });
|
|
24
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await stack.cleanup();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
await resetTestTables(stack.db, [userTable]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("selectUsersDueForForgetCleanup — kumiko-framework#1525: no ambient Temporal global", () => {
|
|
36
|
+
test("string cutoff resolves without relying on globalThis.Temporal", async () => {
|
|
37
|
+
const pastGracePeriodEnd = getTemporal().Now.instant().subtract({ hours: 1 });
|
|
38
|
+
await seedRow(stack.db, userTable, {
|
|
39
|
+
id: USER_ID,
|
|
40
|
+
tenantId: TENANT,
|
|
41
|
+
email: "due@example.com",
|
|
42
|
+
passwordHash: "hashed",
|
|
43
|
+
displayName: "Due User",
|
|
44
|
+
locale: "de",
|
|
45
|
+
emailVerified: true,
|
|
46
|
+
roles: '["Member"]',
|
|
47
|
+
status: USER_STATUS.DeletionRequested,
|
|
48
|
+
gracePeriodEnd: pastGracePeriodEnd,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Computed with Temporal intact — only the query call itself runs
|
|
52
|
+
// ambient-free below.
|
|
53
|
+
const cutoffIso = getTemporal().Now.instant().toString();
|
|
54
|
+
|
|
55
|
+
const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
|
|
56
|
+
delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
57
|
+
try {
|
|
58
|
+
const due = await selectUsersDueForForgetCleanup(
|
|
59
|
+
stack.db,
|
|
60
|
+
USER_STATUS.DeletionRequested,
|
|
61
|
+
cutoffIso,
|
|
62
|
+
);
|
|
63
|
+
expect(due.map((u) => u.id)).toContain(USER_ID);
|
|
64
|
+
} finally {
|
|
65
|
+
if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
66
|
+
else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|