@cosmicdrift/kumiko-bundled-features 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +41 -7
- package/src/__tests__/access-invalidation-consumer.integration.test.ts +279 -0
- package/src/__tests__/access-invalidation-stream.integration.test.ts +217 -0
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +200 -0
- package/src/admin-shell/__tests__/admin-shell.boot.test.ts +147 -0
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +43 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +35 -0
- package/src/admin-shell/constants.ts +11 -0
- package/src/admin-shell/feature.ts +135 -0
- package/src/admin-shell/i18n.ts +36 -0
- package/src/admin-shell/index.ts +17 -0
- package/src/admin-shell/overview-allowlist.ts +37 -0
- package/src/admin-shell/web/client-plugin.tsx +26 -0
- package/src/admin-shell/web/i18n.ts +16 -0
- package/src/admin-shell/web/index.ts +2 -0
- package/src/admin-shell/web/overview-layout.tsx +69 -0
- package/src/admin-shell/web/overview-query.ts +15 -0
- package/src/admin-shell/web/platform-overview-screen.tsx +90 -0
- package/src/admin-shell/web/tenant-overview-screen.tsx +90 -0
- package/src/agent-tools/__tests__/tool-catalog.test.ts +179 -0
- package/src/agent-tools/__tests__/tool-dispatch.integration.test.ts +100 -0
- package/src/agent-tools/__tests__/tool-dispatch.test.ts +126 -0
- package/src/agent-tools/index.ts +9 -0
- package/src/agent-tools/tool-catalog.ts +130 -0
- package/src/agent-tools/tool-dispatch.ts +82 -0
- package/src/agent-tools/types.ts +38 -0
- package/src/audit/__tests__/audit-screens.boot.test.ts +51 -0
- package/src/audit/__tests__/audit-security.integration.test.ts +201 -0
- package/src/audit/__tests__/audit.integration.test.ts +50 -25
- package/src/audit/constants.ts +9 -2
- package/src/audit/feature.ts +33 -1
- package/src/audit/handlers/details.query.ts +47 -0
- package/src/audit/handlers/list.query.ts +3 -2
- package/src/audit/i18n.ts +31 -0
- package/src/audit/web/audit-log-detail-screen.tsx +131 -0
- package/src/audit/web/audit-log-screen.tsx +250 -0
- package/src/audit/web/client-plugin.tsx +22 -0
- package/src/audit/web/i18n.ts +12 -0
- package/src/audit/web/index.ts +5 -0
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +4 -4
- package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +4 -4
- package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +10 -11
- package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
- package/src/auth-email-password/__tests__/auth.integration.test.ts +33 -6
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +9 -6
- package/src/auth-email-password/__tests__/feature-options.test.ts +33 -0
- package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +283 -0
- package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +35 -1
- package/src/auth-email-password/__tests__/login-mfa-setup-required.integration.test.ts +129 -0
- package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +19 -6
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.test.ts +1 -1
- package/src/auth-email-password/__tests__/self-registration-toggle.integration.test.ts +264 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +20 -4
- package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +91 -0
- package/src/auth-email-password/constants.ts +25 -0
- package/src/auth-email-password/email-templates.ts +32 -0
- package/src/auth-email-password/errors.ts +9 -0
- package/src/auth-email-password/feature.ts +59 -11
- package/src/auth-email-password/handlers/change-password.write.ts +14 -13
- package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
- package/src/auth-email-password/handlers/confirm-token-flow.ts +34 -105
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +12 -5
- package/src/auth-email-password/handlers/invite-accept.write.ts +9 -2
- package/src/auth-email-password/handlers/invite-create.write.ts +2 -2
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +12 -4
- package/src/auth-email-password/handlers/login.write.ts +86 -16
- package/src/auth-email-password/handlers/logout.write.ts +4 -2
- package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
- package/src/auth-email-password/handlers/reset-password.write.ts +3 -2
- package/src/auth-email-password/handlers/self-registration-status.query.ts +23 -0
- package/src/auth-email-password/handlers/signup-confirm.write.ts +19 -11
- package/src/auth-email-password/handlers/signup-request.write.ts +22 -0
- package/src/auth-email-password/handlers/token-request-handler.ts +4 -2
- package/src/auth-email-password/i18n.ts +52 -0
- package/src/auth-email-password/index.ts +13 -2
- package/src/auth-email-password/lockout-store.ts +10 -5
- package/src/auth-email-password/password-policy.ts +2108 -0
- package/src/auth-email-password/seeding.ts +55 -27
- package/src/auth-email-password/self-registration-toggle.ts +25 -0
- package/src/auth-email-password/signed-token.ts +1 -0
- package/src/auth-email-password/unlock-token.ts +28 -0
- package/src/auth-email-password/web/__tests__/auth-client.test.ts +542 -0
- package/src/auth-email-password/web/__tests__/auth-form-logic.test.ts +44 -0
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +194 -6
- package/src/auth-email-password/web/__tests__/client-plugin.test.tsx +34 -0
- package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +64 -0
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +129 -2
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +149 -5
- package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +137 -0
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +5 -1
- package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +134 -0
- package/src/auth-email-password/web/__tests__/signup-screen.test.tsx +93 -0
- package/src/auth-email-password/web/__tests__/test-utils.tsx +12 -2
- package/src/auth-email-password/web/__tests__/use-url-token.test.tsx +29 -0
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +5 -1
- package/src/auth-email-password/web/auth-client.ts +96 -18
- package/src/auth-email-password/web/auth-form-logic.ts +30 -0
- package/src/auth-email-password/web/auth-form-primitives.tsx +22 -20
- package/src/auth-email-password/web/auth-gate.tsx +140 -19
- package/src/auth-email-password/web/client-plugin.ts +24 -2
- package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
- package/src/auth-email-password/web/forgot-password-screen.tsx +6 -6
- package/src/auth-email-password/web/index.ts +16 -4
- package/src/auth-email-password/web/invite-accept-screen.tsx +5 -5
- package/src/auth-email-password/web/login-screen.tsx +78 -22
- package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
- package/src/auth-email-password/web/reset-password-screen.tsx +15 -18
- package/src/auth-email-password/web/session.tsx +11 -6
- package/src/auth-email-password/web/signup-complete-screen.tsx +13 -15
- package/src/auth-email-password/web/signup-screen.tsx +8 -10
- package/src/auth-email-password/web/use-shell-user.ts +5 -2
- package/src/auth-email-password/web/user-menu.tsx +3 -1
- package/src/auth-email-password/web/verify-email-screen.tsx +10 -14
- package/src/auth-foundation/__tests__/feature.test.ts +46 -0
- package/src/auth-foundation/__tests__/session-store.test.ts +77 -0
- package/src/auth-foundation/__tests__/tenant-providers.test.ts +160 -0
- package/src/auth-foundation/__tests__/token-verifier.test.ts +150 -0
- package/src/auth-foundation/boot-checks.ts +150 -0
- package/src/auth-foundation/feature.ts +157 -0
- package/src/auth-foundation/index.ts +37 -0
- package/src/auth-foundation/resolve-anonymous-access.ts +62 -0
- package/src/auth-foundation/types.ts +170 -0
- package/src/auth-mfa/__tests__/base32.test.ts +28 -0
- package/src/auth-mfa/__tests__/disable-and-regenerate.integration.test.ts +265 -0
- package/src/auth-mfa/__tests__/enable-confirm-preauth.integration.test.ts +321 -0
- package/src/auth-mfa/__tests__/enable-flow.integration.test.ts +322 -0
- package/src/auth-mfa/__tests__/enable-start-preauth.integration.test.ts +237 -0
- package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +39 -0
- package/src/auth-mfa/__tests__/mfa-preauth-setup-token.test.ts +113 -0
- package/src/auth-mfa/__tests__/no-redis.integration.test.ts +209 -0
- package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +139 -0
- package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +247 -0
- package/src/auth-mfa/__tests__/session-auto-revoke.integration.test.ts +169 -0
- package/src/auth-mfa/__tests__/totp.test.ts +131 -0
- package/src/auth-mfa/__tests__/verify.integration.test.ts +319 -0
- package/src/auth-mfa/base32.ts +51 -0
- package/src/auth-mfa/config.ts +26 -0
- package/src/auth-mfa/constants.ts +45 -0
- package/src/auth-mfa/db/queries.ts +45 -0
- package/src/auth-mfa/errors.ts +66 -0
- package/src/auth-mfa/feature.ts +170 -0
- package/src/auth-mfa/handlers/disable.write.ts +67 -0
- package/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +200 -0
- package/src/auth-mfa/handlers/enable-confirm.write.ts +106 -0
- package/src/auth-mfa/handlers/enable-start-preauth.write.ts +87 -0
- package/src/auth-mfa/handlers/enable-start.write.ts +66 -0
- package/src/auth-mfa/handlers/reencrypt.job.ts +234 -0
- package/src/auth-mfa/handlers/regenerate-recovery.write.ts +77 -0
- package/src/auth-mfa/handlers/status.query.ts +23 -0
- package/src/auth-mfa/handlers/verify.write.ts +179 -0
- package/src/auth-mfa/i18n.ts +12 -0
- package/src/auth-mfa/index.ts +19 -0
- package/src/auth-mfa/mfa-challenge-token.ts +90 -0
- package/src/auth-mfa/mfa-preauth-setup-token.ts +86 -0
- package/src/auth-mfa/mfa-setup-token.ts +96 -0
- package/src/auth-mfa/mfa-status-checker.ts +84 -0
- package/src/auth-mfa/mfa-verify-attempts.ts +90 -0
- package/src/auth-mfa/otpauth-uri.ts +21 -0
- package/src/auth-mfa/recovery-codes.ts +54 -0
- package/src/auth-mfa/schema/user-mfa.ts +60 -0
- package/src/auth-mfa/testing.ts +5 -0
- package/src/auth-mfa/totp.ts +67 -0
- package/src/auth-mfa/verify-factor.ts +58 -0
- package/src/auth-mfa/web/__tests__/mfa-client.test.ts +265 -0
- package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +182 -0
- package/src/auth-mfa/web/__tests__/mfa-setup-preauth-screen.test.tsx +379 -0
- package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +104 -0
- package/src/auth-mfa/web/client-plugin.ts +37 -0
- package/src/auth-mfa/web/i18n.ts +136 -0
- package/src/auth-mfa/web/index.ts +27 -0
- package/src/auth-mfa/web/mfa-client.ts +184 -0
- package/src/auth-mfa/web/mfa-disable-dialog.tsx +69 -0
- package/src/auth-mfa/web/mfa-enable-screen.tsx +228 -0
- package/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +53 -0
- package/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +68 -0
- package/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +267 -0
- package/src/auth-mfa/web/mfa-verify-screen.tsx +117 -0
- package/src/auth-mfa/web/qrcode-browser.d.ts +12 -0
- package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +134 -0
- package/src/auth-mfa-user-data/hooks.ts +53 -0
- package/src/auth-mfa-user-data/index.ts +21 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +206 -2
- package/src/billing-foundation/__tests__/subscription-provider-contract.ts +93 -0
- package/src/billing-foundation/entities.ts +28 -4
- package/src/billing-foundation/events.ts +6 -2
- package/src/billing-foundation/feature.ts +9 -1
- package/src/billing-foundation/get-subscription-for-tenant.ts +16 -5
- package/src/billing-foundation/handlers/create-portal-session.write.ts +17 -1
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +15 -1
- package/src/billing-foundation/handlers/process-event.write.ts +39 -3
- package/src/billing-foundation/tenant-destroy-hook.ts +46 -0
- package/src/cap-counter/__tests__/cap-list.boot.test.ts +27 -0
- package/src/cap-counter/__tests__/cap-list.security.integration.test.ts +53 -0
- package/src/cap-counter/__tests__/entity-ddl.test.ts +15 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +8 -5
- package/src/cap-counter/constants.ts +2 -0
- package/src/cap-counter/enforce-cap.ts +15 -18
- package/src/cap-counter/entity.ts +3 -3
- package/src/cap-counter/feature.ts +28 -1
- package/src/cap-counter/i18n.ts +12 -0
- package/src/cap-counter/with-cap-enforcement.ts +2 -2
- package/src/channel-email/__tests__/pii-guard.test.ts +56 -0
- package/src/channel-email/email-channel.ts +2 -1
- package/src/channel-email/index.ts +1 -0
- package/src/channel-email/pii-guard.ts +39 -0
- package/src/compliance-profiles/__tests__/profile-picker.boot.test.ts +28 -0
- package/src/compliance-profiles/__tests__/profile-picker.security.integration.test.ts +91 -0
- package/src/compliance-profiles/constants.ts +15 -0
- package/src/compliance-profiles/feature.ts +22 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +2 -2
- package/src/compliance-profiles/i18n.ts +6 -0
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/web/client-plugin.tsx +22 -0
- package/src/compliance-profiles/web/compliance-profile-screen.tsx +152 -0
- package/src/compliance-profiles/web/i18n.ts +25 -0
- package/src/compliance-profiles/web/index.ts +6 -0
- package/src/config/__tests__/app-overrides.test.ts +11 -0
- package/src/config/__tests__/backing-secrets.integration.test.ts +22 -0
- package/src/config/__tests__/cascade.integration.test.ts +35 -0
- package/src/config/__tests__/config.integration.test.ts +71 -10
- package/src/config/__tests__/deserialize-value.test.ts +5 -8
- package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
- package/src/config/__tests__/pii-encrypted.integration.test.ts +179 -0
- package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +234 -0
- package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +243 -0
- package/src/config/__tests__/write-helpers.test.ts +23 -0
- package/src/config/db/queries/resolver.ts +3 -3
- package/src/config/feature.ts +15 -8
- package/src/config/handlers/cascade.query.ts +2 -1
- package/src/config/handlers/reencrypt.job.ts +209 -0
- package/src/config/handlers/set.write.ts +55 -3
- package/src/config/handlers/values.query.ts +2 -1
- package/src/config/i18n.ts +9 -0
- package/src/config/index.ts +5 -4
- package/src/config/resolver.ts +75 -61
- package/src/config/table.ts +1 -1
- package/src/config/write-helpers.ts +22 -0
- package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +148 -0
- package/src/crypto-shredding/constants.ts +5 -0
- package/src/crypto-shredding/feature.ts +18 -0
- package/src/crypto-shredding/handlers/forget-subject.write.ts +91 -0
- package/src/crypto-shredding/index.ts +11 -0
- package/src/custom-fields/__tests__/audit-integration.integration.test.ts +1 -1
- package/src/custom-fields/__tests__/custom-fields.integration.test.ts +92 -53
- package/src/custom-fields/__tests__/field-access.integration.test.ts +25 -0
- package/src/custom-fields/__tests__/parse-serialized-field.test.ts +11 -3
- package/src/custom-fields/__tests__/retention.integration.test.ts +40 -0
- package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +26 -79
- package/src/custom-fields/constants.ts +1 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +0 -20
- package/src/custom-fields/entity.ts +1 -1
- package/src/custom-fields/events.ts +4 -3
- package/src/custom-fields/feature.ts +8 -0
- package/src/custom-fields/handlers/delete-system-field.write.ts +2 -1
- package/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -1
- package/src/custom-fields/handlers/set-custom-field.write.ts +3 -44
- package/src/custom-fields/handlers/update-tenant-field.write.ts +0 -17
- package/src/custom-fields/lib/field-access.ts +8 -1
- package/src/custom-fields/lib/parse-serialized-field.ts +12 -4
- package/src/custom-fields/lib/value-schema.ts +1 -1
- package/src/custom-fields/run-retention.ts +9 -1
- package/src/custom-fields/schemas.ts +9 -9
- package/src/custom-fields/wire-for-entity.ts +110 -78
- package/src/custom-fields/wire-user-data-rights.ts +5 -54
- package/src/data-retention/__tests__/parse-override.test.ts +14 -13
- package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +145 -8
- package/src/data-retention/feature.ts +3 -3
- package/src/data-retention/run-retention-cleanup.ts +102 -19
- package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +171 -0
- package/src/delivery/__tests__/delivery-screens.boot.test.ts +27 -0
- package/src/delivery/__tests__/delivery-security.integration.test.ts +106 -0
- package/src/delivery/constants.ts +13 -51
- package/src/delivery/feature.ts +29 -5
- package/src/delivery/handlers/log.query.ts +28 -7
- package/src/delivery/i18n.ts +6 -0
- package/src/delivery/index.ts +6 -1
- package/src/delivery/public-names.ts +36 -0
- package/src/delivery/tables.ts +3 -3
- package/src/delivery/web/client-plugin.tsx +20 -0
- package/src/delivery/web/delivery-log-screen.tsx +103 -0
- package/src/delivery/web/i18n.ts +25 -0
- package/src/delivery/web/index.ts +3 -0
- package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +147 -0
- package/src/document-ingest-foundation/__tests__/feature.test.ts +105 -0
- package/src/document-ingest-foundation/__tests__/pages.test.ts +37 -0
- package/src/document-ingest-foundation/entity.ts +43 -0
- package/src/document-ingest-foundation/events.ts +25 -0
- package/src/document-ingest-foundation/feature.ts +126 -0
- package/src/document-ingest-foundation/index.ts +12 -0
- package/src/document-ingest-foundation/pages.ts +34 -0
- package/src/feature-toggles/__tests__/compose-tier-resolver.test.ts +304 -0
- package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +7 -6
- package/src/feature-toggles/__tests__/toggle-admin.boot.test.ts +31 -0
- package/src/feature-toggles/__tests__/toggle-admin.security.integration.test.ts +69 -0
- package/src/feature-toggles/compose-tier-resolver.ts +134 -0
- package/src/feature-toggles/constants.ts +14 -0
- package/src/feature-toggles/feature.ts +23 -3
- package/src/feature-toggles/global-feature-state-table.ts +20 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +1 -1
- package/src/feature-toggles/i18n.ts +6 -0
- package/src/feature-toggles/index.ts +8 -0
- package/src/feature-toggles/toggle-runtime.ts +14 -0
- package/src/feature-toggles/web/client-plugin.tsx +22 -0
- package/src/feature-toggles/web/i18n.ts +29 -0
- package/src/feature-toggles/web/index.ts +6 -0
- package/src/feature-toggles/web/toggle-admin-screen.tsx +136 -0
- package/src/file-foundation/__tests__/feature.test.ts +5 -5
- package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
- package/src/file-foundation/feature.ts +10 -11
- package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +27 -2
- package/src/files-provider-s3/s3-provider.ts +12 -1
- package/src/folders/__tests__/folders.integration.test.ts +3 -0
- package/src/folders/web/__tests__/folder-manager.test.tsx +43 -2
- package/src/folders/web/__tests__/tree.test.ts +8 -0
- package/src/folders/web/folder-manager.tsx +2 -3
- package/src/folders/web/i18n.ts +0 -2
- package/src/folders/web/tree.ts +13 -2
- package/src/folders-user-data/__tests__/hooks.integration.test.ts +1 -0
- package/src/folders-user-data/index.ts +4 -4
- package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
- package/src/inbound-mail-foundation/__tests__/connect-routes.integration.test.ts +307 -0
- package/src/inbound-mail-foundation/__tests__/feature.test.ts +169 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +644 -0
- package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +111 -0
- package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +237 -0
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +433 -0
- package/src/inbound-mail-foundation/aggregate-id.ts +40 -0
- package/src/inbound-mail-foundation/connect-routes.ts +230 -0
- package/src/inbound-mail-foundation/constants.ts +80 -0
- package/src/inbound-mail-foundation/db/queries/inbound-projections.ts +38 -0
- package/src/inbound-mail-foundation/entities.ts +172 -0
- package/src/inbound-mail-foundation/events.ts +147 -0
- package/src/inbound-mail-foundation/feature.ts +243 -0
- package/src/inbound-mail-foundation/handlers/account-state.ts +23 -0
- package/src/inbound-mail-foundation/handlers/connect-account.write.ts +112 -0
- package/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +69 -0
- package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +333 -0
- package/src/inbound-mail-foundation/handlers/list-accounts.query.ts +44 -0
- package/src/inbound-mail-foundation/handlers/list-messages.query.ts +58 -0
- package/src/inbound-mail-foundation/handlers/scope-visibility.ts +14 -0
- package/src/inbound-mail-foundation/handlers/update-account.write.ts +78 -0
- package/src/inbound-mail-foundation/index.ts +97 -0
- package/src/inbound-mail-foundation/oauth-state.ts +103 -0
- package/src/inbound-mail-foundation/projection.ts +152 -0
- package/src/inbound-mail-foundation/provider-factory.ts +53 -0
- package/src/inbound-mail-foundation/retention-sweep.ts +168 -0
- package/src/inbound-mail-foundation/tenant-destroy-hook.ts +93 -0
- package/src/inbound-mail-foundation/types.ts +250 -0
- package/src/inbound-mail-foundation/watch-supervisor.ts +482 -0
- package/src/inbound-provider-imap/__tests__/feature.test.ts +115 -0
- package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +302 -0
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +206 -0
- package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +358 -0
- package/src/inbound-provider-imap/credential-document.ts +51 -0
- package/src/inbound-provider-imap/feature.ts +283 -0
- package/src/inbound-provider-imap/imap-client.ts +158 -0
- package/src/inbound-provider-imap/index.ts +17 -0
- package/src/inbound-provider-inmemory/__tests__/feature.test.ts +52 -0
- package/src/inbound-provider-inmemory/feature.ts +154 -0
- package/src/inbound-provider-inmemory/index.ts +12 -0
- package/src/jobs/__tests__/entity-ddl.test.ts +15 -0
- package/src/jobs/__tests__/job-instant.test.ts +15 -0
- package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
- package/src/jobs/__tests__/jobs-events.integration.test.ts +37 -1
- package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +120 -0
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +36 -0
- package/src/jobs/__tests__/jobs-security.integration.test.ts +108 -0
- package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +169 -0
- package/src/jobs/constants.ts +8 -1
- package/src/jobs/feature.ts +52 -8
- package/src/jobs/handlers/detail.query.ts +6 -0
- package/src/jobs/handlers/list.query.ts +8 -1
- package/src/jobs/handlers/projection-rebuild.job.ts +14 -4
- package/src/jobs/handlers/reindex-entity.job.ts +49 -0
- package/src/jobs/i18n.ts +37 -0
- package/src/jobs/job-instant.ts +9 -0
- package/src/jobs/job-run-table.ts +4 -4
- package/src/jobs/web/client-plugin.tsx +22 -0
- package/src/jobs/web/i18n.ts +12 -0
- package/src/jobs/web/index.ts +4 -0
- package/src/jobs/web/job-run-detail-screen.tsx +180 -0
- package/src/jobs/web/job-runs-screen.tsx +146 -0
- package/src/ledger/__tests__/ledger.integration.test.ts +8 -3
- package/src/ledger/handlers/confirm-schedule-period.write.ts +17 -4
- package/src/ledger/recurring.ts +17 -8
- package/src/ledger/web/index.ts +3 -2
- package/src/legal-pages/__tests__/legal-pages.integration.test.ts +55 -0
- package/src/legal-pages/feature.ts +24 -31
- package/src/legal-pages/index.ts +1 -0
- package/src/mail-foundation/__tests__/feature.test.ts +5 -6
- package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
- package/src/mail-foundation/__tests__/mail-transport-contract.ts +58 -0
- package/src/mail-foundation/feature.ts +26 -24
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +23 -0
- package/src/mail-transport-smtp/__tests__/feature.test.ts +65 -0
- package/src/managed-pages/__tests__/branding-coerce.test.ts +2 -6
- package/src/managed-pages/__tests__/i18n-surface.boot.test.ts +30 -0
- package/src/managed-pages/__tests__/managed-pages.integration.test.ts +137 -0
- package/src/managed-pages/feature.ts +45 -47
- package/src/managed-pages/handlers/branding.query.ts +5 -1
- package/src/managed-pages/handlers/by-tenant-published.query.ts +52 -0
- package/src/managed-pages/i18n.ts +66 -0
- package/src/managed-pages/screens/page-screens.ts +5 -1
- package/src/managed-pages/web/client-plugin.tsx +22 -0
- package/src/managed-pages/web/i18n.ts +24 -0
- package/src/managed-pages/web/index.ts +3 -0
- package/src/page-render/__tests__/layout.test.ts +63 -0
- package/src/page-render/__tests__/markdown.test.ts +29 -0
- package/src/page-render/branding.ts +4 -3
- package/src/page-render/index.ts +1 -1
- package/src/page-render/layout.ts +18 -3
- package/src/page-render/markdown.ts +1 -9
- package/src/page-render/web.ts +13 -0
- package/src/personal-access-tokens/__tests__/feature.test.ts +42 -0
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +239 -0
- package/src/personal-access-tokens/__tests__/scopes.test.ts +42 -0
- package/src/personal-access-tokens/constants.ts +33 -0
- package/src/personal-access-tokens/feature.ts +105 -0
- package/src/personal-access-tokens/handlers/available-scopes.query.ts +21 -0
- package/src/personal-access-tokens/handlers/create.write.ts +47 -0
- package/src/personal-access-tokens/handlers/list.query.ts +50 -0
- package/src/personal-access-tokens/handlers/revoke.write.ts +37 -0
- package/src/personal-access-tokens/hash.ts +17 -0
- package/src/personal-access-tokens/i18n.ts +5 -0
- package/src/personal-access-tokens/index.ts +31 -0
- package/src/personal-access-tokens/resolver.ts +58 -0
- package/src/personal-access-tokens/roles.ts +29 -0
- package/src/personal-access-tokens/schema/api-token.ts +63 -0
- package/src/personal-access-tokens/scopes.ts +43 -0
- package/src/personal-access-tokens/web/client-plugin.tsx +29 -0
- package/src/personal-access-tokens/web/i18n.ts +69 -0
- package/src/personal-access-tokens/web/index.ts +7 -0
- package/src/personal-access-tokens/web/pat-tokens-screen.tsx +265 -0
- package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
- package/src/renderer-simple/__tests__/resolve-variables.test.ts +37 -0
- package/src/renderer-simple/resolve-variables.ts +3 -1
- package/src/secrets/__tests__/rotate.integration.test.ts +115 -18
- package/src/secrets/handlers/rotate.job.ts +77 -98
- package/src/secrets/secrets-context.ts +7 -38
- package/src/secrets/table.ts +7 -8
- package/src/seo/__tests__/llms-txt.test.ts +38 -0
- package/src/seo/__tests__/robots-txt.test.ts +18 -0
- package/src/seo/__tests__/schema-builders.test.ts +57 -0
- package/src/seo/__tests__/seo.integration.test.ts +350 -0
- package/src/seo/__tests__/sitemap.test.ts +38 -0
- package/src/seo/constants.ts +60 -0
- package/src/seo/feature.ts +316 -0
- package/src/seo/handlers/seo-config.query.ts +35 -0
- package/src/seo/index.ts +19 -0
- package/src/seo/llms-txt.ts +33 -0
- package/src/seo/robots-txt.ts +13 -0
- package/src/seo/schema-builders.ts +57 -0
- package/src/seo/sitemap.ts +33 -0
- package/src/sessions/__tests__/auto-revoke-binding.test.ts +71 -0
- package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
- package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +11 -7
- package/src/sessions/__tests__/rebuild-survival.integration.test.ts +18 -7
- package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
- package/src/sessions/__tests__/sessions-screens.boot.test.ts +55 -0
- package/src/sessions/__tests__/sessions.integration.test.ts +272 -20
- package/src/sessions/__tests__/test-helpers.ts +16 -3
- package/src/sessions/constants.ts +5 -0
- package/src/sessions/db/queries/cleanup.ts +2 -2
- package/src/sessions/feature.ts +175 -26
- package/src/sessions/handlers/detail.query.ts +36 -0
- package/src/sessions/handlers/list.query.ts +14 -9
- package/src/sessions/handlers/mine.query.ts +13 -8
- package/src/sessions/handlers/revoke-all-for-user.write.ts +51 -12
- package/src/sessions/handlers/revoke-all-others.write.ts +19 -1
- package/src/sessions/handlers/revoke.write.ts +13 -0
- package/src/sessions/i18n.ts +23 -0
- package/src/sessions/index.ts +9 -2
- package/src/sessions/schema/user-session.ts +8 -2
- package/src/sessions/session-callbacks.ts +99 -14
- package/src/sessions/session-checker-fail-open.test.ts +77 -0
- package/src/sessions/session-revoked-event.ts +29 -0
- package/src/sessions/testing.ts +14 -1
- package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
- package/src/shared/chunked-entity-migration.ts +89 -0
- package/src/shared/decrypt-stored-pii.ts +26 -0
- package/src/shared/encrypt-for-direct-write.ts +23 -0
- package/src/shared/entities-of.ts +17 -0
- package/src/shared/grace-period.test.ts +26 -0
- package/src/shared/grace-period.ts +20 -0
- package/src/{auth-email-password/__tests__ → shared}/identity-v3-hash.test.ts +2 -2
- package/src/shared/index.ts +16 -0
- package/src/shared/map-with-concurrency.test.ts +104 -0
- package/src/shared/map-with-concurrency.ts +38 -0
- package/src/shared/password-hashing.test.ts +35 -0
- package/src/{auth-email-password → shared}/password-hashing.ts +11 -0
- package/src/shared/system-query.ts +5 -0
- package/src/{auth-email-password → shared}/token-burn-store.ts +1 -1
- package/src/subscription-mollie/__tests__/feature.test.ts +97 -0
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.test.ts +12 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +21 -0
- package/src/subscription-mollie/verify-webhook.ts +1 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +127 -1
- package/src/subscription-stripe/__tests__/runtime.test.ts +14 -24
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +15 -5
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +20 -0
- package/src/subscription-stripe/feature.ts +2 -0
- package/src/subscription-stripe/verify-webhook.ts +3 -2
- package/src/tags/__tests__/schemas.test.ts +34 -0
- package/src/tags/__tests__/tags.integration.test.ts +12 -1
- package/src/tags/entity.ts +6 -1
- package/src/tags/feature.ts +2 -0
- package/src/tags/i18n.ts +5 -0
- package/src/tags/schemas.ts +9 -2
- package/src/tags/web/__tests__/tag-manager.test.tsx +333 -0
- package/src/tags/web/__tests__/tag-section.test.tsx +47 -1
- package/src/tags/web/tag-manager.tsx +1 -1
- package/src/tags/web/tag-section.tsx +6 -3
- package/src/template-resolver/testing.ts +2 -1
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
- package/src/tenant/__tests__/members-screens.boot.test.ts +51 -0
- package/src/tenant/__tests__/multi-tenant.integration.test.ts +1 -1
- package/src/tenant/__tests__/tenant-security.integration.test.ts +310 -0
- package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
- package/src/tenant/constants.ts +5 -0
- package/src/tenant/feature.ts +19 -0
- package/src/tenant/handlers/active-tenant-ids.query.ts +8 -2
- package/src/tenant/handlers/cancel-invitation.write.ts +2 -2
- package/src/tenant/handlers/invitations.query.ts +21 -4
- package/src/tenant/handlers/members.query.ts +41 -6
- package/src/tenant/i18n.ts +28 -0
- package/src/tenant/index.ts +7 -2
- package/src/tenant/invitation-table.ts +3 -2
- package/src/tenant/schema/tenant.ts +27 -2
- package/src/tenant/screens.ts +3 -2
- package/src/tenant/seeding.ts +136 -3
- package/src/tenant/web/client-plugin.tsx +20 -0
- package/src/tenant/web/i18n.ts +43 -0
- package/src/tenant/web/index.ts +4 -0
- package/src/tenant/web/members-screen.tsx +270 -0
- package/src/tenant-lifecycle/__tests__/stages.test.ts +22 -0
- package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +471 -0
- package/src/tenant-lifecycle/boot-checks.ts +45 -0
- package/src/tenant-lifecycle/constants.ts +56 -0
- package/src/tenant-lifecycle/events.ts +46 -0
- package/src/tenant-lifecycle/feature.ts +112 -0
- package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +79 -0
- package/src/tenant-lifecycle/handlers/request-destruction.write.ts +83 -0
- package/src/tenant-lifecycle/index.ts +7 -0
- package/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +16 -0
- package/src/tenant-lifecycle/lifecycle-gate.ts +65 -0
- package/src/tenant-lifecycle/run-tenant-destroy.ts +368 -0
- package/src/tenant-lifecycle/stages.ts +206 -0
- package/src/tenant-settings/__tests__/tenant-settings.integration.test.ts +115 -0
- package/src/tenant-settings/config.ts +44 -0
- package/src/tenant-settings/constants.ts +16 -0
- package/src/tenant-settings/feature.ts +49 -0
- package/src/tenant-settings/index.ts +8 -0
- package/src/tenant-settings/tenant-defaults.ts +67 -0
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +33 -6
- package/src/text-content/web/__tests__/group-blocks.test.ts +28 -3
- package/src/text-content/web/client-plugin.tsx +126 -104
- package/src/tier-engine/__tests__/auto-default-tier.integration.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.test.ts +1 -1
- package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
- package/src/tier-engine/__tests__/trial-config-validation.test.ts +30 -0
- package/src/tier-engine/feature.ts +13 -6
- package/src/tier-engine/i18n.ts +8 -0
- package/src/tier-engine/web/tier-admin-screen.tsx +26 -19
- package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +186 -0
- package/src/user/__tests__/user.integration.test.ts +26 -0
- package/src/user/constants.ts +1 -0
- package/src/user/db/queries/stream-tenant-backfill.ts +112 -0
- package/src/user/feature.ts +3 -0
- package/src/user/handlers/find-for-auth.query.ts +8 -6
- package/src/user/i18n.ts +20 -0
- package/src/user/index.ts +4 -0
- package/src/user/schema/user.ts +6 -2
- package/src/user/screens.ts +3 -15
- package/src/user/seeding.ts +8 -0
- package/src/user-data-rights/COMPLIANCE.md +21 -1
- package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +145 -0
- package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
- package/src/user-data-rights/__tests__/audit-log.integration.test.ts +67 -0
- package/src/user-data-rights/__tests__/boot-checks.test.ts +170 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +169 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +101 -33
- package/src/user-data-rights/__tests__/email-templates.test.ts +15 -0
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +154 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-retention.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +41 -62
- package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +110 -0
- package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +14 -0
- package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +19 -2
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +25 -14
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +4 -1
- package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +172 -19
- package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +257 -26
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +339 -5
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +77 -1
- package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
- package/src/user-data-rights/boot-checks.ts +70 -0
- package/src/user-data-rights/db/queries/__tests__/forget-cleanup.integration.test.ts +69 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +9 -2
- package/src/user-data-rights/email-templates.ts +6 -6
- package/src/user-data-rights/feature.ts +162 -43
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +8 -11
- package/src/user-data-rights/handlers/deletion-grace-period.ts +4 -1
- package/src/user-data-rights/handlers/download-by-job.query.ts +26 -18
- package/src/user-data-rights/handlers/download-by-token.query.ts +23 -15
- package/src/user-data-rights/handlers/lift-restriction.write.ts +44 -19
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +1 -0
- package/src/user-data-rights/handlers/request-deletion-by-email.write.ts +3 -1
- package/src/user-data-rights/handlers/restrict-account.write.ts +58 -7
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +2 -0
- package/src/user-data-rights/i18n.ts +35 -32
- package/src/user-data-rights/lib/is-admin-actor.ts +19 -0
- package/src/user-data-rights/lib/storage-provider-resolver.ts +6 -5
- package/src/user-data-rights/lib/update-user-lifecycle.ts +8 -24
- package/src/user-data-rights/run-export-jobs.ts +55 -19
- package/src/user-data-rights/run-forget-cleanup.ts +112 -7
- package/src/user-data-rights/run-user-export.ts +70 -4
- package/src/user-data-rights/schema/download-token.ts +5 -3
- package/src/user-data-rights/schema/export-job.ts +2 -0
- package/src/user-data-rights/screens.ts +2 -1
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -0
- package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +7 -1
- package/src/user-data-rights/web/client-plugin.tsx +6 -1
- package/src/user-data-rights/web/i18n.ts +30 -10
- package/src/user-data-rights/web/privacy-center-screen.tsx +5 -3
- package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +455 -0
- package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +33 -9
- package/src/user-data-rights-defaults/feature.ts +78 -1
- package/src/user-data-rights-defaults/hooks/api-token.userdata-hook.ts +37 -0
- package/src/user-data-rights-defaults/hooks/config-value.userdata-hook.ts +51 -0
- package/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +37 -0
- package/src/user-data-rights-defaults/hooks/feature-mounted.ts +10 -0
- package/src/user-data-rights-defaults/hooks/in-app-message.userdata-hook.ts +38 -0
- package/src/user-data-rights-defaults/hooks/job-run.userdata-hook.ts +35 -0
- package/src/user-data-rights-defaults/hooks/notification-preference.userdata-hook.ts +54 -0
- package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +112 -0
- package/src/user-data-rights-defaults/hooks/user-session.userdata-hook.ts +42 -0
- package/src/user-data-rights-defaults/index.ts +26 -0
- package/src/user-profile/__tests__/change-email.integration.test.ts +7 -1
- package/src/user-profile/__tests__/profile-screen.test.tsx +5 -0
- package/src/user-profile/feature.ts +4 -1
- package/src/user-profile/handlers/change-email.write.ts +15 -10
- package/src/user-profile/web/profile-screen.tsx +15 -3
- package/src/auth-email-password/stream-tenant.ts +0 -31
- package/src/feature-toggles/db/queries/toggle-state.ts +0 -25
- package/src/legal-pages/security-headers.ts +0 -1
- package/src/sessions/schema/index.ts +0 -5
- package/src/tenant/schema/index.ts +0 -5
- package/src/user/schema/index.ts +0 -5
- /package/src/{auth-email-password → shared}/identity-v3-hash.ts +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// HMAC-signierter OAuth-`state`-Parameter für den Connect-Flow.
|
|
2
|
+
//
|
|
3
|
+
// Bindet {tenantId, ownerUserId?, providerKey, mailbox, nonce} an den
|
|
4
|
+
// Redirect: der anonyme Callback (außerhalb /api/*) vertraut NUR dem
|
|
5
|
+
// verifizierten state — verhindert CSRF und Fremd-Claiming (ein
|
|
6
|
+
// Angreifer kann den Callback nicht auf einen fremden Tenant/User
|
|
7
|
+
// umbiegen, ohne die Signatur zu brechen).
|
|
8
|
+
//
|
|
9
|
+
// Format: <payload-base64url>.<expiresAtMs>.<hmac-base64url>
|
|
10
|
+
// Muster: auth-email-password/signed-token.ts (HMAC-SHA256, purpose im
|
|
11
|
+
// HMAC-Input, timing-safe compare, Expiry im Klartext-Segment).
|
|
12
|
+
|
|
13
|
+
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
14
|
+
import { Temporal } from "temporal-polyfill";
|
|
15
|
+
|
|
16
|
+
const STATE_PURPOSE = "inbound-mail-oauth-connect";
|
|
17
|
+
|
|
18
|
+
export type OAuthStatePayload = {
|
|
19
|
+
readonly tenantId: string;
|
|
20
|
+
/** null = shared-Postfach, sonst persönliches Postfach dieses Users. */
|
|
21
|
+
readonly ownerUserId: string | null;
|
|
22
|
+
readonly providerKey: string;
|
|
23
|
+
/** Postfach-Adresse aus dem Connect-Request — landet nach dem
|
|
24
|
+
* Token-Exchange im MailAccount. */
|
|
25
|
+
readonly mailbox: string;
|
|
26
|
+
/** Einmal-Nonce — macht jeden state einzigartig (kein Replay über
|
|
27
|
+
* zwei parallele Connect-Flows hinweg). */
|
|
28
|
+
readonly nonce: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type VerifyOAuthStateResult =
|
|
32
|
+
| { readonly ok: true; readonly payload: OAuthStatePayload }
|
|
33
|
+
| { readonly ok: false; readonly reason: "malformed" | "bad_signature" | "expired" };
|
|
34
|
+
|
|
35
|
+
function sign(input: string, secret: string): string {
|
|
36
|
+
return createHmac("sha256", secret).update(input).digest("base64url");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hmacInput(payloadB64: string, expiresAtMs: number): string {
|
|
40
|
+
return `${STATE_PURPOSE}:${payloadB64}.${expiresAtMs}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function signOAuthState(
|
|
44
|
+
payload: Omit<OAuthStatePayload, "nonce">,
|
|
45
|
+
ttlMinutes: number,
|
|
46
|
+
secret: string,
|
|
47
|
+
now: Temporal.Instant = Temporal.Now.instant(),
|
|
48
|
+
): string {
|
|
49
|
+
const full: OAuthStatePayload = { ...payload, nonce: randomUUID() };
|
|
50
|
+
const payloadB64 = Buffer.from(JSON.stringify(full), "utf8").toString("base64url");
|
|
51
|
+
const expiresAtMs = now.add({ minutes: ttlMinutes }).epochMilliseconds;
|
|
52
|
+
const signature = sign(hmacInput(payloadB64, expiresAtMs), secret);
|
|
53
|
+
return `${payloadB64}.${expiresAtMs}.${signature}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function verifyOAuthState(
|
|
57
|
+
state: string,
|
|
58
|
+
secret: string,
|
|
59
|
+
now: Temporal.Instant = Temporal.Now.instant(),
|
|
60
|
+
): VerifyOAuthStateResult {
|
|
61
|
+
const parts = state.split(".");
|
|
62
|
+
if (parts.length !== 3) return { ok: false, reason: "malformed" };
|
|
63
|
+
const [payloadB64, expiresAtRaw, providedSig] = parts;
|
|
64
|
+
if (!payloadB64 || !expiresAtRaw || !providedSig) return { ok: false, reason: "malformed" };
|
|
65
|
+
|
|
66
|
+
const expiresAtMs = Number(expiresAtRaw);
|
|
67
|
+
if (!Number.isFinite(expiresAtMs) || String(expiresAtMs) !== expiresAtRaw) {
|
|
68
|
+
return { ok: false, reason: "malformed" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const expected = sign(hmacInput(payloadB64, expiresAtMs), secret);
|
|
72
|
+
const expectedBuf = Buffer.from(expected, "base64url");
|
|
73
|
+
const providedBuf = Buffer.from(providedSig, "base64url");
|
|
74
|
+
// Length-check VOR timingSafeEqual — die würde bei Längen-Mismatch
|
|
75
|
+
// werfen, und der Throw selbst leakt via Timing.
|
|
76
|
+
if (expectedBuf.length !== providedBuf.length) return { ok: false, reason: "bad_signature" };
|
|
77
|
+
if (!timingSafeEqual(expectedBuf, providedBuf)) return { ok: false, reason: "bad_signature" };
|
|
78
|
+
|
|
79
|
+
if (Temporal.Instant.compare(now, Temporal.Instant.fromEpochMilliseconds(expiresAtMs)) > 0) {
|
|
80
|
+
return { ok: false, reason: "expired" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let parsed: unknown;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
86
|
+
} catch {
|
|
87
|
+
return { ok: false, reason: "malformed" };
|
|
88
|
+
}
|
|
89
|
+
if (!isOAuthStatePayload(parsed)) return { ok: false, reason: "malformed" };
|
|
90
|
+
return { ok: true, payload: parsed };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isOAuthStatePayload(v: unknown): v is OAuthStatePayload {
|
|
94
|
+
if (typeof v !== "object" || v === null) return false;
|
|
95
|
+
const o = v as Record<string, unknown>;
|
|
96
|
+
return (
|
|
97
|
+
typeof o["tenantId"] === "string" &&
|
|
98
|
+
(o["ownerUserId"] === null || typeof o["ownerUserId"] === "string") &&
|
|
99
|
+
typeof o["providerKey"] === "string" &&
|
|
100
|
+
typeof o["mailbox"] === "string" &&
|
|
101
|
+
typeof o["nonce"] === "string"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Inline-projections für die drei event-sourced Streams:
|
|
2
|
+
// mail-account → read_mail_accounts (UPSERT, current state)
|
|
3
|
+
// inbound-message → read_inbound_messages (INSERT-ignore, append-only)
|
|
4
|
+
// mail-thread → read_mail_threads (UPSERT, Rollup)
|
|
5
|
+
//
|
|
6
|
+
// Apply läuft in derselben TX wie ctx.unsafeAppendEvent — read-your-
|
|
7
|
+
// own-write ohne dispatcher-tick (Muster billing-foundation).
|
|
8
|
+
//
|
|
9
|
+
// **Production-deployment caveat** (gleich wie billing-foundation):
|
|
10
|
+
// die Tables sind als raw drizzle-pgTable in r.projection registriert,
|
|
11
|
+
// NICHT als r.entity — Apps müssen sie in ihre drizzle/generate.ts
|
|
12
|
+
// aufnehmen (via *-ProjectionTable-Imports). setupTestStack pusht sie
|
|
13
|
+
// automatisch via r.projection.table.
|
|
14
|
+
//
|
|
15
|
+
// **PII:** die Payload-Felder (address/from/to/cc/subject/snippet)
|
|
16
|
+
// kommen bereits als Ciphertext an — encrypted VOR dem append im
|
|
17
|
+
// write-handler. Die applies kopieren nur durch; decrypted wird erst
|
|
18
|
+
// in den list-queries.
|
|
19
|
+
|
|
20
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
21
|
+
import { defineApply } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
|
+
import { insertIgnoreProjectionRow, upsertProjectionRow } from "./db/queries/inbound-projections";
|
|
23
|
+
import { inboundMessageEntity, mailAccountEntity, mailThreadEntity } from "./entities";
|
|
24
|
+
import type {
|
|
25
|
+
InboundMessageEventPayload,
|
|
26
|
+
MailAccountEventPayload,
|
|
27
|
+
MailThreadEventPayload,
|
|
28
|
+
} from "./events";
|
|
29
|
+
|
|
30
|
+
// Drizzle-table-instances aus den entity-shapes — geteilt zwischen
|
|
31
|
+
// projection-applies und list-queries (ein column-namespace).
|
|
32
|
+
export const mailAccountsProjectionTable = buildEntityTable("mail-account", mailAccountEntity);
|
|
33
|
+
export const inboundMessagesProjectionTable = buildEntityTable(
|
|
34
|
+
"inbound-message",
|
|
35
|
+
inboundMessageEntity,
|
|
36
|
+
);
|
|
37
|
+
export const mailThreadsProjectionTable = buildEntityTable("mail-thread", mailThreadEntity);
|
|
38
|
+
|
|
39
|
+
function tableNameOf(table: unknown): string {
|
|
40
|
+
return (table as { tableName: string }).tableName;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// mail-account — connected/updated/disconnected teilen den payload-shape,
|
|
45
|
+
// der event-type taggt was passiert ist. Alle drei UPSERT-full für
|
|
46
|
+
// defensive consistency (rebuild-aus-dem-Nichts, out-of-order).
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
const ACCOUNT_SET_CLAUSES = [
|
|
50
|
+
`"provider" = EXCLUDED."provider"`,
|
|
51
|
+
`"auth_method" = EXCLUDED."auth_method"`,
|
|
52
|
+
`"owner_user_id" = EXCLUDED."owner_user_id"`,
|
|
53
|
+
`"display_name" = EXCLUDED."display_name"`,
|
|
54
|
+
`"address" = EXCLUDED."address"`,
|
|
55
|
+
`"status" = EXCLUDED."status"`,
|
|
56
|
+
`"watch_state" = EXCLUDED."watch_state"`,
|
|
57
|
+
// connected_at bewusst NICHT im SET: der Erst-Connect-Zeitpunkt
|
|
58
|
+
// bleibt stehen, updated/disconnected überschreiben ihn nicht.
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const applyMailAccountUpsert = defineApply<MailAccountEventPayload>(async (event, tx) => {
|
|
62
|
+
const p = event.payload;
|
|
63
|
+
const insertCols = {
|
|
64
|
+
id: event.aggregateId,
|
|
65
|
+
tenant_id: event.tenantId,
|
|
66
|
+
provider: p.provider,
|
|
67
|
+
auth_method: p.authMethod,
|
|
68
|
+
owner_user_id: p.ownerUserId,
|
|
69
|
+
display_name: p.displayName,
|
|
70
|
+
address: p.address,
|
|
71
|
+
status: p.status,
|
|
72
|
+
watch_state: p.watchState,
|
|
73
|
+
// Insert-Pfad = Erst-Connect (bzw. Rebuild: createdAt des ersten
|
|
74
|
+
// events des Streams — fachlich derselbe Zeitpunkt).
|
|
75
|
+
connected_at: event.createdAt.toString(),
|
|
76
|
+
};
|
|
77
|
+
await upsertProjectionRow(
|
|
78
|
+
tx,
|
|
79
|
+
tableNameOf(mailAccountsProjectionTable),
|
|
80
|
+
insertCols,
|
|
81
|
+
ACCOUNT_SET_CLAUSES,
|
|
82
|
+
Object.values(insertCols),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** mail-account-connected → UPSERT full. */
|
|
87
|
+
export const applyMailAccountConnected = applyMailAccountUpsert;
|
|
88
|
+
/** mail-account-updated → UPSERT full (status/watchState-Übergänge). */
|
|
89
|
+
export const applyMailAccountUpdated = applyMailAccountUpsert;
|
|
90
|
+
/** mail-account-disconnected → UPSERT full (payload.status=disconnected).
|
|
91
|
+
* Row bleibt (Audit-Sicht in der Account-Liste), Stream bleibt. */
|
|
92
|
+
export const applyMailAccountDisconnected = applyMailAccountUpsert;
|
|
93
|
+
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// inbound-message — genau EIN received-event pro Stream (deterministic
|
|
96
|
+
// aggregateId). INSERT-ignore: Replays no-op'en auf der PK statt zu
|
|
97
|
+
// knallen; es gibt keinen update-Fall.
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
/** inbound-message-received → INSERT ... ON CONFLICT DO NOTHING. */
|
|
101
|
+
export const applyInboundMessageReceived = defineApply<InboundMessageEventPayload>(
|
|
102
|
+
async (event, tx) => {
|
|
103
|
+
const p = event.payload;
|
|
104
|
+
await insertIgnoreProjectionRow(tx, tableNameOf(inboundMessagesProjectionTable), {
|
|
105
|
+
id: event.aggregateId,
|
|
106
|
+
tenant_id: event.tenantId,
|
|
107
|
+
account_id: p.accountId,
|
|
108
|
+
owner_user_id: p.ownerUserId,
|
|
109
|
+
message_id_header: p.messageIdHeader,
|
|
110
|
+
thread_key: p.threadKey,
|
|
111
|
+
from: p.from,
|
|
112
|
+
to: p.to,
|
|
113
|
+
cc: p.cc,
|
|
114
|
+
subject: p.subject,
|
|
115
|
+
snippet: p.snippet,
|
|
116
|
+
received_at: p.receivedAtIso,
|
|
117
|
+
body_ref: p.bodyRef,
|
|
118
|
+
scope: p.scope,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// mail-thread — Rollup pro (tenantId, threadKey). Der write-handler
|
|
125
|
+
// berechnet messageCount/lastMessageAt und appendet den updated-event
|
|
126
|
+
// mit dem NEUEN Stand — die apply ist ein dummer UPSERT-full (kein
|
|
127
|
+
// increment in der apply: Replays wären sonst nicht idempotent).
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
/** mail-thread-updated → UPSERT full mit dem Payload-Snapshot. */
|
|
131
|
+
export const applyMailThreadUpdated = defineApply<MailThreadEventPayload>(async (event, tx) => {
|
|
132
|
+
const p = event.payload;
|
|
133
|
+
const insertCols = {
|
|
134
|
+
id: event.aggregateId,
|
|
135
|
+
tenant_id: event.tenantId,
|
|
136
|
+
thread_key: p.threadKey,
|
|
137
|
+
subject: p.subject,
|
|
138
|
+
last_message_at: p.lastMessageAtIso,
|
|
139
|
+
message_count: p.messageCount,
|
|
140
|
+
};
|
|
141
|
+
await upsertProjectionRow(
|
|
142
|
+
tx,
|
|
143
|
+
tableNameOf(mailThreadsProjectionTable),
|
|
144
|
+
insertCols,
|
|
145
|
+
[
|
|
146
|
+
`"subject" = EXCLUDED."subject"`,
|
|
147
|
+
`"last_message_at" = EXCLUDED."last_message_at"`,
|
|
148
|
+
`"message_count" = EXCLUDED."message_count"`,
|
|
149
|
+
],
|
|
150
|
+
Object.values(insertCols),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Provider-Factory — Mirror von mail-foundation's createTransportForTenant
|
|
2
|
+
// inkl. der drei Fehlerpfade (kein registry / nicht registriert / falsche
|
|
3
|
+
// Plugin-Shape). Abweichung zum Outbound-Muster (Plan §1.3): KEIN
|
|
4
|
+
// per-Tenant-Config-Selector — der Provider steht pro MailAccount
|
|
5
|
+
// (`account.provider`), ein Tenant kann mehrere Provider parallel nutzen.
|
|
6
|
+
|
|
7
|
+
import { INBOUND_MAIL_FOUNDATION_FEATURE, INBOUND_MAIL_PROVIDER_EXTENSION } from "./constants";
|
|
8
|
+
import {
|
|
9
|
+
type InboundMailContext,
|
|
10
|
+
type InboundMailProviderPlugin,
|
|
11
|
+
isInboundMailProviderPlugin,
|
|
12
|
+
type MailAccountRecord,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
/** Lookup per Provider-Key ("imap", "m365-graph", ...) — genutzt von den
|
|
16
|
+
* OAuth-Connect-Routen, wo noch kein Account existiert. */
|
|
17
|
+
export function resolveInboundProviderForKey(
|
|
18
|
+
ctx: InboundMailContext,
|
|
19
|
+
providerKey: string,
|
|
20
|
+
handlerName = "inbound-mail-foundation:provider-factory",
|
|
21
|
+
): InboundMailProviderPlugin {
|
|
22
|
+
if (!ctx.registry) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`${handlerName}: ctx.registry is missing — required to look up registered inbound-mail provider plugins`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const usages = ctx.registry.getExtensionUsages(INBOUND_MAIL_PROVIDER_EXTENSION);
|
|
28
|
+
const usage = usages.find((u) => u.entityName === providerKey);
|
|
29
|
+
if (!usage) {
|
|
30
|
+
const known = usages.map((u) => u.entityName).join(", ") || "<none>";
|
|
31
|
+
throw new Error(
|
|
32
|
+
`${INBOUND_MAIL_FOUNDATION_FEATURE}: provider "${providerKey}" not registered. Known: ${known}. ` +
|
|
33
|
+
`Mount the matching inbound-provider-${providerKey} feature.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (!isInboundMailProviderPlugin(usage.options)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${INBOUND_MAIL_FOUNDATION_FEATURE}: provider "${providerKey}" registered without verify()/fetch() — ` +
|
|
39
|
+
`extension options must be an InboundMailProviderPlugin.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return usage.options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Lookup für einen konkreten Account — der Standard-Pfad in Poll-Job
|
|
46
|
+
* und Watch-Supervisor. */
|
|
47
|
+
export function resolveInboundProviderForAccount(
|
|
48
|
+
ctx: InboundMailContext,
|
|
49
|
+
account: MailAccountRecord,
|
|
50
|
+
handlerName = "inbound-mail-foundation:provider-factory",
|
|
51
|
+
): InboundMailProviderPlugin {
|
|
52
|
+
return resolveInboundProviderForKey(ctx, account.provider, handlerName);
|
|
53
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Inbound-mail data-retention (#957). DSGVO Art. 5 (Datenminimierung):
|
|
2
|
+
// ohne Retention wachsen read_inbound_messages + die File-Storage-Bodies
|
|
3
|
+
// unbegrenzt. Zwei Targets, zwei Mechanismen — der Unterschied ist
|
|
4
|
+
// event-sourced vs. unmanaged:
|
|
5
|
+
//
|
|
6
|
+
// read_inbound_messages (event-sourced, EXPLIZITE r.projection):
|
|
7
|
+
// pro Row aelter als messageRetentionDays → archiveStream + Row-Delete
|
|
8
|
+
// (+ bodyRef-Objekt loeschen). archiveStream statt executor.forget, weil
|
|
9
|
+
// die Projection nur das received-Domain-Event mappt, nicht den
|
|
10
|
+
// forgotten-Auto-Verb (den registriert nur ImplicitProjection). Ein
|
|
11
|
+
// forget-Event wuerde beim Rebuild ignoriert und die Row aus dem
|
|
12
|
+
// received-Event resurrecten (#648-Klasse). Der archivierte Stream macht
|
|
13
|
+
// loadAggregate leer → received wird gar nicht erst repliziert. Gleiches
|
|
14
|
+
// Muster wie tenant-destroy-hook, nur per-Row-cutoff statt ganzer Tenant.
|
|
15
|
+
//
|
|
16
|
+
// store_mail_seen_messages (unmanaged, direct-write, nicht event-sourced):
|
|
17
|
+
// Dedup-Anker braucht nur das Backfill-/Replay-Fenster → seenAt-cutoff
|
|
18
|
+
// via plain deleteMany, kein Stream/Archiv. Gescoped direkt über die
|
|
19
|
+
// tenant_id-Spalte der Tabelle (table-builder-Konvention), kein Account-Join.
|
|
20
|
+
//
|
|
21
|
+
// GRENZE (#957 Teil 2, Plan §3.4): die PII in den Message-Events bleibt
|
|
22
|
+
// unter dem TENANT-Subject-Key entschluesselbar — per-Row-Retention
|
|
23
|
+
// shreddet keinen Key. Echte Art.-17-Erasure der Event-Payloads passiert
|
|
24
|
+
// erst bei tenant-destroy (Key-Erase). Read-Row + Body-File sind nach dem
|
|
25
|
+
// Sweep weg; das Event-Log ist append-only by design.
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
type DbRunner,
|
|
29
|
+
deleteMany,
|
|
30
|
+
type EntityTableMeta,
|
|
31
|
+
selectMany,
|
|
32
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
33
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
34
|
+
import { archiveStream } from "@cosmicdrift/kumiko-framework/event-store";
|
|
35
|
+
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
36
|
+
import { seenMessageTable } from "./entities";
|
|
37
|
+
import { INBOUND_MESSAGE_AGGREGATE_TYPE } from "./events";
|
|
38
|
+
import { inboundMessagesProjectionTable } from "./projection";
|
|
39
|
+
|
|
40
|
+
// Temporal.Instant ohne den Caller zwingen es aus globalThis zu ziehen
|
|
41
|
+
// (Muster data-retention/keep-for.ts).
|
|
42
|
+
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
43
|
+
|
|
44
|
+
export const MESSAGE_RETENTION_DAYS = 365;
|
|
45
|
+
export const SEEN_RETENTION_DAYS = 90;
|
|
46
|
+
const BATCH_LIMIT = 1000;
|
|
47
|
+
const ARCHIVED_BY = "inbound-mail:retention";
|
|
48
|
+
const REASON = "retention_expired";
|
|
49
|
+
|
|
50
|
+
export interface InboundRetentionArgs {
|
|
51
|
+
readonly db: DbRunner;
|
|
52
|
+
readonly tenantId: TenantId;
|
|
53
|
+
/** Now-Injection — Tests pinnen den Wert ohne Date-Mock (Pattern keep-for.ts). */
|
|
54
|
+
readonly now: Instant;
|
|
55
|
+
readonly messageRetentionDays?: number;
|
|
56
|
+
readonly seenRetentionDays?: number;
|
|
57
|
+
/** Loescht ein file-storage-Body-Objekt per bodyRef-key. Vom Job lazy
|
|
58
|
+
* verdrahtet (createFileProviderForTenant). Fehlt = kein Body-Delete. */
|
|
59
|
+
readonly deleteBodyObject?: (bodyRef: string) => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface InboundRetentionReport {
|
|
63
|
+
readonly messagesPurged: number;
|
|
64
|
+
readonly bodyObjectsDeleted: number;
|
|
65
|
+
readonly bodyObjectErrors: number;
|
|
66
|
+
readonly seenPurged: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runInboundMailRetention(
|
|
70
|
+
args: InboundRetentionArgs,
|
|
71
|
+
): Promise<InboundRetentionReport> {
|
|
72
|
+
const messageDays = args.messageRetentionDays ?? MESSAGE_RETENTION_DAYS;
|
|
73
|
+
const seenDays = args.seenRetentionDays ?? SEEN_RETENTION_DAYS;
|
|
74
|
+
const messageCutoff = args.now.subtract({ hours: messageDays * 24 });
|
|
75
|
+
const seenCutoff = args.now.subtract({ hours: seenDays * 24 });
|
|
76
|
+
|
|
77
|
+
const messages = await purgeExpiredMessages(args, messageCutoff);
|
|
78
|
+
const seenPurged = await purgeExpiredSeenAnchors(args.db, args.tenantId, seenCutoff);
|
|
79
|
+
|
|
80
|
+
return { ...messages, seenPurged };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type ExpiredMessageRow = { readonly id: string; readonly bodyRef: string | null };
|
|
84
|
+
|
|
85
|
+
async function purgeExpiredMessages(
|
|
86
|
+
args: InboundRetentionArgs,
|
|
87
|
+
cutoff: Instant,
|
|
88
|
+
): Promise<Omit<InboundRetentionReport, "seenPurged">> {
|
|
89
|
+
let messagesPurged = 0;
|
|
90
|
+
let bodyObjectsDeleted = 0;
|
|
91
|
+
let bodyObjectErrors = 0;
|
|
92
|
+
|
|
93
|
+
// Drain-Loop: jede Page loescht die Rows die sie liest, die naechste ist
|
|
94
|
+
// damit frisch. archiveStream VOR dem Delete (crash dazwischen → naechster
|
|
95
|
+
// Lauf re-archiviert idempotent + loescht). Delete strikt nach den
|
|
96
|
+
// archivierten ids, nie per cutoff-WHERE — sonst koennte eine zwischen
|
|
97
|
+
// select und delete eingegangene Alt-Mail unarchiviert geloescht werden
|
|
98
|
+
// und beim Rebuild resurrecten.
|
|
99
|
+
//
|
|
100
|
+
// Ein Row deren deleteBodyObject fehlschlaegt wird NICHT geloescht — sonst
|
|
101
|
+
// ginge die einzige Referenz auf das (dann verwaiste) File-Objekt verloren
|
|
102
|
+
// (kein Retry, kein Dead-Letter). Die Row bleibt fuer den naechsten Sweep
|
|
103
|
+
// stehen (archiveStream ist idempotent). failedIds verhindert, dass so ein
|
|
104
|
+
// Row innerhalb DIESES Laufs endlos re-selektiert wird.
|
|
105
|
+
const failedIds = new Set<string>();
|
|
106
|
+
for (;;) {
|
|
107
|
+
const rows = (
|
|
108
|
+
await selectMany<ExpiredMessageRow>(
|
|
109
|
+
args.db,
|
|
110
|
+
inboundMessagesProjectionTable,
|
|
111
|
+
{ receivedAt: { lt: cutoff }, tenantId: args.tenantId },
|
|
112
|
+
{ limit: BATCH_LIMIT },
|
|
113
|
+
)
|
|
114
|
+
).filter((row) => !failedIds.has(row.id));
|
|
115
|
+
if (rows.length === 0) break;
|
|
116
|
+
|
|
117
|
+
const ids: string[] = [];
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
await archiveStream(args.db, {
|
|
120
|
+
tenantId: args.tenantId,
|
|
121
|
+
aggregateId: row.id,
|
|
122
|
+
aggregateType: INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
123
|
+
archivedBy: ARCHIVED_BY,
|
|
124
|
+
reason: REASON,
|
|
125
|
+
});
|
|
126
|
+
if (row.bodyRef && args.deleteBodyObject) {
|
|
127
|
+
try {
|
|
128
|
+
await args.deleteBodyObject(row.bodyRef);
|
|
129
|
+
bodyObjectsDeleted++;
|
|
130
|
+
} catch {
|
|
131
|
+
bodyObjectErrors++;
|
|
132
|
+
failedIds.add(row.id);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
ids.push(row.id);
|
|
137
|
+
}
|
|
138
|
+
if (ids.length > 0) {
|
|
139
|
+
await deleteMany(args.db, inboundMessagesProjectionTable as EntityTableMeta, {
|
|
140
|
+
id: { in: ids },
|
|
141
|
+
});
|
|
142
|
+
messagesPurged += ids.length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (rows.length < BATCH_LIMIT) break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { messagesPurged, bodyObjectsDeleted, bodyObjectErrors };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function purgeExpiredSeenAnchors(
|
|
152
|
+
db: DbRunner,
|
|
153
|
+
tenantId: TenantId,
|
|
154
|
+
cutoff: Instant,
|
|
155
|
+
): Promise<number> {
|
|
156
|
+
// Seen-Anker erben tenantId (table-builder: jede Entity hat tenant_id) →
|
|
157
|
+
// direkt tenant-scoped, kein Account-Join. Trifft auch Anker, deren Account
|
|
158
|
+
// schon disconnected/geloescht ist.
|
|
159
|
+
const where = { tenantId, seenAt: { lt: cutoff } };
|
|
160
|
+
const expired = await selectMany<{ accountId: string }>(
|
|
161
|
+
db,
|
|
162
|
+
seenMessageTable as EntityTableMeta,
|
|
163
|
+
where,
|
|
164
|
+
);
|
|
165
|
+
if (expired.length === 0) return 0;
|
|
166
|
+
await deleteMany(db, seenMessageTable as EntityTableMeta, where);
|
|
167
|
+
return expired.length;
|
|
168
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Tenant-destroy hooks — räumen beim Tenant-Destroy alle Inbound-Mail-
|
|
2
|
+
// Spuren des Tenants ab. Ein Hook PRO tenant-subject-Entity (der
|
|
3
|
+
// gdpr-storage-Boot-Validator prüft die EXT_TENANT_DATA-Registrierung
|
|
4
|
+
// entity-genau), jeweils:
|
|
5
|
+
//
|
|
6
|
+
// 1. Streams archivieren — ein künftiger Projection-Rebuild kann
|
|
7
|
+
// Gelöschtes nicht resurrecten (Muster billing-foundation #800).
|
|
8
|
+
// Message-Streams sind viele → Loop über die Projection-Rows
|
|
9
|
+
// (Row-PK = Stream-ID); tenant-destroy ist ein seltener
|
|
10
|
+
// Batch-Vorgang, O(n) ist ok.
|
|
11
|
+
// 2. Projection-Rows löschen.
|
|
12
|
+
// 3. Nur mail-account: unmanaged Sync-State (Cursors, Seen-Anchors)
|
|
13
|
+
// löschen — gekeyt per accountId, deshalb aus den Account-Rows
|
|
14
|
+
// VOR dem Delete eingesammelt.
|
|
15
|
+
//
|
|
16
|
+
// Die eigentliche PII-Erasure macht crypto-shredding: eraseSubjectKeys
|
|
17
|
+
// löscht den Tenant-Subject-Key, damit werden Event-Log-Payloads UND
|
|
18
|
+
// etwaige Ciphertext-Kopien unlesbar. Diese Hooks entsorgen die Rows.
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type DbRunner,
|
|
22
|
+
deleteMany,
|
|
23
|
+
type EntityTableMeta,
|
|
24
|
+
selectMany,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
26
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
27
|
+
import { archiveStream } from "@cosmicdrift/kumiko-framework/event-store";
|
|
28
|
+
import { seenMessageTable, syncCursorTable } from "./entities";
|
|
29
|
+
import {
|
|
30
|
+
INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
31
|
+
MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
32
|
+
MAIL_THREAD_AGGREGATE_TYPE,
|
|
33
|
+
} from "./events";
|
|
34
|
+
import {
|
|
35
|
+
inboundMessagesProjectionTable,
|
|
36
|
+
mailAccountsProjectionTable,
|
|
37
|
+
mailThreadsProjectionTable,
|
|
38
|
+
} from "./projection";
|
|
39
|
+
|
|
40
|
+
const ARCHIVED_BY = "tenant-lifecycle:destroy";
|
|
41
|
+
const REASON = "tenant_destroy";
|
|
42
|
+
|
|
43
|
+
type DestroyCtx = {
|
|
44
|
+
readonly db: DbRunner;
|
|
45
|
+
readonly tenantId: TenantId;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function archiveAndDeleteRows(
|
|
49
|
+
ctx: DestroyCtx,
|
|
50
|
+
table: EntityTableMeta,
|
|
51
|
+
aggregateType: string,
|
|
52
|
+
): Promise<readonly string[]> {
|
|
53
|
+
const rows = await selectMany<{ id: string }>(ctx.db, table, { tenantId: ctx.tenantId });
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
await archiveStream(ctx.db, {
|
|
56
|
+
tenantId: ctx.tenantId,
|
|
57
|
+
aggregateId: row.id,
|
|
58
|
+
aggregateType,
|
|
59
|
+
archivedBy: ARCHIVED_BY,
|
|
60
|
+
reason: REASON,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
await deleteMany(ctx.db, table, { tenantId: ctx.tenantId });
|
|
64
|
+
return rows.map((row) => row.id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function mailAccountTenantDestroyHook(ctx: DestroyCtx): Promise<void> {
|
|
68
|
+
const accountIds = await archiveAndDeleteRows(
|
|
69
|
+
ctx,
|
|
70
|
+
mailAccountsProjectionTable as EntityTableMeta,
|
|
71
|
+
MAIL_ACCOUNT_AGGREGATE_TYPE,
|
|
72
|
+
);
|
|
73
|
+
for (const accountId of accountIds) {
|
|
74
|
+
await deleteMany(ctx.db, syncCursorTable as EntityTableMeta, { accountId });
|
|
75
|
+
await deleteMany(ctx.db, seenMessageTable as EntityTableMeta, { accountId });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function inboundMessageTenantDestroyHook(ctx: DestroyCtx): Promise<void> {
|
|
80
|
+
await archiveAndDeleteRows(
|
|
81
|
+
ctx,
|
|
82
|
+
inboundMessagesProjectionTable as EntityTableMeta,
|
|
83
|
+
INBOUND_MESSAGE_AGGREGATE_TYPE,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function mailThreadTenantDestroyHook(ctx: DestroyCtx): Promise<void> {
|
|
88
|
+
await archiveAndDeleteRows(
|
|
89
|
+
ctx,
|
|
90
|
+
mailThreadsProjectionTable as EntityTableMeta,
|
|
91
|
+
MAIL_THREAD_AGGREGATE_TYPE,
|
|
92
|
+
);
|
|
93
|
+
}
|