@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
5
5
|
import {
|
|
6
6
|
createTestUser,
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
unsafeCreateEntityTable,
|
|
12
12
|
unsafePushTables,
|
|
13
13
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createTestEnvelopeCipher,
|
|
16
|
+
expectErrorIncludes,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
15
18
|
import { createConfigFeature } from "../../config";
|
|
16
19
|
import { type ConfigResolver, createConfigResolver } from "../../config/resolver";
|
|
17
20
|
import { configValuesTable } from "../../config/table";
|
|
@@ -38,8 +41,8 @@ const tenantFeature = createTenantFeature();
|
|
|
38
41
|
const testEncryptionKey = randomBytes(32).toString("base64");
|
|
39
42
|
|
|
40
43
|
beforeAll(async () => {
|
|
41
|
-
const encryption =
|
|
42
|
-
resolver = createConfigResolver({ encryption });
|
|
44
|
+
const encryption = createTestEnvelopeCipher(testEncryptionKey);
|
|
45
|
+
resolver = createConfigResolver({ cipher: encryption });
|
|
43
46
|
|
|
44
47
|
stack = await setupTestStack({
|
|
45
48
|
features: [configFeature, tenantFeature, tierEngineFeature],
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { TierMap } from "../compose-app";
|
|
3
|
+
import { createTierEngineFeature } from "../feature";
|
|
4
|
+
|
|
5
|
+
type Caps = { readonly apps: number };
|
|
6
|
+
|
|
7
|
+
const tierMap: TierMap<Caps> = {
|
|
8
|
+
free: { features: [], caps: { apps: 1 } },
|
|
9
|
+
pro: { features: ["designer"], caps: { apps: 5 } },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("createTierEngineFeature trial config validation", () => {
|
|
13
|
+
test("throws at construction when trial.tier is not a key in tierMap", () => {
|
|
14
|
+
expect(() =>
|
|
15
|
+
createTierEngineFeature({
|
|
16
|
+
tierMap,
|
|
17
|
+
trial: { tier: "enterprise", durationHours: 72 },
|
|
18
|
+
}),
|
|
19
|
+
).toThrow(/trial\.tier "enterprise" is not a key in tierMap/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("does not throw when trial.tier is a valid tierMap key", () => {
|
|
23
|
+
expect(() =>
|
|
24
|
+
createTierEngineFeature({
|
|
25
|
+
tierMap,
|
|
26
|
+
trial: { tier: "pro", durationHours: 72 },
|
|
27
|
+
}),
|
|
28
|
+
).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
type TierResolverPlugin,
|
|
63
63
|
type TrialGate,
|
|
64
64
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
65
|
-
import {
|
|
65
|
+
import { getUnscopedAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
|
|
66
66
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
67
67
|
import { z } from "zod";
|
|
68
68
|
import { tenantTable } from "../tenant";
|
|
@@ -73,6 +73,7 @@ import { tierAssignmentEntity } from "./entity";
|
|
|
73
73
|
import { getActiveTierQuery } from "./handlers/active-tier.query";
|
|
74
74
|
import { getTenantTierQuery } from "./handlers/get-tenant-tier.query";
|
|
75
75
|
import { createSetTenantTierWrite } from "./handlers/set-tenant-tier.write";
|
|
76
|
+
import { TIER_ENGINE_I18N } from "./i18n";
|
|
76
77
|
import { isTrialActive, type TrialPolicy } from "./trial";
|
|
77
78
|
|
|
78
79
|
// Drizzle-table for the tier-assignment-entity. Built once at module-load
|
|
@@ -234,6 +235,7 @@ export function createTierEngineFeature<
|
|
|
234
235
|
// verdrahtet (Platform-Admin-Hoheit, nicht App-konfigurierbar). App
|
|
235
236
|
// platziert ihn nur via r.nav("tier-engine:screen:tier-admin"); die
|
|
236
237
|
// Komponente liefert tierEngineClient() aus dem ./web-subpath.
|
|
238
|
+
r.translations({ keys: TIER_ENGINE_I18N });
|
|
237
239
|
r.screen({
|
|
238
240
|
id: TIER_ADMIN_SCREEN_ID,
|
|
239
241
|
type: "custom",
|
|
@@ -268,6 +270,11 @@ export function createTierEngineFeature<
|
|
|
268
270
|
// Sekunde 1 nach Signup. Darum lebt er als async trialGate (unten, am
|
|
269
271
|
// build-Ende angehängt), den der dispatcher nur auf dem disabled-Pfad
|
|
270
272
|
// konsultiert. trialFeatures = fixe Feature-Menge des Trial-Tiers.
|
|
273
|
+
if (opts.trial && !tierMap[opts.trial.tier]) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`tier-engine: trial.tier "${opts.trial.tier}" is not a key in tierMap — trial would unlock no features`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
271
278
|
const trialFeatures: ReadonlySet<string> = opts.trial
|
|
272
279
|
? featuresForTier(tierMap, opts.trial.tier)
|
|
273
280
|
: new Set();
|
|
@@ -283,7 +290,7 @@ export function createTierEngineFeature<
|
|
|
283
290
|
};
|
|
284
291
|
|
|
285
292
|
// Invalidation: tier-assignment events update the cache.
|
|
286
|
-
r.
|
|
293
|
+
r.hook("postSave", { allOf: "tier-assignment" }, async (result) => {
|
|
287
294
|
// result.data has tenantId + tier (after entity-update merge)
|
|
288
295
|
const data = result.data as { tenantId?: unknown; tier?: unknown }; // @cast-boundary engine-payload
|
|
289
296
|
// skip: defensive type-guard auf payload-shape. Bei korrekt gerenderten
|
|
@@ -295,7 +302,7 @@ export function createTierEngineFeature<
|
|
|
295
302
|
const tenantId = data.tenantId as TenantId;
|
|
296
303
|
cache.set(tenantId, mergeAlwaysOn(alwaysOnHolder.set, featuresForTier(tierMap, data.tier)));
|
|
297
304
|
});
|
|
298
|
-
r.
|
|
305
|
+
r.hook("postDelete", { allOf: "tier-assignment" }, async (payload) => {
|
|
299
306
|
const data = payload.data as { tenantId?: unknown }; // @cast-boundary engine-payload
|
|
300
307
|
// skip: gleiche type-guard semantik wie postSave-hook oben.
|
|
301
308
|
if (typeof data.tenantId !== "string") return;
|
|
@@ -316,9 +323,9 @@ export function createTierEngineFeature<
|
|
|
316
323
|
// neuer Tenant (Memory `feedback_event_store_tenant_consistency`).
|
|
317
324
|
if (opts.defaultTier !== undefined) {
|
|
318
325
|
const defaultTier = opts.defaultTier;
|
|
319
|
-
r.
|
|
326
|
+
r.hook(
|
|
320
327
|
"postSave",
|
|
321
|
-
"tenant",
|
|
328
|
+
{ allOf: "tenant" },
|
|
322
329
|
async (result, ctx) => {
|
|
323
330
|
// result-shape: kumiko-framework's SaveContext mit isNew + data
|
|
324
331
|
const saveResult = result as { isNew?: unknown; data?: unknown }; // @cast-boundary engine-payload
|
|
@@ -358,7 +365,7 @@ export function createTierEngineFeature<
|
|
|
358
365
|
|
|
359
366
|
// Idempotency: stream-existence-check vor create. Pattern aus
|
|
360
367
|
// seedTenant.ts. Bei re-replay (rebuild) nicht versionsbumpen.
|
|
361
|
-
const streamVersion = await
|
|
368
|
+
const streamVersion = await getUnscopedAggregateStreamMaxVersion(rawDb, aggregateId);
|
|
362
369
|
// skip: idempotent — tier-assignment stream already seeded (rebuild/replay).
|
|
363
370
|
if (streamVersion > 0) return;
|
|
364
371
|
|
package/src/tier-engine/i18n.ts
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
7
7
|
|
|
8
|
+
type LocalizedString = { readonly de: string; readonly en: string };
|
|
9
|
+
|
|
10
|
+
export const TIER_ENGINE_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
11
|
+
"screen:tier-admin.title": { de: "Tier manuell zuweisen", en: "Assign tier manually" },
|
|
12
|
+
};
|
|
13
|
+
|
|
8
14
|
export const defaultTranslations: TranslationsByLocale = {
|
|
9
15
|
de: {
|
|
16
|
+
"screen:tier-admin.title": "Tier manuell zuweisen",
|
|
10
17
|
"tier-admin.title": "Tier manuell zuweisen",
|
|
11
18
|
"tier-admin.explainer":
|
|
12
19
|
"Weise einem Tenant ein Tier ohne Kauf zu. Der Grant wird als „manuell“ markiert und von einem späteren Billing-Sync nicht überschrieben.",
|
|
@@ -22,6 +29,7 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
22
29
|
"Diese App hat keine TierMap konfiguriert — es gibt keine zuweisbaren Tiers.",
|
|
23
30
|
},
|
|
24
31
|
en: {
|
|
32
|
+
"screen:tier-admin.title": "Assign tier manually",
|
|
25
33
|
"tier-admin.title": "Assign tier manually",
|
|
26
34
|
"tier-admin.explainer":
|
|
27
35
|
"Grant a tenant a tier without a purchase. The grant is marked as “manual” and a later billing sync won't overwrite it.",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
useQuery,
|
|
14
14
|
useTranslation,
|
|
15
15
|
} from "@cosmicdrift/kumiko-renderer";
|
|
16
|
+
import { FormScreenShell } from "@cosmicdrift/kumiko-renderer-web";
|
|
16
17
|
import { type ReactNode, useEffect, useState } from "react";
|
|
17
18
|
import { TierEngineHandlers, TierEngineQueries } from "../constants";
|
|
18
19
|
|
|
@@ -36,7 +37,7 @@ type Status =
|
|
|
36
37
|
|
|
37
38
|
export function TierAdminScreen(): ReactNode {
|
|
38
39
|
const t = useTranslation();
|
|
39
|
-
const {
|
|
40
|
+
const { Field, Input, Button, Banner, Form, Text } = usePrimitives();
|
|
40
41
|
const dispatcher = useDispatcher();
|
|
41
42
|
|
|
42
43
|
// ponytail: nur die erste Seite (default-limit, nextCursor ignoriert) —
|
|
@@ -91,10 +92,7 @@ export function TierAdminScreen(): ReactNode {
|
|
|
91
92
|
const canSubmit = tenantId !== "" && tier !== "" && !submitting;
|
|
92
93
|
|
|
93
94
|
return (
|
|
94
|
-
<
|
|
95
|
-
<Heading variant="page">{t("tier-admin.title")}</Heading>
|
|
96
|
-
<p className="text-sm text-muted-foreground">{t("tier-admin.explainer")}</p>
|
|
97
|
-
|
|
95
|
+
<FormScreenShell testId="tier-admin-screen" className="flex flex-col gap-6">
|
|
98
96
|
{tenantsQuery.error !== null && (
|
|
99
97
|
<Banner variant="error" testId="tier-admin-load-error">
|
|
100
98
|
{t("tier-admin.error.load")}
|
|
@@ -106,7 +104,25 @@ export function TierAdminScreen(): ReactNode {
|
|
|
106
104
|
</Banner>
|
|
107
105
|
)}
|
|
108
106
|
|
|
109
|
-
<
|
|
107
|
+
<Form
|
|
108
|
+
testId="tier-admin-form"
|
|
109
|
+
onSubmit={(e) => {
|
|
110
|
+
e?.preventDefault();
|
|
111
|
+
void onSave();
|
|
112
|
+
}}
|
|
113
|
+
actions={
|
|
114
|
+
<Button
|
|
115
|
+
type="submit"
|
|
116
|
+
disabled={!canSubmit}
|
|
117
|
+
loading={submitting}
|
|
118
|
+
testId="tier-admin-submit"
|
|
119
|
+
>
|
|
120
|
+
{t("tier-admin.submit")}
|
|
121
|
+
</Button>
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
<Text variant="small">{t("tier-admin.explainer")}</Text>
|
|
125
|
+
|
|
110
126
|
<Field id="tier-admin-tenant" label={t("tier-admin.tenant.label")} required>
|
|
111
127
|
<Input
|
|
112
128
|
kind="select"
|
|
@@ -119,9 +135,9 @@ export function TierAdminScreen(): ReactNode {
|
|
|
119
135
|
</Field>
|
|
120
136
|
|
|
121
137
|
{tenantId !== "" && (
|
|
122
|
-
<
|
|
138
|
+
<Text variant="small" testId="tier-admin-current">
|
|
123
139
|
{t("tier-admin.current.label")}: {currentTier ?? t("tier-admin.current.none")}
|
|
124
|
-
</
|
|
140
|
+
</Text>
|
|
125
141
|
)}
|
|
126
142
|
|
|
127
143
|
<Field id="tier-admin-tier" label={t("tier-admin.tier.label")} required>
|
|
@@ -146,16 +162,7 @@ export function TierAdminScreen(): ReactNode {
|
|
|
146
162
|
{t(status.messageKey)}
|
|
147
163
|
</Banner>
|
|
148
164
|
)}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
onClick={() => void onSave()}
|
|
152
|
-
disabled={!canSubmit}
|
|
153
|
-
loading={submitting}
|
|
154
|
-
testId="tier-admin-submit"
|
|
155
|
-
>
|
|
156
|
-
{t("tier-admin.submit")}
|
|
157
|
-
</Button>
|
|
158
|
-
</Section>
|
|
159
|
-
</div>
|
|
165
|
+
</Form>
|
|
166
|
+
</FormScreenShell>
|
|
160
167
|
);
|
|
161
168
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// #762 — backfillUserStreamTenants: pre-#497 user streams live on the
|
|
2
|
+
// creating tenant; post-#497 the executor addresses SYSTEM_TENANT_ID, so
|
|
3
|
+
// legacy users version-conflict on every write (password-reset collapses to
|
|
4
|
+
// invalid_token). The backfill retenants + renumbers per aggregate in global
|
|
5
|
+
// id order — including the split-stream case the raw #497-changeset SQL
|
|
6
|
+
// crashes on (legacy tenant v1..n AND post-#497 SYSTEM events for the same
|
|
7
|
+
// aggregate → events_aggregate_version_uq violation).
|
|
8
|
+
|
|
9
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
11
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
12
|
+
import {
|
|
13
|
+
createSystemUser,
|
|
14
|
+
SYSTEM_TENANT_ID,
|
|
15
|
+
type TenantId,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
17
|
+
import { append, createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
18
|
+
import {
|
|
19
|
+
createTestDb,
|
|
20
|
+
type TestDb,
|
|
21
|
+
unsafeCreateEntityTable,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
|
+
import { backfillUserStreamTenants } from "../db/queries/stream-tenant-backfill";
|
|
24
|
+
import { userEntity, userTable } from "../schema/user";
|
|
25
|
+
|
|
26
|
+
const T1 = "00000000-0000-4000-8000-000000000011" as TenantId;
|
|
27
|
+
const LEGACY_USER = "aaaaaaaa-0000-4000-8000-000000000001";
|
|
28
|
+
const SPLIT_USER = "aaaaaaaa-0000-4000-8000-000000000002";
|
|
29
|
+
const MODERN_USER = "aaaaaaaa-0000-4000-8000-000000000003";
|
|
30
|
+
|
|
31
|
+
let testDb: TestDb;
|
|
32
|
+
|
|
33
|
+
const executor = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
testDb = await createTestDb();
|
|
37
|
+
await unsafeCreateEntityTable(testDb.db, userEntity, "user");
|
|
38
|
+
await createEventsTable(testDb.db);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterAll(async () => {
|
|
42
|
+
await testDb.cleanup();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
await asRawClient(testDb.db).unsafe(
|
|
47
|
+
`TRUNCATE kumiko_events, kumiko_snapshots, kumiko_archived_streams, read_users RESTART IDENTITY CASCADE`,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function appendUserEvent(
|
|
52
|
+
aggregateId: string,
|
|
53
|
+
tenantId: TenantId,
|
|
54
|
+
expectedVersion: number,
|
|
55
|
+
type: string,
|
|
56
|
+
payload: Record<string, unknown>,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
await append(testDb.db, {
|
|
59
|
+
aggregateId,
|
|
60
|
+
aggregateType: "user",
|
|
61
|
+
tenantId,
|
|
62
|
+
expectedVersion,
|
|
63
|
+
type,
|
|
64
|
+
payload,
|
|
65
|
+
metadata: { userId: "test-migrator" },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function streamRows(
|
|
70
|
+
aggregateId: string,
|
|
71
|
+
): Promise<ReadonlyArray<{ tenant_id: string; version: number; type: string }>> {
|
|
72
|
+
return (await asRawClient(testDb.db).unsafe(
|
|
73
|
+
`SELECT "tenant_id", "version", "type" FROM "kumiko_events"
|
|
74
|
+
WHERE "aggregate_id" = $1::uuid ORDER BY "id" ASC`,
|
|
75
|
+
[aggregateId],
|
|
76
|
+
)) as ReadonlyArray<{ tenant_id: string; version: number; type: string }>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function seedUserRow(id: string, tenantId: TenantId, version: number) {
|
|
80
|
+
return asRawClient(testDb.db).unsafe(
|
|
81
|
+
`INSERT INTO "read_users" ("id", "tenant_id", "email", "display_name", "locale", "password_hash", "status", "version")
|
|
82
|
+
VALUES ($1::uuid, $2::uuid, $3, 'Legacy', 'de', 'x', 'active', $4)`,
|
|
83
|
+
[id, tenantId, `${id.slice(0, 8)}@example.com`, version],
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("backfillUserStreamTenants (#762)", () => {
|
|
88
|
+
test("legacy stream: retenanted to SYSTEM, versions contiguous, executor write works again", async () => {
|
|
89
|
+
await appendUserEvent(LEGACY_USER, T1, 0, "user.created", { email: "l@example.com" });
|
|
90
|
+
await appendUserEvent(LEGACY_USER, T1, 1, "user.updated", { changes: { displayName: "L" } });
|
|
91
|
+
await seedUserRow(LEGACY_USER, T1, 2);
|
|
92
|
+
|
|
93
|
+
const result = await backfillUserStreamTenants(testDb.db);
|
|
94
|
+
expect(result.aggregatesMigrated).toBe(1);
|
|
95
|
+
expect(result.eventsMigrated).toBe(2);
|
|
96
|
+
expect(result.failed).toHaveLength(0);
|
|
97
|
+
|
|
98
|
+
const rows = await streamRows(LEGACY_USER);
|
|
99
|
+
expect(rows.map((r) => r.tenant_id)).toEqual([SYSTEM_TENANT_ID, SYSTEM_TENANT_ID]);
|
|
100
|
+
expect(rows.map((r) => r.version)).toEqual([1, 2]);
|
|
101
|
+
|
|
102
|
+
// The actual #762 symptom: an optimistic-locked executor update with the
|
|
103
|
+
// row version now targets a stream that HAS that version — no conflict.
|
|
104
|
+
const tdb = createTenantDb(testDb.db, SYSTEM_TENANT_ID, "system");
|
|
105
|
+
const writeRes = await executor.update(
|
|
106
|
+
{ id: LEGACY_USER, version: 2, changes: { displayName: "After" } },
|
|
107
|
+
createSystemUser(SYSTEM_TENANT_ID),
|
|
108
|
+
tdb,
|
|
109
|
+
);
|
|
110
|
+
expect(writeRes.isSuccess).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("split stream (legacy + post-#497 SYSTEM events) merges in global id order", async () => {
|
|
114
|
+
// Legacy: created + updated on T1 (v1, v2). Then a post-#497 lifecycle
|
|
115
|
+
// write appended v1 on SYSTEM — the split the raw changeset-SQL trips on.
|
|
116
|
+
await appendUserEvent(SPLIT_USER, T1, 0, "user.created", { email: "s@example.com" });
|
|
117
|
+
await appendUserEvent(SPLIT_USER, T1, 1, "user.updated", { changes: { a: 1 } });
|
|
118
|
+
await appendUserEvent(SPLIT_USER, SYSTEM_TENANT_ID, 0, "user.updated", {
|
|
119
|
+
changes: { status: "restricted" },
|
|
120
|
+
});
|
|
121
|
+
await seedUserRow(SPLIT_USER, T1, 2);
|
|
122
|
+
|
|
123
|
+
const result = await backfillUserStreamTenants(testDb.db);
|
|
124
|
+
expect(result.aggregatesMigrated).toBe(1);
|
|
125
|
+
expect(result.eventsMigrated).toBe(3);
|
|
126
|
+
expect(result.failed).toHaveLength(0);
|
|
127
|
+
|
|
128
|
+
const rows = await streamRows(SPLIT_USER);
|
|
129
|
+
expect(rows.map((r) => r.tenant_id)).toEqual([
|
|
130
|
+
SYSTEM_TENANT_ID,
|
|
131
|
+
SYSTEM_TENANT_ID,
|
|
132
|
+
SYSTEM_TENANT_ID,
|
|
133
|
+
]);
|
|
134
|
+
// Renumbered by global event id: created, updated(T1), updated(SYSTEM).
|
|
135
|
+
expect(rows.map((r) => r.version)).toEqual([1, 2, 3]);
|
|
136
|
+
expect(rows[0]?.type).toBe("user.created");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("idempotent: second run finds nothing; modern SYSTEM streams untouched", async () => {
|
|
140
|
+
await appendUserEvent(MODERN_USER, SYSTEM_TENANT_ID, 0, "user.created", {
|
|
141
|
+
email: "m@example.com",
|
|
142
|
+
});
|
|
143
|
+
await appendUserEvent(LEGACY_USER, T1, 0, "user.created", { email: "l@example.com" });
|
|
144
|
+
|
|
145
|
+
const first = await backfillUserStreamTenants(testDb.db);
|
|
146
|
+
expect(first.aggregatesMigrated).toBe(1);
|
|
147
|
+
|
|
148
|
+
const second = await backfillUserStreamTenants(testDb.db);
|
|
149
|
+
expect(second.aggregatesMigrated).toBe(0);
|
|
150
|
+
expect(second.eventsMigrated).toBe(0);
|
|
151
|
+
|
|
152
|
+
const modern = await streamRows(MODERN_USER);
|
|
153
|
+
expect(modern).toHaveLength(1);
|
|
154
|
+
expect(modern[0]?.version).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("stale snapshots dropped, archived-stream marker moves to SYSTEM", async () => {
|
|
158
|
+
await appendUserEvent(LEGACY_USER, T1, 0, "user.created", { email: "l@example.com" });
|
|
159
|
+
await asRawClient(testDb.db).unsafe(
|
|
160
|
+
`INSERT INTO "kumiko_snapshots" ("aggregate_id", "aggregate_type", "tenant_id", "version", "state")
|
|
161
|
+
VALUES ($1::uuid, 'user', $2::uuid, 1, '{}'::jsonb)`,
|
|
162
|
+
[LEGACY_USER, T1],
|
|
163
|
+
);
|
|
164
|
+
await asRawClient(testDb.db).unsafe(
|
|
165
|
+
`INSERT INTO "kumiko_archived_streams" ("tenant_id", "aggregate_id", "aggregate_type", "archived_by")
|
|
166
|
+
VALUES ($1::uuid, $2::uuid, 'user', 'test')`,
|
|
167
|
+
[T1, LEGACY_USER],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const result = await backfillUserStreamTenants(testDb.db);
|
|
171
|
+
expect(result.failed).toHaveLength(0);
|
|
172
|
+
|
|
173
|
+
const snapshots = (await asRawClient(testDb.db).unsafe(
|
|
174
|
+
`SELECT count(*)::int AS n FROM "kumiko_snapshots" WHERE "aggregate_id" = $1::uuid`,
|
|
175
|
+
[LEGACY_USER],
|
|
176
|
+
)) as ReadonlyArray<{ n: number }>;
|
|
177
|
+
expect(snapshots[0]?.n).toBe(0);
|
|
178
|
+
|
|
179
|
+
const archived = (await asRawClient(testDb.db).unsafe(
|
|
180
|
+
`SELECT "tenant_id" FROM "kumiko_archived_streams" WHERE "aggregate_id" = $1::uuid`,
|
|
181
|
+
[LEGACY_USER],
|
|
182
|
+
)) as ReadonlyArray<{ tenant_id: string }>;
|
|
183
|
+
expect(archived).toHaveLength(1);
|
|
184
|
+
expect(archived[0]?.tenant_id).toBe(SYSTEM_TENANT_ID);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -109,6 +109,32 @@ describe("scenario 2: field-level read access", () => {
|
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
// --- find-for-auth is system-only: it returns the raw row incl. passwordHash,
|
|
113
|
+
// and every query handler is reachable via POST /api/query — so even a
|
|
114
|
+
// SystemAdmin (assignable app role) must be denied over HTTP. ---
|
|
115
|
+
|
|
116
|
+
describe("scenario 2b: find-for-auth is system-only", () => {
|
|
117
|
+
test("SystemAdmin over HTTP is denied — passwordHash must not leak", async () => {
|
|
118
|
+
await seedUser({
|
|
119
|
+
email: "auth-lookup@example.com",
|
|
120
|
+
displayName: "AuthLookup",
|
|
121
|
+
passwordHash: "must-never-leave-the-server",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const res = await stack.http.queryWithHeaders(
|
|
125
|
+
UserQueries.findForAuth,
|
|
126
|
+
{ email: "auth-lookup@example.com" },
|
|
127
|
+
systemAdmin,
|
|
128
|
+
{},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(res.status).toBeGreaterThanOrEqual(400);
|
|
132
|
+
const body = await res.text();
|
|
133
|
+
expect(body).toContain("access_denied");
|
|
134
|
+
expect(body).not.toContain("must-never-leave-the-server");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
112
138
|
// --- Scenario 3: user edits own profile, email/passwordHash are system-locked ---
|
|
113
139
|
|
|
114
140
|
describe("scenario 3: self-update + field-level write access", () => {
|
package/src/user/constants.ts
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// #762 — one-time migration tooling for pre-#497 user event streams.
|
|
2
|
+
//
|
|
3
|
+
// #497 made `user` a systemStream entity: every executor op addresses the
|
|
4
|
+
// stream on SYSTEM_TENANT_ID. Streams created before that live on whichever
|
|
5
|
+
// tenant created them ("scattered"), so post-#497 writes version-conflict
|
|
6
|
+
// against the empty SYSTEM stream. The raw UPDATE documented in the #497
|
|
7
|
+
// changeset breaks as soon as an aggregate has BOTH a legacy stream and
|
|
8
|
+
// post-#497 SYSTEM events (split stream — e.g. a skipOptimisticLock
|
|
9
|
+
// lifecycle write appended v1 on SYSTEM while v1..n live on the old
|
|
10
|
+
// tenant): retenanting then trips events_aggregate_version_uq.
|
|
11
|
+
//
|
|
12
|
+
// This tool merges per aggregate: all user events, ordered by global event
|
|
13
|
+
// id, are renumbered 1..n and retenanted to SYSTEM_TENANT_ID — two-phase
|
|
14
|
+
// version writes (negative interim values) dodge the unique index while the
|
|
15
|
+
// per-aggregate transaction is in flight. Snapshots of migrated aggregates
|
|
16
|
+
// are dropped (version numbering changed; the next snapshotting load
|
|
17
|
+
// recreates them); archived-stream markers move with the stream.
|
|
18
|
+
//
|
|
19
|
+
// Idempotent: the candidate query only matches aggregates that still have
|
|
20
|
+
// non-SYSTEM events. One failing aggregate does not abort the run — the
|
|
21
|
+
// rest of the estate migrates, failures are reported.
|
|
22
|
+
//
|
|
23
|
+
// After a run, rebuild the user projection so read_users.tenant_id reflects
|
|
24
|
+
// the stream move: rebuildProjection("user:projection:user-entity", ...) or
|
|
25
|
+
// the jobs:job:projection-rebuild job.
|
|
26
|
+
|
|
27
|
+
import { asRawClient, transaction } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
28
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
29
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
30
|
+
|
|
31
|
+
export type UserStreamBackfillResult = {
|
|
32
|
+
readonly aggregatesMigrated: number;
|
|
33
|
+
readonly eventsMigrated: number;
|
|
34
|
+
readonly failed: ReadonlyArray<{ readonly aggregateId: string; readonly error: string }>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export async function backfillUserStreamTenants(
|
|
38
|
+
db: DbConnection,
|
|
39
|
+
): Promise<UserStreamBackfillResult> {
|
|
40
|
+
const candidates = (await asRawClient(db).unsafe(
|
|
41
|
+
`SELECT DISTINCT "aggregate_id" FROM "kumiko_events"
|
|
42
|
+
WHERE "aggregate_type" = 'user' AND "tenant_id" <> $1::uuid`,
|
|
43
|
+
[SYSTEM_TENANT_ID],
|
|
44
|
+
)) as ReadonlyArray<{ aggregate_id: string }>;
|
|
45
|
+
|
|
46
|
+
let aggregatesMigrated = 0;
|
|
47
|
+
let eventsMigrated = 0;
|
|
48
|
+
const failed: Array<{ aggregateId: string; error: string }> = [];
|
|
49
|
+
|
|
50
|
+
for (const { aggregate_id } of candidates) {
|
|
51
|
+
try {
|
|
52
|
+
eventsMigrated += await migrateAggregate(db, aggregate_id);
|
|
53
|
+
aggregatesMigrated++;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
failed.push({
|
|
56
|
+
aggregateId: aggregate_id,
|
|
57
|
+
error: e instanceof Error ? e.message : String(e),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { aggregatesMigrated, eventsMigrated, failed };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function migrateAggregate(db: DbConnection, aggregateId: string): Promise<number> {
|
|
65
|
+
return transaction(db, async (tx) => {
|
|
66
|
+
const raw = asRawClient(tx);
|
|
67
|
+
// Global id order = commit-adjacent replay order; merges a split stream
|
|
68
|
+
// (legacy tenant + SYSTEM) into one consistent version sequence.
|
|
69
|
+
const events = (await raw.unsafe(
|
|
70
|
+
`SELECT "id" FROM "kumiko_events"
|
|
71
|
+
WHERE "aggregate_type" = 'user' AND "aggregate_id" = $1::uuid
|
|
72
|
+
ORDER BY "id" ASC
|
|
73
|
+
FOR UPDATE`,
|
|
74
|
+
[aggregateId],
|
|
75
|
+
)) as ReadonlyArray<{ id: bigint | string }>;
|
|
76
|
+
|
|
77
|
+
// Phase 1: negative interim versions — real versions are always >= 1, so
|
|
78
|
+
// nothing can collide with events_aggregate_version_uq mid-migration.
|
|
79
|
+
for (const [i, ev] of events.entries()) {
|
|
80
|
+
await raw.unsafe(
|
|
81
|
+
`UPDATE "kumiko_events" SET "tenant_id" = $1::uuid, "version" = $2 WHERE "id" = $3`,
|
|
82
|
+
[SYSTEM_TENANT_ID, -(i + 1), ev.id],
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
// Phase 2: final contiguous 1..n.
|
|
86
|
+
for (const [i, ev] of events.entries()) {
|
|
87
|
+
await raw.unsafe(`UPDATE "kumiko_events" SET "version" = $1 WHERE "id" = $2`, [i + 1, ev.id]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Version numbering changed → any snapshot of this aggregate is stale.
|
|
91
|
+
await raw.unsafe(`DELETE FROM "kumiko_snapshots" WHERE "aggregate_id" = $1::uuid`, [
|
|
92
|
+
aggregateId,
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
// Archived markers key on (tenant_id, aggregate_id) — move them with the
|
|
96
|
+
// stream, keeping an existing SYSTEM marker if both exist.
|
|
97
|
+
await raw.unsafe(
|
|
98
|
+
`INSERT INTO "kumiko_archived_streams" ("tenant_id", "aggregate_id", "aggregate_type", "archived_at", "archived_by", "reason")
|
|
99
|
+
SELECT $1::uuid, "aggregate_id", "aggregate_type", "archived_at", "archived_by", "reason"
|
|
100
|
+
FROM "kumiko_archived_streams"
|
|
101
|
+
WHERE "aggregate_id" = $2::uuid AND "tenant_id" <> $1::uuid
|
|
102
|
+
ON CONFLICT DO NOTHING`,
|
|
103
|
+
[SYSTEM_TENANT_ID, aggregateId],
|
|
104
|
+
);
|
|
105
|
+
await raw.unsafe(
|
|
106
|
+
`DELETE FROM "kumiko_archived_streams" WHERE "aggregate_id" = $1::uuid AND "tenant_id" <> $2::uuid`,
|
|
107
|
+
[aggregateId, SYSTEM_TENANT_ID],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return events.length;
|
|
111
|
+
});
|
|
112
|
+
}
|
package/src/user/feature.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { findForAuthQuery } from "./handlers/find-for-auth.query";
|
|
|
5
5
|
import { listQuery } from "./handlers/list.query";
|
|
6
6
|
import { meQuery } from "./handlers/me.query";
|
|
7
7
|
import { updateWrite } from "./handlers/update.write";
|
|
8
|
+
import { USER_I18N } from "./i18n";
|
|
8
9
|
import { userEntity } from "./schema/user";
|
|
9
10
|
import { userEditScreen, userListScreen } from "./screens";
|
|
10
11
|
|
|
@@ -43,6 +44,8 @@ export function createUserFeature(): FeatureDefinition {
|
|
|
43
44
|
r.screen(userListScreen);
|
|
44
45
|
r.screen(userEditScreen);
|
|
45
46
|
|
|
47
|
+
r.translations({ keys: USER_I18N });
|
|
48
|
+
|
|
46
49
|
return { handlers, queries };
|
|
47
50
|
});
|
|
48
51
|
}
|
|
@@ -7,10 +7,12 @@ import { userTable } from "../schema/user";
|
|
|
7
7
|
// by email OR id (exactly one, enforced by the schema). Used by the auth
|
|
8
8
|
// features via ctx.queryAs(systemUser, ...).
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// Access is system-only, NOT access.privileged: every query handler is
|
|
11
|
+
// reachable via POST /api/query, and "SystemAdmin" is an assignable app
|
|
12
|
+
// role — privileged would hand any SystemAdmin the raw passwordHash over
|
|
13
|
+
// HTTP. The "system" role can't be minted into a session (only
|
|
14
|
+
// createSystemUser() carries it), so system-only keeps this handler
|
|
15
|
+
// internal to ctx.queryAs(systemUser, ...) callers.
|
|
14
16
|
export const findForAuthQuery = defineQueryHandler({
|
|
15
17
|
name: "user:find-for-auth",
|
|
16
18
|
schema: z
|
|
@@ -24,9 +26,9 @@ export const findForAuthQuery = defineQueryHandler({
|
|
|
24
26
|
(v) => (v.email !== undefined) !== (v.id !== undefined),
|
|
25
27
|
{ message: "exactly one of email or id must be set" },
|
|
26
28
|
),
|
|
27
|
-
access: { roles: access.
|
|
29
|
+
access: { roles: access.system },
|
|
28
30
|
handler: async (query, ctx) => {
|
|
29
|
-
const where =
|
|
31
|
+
const where: { email: string } | { id: string } =
|
|
30
32
|
query.payload.email !== undefined
|
|
31
33
|
? { email: query.payload.email }
|
|
32
34
|
: { id: query.payload.id as string }; // @cast-boundary engine-payload
|
package/src/user/i18n.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type LocalizedString = { readonly de: string; readonly en: string };
|
|
2
|
+
|
|
3
|
+
export const USER_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
4
|
+
"screen:user-list.title": { de: "Benutzer", en: "Users" },
|
|
5
|
+
"screen:user-edit.title": { de: "Benutzer bearbeiten", en: "Edit user" },
|
|
6
|
+
"user:entity:user:field:email": { de: "E-Mail", en: "Email" },
|
|
7
|
+
"user:entity:user:field:displayName": { de: "Anzeigename", en: "Display name" },
|
|
8
|
+
"user:entity:user:field:status": { de: "Status", en: "Status" },
|
|
9
|
+
"user:entity:user:field:emailVerified": { de: "E-Mail bestätigt", en: "Email verified" },
|
|
10
|
+
"user:entity:user:field:locale": { de: "Sprache", en: "Locale" },
|
|
11
|
+
"user:entity:user:field:status:option:active": { de: "Aktiv", en: "Active" },
|
|
12
|
+
"user:entity:user:field:status:option:restricted": { de: "Eingeschränkt", en: "Restricted" },
|
|
13
|
+
"user:entity:user:field:status:option:deletionRequested": {
|
|
14
|
+
de: "Löschung angefordert",
|
|
15
|
+
en: "Deletion requested",
|
|
16
|
+
},
|
|
17
|
+
"user:entity:user:field:status:option:deleted": { de: "Gelöscht", en: "Deleted" },
|
|
18
|
+
"user:entity:user:field:emailVerified:option:true": { de: "Ja", en: "Yes" },
|
|
19
|
+
"user:entity:user:field:emailVerified:option:false": { de: "Nein", en: "No" },
|
|
20
|
+
};
|
package/src/user/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { UserCommandSchemas } from "./command-schemas";
|
|
2
2
|
export { USER_FEATURE, UserErrors, UserHandlers, UserQueries } from "./constants";
|
|
3
|
+
export {
|
|
4
|
+
backfillUserStreamTenants,
|
|
5
|
+
type UserStreamBackfillResult,
|
|
6
|
+
} from "./db/queries/stream-tenant-backfill";
|
|
3
7
|
export { createUserFeature } from "./feature";
|
|
4
8
|
export type { UserStatus } from "./schema/user";
|
|
5
9
|
export {
|