@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,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EffectiveFeaturesResolver,
|
|
3
|
+
isToggleableFeature,
|
|
4
|
+
type Registry,
|
|
5
|
+
SYSTEM_TENANT_ID,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import type { GlobalFeatureToggleRuntime } from "./toggle-runtime";
|
|
8
|
+
|
|
9
|
+
// Composes a tenantTierResolver (tier-engine) with a GlobalFeatureToggleRuntime
|
|
10
|
+
// (feature-toggles) into a single resolver an app can pass as
|
|
11
|
+
// runProdApp/runDevApp's `effectiveFeatures`.
|
|
12
|
+
//
|
|
13
|
+
// The framework only auto-wires ONE tenantTierResolver plugin (single-plugin
|
|
14
|
+
// assumption, see tier-resolver-extension.ts) — an app running BOTH
|
|
15
|
+
// tier-engine (per-tenant feature cuts) and feature-toggles (global runtime
|
|
16
|
+
// switches, e.g. an operator kill-switch not tied to any tier) needs to
|
|
17
|
+
// combine them itself. This is that combination, factored out here instead
|
|
18
|
+
// of duplicated per app.
|
|
19
|
+
//
|
|
20
|
+
// Two disjoint classes of toggleable feature exist once both are mounted:
|
|
21
|
+
// - tier-managed (e.g. a Team-tier perk like personal-access-tokens): the
|
|
22
|
+
// TIER decides membership, not the feature's own default — a toggleable
|
|
23
|
+
// feature with no override row must stay governed by its tier, or every
|
|
24
|
+
// tier-gated toggleable would silently leak/vanish the moment
|
|
25
|
+
// feature-toggles is composed in. The global layer may only NARROW this
|
|
26
|
+
// (`enabled: false` removes it; no row / `true` leaves the tier's grant
|
|
27
|
+
// untouched).
|
|
28
|
+
// - tier-unaware (e.g. a pure operator kill-switch with no tier
|
|
29
|
+
// differentiation at all, such as auth-self-registration): no tier's
|
|
30
|
+
// `features` list ever mentions it, so it would never appear in ANY
|
|
31
|
+
// tenant's tier-set — narrowing alone can only remove features, never
|
|
32
|
+
// grant this one. Its membership must come from computeEffectiveFeatures'
|
|
33
|
+
// normal cascade (override ?? toggleableDefault) instead.
|
|
34
|
+
//
|
|
35
|
+
// `tierResolver(SYSTEM_TENANT_ID)` is tier-engine's documented convention for
|
|
36
|
+
// "union of every tier's features" (operator-tooling/async-dispatch
|
|
37
|
+
// convention) — used here purely to classify which toggleable names are
|
|
38
|
+
// tier-managed, without needing the app's TierMap as a separate parameter.
|
|
39
|
+
export function composeTierResolverWithGlobalToggles(
|
|
40
|
+
tierResolver: EffectiveFeaturesResolver,
|
|
41
|
+
toggleRuntime: GlobalFeatureToggleRuntime,
|
|
42
|
+
// registry needed for toggleability lookups; toggleRuntime keeps its own registry reference private.
|
|
43
|
+
registry: Registry,
|
|
44
|
+
): EffectiveFeaturesResolver {
|
|
45
|
+
// Lazy + memoized: computed on first composed(tenantId) call, not at
|
|
46
|
+
// compose-time. Late-bound holder wiring (e.g. run-dev-app.ts's tier
|
|
47
|
+
// resolver plugin) may not have its real tierResolver built yet when
|
|
48
|
+
// composeTierResolverWithGlobalToggles itself runs.
|
|
49
|
+
let tierManaged: ReadonlySet<string> | undefined;
|
|
50
|
+
|
|
51
|
+
const composed: EffectiveFeaturesResolver = (tenantId) => {
|
|
52
|
+
if (tierManaged === undefined) {
|
|
53
|
+
const managed = tierResolver(SYSTEM_TENANT_ID);
|
|
54
|
+
// fail-loud: an empty union means the resolver doesn't implement the SYSTEM_TENANT_ID convention at all.
|
|
55
|
+
if (managed.size === 0) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"composeTierResolverWithGlobalToggles: tierResolver(SYSTEM_TENANT_ID) " +
|
|
58
|
+
"returned an empty set. tier-engine's SYSTEM_TENANT_ID convention " +
|
|
59
|
+
"(union of every tier's features, plus always-on features) must " +
|
|
60
|
+
"never be empty — an empty result means the tierResolver passed in " +
|
|
61
|
+
"doesn't implement that convention, which would silently disable " +
|
|
62
|
+
"all tier-gating (every feature would be treated as tier-unaware).",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
// Structural Rule 1: only the TOGGLEABLE subset of the union is tier-managed.
|
|
66
|
+
// Non-toggleable (always-on) names must keep reaching `result` through the
|
|
67
|
+
// globalCascade loop below even if this resolver's SYSTEM_TENANT_ID answer
|
|
68
|
+
// satisfies the emptiness guard above without also merging always-on into
|
|
69
|
+
// every per-tenant tierSet (framework#1528).
|
|
70
|
+
tierManaged = new Set(
|
|
71
|
+
[...managed].filter((name) => {
|
|
72
|
+
const feature = registry.getFeature(name);
|
|
73
|
+
return feature !== undefined && isToggleableFeature(feature);
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tierSet = tierResolver(tenantId);
|
|
79
|
+
const result = new Set<string>();
|
|
80
|
+
for (const name of tierSet) {
|
|
81
|
+
// Only a toggleable feature's override can narrow the tier's grant —
|
|
82
|
+
// computeEffectiveFeatures' Rule 1 ignores overrides for features
|
|
83
|
+
// without a toggleableDefault, and an override row can otherwise
|
|
84
|
+
// reach a non-toggleable feature via seed-scripts/ops-SQL/an
|
|
85
|
+
// r.toggleable() removal after the fact (readOverride/refresh() have
|
|
86
|
+
// no toggleability gate of their own).
|
|
87
|
+
const feature = registry.getFeature(name);
|
|
88
|
+
const isToggleable = feature !== undefined && isToggleableFeature(feature);
|
|
89
|
+
if (!isToggleable || toggleRuntime.readOverride(name) !== false) {
|
|
90
|
+
result.add(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// toggleRuntime.effectiveFeatures() already applies computeEffectiveFeatures'
|
|
94
|
+
// full rule set (toggleableDefault ?? override, THEN the requires() cascade)
|
|
95
|
+
// — reusing it here instead of calling computeEffectiveFeatures directly
|
|
96
|
+
// keeps this in lockstep with the runtime's own dispatcher-facing callback.
|
|
97
|
+
const globalCascade = toggleRuntime.effectiveFeatures();
|
|
98
|
+
for (const name of globalCascade) {
|
|
99
|
+
if (!tierManaged.has(name)) result.add(name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// requires() cascade over the combined result: a feature narrowed off by
|
|
103
|
+
// either loop above (tier-managed override, or absent from both sets)
|
|
104
|
+
// must also drop anything that itself requires it — computeEffectiveFeatures
|
|
105
|
+
// enforces this for its own single reader, but tierSet's membership isn't
|
|
106
|
+
// reader-driven, so the cascade has to be re-applied over the union.
|
|
107
|
+
let changed = true;
|
|
108
|
+
while (changed) {
|
|
109
|
+
changed = false;
|
|
110
|
+
for (const name of [...result]) {
|
|
111
|
+
const feature = registry.getFeature(name);
|
|
112
|
+
if (!feature) {
|
|
113
|
+
result.delete(name);
|
|
114
|
+
changed = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
for (const dep of feature.requires) {
|
|
118
|
+
if (!result.has(dep)) {
|
|
119
|
+
result.delete(name);
|
|
120
|
+
changed = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (tierResolver.trialGate) {
|
|
131
|
+
return Object.assign(composed, { trialGate: tierResolver.trialGate });
|
|
132
|
+
}
|
|
133
|
+
return composed;
|
|
134
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
export const FEATURE_TOGGLES_FEATURE = "feature-toggles" as const;
|
|
3
|
+
|
|
1
4
|
// Fully-qualified event name for feature-toggle changes. Kept as a constant
|
|
2
5
|
// so write-handler + tests reference one source.
|
|
3
6
|
export const FEATURE_TOGGLE_SET_EVENT_NAME = "feature-toggles:event:toggle-set";
|
|
@@ -18,3 +21,14 @@ export const FeatureToggleErrors = {
|
|
|
18
21
|
// Prevents typos from silently piling up orphan rows.
|
|
19
22
|
unknownFeature: "unknown_feature",
|
|
20
23
|
} as const;
|
|
24
|
+
|
|
25
|
+
export const FeatureToggleHandlers = {
|
|
26
|
+
set: "feature-toggles:write:set",
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export const FeatureToggleQueries = {
|
|
30
|
+
list: "feature-toggles:query:list",
|
|
31
|
+
registered: "feature-toggles:query:registered",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
export const TOGGLE_ADMIN_SCREEN_ID = "toggle-admin" as const;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { FEATURE_TOGGLE_SET_EVENT_NAME } from "./constants";
|
|
2
|
+
import { FEATURE_TOGGLE_SET_EVENT_NAME, TOGGLE_ADMIN_SCREEN_ID } from "./constants";
|
|
3
3
|
import { featureToggleSetSchema } from "./events";
|
|
4
|
+
import { globalFeatureStateTableMeta } from "./global-feature-state-table";
|
|
4
5
|
import { listQuery } from "./handlers/list.query";
|
|
5
6
|
import { registeredQuery } from "./handlers/registered.query";
|
|
6
7
|
import { createSetWriteHandler } from "./handlers/set.write";
|
|
8
|
+
import { FEATURE_TOGGLES_I18N } from "./i18n";
|
|
7
9
|
import type { GlobalFeatureToggleRuntime } from "./toggle-runtime";
|
|
8
10
|
|
|
9
11
|
// IMPORTANT: feature-toggles itself is NOT r.toggleable. Making it
|
|
@@ -38,7 +40,7 @@ export function createFeatureTogglesFeature(
|
|
|
38
40
|
): FeatureDefinition {
|
|
39
41
|
return defineFeature("feature-toggles", (r) => {
|
|
40
42
|
r.describe(
|
|
41
|
-
'Persists per-feature enabled/disabled state in the `
|
|
43
|
+
'Persists per-feature enabled/disabled state in the `store_global_feature_state` table and exposes a `set` write-handler plus `list`/`registered` query-handlers so operators can flip features at runtime without redeploying. Each API instance keeps an in-memory `GlobalFeatureToggleRuntime` snapshot (initialize it via `createFeatureToggleRuntime`, pass a `() => runtime` accessor to `createFeatureTogglesFeature`) that the dispatcher gate reads on every request; a `toggle-cache-sync` multi-stream projection with `delivery: "per-instance"` syncs the snapshot across instances whenever a `toggle-set` event is appended.',
|
|
42
44
|
);
|
|
43
45
|
r.uiHints({
|
|
44
46
|
displayLabel: "Feature Toggles · Operator Switches",
|
|
@@ -46,6 +48,9 @@ export function createFeatureTogglesFeature(
|
|
|
46
48
|
recommended: false,
|
|
47
49
|
});
|
|
48
50
|
r.systemScope();
|
|
51
|
+
r.storeTable(globalFeatureStateTableMeta, {
|
|
52
|
+
reason: "feature_toggles.global_override_snapshot",
|
|
53
|
+
});
|
|
49
54
|
|
|
50
55
|
// Toggle-change domain event. The event ends up in the events-table
|
|
51
56
|
// alongside every other write — audit.list picks it up automatically,
|
|
@@ -62,6 +67,21 @@ export function createFeatureTogglesFeature(
|
|
|
62
67
|
registered: r.queryHandler(registeredQuery),
|
|
63
68
|
};
|
|
64
69
|
|
|
70
|
+
r.screen({
|
|
71
|
+
id: TOGGLE_ADMIN_SCREEN_ID,
|
|
72
|
+
type: "custom",
|
|
73
|
+
renderer: { react: { __component: "ToggleAdminScreen" } },
|
|
74
|
+
access: { roles: ["SystemAdmin"] },
|
|
75
|
+
});
|
|
76
|
+
r.nav({
|
|
77
|
+
id: "toggle-admin",
|
|
78
|
+
label: "feature-toggles:nav.toggleAdmin",
|
|
79
|
+
screen: "feature-toggles:screen:toggle-admin",
|
|
80
|
+
order: 20,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
r.translations({ keys: FEATURE_TOGGLES_I18N });
|
|
84
|
+
|
|
65
85
|
// toggle-cache-sync — multi-instance snapshot propagation. Every
|
|
66
86
|
// API/worker instance runs its own dispatcher cursor on this MSP
|
|
67
87
|
// (delivery: "per-instance") and converges its in-memory snapshot on
|
|
@@ -111,7 +131,7 @@ export function createFeatureTogglesFeature(
|
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
export { FEATURE_TOGGLE_SET_EVENT_NAME, FeatureToggleErrors } from "./constants";
|
|
114
|
-
export { globalFeatureStateTable } from "./global-feature-state-table";
|
|
134
|
+
export { globalFeatureStateTable, globalFeatureStateTableMeta } from "./global-feature-state-table";
|
|
115
135
|
// Re-export the runtime factory + class so app-boot code has a single
|
|
116
136
|
// import path: "@cosmicdrift/kumiko-bundled-features/feature-toggles".
|
|
117
137
|
export {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { asEntityTableMeta } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import {
|
|
2
3
|
boolean,
|
|
4
|
+
type EntityTableMeta,
|
|
3
5
|
instant,
|
|
4
6
|
integer,
|
|
5
7
|
table as pgTable,
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
// name IS the identity here. No tenantId: this is a global override that
|
|
16
18
|
// applies across every tenant (per-tenant toggles are intentionally out of
|
|
17
19
|
// scope, see core-feature-toggles.md).
|
|
18
|
-
export const globalFeatureStateTable = pgTable("
|
|
20
|
+
export const globalFeatureStateTable = pgTable("store_global_feature_state", {
|
|
19
21
|
featureName: text("feature_name").primaryKey(),
|
|
20
22
|
enabled: boolean("enabled").notNull(),
|
|
21
23
|
// Optimistic-lock column. The set-handler reads the existing row, then
|
|
@@ -26,3 +28,20 @@ export const globalFeatureStateTable = pgTable("read_global_feature_state", {
|
|
|
26
28
|
// UserId (text — SessionUser.id is a uuid string post-ES).
|
|
27
29
|
updatedBy: text("updated_by"),
|
|
28
30
|
});
|
|
31
|
+
|
|
32
|
+
// r.storeTable meta — without this, collectTableMetas(FEATURES) never
|
|
33
|
+
// sees the table, so `kumiko schema generate` reports no changes and no
|
|
34
|
+
// app ever gets a migration for it (framework gap, not app-local). Derived
|
|
35
|
+
// from the pgTable above instead of hand-written a second time — the two
|
|
36
|
+
// used to drift independently (framework#1529): a column added only to
|
|
37
|
+
// the pgTable made queries type-check while the migration generator stayed
|
|
38
|
+
// blind to it, failing in prod with "column ... does not exist" while every
|
|
39
|
+
// mocked-stack test stayed green.
|
|
40
|
+
const derivedMeta = asEntityTableMeta(globalFeatureStateTable);
|
|
41
|
+
if (!derivedMeta) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"global-feature-state-table: asEntityTableMeta(globalFeatureStateTable) returned undefined " +
|
|
44
|
+
"— the pgTable definition no longer round-trips through the unmanaged-table meta builder.",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
export const globalFeatureStateTableMeta: EntityTableMeta = derivedMeta;
|
|
@@ -10,7 +10,7 @@ import { globalFeatureStateTable } from "../global-feature-state-table";
|
|
|
10
10
|
export const listQuery = defineQueryHandler({
|
|
11
11
|
name: "list",
|
|
12
12
|
schema: z.object({}),
|
|
13
|
-
access: { roles: ["SystemAdmin"
|
|
13
|
+
access: { roles: ["SystemAdmin"] },
|
|
14
14
|
handler: async (_event, ctx) => {
|
|
15
15
|
type Row = {
|
|
16
16
|
featureName: string;
|
|
@@ -14,7 +14,7 @@ import { globalFeatureStateTable } from "../global-feature-state-table";
|
|
|
14
14
|
export const registeredQuery = defineQueryHandler({
|
|
15
15
|
name: "registered",
|
|
16
16
|
schema: z.object({}),
|
|
17
|
-
access: { roles: ["SystemAdmin"
|
|
17
|
+
access: { roles: ["SystemAdmin"] },
|
|
18
18
|
handler: async (_event, ctx) => {
|
|
19
19
|
type OverrideRow = { featureName: string; enabled: boolean };
|
|
20
20
|
const overrideRows = await selectMany<OverrideRow>(ctx.db.raw, globalFeatureStateTable);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type LocalizedString = { readonly de: string; readonly en: string };
|
|
2
|
+
|
|
3
|
+
export const FEATURE_TOGGLES_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
4
|
+
"screen:toggle-admin.title": { de: "Feature-Toggles", en: "Feature toggles" },
|
|
5
|
+
"feature-toggles:nav.toggleAdmin": { de: "Feature-Toggles", en: "Feature toggles" },
|
|
6
|
+
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export { composeTierResolverWithGlobalToggles } from "./compose-tier-resolver";
|
|
2
|
+
export {
|
|
3
|
+
FEATURE_TOGGLES_FEATURE,
|
|
4
|
+
FeatureToggleHandlers,
|
|
5
|
+
FeatureToggleQueries,
|
|
6
|
+
TOGGLE_ADMIN_SCREEN_ID,
|
|
7
|
+
} from "./constants";
|
|
1
8
|
export {
|
|
2
9
|
createFeatureToggleRuntime,
|
|
3
10
|
createFeatureTogglesFeature,
|
|
@@ -6,4 +13,5 @@ export {
|
|
|
6
13
|
type FeatureTogglesOptions,
|
|
7
14
|
GlobalFeatureToggleRuntime,
|
|
8
15
|
globalFeatureStateTable,
|
|
16
|
+
globalFeatureStateTableMeta,
|
|
9
17
|
} from "./feature";
|
|
@@ -47,6 +47,20 @@ export class GlobalFeatureToggleRuntime {
|
|
|
47
47
|
this.snapshot.set(featureName, enabled);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Raw per-feature override, bypassing the requires() cascade —
|
|
51
|
+
// `undefined` means "no explicit row, inherits toggleableDefault"
|
|
52
|
+
// (distinct from an explicit `false`). Used by composeTierResolver to
|
|
53
|
+
// layer a global kill-switch UNDER a tenantTierResolver: the composed
|
|
54
|
+
// resolver must only ever narrow what the tier already grants, never
|
|
55
|
+
// fall back to a toggleable feature's declared default the way
|
|
56
|
+
// effectiveFeatures()/computeEffectiveFeatures do — that default exists
|
|
57
|
+
// for apps with no tier resolver, and would otherwise turn tier-gated
|
|
58
|
+
// toggleable features (e.g. a Team-tier feature defaulting off) globally
|
|
59
|
+
// off the moment feature-toggles is composed in.
|
|
60
|
+
readOverride(featureName: string): boolean | undefined {
|
|
61
|
+
return this.snapshot.get(featureName);
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
// The callback shape the dispatcher expects. Computes the effective
|
|
51
65
|
// feature set from the current snapshot + the registry's requires()
|
|
52
66
|
// cascade every call. Cheap: the cascade is a DFS over O(features);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import { mergeTranslations, type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
4
|
+
import { FEATURE_TOGGLES_FEATURE, TOGGLE_ADMIN_SCREEN_ID } from "../constants";
|
|
5
|
+
import { defaultTranslations } from "./i18n";
|
|
6
|
+
import { ToggleAdminScreen } from "./toggle-admin-screen";
|
|
7
|
+
|
|
8
|
+
export type FeatureTogglesClientOptions = {
|
|
9
|
+
readonly translations?: TranslationsByLocale;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function featureTogglesClient(
|
|
13
|
+
options?: FeatureTogglesClientOptions,
|
|
14
|
+
): ClientFeatureDefinition {
|
|
15
|
+
return {
|
|
16
|
+
name: FEATURE_TOGGLES_FEATURE,
|
|
17
|
+
translations: mergeTranslations(defaultTranslations, options?.translations ?? {}),
|
|
18
|
+
components: {
|
|
19
|
+
[TOGGLE_ADMIN_SCREEN_ID]: ToggleAdminScreen,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
|
|
4
|
+
export const defaultTranslations: TranslationsByLocale = {
|
|
5
|
+
de: {
|
|
6
|
+
"feature-toggles.admin.title": "Feature-Toggles",
|
|
7
|
+
"feature-toggles.admin.loading": "Lade Feature-Toggles…",
|
|
8
|
+
"feature-toggles.admin.col.feature": "Feature",
|
|
9
|
+
"feature-toggles.admin.col.default": "Default",
|
|
10
|
+
"feature-toggles.admin.col.override": "Override",
|
|
11
|
+
"feature-toggles.admin.col.effective": "Effektiv",
|
|
12
|
+
"feature-toggles.admin.toggle": "Umschalten",
|
|
13
|
+
"feature-toggles.admin.saving": "Speichert…",
|
|
14
|
+
"feature-toggles.admin.alwaysOn": "immer an",
|
|
15
|
+
"feature-toggles:nav.toggleAdmin": "Feature-Toggles",
|
|
16
|
+
},
|
|
17
|
+
en: {
|
|
18
|
+
"feature-toggles.admin.title": "Feature toggles",
|
|
19
|
+
"feature-toggles.admin.loading": "Loading feature toggles…",
|
|
20
|
+
"feature-toggles.admin.col.feature": "Feature",
|
|
21
|
+
"feature-toggles.admin.col.default": "Default",
|
|
22
|
+
"feature-toggles.admin.col.override": "Override",
|
|
23
|
+
"feature-toggles.admin.col.effective": "Effective",
|
|
24
|
+
"feature-toggles.admin.toggle": "Toggle",
|
|
25
|
+
"feature-toggles.admin.saving": "Saving…",
|
|
26
|
+
"feature-toggles.admin.alwaysOn": "always on",
|
|
27
|
+
"feature-toggles:nav.toggleAdmin": "Feature toggles",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { FormScreenShell } from "@cosmicdrift/kumiko-renderer-web";
|
|
4
|
+
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { FeatureToggleHandlers, FeatureToggleQueries } from "../constants";
|
|
6
|
+
|
|
7
|
+
type ToggleRow = {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly toggleable: boolean;
|
|
10
|
+
readonly default: boolean | null;
|
|
11
|
+
readonly override: boolean | null;
|
|
12
|
+
readonly effective: boolean | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type RegisteredResponse = {
|
|
16
|
+
readonly items: readonly ToggleRow[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type State =
|
|
20
|
+
| { readonly kind: "loading" }
|
|
21
|
+
| { readonly kind: "error"; readonly message: string }
|
|
22
|
+
| { readonly kind: "ready"; readonly items: readonly ToggleRow[] };
|
|
23
|
+
|
|
24
|
+
export function ToggleAdminScreen(): ReactNode {
|
|
25
|
+
const t = useTranslation();
|
|
26
|
+
const { Banner, Card, DataTable, Heading, Text } = usePrimitives();
|
|
27
|
+
const dispatcher = useDispatcher();
|
|
28
|
+
const [state, setState] = useState<State>({ kind: "loading" });
|
|
29
|
+
const [actionError, setActionError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
const load = useCallback(async (): Promise<void> => {
|
|
32
|
+
setState({ kind: "loading" });
|
|
33
|
+
const res = await dispatcher.query<RegisteredResponse>(FeatureToggleQueries.registered, {});
|
|
34
|
+
if (!res.isSuccess) {
|
|
35
|
+
setState({ kind: "error", message: res.error.message });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setState({ kind: "ready", items: res.data.items });
|
|
39
|
+
}, [dispatcher]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
void load();
|
|
43
|
+
}, [load]);
|
|
44
|
+
|
|
45
|
+
const onSet = async (featureName: string, enabled: boolean): Promise<void> => {
|
|
46
|
+
setActionError(null);
|
|
47
|
+
const res = await dispatcher.write(FeatureToggleHandlers.set, { featureName, enabled });
|
|
48
|
+
if (!res.isSuccess) {
|
|
49
|
+
setActionError(res.error.message);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await load();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (state.kind === "loading") {
|
|
56
|
+
return (
|
|
57
|
+
<FormScreenShell testId="toggle-admin-screen">
|
|
58
|
+
<Text variant="small">{t("feature-toggles.admin.loading")}</Text>
|
|
59
|
+
</FormScreenShell>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (state.kind === "error") {
|
|
64
|
+
return (
|
|
65
|
+
<FormScreenShell testId="toggle-admin-screen">
|
|
66
|
+
<Banner variant="error">{state.message}</Banner>
|
|
67
|
+
</FormScreenShell>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<FormScreenShell testId="toggle-admin-screen" maxWidth="4xl" className="flex flex-col gap-6">
|
|
73
|
+
<Heading variant="page">{t("feature-toggles.admin.title")}</Heading>
|
|
74
|
+
|
|
75
|
+
{actionError !== null && <Banner variant="error">{actionError}</Banner>}
|
|
76
|
+
|
|
77
|
+
<Card options={{ padded: false }}>
|
|
78
|
+
<DataTable
|
|
79
|
+
testId="toggle-admin-table"
|
|
80
|
+
columns={[
|
|
81
|
+
{
|
|
82
|
+
field: "feature",
|
|
83
|
+
label: t("feature-toggles.admin.col.feature"),
|
|
84
|
+
type: "string",
|
|
85
|
+
sortable: false,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
field: "default",
|
|
89
|
+
label: t("feature-toggles.admin.col.default"),
|
|
90
|
+
type: "string",
|
|
91
|
+
sortable: false,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
field: "override",
|
|
95
|
+
label: t("feature-toggles.admin.col.override"),
|
|
96
|
+
type: "string",
|
|
97
|
+
sortable: false,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
field: "effective",
|
|
101
|
+
label: t("feature-toggles.admin.col.effective"),
|
|
102
|
+
type: "string",
|
|
103
|
+
sortable: false,
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
rows={state.items.map((row) => ({
|
|
107
|
+
id: row.name,
|
|
108
|
+
values: {
|
|
109
|
+
feature: row.name,
|
|
110
|
+
default: formatFlag(row.default),
|
|
111
|
+
override: formatFlag(row.override),
|
|
112
|
+
effective: formatFlag(row.effective),
|
|
113
|
+
toggleable: row.toggleable,
|
|
114
|
+
effectiveBool: row.effective ?? row.default ?? false,
|
|
115
|
+
},
|
|
116
|
+
}))}
|
|
117
|
+
rowActions={[
|
|
118
|
+
{
|
|
119
|
+
id: "toggle",
|
|
120
|
+
label: t("feature-toggles.admin.toggle"),
|
|
121
|
+
style: "secondary",
|
|
122
|
+
isVisible: (row) => row.values["toggleable"] === true,
|
|
123
|
+
onTrigger: (row) => void onSet(row.id, !(row.values["effectiveBool"] as boolean)),
|
|
124
|
+
},
|
|
125
|
+
]}
|
|
126
|
+
rowActionMode="inline"
|
|
127
|
+
/>
|
|
128
|
+
</Card>
|
|
129
|
+
</FormScreenShell>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatFlag(value: boolean | null): string {
|
|
134
|
+
if (value === null) return "inherit";
|
|
135
|
+
return value ? "on" : "off";
|
|
136
|
+
}
|
|
@@ -20,11 +20,11 @@ describe("fileFoundationFeature — shape", () => {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
describe("fileFoundationFeature.exports — typed handles", () => {
|
|
23
|
-
test("exposes
|
|
24
|
-
|
|
25
|
-
expect(
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
test("exposes the provider-selector handle", () => {
|
|
24
|
+
expect(fileFoundationFeature.exports.providerConfigKey).toBeDefined();
|
|
25
|
+
expect(fileFoundationFeature.exports.providerConfigKey.name).toBe(
|
|
26
|
+
"file-foundation:config:provider",
|
|
27
|
+
);
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
8
8
|
import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
10
10
|
import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
19
19
|
import {
|
|
20
20
|
createMutableMasterKeyProvider,
|
|
21
|
+
createTestEnvelopeCipher,
|
|
21
22
|
type MutableMasterKeyProvider,
|
|
22
23
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
23
24
|
import { z } from "zod";
|
|
@@ -73,8 +74,8 @@ let providerRef: MutableMasterKeyProvider;
|
|
|
73
74
|
const testEncryptionKey = randomBytes(32).toString("base64");
|
|
74
75
|
|
|
75
76
|
beforeAll(async () => {
|
|
76
|
-
const encryption =
|
|
77
|
-
resolver = createConfigResolver({ encryption });
|
|
77
|
+
const encryption = createTestEnvelopeCipher(testEncryptionKey);
|
|
78
|
+
resolver = createConfigResolver({ cipher: encryption });
|
|
78
79
|
|
|
79
80
|
const initialKp = createEnvMasterKeyProvider({
|
|
80
81
|
env: {
|
|
@@ -54,18 +54,17 @@ export const fileFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
57
|
+
const providerConfigKey = r.config(
|
|
58
|
+
"provider",
|
|
59
|
+
createTenantConfig("text", {
|
|
60
|
+
default: "",
|
|
61
|
+
write: access.roles("TenantAdmin", "SystemAdmin"),
|
|
62
|
+
read: access.roles("TenantAdmin", "SystemAdmin", "User"),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
66
65
|
// Readiness gating: provider-plugins' required keys/secrets count only
|
|
67
66
|
// while their plugin is the one this key selects.
|
|
68
|
-
r.extensionSelector(EXT_FILE_PROVIDER,
|
|
67
|
+
r.extensionSelector(EXT_FILE_PROVIDER, providerConfigKey);
|
|
69
68
|
|
|
70
|
-
return {
|
|
69
|
+
return { providerConfigKey };
|
|
71
70
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import type { FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
|
|
3
|
+
import { describeFileProviderContract } from "@cosmicdrift/kumiko-framework/testing";
|
|
3
4
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
4
5
|
import { createS3ProviderFromEnv } from "../env-helper";
|
|
5
6
|
import { createS3Provider } from "../s3-provider";
|
|
@@ -28,13 +29,31 @@ function uniqueKey(suffix: string): string {
|
|
|
28
29
|
|
|
29
30
|
let provider: FileStorageProvider;
|
|
30
31
|
|
|
31
|
-
beforeAll(() => {
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
const endpoint = requireEnv("MINIO_ENDPOINT");
|
|
34
|
+
// Fail loud with an actionable message when Minio is down (otherwise
|
|
35
|
+
// S3 networking errors look like provider bugs).
|
|
36
|
+
try {
|
|
37
|
+
const health = await fetch(`${endpoint.replace(/\/$/, "")}/minio/health/live`, {
|
|
38
|
+
signal: AbortSignal.timeout(2000),
|
|
39
|
+
});
|
|
40
|
+
if (!health.ok) {
|
|
41
|
+
throw new Error(`HTTP ${health.status}`);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`MinIO not reachable at ${endpoint} — run: docker compose up -d minio minio-init ` +
|
|
46
|
+
`(kumiko-framework compose, default port 19000). ` +
|
|
47
|
+
`Cause: ${err instanceof Error ? err.message : String(err)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
// forcePathStyle is deliberately NOT set — resolveForcePathStyle() must
|
|
33
52
|
// auto-detect path-style from the `endpoint` presence. If auto-detection
|
|
34
53
|
// regressed, Minio would reject virtual-host-style URLs (bucket.host/key)
|
|
35
54
|
// and every round-trip below would fail. That's the proof.
|
|
36
55
|
provider = createS3Provider({
|
|
37
|
-
endpoint
|
|
56
|
+
endpoint,
|
|
38
57
|
region: requireEnv("MINIO_REGION"),
|
|
39
58
|
accessKeyId: requireEnv("MINIO_ACCESS_KEY"),
|
|
40
59
|
secretAccessKey: requireEnv("MINIO_SECRET_KEY"),
|
|
@@ -201,3 +220,9 @@ describe("createS3ProviderFromEnv", () => {
|
|
|
201
220
|
expect(() => createS3ProviderFromEnv("NON_EXISTENT_PREFIX_")).toThrow(/missing_env/);
|
|
202
221
|
});
|
|
203
222
|
});
|
|
223
|
+
|
|
224
|
+
describeFileProviderContract("S3Provider (Minio)", () => provider);
|
|
225
|
+
|
|
226
|
+
describeFileProviderContract("createS3ProviderFromEnv (Minio)", () =>
|
|
227
|
+
createS3ProviderFromEnv("MINIO_"),
|
|
228
|
+
);
|