@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
|
@@ -9,13 +9,17 @@
|
|
|
9
9
|
// - hardDelete → executor.forget pro Row (Event → rebuild-safe hard purge)
|
|
10
10
|
// - softDelete → executor.delete pro Row (Event → rebuild-safe soft-delete),
|
|
11
11
|
// nur auf noch-nicht-geloeschte (isDeleted:false-Filter)
|
|
12
|
-
// - anonymize →
|
|
12
|
+
// - anonymize → executor.update pro Row mit den Werten der per-Feld
|
|
13
|
+
// anonymize-Funktionen; Row bleibt (fields.ts-Contract)
|
|
14
|
+
// - blockDelete → waehrend keepFor unangetastet (Aufbewahrungs-Pflicht;
|
|
15
|
+
// user-forget loest anonymize aus). NACH Ablauf laufen die
|
|
16
|
+
// anonymize-Funktionen — Geschaeftsdaten bleiben, Personen-
|
|
17
|
+
// Bezug raus (fields.ts:44-47).
|
|
13
18
|
//
|
|
14
19
|
// Über den Executor statt Batch-deleteMany/updateMany: ein eventloser Batch-Write
|
|
15
20
|
// auf die (erased) Projektions-Tabelle wird beim Projection-Rebuild
|
|
16
21
|
// gewischt/resurrektiert — das #648-Loch. Kosten: N Events pro Cleanup statt ein
|
|
17
22
|
// Batch-Statement (per-Row-Ceiling, Batch-Event-Variante als Follow-up).
|
|
18
|
-
// - blockDelete → ignoriert (Aufbewahrungs-Pflicht; user-forget loest anonymize)
|
|
19
23
|
//
|
|
20
24
|
// **Schaerfer als soft-delete-cleanup:** dieser Cron hardDeleted LIVE Rows
|
|
21
25
|
// (keyed auf reference, Default createdAt), nicht bereits-soft-geloeschte.
|
|
@@ -23,12 +27,12 @@
|
|
|
23
27
|
// reference-Spalte wuerde sonst ein malformed/all-matching WHERE ergeben und
|
|
24
28
|
// pauschal loeschen.
|
|
25
29
|
//
|
|
26
|
-
// **anonymize
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
30
|
+
// **anonymize-Idempotenz ohne Marker-Spalte:** die anonymize-Funktionen sind
|
|
31
|
+
// row-unabhaengig (`() => unknown`) — einmal pro Entity ausgewertet ergeben
|
|
32
|
+
// sie die Ziel-Werte. Eine Row die sie schon traegt wird uebersprungen, der
|
|
33
|
+
// taegliche Re-Lauf appended also null Events. Der WHERE matcht anonymisierte
|
|
34
|
+
// Rows weiter (reference bleibt alt), deshalb id-Cursor-Paging statt einer
|
|
35
|
+
// einzelnen Page — sonst verstopfen erledigte Rows das batchLimit-Fenster.
|
|
32
36
|
|
|
33
37
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
34
38
|
import {
|
|
@@ -76,15 +80,18 @@ export interface RunRetentionCleanupArgs {
|
|
|
76
80
|
|
|
77
81
|
export interface RetentionCleanupSkip {
|
|
78
82
|
readonly entityName: string;
|
|
79
|
-
readonly reason:
|
|
83
|
+
readonly reason:
|
|
84
|
+
| "missing_reference_column"
|
|
85
|
+
| "missing_softdelete_columns"
|
|
86
|
+
| "missing_anonymize_fields";
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export interface RunRetentionCleanupResult {
|
|
83
90
|
readonly hardDeleted: number;
|
|
84
91
|
readonly softDeleted: number;
|
|
85
|
-
/**
|
|
86
|
-
readonly
|
|
87
|
-
/** Anomalien: Policy referenziert eine Spalte die die Tabelle nicht hat. */
|
|
92
|
+
/** Rows deren anonymize-Feld-Funktionen angewendet wurden (anonymize + abgelaufene blockDelete). */
|
|
93
|
+
readonly anonymized: number;
|
|
94
|
+
/** Anomalien: Policy referenziert eine Spalte die die Tabelle nicht hat, oder anonymize ohne anonymize-Felder. */
|
|
88
95
|
readonly skipped: readonly RetentionCleanupSkip[];
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -106,6 +113,65 @@ async function purgeMatchingRows(
|
|
|
106
113
|
return count;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
// PiiAnnotations.anonymize — structural view, avoids reaching into the
|
|
117
|
+
// engine's field-type internals from here.
|
|
118
|
+
type AnonymizeCapableField = { readonly anonymize?: () => unknown | Promise<unknown> };
|
|
119
|
+
|
|
120
|
+
// Evaluate the entity's per-field anonymize functions ONCE — they are
|
|
121
|
+
// row-independent by signature, so the result doubles as the idempotency
|
|
122
|
+
// probe: a row already carrying these values needs no event.
|
|
123
|
+
async function resolveAnonymizeTargets(
|
|
124
|
+
fields: Readonly<Record<string, unknown>>,
|
|
125
|
+
): Promise<Record<string, unknown> | null> {
|
|
126
|
+
const targets: Record<string, unknown> = {};
|
|
127
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
128
|
+
const annot = field as AnonymizeCapableField; // @cast-boundary schema-walk
|
|
129
|
+
if (typeof annot.anonymize === "function") targets[name] = await annot.anonymize();
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(targets).length > 0 ? targets : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rowNeedsAnonymize(
|
|
135
|
+
row: Record<string, unknown>,
|
|
136
|
+
targets: Record<string, unknown>,
|
|
137
|
+
): boolean {
|
|
138
|
+
return Object.entries(targets).some(([field, value]) => row[field] !== value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Anonymize every matching row via the executor (event → rebuild-safe).
|
|
142
|
+
// id-Cursor-paged because the where keeps matching already-anonymized rows;
|
|
143
|
+
// the diff-check keeps the daily re-run event-free. batchLimit bounds the
|
|
144
|
+
// UPDATES per run, not the scan.
|
|
145
|
+
// ponytail: scans all past-cutoff rows per run — add an anonymized-marker
|
|
146
|
+
// column if a hot entity accumulates millions of held rows.
|
|
147
|
+
async function anonymizeMatchingRows(
|
|
148
|
+
db: DbRunner,
|
|
149
|
+
table: Parameters<typeof selectMany>[1],
|
|
150
|
+
where: WhereObject,
|
|
151
|
+
batchLimit: number,
|
|
152
|
+
targets: Record<string, unknown>,
|
|
153
|
+
op: (id: EntityId, changes: Record<string, unknown>) => Promise<{ readonly isSuccess: boolean }>,
|
|
154
|
+
): Promise<number> {
|
|
155
|
+
let count = 0;
|
|
156
|
+
let cursor: EntityId | null = null;
|
|
157
|
+
while (count < batchLimit) {
|
|
158
|
+
const pageWhere: WhereObject = cursor === null ? where : { ...where, id: { gt: cursor } };
|
|
159
|
+
const rows = await selectMany<Record<string, unknown>>(db, table, pageWhere, {
|
|
160
|
+
limit: batchLimit,
|
|
161
|
+
orderBy: { col: "id" },
|
|
162
|
+
});
|
|
163
|
+
if (rows.length === 0) break;
|
|
164
|
+
for (const row of rows) {
|
|
165
|
+
cursor = row["id"] as EntityId; // @cast-boundary db-row
|
|
166
|
+
if (!rowNeedsAnonymize(row, targets)) continue;
|
|
167
|
+
const res = await op(cursor, targets);
|
|
168
|
+
if (res.isSuccess) count++;
|
|
169
|
+
if (count >= batchLimit) break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return count;
|
|
173
|
+
}
|
|
174
|
+
|
|
109
175
|
export async function runRetentionCleanup(
|
|
110
176
|
args: RunRetentionCleanupArgs,
|
|
111
177
|
): Promise<RunRetentionCleanupResult> {
|
|
@@ -114,7 +180,7 @@ export async function runRetentionCleanup(
|
|
|
114
180
|
|
|
115
181
|
let hardDeleted = 0;
|
|
116
182
|
let softDeleted = 0;
|
|
117
|
-
|
|
183
|
+
let anonymized = 0;
|
|
118
184
|
const skipped: RetentionCleanupSkip[] = [];
|
|
119
185
|
|
|
120
186
|
// Retention writes go through the executor (events) so a projection rebuild
|
|
@@ -198,16 +264,33 @@ export async function runRetentionCleanup(
|
|
|
198
264
|
);
|
|
199
265
|
break;
|
|
200
266
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
267
|
+
// blockDelete = Legal-Hold: rows stay untouched during keepFor (the
|
|
268
|
+
// cutoff-WHERE guarantees that), user-forget anonymizes instead of
|
|
269
|
+
// deleting (run-forget-cleanup maps blockDelete → anonymize). AFTER the
|
|
270
|
+
// hold expires the anonymize-field functions run — business data stays,
|
|
271
|
+
// the person link goes (fields.ts contract). Same executor path as the
|
|
272
|
+
// anonymize strategy, so both cases share it.
|
|
273
|
+
case "anonymize":
|
|
205
274
|
case "blockDelete": {
|
|
206
|
-
|
|
275
|
+
const targets = await resolveAnonymizeTargets(entity.fields);
|
|
276
|
+
if (!targets) {
|
|
277
|
+
skipped.push({ entityName, reason: "missing_anonymize_fields" });
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
const executor = createEventStoreExecutor(proj.table, entity, { entityName });
|
|
281
|
+
anonymized += await anonymizeMatchingRows(
|
|
282
|
+
db,
|
|
283
|
+
proj.table,
|
|
284
|
+
where,
|
|
285
|
+
batchLimit,
|
|
286
|
+
targets,
|
|
287
|
+
(id, changes) =>
|
|
288
|
+
executor.update({ id, changes }, systemUser, tdb, { skipOptimisticLock: true }),
|
|
289
|
+
);
|
|
207
290
|
break;
|
|
208
291
|
}
|
|
209
292
|
}
|
|
210
293
|
}
|
|
211
294
|
|
|
212
|
-
return { hardDeleted, softDeleted,
|
|
295
|
+
return { hardDeleted, softDeleted, anonymized, skipped };
|
|
213
296
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Delivery-attempt log under KMS (#799): recipientAddress is written via
|
|
2
|
+
// LOW-LEVEL append() in attempt-log.ts — the event-PII catalog must cover
|
|
3
|
+
// it. Stored event + projected row carry ciphertext under the recipient's
|
|
4
|
+
// DEK, the outgoing mail still reaches the PLAINTEXT address, the admin
|
|
5
|
+
// log.query decrypts for display, and erasing the recipient's key flips
|
|
6
|
+
// the log entry to [[erased]] without touching the append-only stream.
|
|
7
|
+
|
|
8
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
|
+
import {
|
|
11
|
+
configurePiiSubjectKms,
|
|
12
|
+
InMemoryKmsAdapter,
|
|
13
|
+
isPiiCiphertext,
|
|
14
|
+
PII_ERASED_SENTINEL,
|
|
15
|
+
resetPiiSubjectKmsForTests,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
|
+
import {
|
|
18
|
+
defineFeature,
|
|
19
|
+
defineWriteHandler,
|
|
20
|
+
type NotifyFn,
|
|
21
|
+
qn,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
23
|
+
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
24
|
+
import {
|
|
25
|
+
createTestUser,
|
|
26
|
+
setupTestStack,
|
|
27
|
+
type TestStack,
|
|
28
|
+
TestUsers,
|
|
29
|
+
unsafePushTables,
|
|
30
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { createChannelEmailFeature } from "../../channel-email/feature";
|
|
33
|
+
import { createInMemoryTransport } from "../../channel-email/types";
|
|
34
|
+
import { createConfigFeature } from "../../config/feature";
|
|
35
|
+
import { configValuesTable } from "../../config/table";
|
|
36
|
+
import { createRendererFoundationFeature } from "../../renderer-foundation/feature";
|
|
37
|
+
import { createRendererSimpleFeature } from "../../renderer-simple/feature";
|
|
38
|
+
import { simpleRenderer } from "../../renderer-simple/simple-renderer";
|
|
39
|
+
import { createTemplateResolverFeature } from "../../template-resolver/feature";
|
|
40
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
41
|
+
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
42
|
+
import { DELIVERY_ATTEMPT_EVENT } from "../constants";
|
|
43
|
+
import { createDeliveryFeature } from "../feature";
|
|
44
|
+
import { deliveryAttemptsTable, notificationPreferencesTable } from "../tables";
|
|
45
|
+
import { createDeliveryTestContext } from "../testing";
|
|
46
|
+
|
|
47
|
+
const emailTransport = createInMemoryTransport();
|
|
48
|
+
const testEmail = (userId: string | number) => `user-${userId}@test.com`;
|
|
49
|
+
|
|
50
|
+
const admin = TestUsers.admin;
|
|
51
|
+
const recipient = createTestUser({ id: 7, roles: ["User"] });
|
|
52
|
+
|
|
53
|
+
const appFeature = defineFeature("app", (r) => {
|
|
54
|
+
r.requires("delivery");
|
|
55
|
+
|
|
56
|
+
r.writeHandler(
|
|
57
|
+
defineWriteHandler({
|
|
58
|
+
name: "ping",
|
|
59
|
+
schema: z.object({ toUserId: z.string() }),
|
|
60
|
+
handler: async (event, ctx) => {
|
|
61
|
+
const notify = ctx.notify as NotifyFn;
|
|
62
|
+
await notify(qn("app", "notify", "pinged"), {
|
|
63
|
+
to: event.payload.toUserId,
|
|
64
|
+
data: { title: "Ping", body: "Du wurdest angepingt" },
|
|
65
|
+
});
|
|
66
|
+
return { isSuccess: true, data: { sent: true } };
|
|
67
|
+
},
|
|
68
|
+
access: { openToAll: true },
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let stack: TestStack;
|
|
74
|
+
let kms: InMemoryKmsAdapter;
|
|
75
|
+
|
|
76
|
+
beforeAll(async () => {
|
|
77
|
+
stack = await setupTestStack({
|
|
78
|
+
features: [
|
|
79
|
+
createConfigFeature(),
|
|
80
|
+
createTenantFeature(),
|
|
81
|
+
createTemplateResolverFeature(),
|
|
82
|
+
createRendererFoundationFeature(),
|
|
83
|
+
createDeliveryFeature(),
|
|
84
|
+
createRendererSimpleFeature(),
|
|
85
|
+
createChannelEmailFeature({
|
|
86
|
+
transport: emailTransport,
|
|
87
|
+
renderer: simpleRenderer,
|
|
88
|
+
resolveEmail: async (userId) => testEmail(userId),
|
|
89
|
+
}),
|
|
90
|
+
appFeature,
|
|
91
|
+
],
|
|
92
|
+
extraContext: (deps) => createDeliveryTestContext(deps),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await unsafePushTables(stack.db, {
|
|
96
|
+
configValuesTable,
|
|
97
|
+
tenantMembershipsTable,
|
|
98
|
+
notificationPreferencesTable,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterAll(async () => {
|
|
103
|
+
await stack.cleanup();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ONE kms for the whole file: rows persist across tests, so a fresh adapter
|
|
107
|
+
// per test would orphan earlier ciphertext (decrypt fails loud on KeyNotFound).
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
kms ??= new InMemoryKmsAdapter();
|
|
110
|
+
configurePiiSubjectKms(kms);
|
|
111
|
+
emailTransport.sent.length = 0;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterAll(() => {
|
|
115
|
+
resetPiiSubjectKmsForTests();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async function pingRecipient(): Promise<void> {
|
|
119
|
+
await stack.http.writeOk(qn("app", "write", "ping"), { toUserId: recipient.id }, admin);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("delivery attempt log under KMS", () => {
|
|
123
|
+
test("mail goes to the plaintext address; event + row store ciphertext", async () => {
|
|
124
|
+
await pingRecipient();
|
|
125
|
+
|
|
126
|
+
expect(emailTransport.sent.length).toBe(1);
|
|
127
|
+
expect(emailTransport.sent[0]?.to).toBe(testEmail(recipient.id));
|
|
128
|
+
|
|
129
|
+
const events = await selectMany(stack.db, eventsTable, { type: DELIVERY_ATTEMPT_EVENT });
|
|
130
|
+
const sent = events
|
|
131
|
+
.map((e) => e.payload as Record<string, unknown>)
|
|
132
|
+
.find((p) => p["channel"] === "email" && p["status"] === "sent");
|
|
133
|
+
expect(sent).toBeDefined();
|
|
134
|
+
expect(isPiiCiphertext(sent?.["recipientAddress"])).toBe(true);
|
|
135
|
+
expect(String(sent?.["recipientAddress"])).toContain(`user:${recipient.id}`);
|
|
136
|
+
expect(sent?.["recipientId"]).toBe(recipient.id);
|
|
137
|
+
|
|
138
|
+
const rows = await selectMany(stack.db, deliveryAttemptsTable, {
|
|
139
|
+
recipientId: recipient.id,
|
|
140
|
+
channel: "email",
|
|
141
|
+
});
|
|
142
|
+
expect(rows.length).toBeGreaterThan(0);
|
|
143
|
+
expect(isPiiCiphertext(rows[0]?.["recipientAddress"])).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("admin log.query decrypts for display; after key-erase it shows [[erased]]", async () => {
|
|
147
|
+
await pingRecipient();
|
|
148
|
+
|
|
149
|
+
const before = await stack.http.queryOk<{ rows: Record<string, unknown>[] }>(
|
|
150
|
+
"delivery:query:log",
|
|
151
|
+
{ limit: 100 },
|
|
152
|
+
admin,
|
|
153
|
+
);
|
|
154
|
+
const entry = before.rows.find(
|
|
155
|
+
(r) => r["recipientId"] === recipient.id && r["channel"] === "email",
|
|
156
|
+
);
|
|
157
|
+
expect(entry?.["recipientAddress"]).toBe(testEmail(recipient.id));
|
|
158
|
+
|
|
159
|
+
await kms.eraseKey({ kind: "user", userId: recipient.id });
|
|
160
|
+
|
|
161
|
+
const after = await stack.http.queryOk<{ rows: Record<string, unknown>[] }>(
|
|
162
|
+
"delivery:query:log",
|
|
163
|
+
{ limit: 100 },
|
|
164
|
+
admin,
|
|
165
|
+
);
|
|
166
|
+
const erased = after.rows.find(
|
|
167
|
+
(r) => r["recipientId"] === recipient.id && r["channel"] === "email",
|
|
168
|
+
);
|
|
169
|
+
expect(erased?.["recipientAddress"]).toBe(PII_ERASED_SENTINEL);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { access, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
|
|
4
|
+
import { DELIVERY_LOG_SCREEN_ID } from "../constants";
|
|
5
|
+
import { createDeliveryFeature } from "../feature";
|
|
6
|
+
|
|
7
|
+
describe("delivery screens + handler access alignment", () => {
|
|
8
|
+
const features = [createDeliveryFeature()];
|
|
9
|
+
|
|
10
|
+
test("boot-validates with delivery-log screen registered", () => {
|
|
11
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("delivery-log screen is custom, access.admin-gated", () => {
|
|
15
|
+
const delivery = createDeliveryFeature();
|
|
16
|
+
const screen = delivery.screens[DELIVERY_LOG_SCREEN_ID];
|
|
17
|
+
expect(screen?.type).toBe("custom");
|
|
18
|
+
if (screen && "access" in screen && screen.access && "roles" in screen.access) {
|
|
19
|
+
expect(screen.access.roles).toEqual(access.admin);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("delivery log handler shares access.admin", () => {
|
|
24
|
+
const delivery = createDeliveryFeature();
|
|
25
|
+
expect(rolesOf(delivery.queryHandlers["log"]?.access)).toEqual([...access.admin]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { insertMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
|
+
import { access, createRegistry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import {
|
|
5
|
+
createTestUser,
|
|
6
|
+
setupTestStack,
|
|
7
|
+
type TestStack,
|
|
8
|
+
testTenantId,
|
|
9
|
+
unsafePushTables,
|
|
10
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
11
|
+
import { rolesOf } from "@cosmicdrift/kumiko-framework/testing";
|
|
12
|
+
import { DELIVERY_LOG_SCREEN_ID, DeliveryQueries } from "../constants";
|
|
13
|
+
import { createDeliveryFeature } from "../feature";
|
|
14
|
+
import { deliveryAttemptsTable, notificationPreferencesTable } from "../tables";
|
|
15
|
+
|
|
16
|
+
let stack: TestStack;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
stack = await setupTestStack({
|
|
20
|
+
features: [createDeliveryFeature()],
|
|
21
|
+
});
|
|
22
|
+
await unsafePushTables(stack.db, { deliveryAttemptsTable, notificationPreferencesTable });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await stack.cleanup();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("delivery access matrix", () => {
|
|
30
|
+
test("delivery log handler and screen share access.admin", () => {
|
|
31
|
+
const registry = createRegistry([createDeliveryFeature()]);
|
|
32
|
+
expect(rolesOf(registry.getQueryHandler(DeliveryQueries.log)?.access)).toEqual([
|
|
33
|
+
...access.admin,
|
|
34
|
+
]);
|
|
35
|
+
const delivery = createDeliveryFeature();
|
|
36
|
+
const screen = delivery.screens[DELIVERY_LOG_SCREEN_ID];
|
|
37
|
+
if (screen && "access" in screen && screen.access && "roles" in screen.access) {
|
|
38
|
+
expect(screen.access.roles).toEqual(access.admin);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("delivery log HTTP access", () => {
|
|
44
|
+
test("TenantAdmin can query delivery log", async () => {
|
|
45
|
+
const user = createTestUser({ id: 11, roles: ["TenantAdmin"] });
|
|
46
|
+
const res = await stack.http.queryOk<{ rows: readonly unknown[] }>(
|
|
47
|
+
DeliveryQueries.log,
|
|
48
|
+
{},
|
|
49
|
+
user,
|
|
50
|
+
);
|
|
51
|
+
expect(Array.isArray(res.rows)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("historic Admin can query delivery log", async () => {
|
|
55
|
+
const user = createTestUser({ id: 12, roles: ["Admin"] });
|
|
56
|
+
const res = await stack.http.queryOk<{ rows: readonly unknown[] }>(
|
|
57
|
+
DeliveryQueries.log,
|
|
58
|
+
{},
|
|
59
|
+
user,
|
|
60
|
+
);
|
|
61
|
+
expect(Array.isArray(res.rows)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("regular User gets 403", async () => {
|
|
65
|
+
const user = createTestUser({ id: 13, roles: ["User"] });
|
|
66
|
+
expect((await stack.http.query(DeliveryQueries.log, {}, user)).status).toBe(403);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("delivery log tenant isolation", () => {
|
|
71
|
+
// delivery runs in system-scope, so the handler must filter by tenantId —
|
|
72
|
+
// otherwise a TenantAdmin reads every tenant's attempts (with decrypted PII).
|
|
73
|
+
test("TenantAdmin only sees their own tenant's delivery attempts", async () => {
|
|
74
|
+
const tenantA = testTenantId(1); // createTestUser default tenant
|
|
75
|
+
const tenantB = testTenantId(2);
|
|
76
|
+
await insertMany(stack.db, deliveryAttemptsTable, [
|
|
77
|
+
{
|
|
78
|
+
id: crypto.randomUUID(),
|
|
79
|
+
tenantId: tenantA,
|
|
80
|
+
notificationType: "welcome",
|
|
81
|
+
channel: "email",
|
|
82
|
+
recipientAddress: null,
|
|
83
|
+
status: "sent",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: crypto.randomUUID(),
|
|
87
|
+
tenantId: tenantB,
|
|
88
|
+
notificationType: "welcome",
|
|
89
|
+
channel: "email",
|
|
90
|
+
recipientAddress: null,
|
|
91
|
+
status: "sent",
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const adminA = createTestUser({ id: 21, roles: ["TenantAdmin"], tenantId: tenantA });
|
|
96
|
+
const res = await stack.http.queryOk<{ rows: readonly { tenantId: string }[] }>(
|
|
97
|
+
DeliveryQueries.log,
|
|
98
|
+
{},
|
|
99
|
+
adminA,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(res.rows.length).toBeGreaterThan(0);
|
|
103
|
+
expect(res.rows.every((r) => r.tenantId === tenantA)).toBe(true);
|
|
104
|
+
expect(res.rows.some((r) => r.tenantId === tenantB)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { QnTypes, qn } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
3
|
+
import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
DELIVERY_ATTEMPT_EVENT,
|
|
7
|
+
DELIVERY_FEATURE,
|
|
8
|
+
DELIVERY_LOG_SCREEN_ID,
|
|
9
|
+
DeliveryErrors,
|
|
10
|
+
DeliveryHandlers,
|
|
11
|
+
DeliveryJobNames,
|
|
12
|
+
DeliveryQueries,
|
|
13
|
+
DeliveryStatus,
|
|
14
|
+
type DeliveryStatusValue,
|
|
15
|
+
} from "./public-names";
|
|
6
16
|
|
|
7
17
|
// notify() priority → BullMQ job priority. Lower number = processed first; all
|
|
8
18
|
// > 0 so prioritised delivery jobs never mix with BullMQ's "0 = unprioritised
|
|
@@ -13,54 +23,6 @@ export const deliveryPriorityRank: Record<NotifyPriority, number> = {
|
|
|
13
23
|
low: 3,
|
|
14
24
|
};
|
|
15
25
|
|
|
16
|
-
// Qualified write handler names (QN format: scope:type:name)
|
|
17
|
-
export const DeliveryHandlers = {
|
|
18
|
-
setPreference: "delivery:write:set-preference",
|
|
19
|
-
} as const;
|
|
20
|
-
|
|
21
|
-
// Qualified query handler names (QN format: scope:type:name)
|
|
22
|
-
export const DeliveryQueries = {
|
|
23
|
-
log: "delivery:query:log",
|
|
24
|
-
preferences: "delivery:query:preferences",
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
// Error codes
|
|
28
|
-
export const DeliveryErrors = {
|
|
29
|
-
noRecipient: "delivery_no_recipient",
|
|
30
|
-
channelFailed: "delivery_channel_failed",
|
|
31
|
-
} as const;
|
|
32
|
-
|
|
33
|
-
// Delivery status values. `queued` is the initial state of an async attempt
|
|
34
|
-
// (email/push) between dispatch and the worker running — it transitions to
|
|
35
|
-
// sent/failed once the delivery.send job finishes. Synchronous attempts
|
|
36
|
-
// (inApp, skips) never observe `queued`.
|
|
37
|
-
export const DeliveryStatus = {
|
|
38
|
-
queued: "queued",
|
|
39
|
-
sent: "sent",
|
|
40
|
-
failed: "failed",
|
|
41
|
-
skipped: "skipped",
|
|
42
|
-
} as const;
|
|
43
|
-
|
|
44
|
-
export type DeliveryStatusValue = (typeof DeliveryStatus)[keyof typeof DeliveryStatus];
|
|
45
|
-
|
|
46
|
-
// Qualified domain-event name. Emitted by the delivery-service on every
|
|
47
|
-
// attempt (sent / failed / skipped). A multi-stream-projection materialises
|
|
48
|
-
// each event into a row in deliveryAttemptsTable for the log-query handler.
|
|
49
|
-
export const DELIVERY_ATTEMPT_EVENT = "delivery:event:attempt" as const;
|
|
50
|
-
|
|
51
|
-
// Background jobs that carry async (queued-mode) channels. render decouples the
|
|
52
|
-
// expensive template step from the send so each retries independently; render
|
|
53
|
-
// dispatches send on success. Channels without a render() (push) skip straight
|
|
54
|
-
// to send.
|
|
55
|
-
//
|
|
56
|
-
// Short names are what r.job() registers; the registry qualifies them to
|
|
57
|
-
// `scope:job:name`. Dispatch (notify + render→send chaining) must use the
|
|
58
|
-
// QUALIFIED name, so DeliveryJobs holds the qualified form via qn().
|
|
59
|
-
export const DeliveryJobNames = {
|
|
60
|
-
render: "render",
|
|
61
|
-
send: "send",
|
|
62
|
-
} as const;
|
|
63
|
-
|
|
64
26
|
export const DeliveryJobs = {
|
|
65
27
|
render: qn(DELIVERY_FEATURE, QnTypes.job, DeliveryJobNames.render),
|
|
66
28
|
send: qn(DELIVERY_FEATURE, QnTypes.job, DeliveryJobNames.send),
|
package/src/delivery/feature.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { upsertByPk } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
defineFeature,
|
|
5
|
+
type FeatureDefinition,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
3
7
|
import type { z } from "zod";
|
|
4
|
-
import { DELIVERY_ATTEMPT_EVENT, DeliveryJobNames } from "./constants";
|
|
8
|
+
import { DELIVERY_ATTEMPT_EVENT, DELIVERY_LOG_SCREEN_ID, DeliveryJobNames } from "./constants";
|
|
5
9
|
import { deliveryAttemptSchema } from "./events";
|
|
6
10
|
import { logQuery } from "./handlers/log.query";
|
|
7
11
|
import { preferencesQuery } from "./handlers/preferences.query";
|
|
8
12
|
import { setPreferenceWrite } from "./handlers/set-preference.write";
|
|
13
|
+
import { DELIVERY_I18N } from "./i18n";
|
|
9
14
|
import { deliveryRenderJob, deliverySendJob } from "./jobs";
|
|
10
15
|
import {
|
|
11
16
|
deliveryAttemptsTable,
|
|
@@ -17,7 +22,7 @@ import {
|
|
|
17
22
|
export function createDeliveryFeature(): FeatureDefinition {
|
|
18
23
|
return defineFeature("delivery", (r) => {
|
|
19
24
|
r.describe(
|
|
20
|
-
"The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). It stores per-user channel preferences in the `notification-preference` entity, logs every attempt to `
|
|
25
|
+
"The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). It stores per-user channel preferences in the `notification-preference` entity, logs every attempt to `store_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything.",
|
|
21
26
|
);
|
|
22
27
|
r.uiHints({
|
|
23
28
|
displayLabel: "Notifications \u00b7 Dispatch Core",
|
|
@@ -31,7 +36,7 @@ export function createDeliveryFeature(): FeatureDefinition {
|
|
|
31
36
|
r.entity("notification-preference", notificationPreferenceEntity, {
|
|
32
37
|
table: notificationPreferencesTable,
|
|
33
38
|
});
|
|
34
|
-
r.
|
|
39
|
+
r.storeTable(deliveryAttemptsTableMeta, {
|
|
35
40
|
reason: "read_side.delivery_attempt_log",
|
|
36
41
|
});
|
|
37
42
|
|
|
@@ -40,7 +45,11 @@ export function createDeliveryFeature(): FeatureDefinition {
|
|
|
40
45
|
// attempt is a fresh stream, no CRUD lifecycle. Framework's
|
|
41
46
|
// boot-validator accepts the projection below because at least one
|
|
42
47
|
// apply-key is a registered domain-event (DELIVERY_ATTEMPT_EVENT).
|
|
43
|
-
|
|
48
|
+
// recipientAddress is the real PII (email address); recipientId stays
|
|
49
|
+
// plaintext — pseudonymous fk, same line as config.userId (#821).
|
|
50
|
+
r.defineEvent("attempt", deliveryAttemptSchema, {
|
|
51
|
+
piiFields: { recipientAddress: { subjectField: "recipientId" } },
|
|
52
|
+
});
|
|
44
53
|
|
|
45
54
|
// Inline projection that materialises every delivery attempt into
|
|
46
55
|
// deliveryAttemptsTable. Runs in the SAME transaction as the low-level
|
|
@@ -113,6 +122,21 @@ export function createDeliveryFeature(): FeatureDefinition {
|
|
|
113
122
|
preferences: r.queryHandler(preferencesQuery),
|
|
114
123
|
};
|
|
115
124
|
|
|
125
|
+
r.screen({
|
|
126
|
+
id: DELIVERY_LOG_SCREEN_ID,
|
|
127
|
+
type: "custom",
|
|
128
|
+
renderer: { react: { __component: "DeliveryLogScreen" } },
|
|
129
|
+
access: { roles: access.admin },
|
|
130
|
+
});
|
|
131
|
+
r.nav({
|
|
132
|
+
id: "delivery-log",
|
|
133
|
+
label: "delivery:nav.deliveryLog",
|
|
134
|
+
screen: "delivery:screen:delivery-log",
|
|
135
|
+
order: 40,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
r.translations({ keys: DELIVERY_I18N });
|
|
139
|
+
|
|
116
140
|
return { handlers, queries };
|
|
117
141
|
});
|
|
118
142
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { decryptStoredPii } from "../../shared";
|
|
4
5
|
import { deliveryAttemptsTable } from "../tables";
|
|
5
6
|
|
|
6
7
|
export const logQuery = defineQueryHandler({
|
|
@@ -8,12 +9,32 @@ export const logQuery = defineQueryHandler({
|
|
|
8
9
|
schema: z.object({
|
|
9
10
|
limit: z.number().min(1).max(100).default(50),
|
|
10
11
|
}),
|
|
11
|
-
access: { roles:
|
|
12
|
+
access: { roles: access.admin },
|
|
12
13
|
handler: async (query, ctx) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
// delivery runs in system-scope (feature.ts r.systemScope()), so ctx.db is
|
|
15
|
+
// NOT auto-tenant-filtered — filter explicitly or a tenant admin reads every
|
|
16
|
+
// tenant's attempts (with decrypted recipient PII below).
|
|
17
|
+
const rows = await selectMany(
|
|
18
|
+
ctx.db,
|
|
19
|
+
deliveryAttemptsTable,
|
|
20
|
+
{ tenantId: query.user.tenantId },
|
|
21
|
+
{
|
|
22
|
+
orderBy: { col: "createdAt", direction: "desc" },
|
|
23
|
+
limit: query.payload.limit,
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
// recipientAddress is stored encrypted under the recipient's DEK (#799)
|
|
27
|
+
// — decrypt for the admin log view; forgotten subjects show [[erased]].
|
|
28
|
+
return {
|
|
29
|
+
rows: await Promise.all(
|
|
30
|
+
rows.map(async (row) => ({
|
|
31
|
+
...row,
|
|
32
|
+
recipientAddress:
|
|
33
|
+
typeof row["recipientAddress"] === "string"
|
|
34
|
+
? await decryptStoredPii(row["recipientAddress"], "recipientAddress", "delivery-log")
|
|
35
|
+
: row["recipientAddress"],
|
|
36
|
+
})),
|
|
37
|
+
),
|
|
38
|
+
};
|
|
18
39
|
},
|
|
19
40
|
});
|