@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,239 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
|
|
4
|
+
import { selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
5
|
+
import {
|
|
6
|
+
configureBlindIndexKey,
|
|
7
|
+
configurePiiSubjectKms,
|
|
8
|
+
InMemoryKmsAdapter,
|
|
9
|
+
isPiiCiphertext,
|
|
10
|
+
resetBlindIndexKeyForTests,
|
|
11
|
+
resetPiiSubjectKmsForTests,
|
|
12
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
13
|
+
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
+
import {
|
|
15
|
+
setupTestStack,
|
|
16
|
+
type TestStack,
|
|
17
|
+
testTenantId,
|
|
18
|
+
unsafeCreateEntityTable,
|
|
19
|
+
unsafePushTables,
|
|
20
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
+
import { createTestEnvelopeCipher, deleteRows } from "@cosmicdrift/kumiko-framework/testing";
|
|
22
|
+
import { Temporal } from "temporal-polyfill";
|
|
23
|
+
import { AuthHandlers } from "../../auth-email-password/constants";
|
|
24
|
+
import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
|
|
25
|
+
import { authFoundationFeature, resolveTokenVerifier } from "../../auth-foundation";
|
|
26
|
+
import { createConfigFeature } from "../../config";
|
|
27
|
+
import { createConfigResolver } from "../../config/resolver";
|
|
28
|
+
import { configValuesTable } from "../../config/table";
|
|
29
|
+
import { makeSessionHelpers } from "../../sessions/__tests__/test-helpers";
|
|
30
|
+
import { SessionQueries } from "../../sessions/constants";
|
|
31
|
+
import { createSessionsFeature } from "../../sessions/feature";
|
|
32
|
+
import { userSessionEntity } from "../../sessions/schema/user-session";
|
|
33
|
+
import { createTenantFeature } from "../../tenant";
|
|
34
|
+
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
35
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
36
|
+
import { createUserFeature } from "../../user/feature";
|
|
37
|
+
import { userEntity } from "../../user/schema/user";
|
|
38
|
+
import { PatHandlers, PatQueries } from "../constants";
|
|
39
|
+
import { createPersonalAccessTokensFeature } from "../feature";
|
|
40
|
+
import { apiTokenEntity, apiTokenTable } from "../schema/api-token";
|
|
41
|
+
import type { PatScopeConfig } from "../scopes";
|
|
42
|
+
|
|
43
|
+
// Full loop, no mocks: mint a PAT via the create handler → use it as a bearer
|
|
44
|
+
// token over real HTTP. The resolver hashes it, resolves live roles, and the
|
|
45
|
+
// API boundary enforces the granted scopes. Proves the security matrix:
|
|
46
|
+
// allowed → 200, out-of-scope → 403 (while a JWT user could call it),
|
|
47
|
+
// revoked/expired/unknown/membership-removed → 401.
|
|
48
|
+
|
|
49
|
+
let stack: TestStack;
|
|
50
|
+
let h: ReturnType<typeof makeSessionHelpers>;
|
|
51
|
+
|
|
52
|
+
const encryptionKey = randomBytes(32).toString("base64");
|
|
53
|
+
const TENANT: TenantId = testTenantId(1);
|
|
54
|
+
|
|
55
|
+
// One domain "tokens" whose read set is exactly the two PAT queries —
|
|
56
|
+
// deliberately NOT sessions:query:user-session:mine, so that QN is the
|
|
57
|
+
// out-of-scope probe. Granted as "tokens:read".
|
|
58
|
+
const SCOPES: PatScopeConfig = {
|
|
59
|
+
tokens: { label: "Tokens", read: [PatQueries.mine, PatQueries.availableScopes] },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
async function mintToken(
|
|
63
|
+
actor: SessionUser,
|
|
64
|
+
opts?: { scopes?: string[]; expiresInDays?: number },
|
|
65
|
+
): Promise<string> {
|
|
66
|
+
const res = await stack.http.writeOk<{ id: string; token: string }>(
|
|
67
|
+
PatHandlers.create,
|
|
68
|
+
{
|
|
69
|
+
name: "test",
|
|
70
|
+
scopes: opts?.scopes ?? ["tokens:read"],
|
|
71
|
+
...(opts?.expiresInDays ? { expiresInDays: opts.expiresInDays } : {}),
|
|
72
|
+
},
|
|
73
|
+
actor,
|
|
74
|
+
);
|
|
75
|
+
return res.token;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeAll(async () => {
|
|
79
|
+
const encryption = createTestEnvelopeCipher(encryptionKey);
|
|
80
|
+
const resolver = createConfigResolver({ cipher: encryption });
|
|
81
|
+
|
|
82
|
+
stack = await setupTestStack({
|
|
83
|
+
features: [
|
|
84
|
+
createConfigFeature(),
|
|
85
|
+
createUserFeature(),
|
|
86
|
+
createTenantFeature(),
|
|
87
|
+
createAuthEmailPasswordFeature(),
|
|
88
|
+
createSessionsFeature(),
|
|
89
|
+
createPersonalAccessTokensFeature({ scopes: SCOPES }),
|
|
90
|
+
authFoundationFeature,
|
|
91
|
+
],
|
|
92
|
+
extraContext: { configResolver: resolver, configEncryption: encryption },
|
|
93
|
+
authConfig: {
|
|
94
|
+
membershipQuery: "tenant:query:memberships",
|
|
95
|
+
loginHandler: AuthHandlers.login,
|
|
96
|
+
// PAT registers itself as a tokenVerifier provider (auth-foundation)
|
|
97
|
+
// instead of the app wiring a dedicated patResolver — resolved
|
|
98
|
+
// generically here, same as run-prod-app/run-dev-app do.
|
|
99
|
+
tokenVerifier: (raw: string) =>
|
|
100
|
+
resolveTokenVerifier({ db: stack.db, registry: stack.registry }, raw),
|
|
101
|
+
// Low per-token cap so the rate-limit test can exhaust it. Other tests do
|
|
102
|
+
// ≤2 requests per (distinct) token, so this ceiling never trips them.
|
|
103
|
+
patRateLimiter: createInMemoryLoginRateLimiter(3, 60_000),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
h = makeSessionHelpers(stack, TENANT);
|
|
107
|
+
|
|
108
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
109
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
110
|
+
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
111
|
+
await unsafeCreateEntityTable(stack.db, apiTokenEntity);
|
|
112
|
+
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterAll(async () => {
|
|
116
|
+
await stack.cleanup();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
async function actorFor(email: string): Promise<SessionUser> {
|
|
120
|
+
const { userId } = await h.seedUser(email, "pw");
|
|
121
|
+
return { id: userId, tenantId: TENANT, roles: ["User"] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Raw login → JWT (no sid: session callbacks aren't wired here, and the PAT
|
|
125
|
+
// path doesn't need them). Used only to prove a non-PAT bearer skips the gate.
|
|
126
|
+
async function loginToken(email: string): Promise<string> {
|
|
127
|
+
const res = await stack.http.raw("POST", "/api/auth/login", { email, password: "pw" });
|
|
128
|
+
expect(res.status).toBe(200);
|
|
129
|
+
return ((await res.json()) as { token: string }).token;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe("PAT auth", () => {
|
|
133
|
+
test("granted scope → 200", async () => {
|
|
134
|
+
const actor = await actorFor("granted@example.com");
|
|
135
|
+
const token = await mintToken(actor);
|
|
136
|
+
const res = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
137
|
+
expect(res.status).toBe(200);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("out-of-scope QN → 403 (but a JWT user may call it)", async () => {
|
|
141
|
+
const actor = await actorFor("scope@example.com");
|
|
142
|
+
const token = await mintToken(actor);
|
|
143
|
+
const denied = await h.authedPost("/api/query", token, {
|
|
144
|
+
type: SessionQueries.mine,
|
|
145
|
+
payload: {},
|
|
146
|
+
});
|
|
147
|
+
expect(denied.status).toBe(403);
|
|
148
|
+
|
|
149
|
+
const jwt = await loginToken("scope@example.com");
|
|
150
|
+
const allowed = await h.authedPost("/api/query", jwt, {
|
|
151
|
+
type: SessionQueries.mine,
|
|
152
|
+
payload: {},
|
|
153
|
+
});
|
|
154
|
+
expect(allowed.status).toBe(200);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("unknown token → 401", async () => {
|
|
158
|
+
const res = await h.authedPost("/api/query", "kpat_deadbeef", {
|
|
159
|
+
type: PatQueries.mine,
|
|
160
|
+
payload: {},
|
|
161
|
+
});
|
|
162
|
+
expect(res.status).toBe(401);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("revoked token → 401", async () => {
|
|
166
|
+
const actor = await actorFor("revoked@example.com");
|
|
167
|
+
const token = await mintToken(actor);
|
|
168
|
+
const rows = await stack.http.queryOk<Array<{ id: string }>>(PatQueries.mine, {}, actor);
|
|
169
|
+
const id = rows[0]?.id;
|
|
170
|
+
expect(id).toBeDefined();
|
|
171
|
+
await stack.http.writeOk(PatHandlers.revoke, { id }, actor);
|
|
172
|
+
const res = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
173
|
+
expect(res.status).toBe(401);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("expired token → 401", async () => {
|
|
177
|
+
const actor = await actorFor("expired@example.com");
|
|
178
|
+
const token = await mintToken(actor);
|
|
179
|
+
const past = Temporal.Now.instant().subtract({ hours: 1 });
|
|
180
|
+
const rows =
|
|
181
|
+
(await stack.http.queryOk<Array<{ id: string }>>(PatQueries.mine, {}, actor)) ?? [];
|
|
182
|
+
await updateMany(stack.db, apiTokenTable, { expiresAt: past }, { id: rows[0]?.id });
|
|
183
|
+
const res = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
184
|
+
expect(res.status).toBe(401);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("per-token rate limit → 429 once the cap is exceeded", async () => {
|
|
188
|
+
const actor = await actorFor("ratelimit@example.com");
|
|
189
|
+
const token = await mintToken(actor);
|
|
190
|
+
for (let i = 0; i < 3; i++) {
|
|
191
|
+
const ok = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
192
|
+
expect(ok.status).toBe(200);
|
|
193
|
+
}
|
|
194
|
+
const limited = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
195
|
+
expect(limited.status).toBe(429);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("membership removed → 401 (live roles, no snapshot)", async () => {
|
|
199
|
+
const actor = await actorFor("removed@example.com");
|
|
200
|
+
const token = await mintToken(actor);
|
|
201
|
+
const ok = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
202
|
+
expect(ok.status).toBe(200);
|
|
203
|
+
// tenantMembershipsTable is ES-managed (executor-only branded); deleteRows
|
|
204
|
+
// is the test-side escape for out-of-band row removal.
|
|
205
|
+
await deleteRows(stack.db, tenantMembershipsTable, { userId: actor.id });
|
|
206
|
+
const res = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
207
|
+
expect(res.status).toBe(401);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("PAT with active KMS (#820): token name is userOwned PII", () => {
|
|
212
|
+
test("create stores ciphertext at rest, list returns the plaintext name", async () => {
|
|
213
|
+
configurePiiSubjectKms(new InMemoryKmsAdapter());
|
|
214
|
+
configureBlindIndexKey(Buffer.alloc(32, 7).toString("base64"));
|
|
215
|
+
try {
|
|
216
|
+
const actor = await actorFor("kms-pat@example.com");
|
|
217
|
+
const token = await mintToken(actor);
|
|
218
|
+
|
|
219
|
+
const rows = await stack.http.queryOk<Array<{ id: string; name: string }>>(
|
|
220
|
+
PatQueries.mine,
|
|
221
|
+
{},
|
|
222
|
+
actor,
|
|
223
|
+
);
|
|
224
|
+
expect(rows[0]?.name).toBe("test");
|
|
225
|
+
|
|
226
|
+
const stored = await selectMany<{ name: string }>(stack.db, apiTokenTable, {
|
|
227
|
+
id: rows[0]?.id,
|
|
228
|
+
});
|
|
229
|
+
expect(isPiiCiphertext(stored[0]?.name)).toBe(true);
|
|
230
|
+
|
|
231
|
+
// Der Token selbst funktioniert weiter (resolver liest tokenHash, nie name).
|
|
232
|
+
const res = await h.authedPost("/api/query", token, { type: PatQueries.mine, payload: {} });
|
|
233
|
+
expect(res.status).toBe(200);
|
|
234
|
+
} finally {
|
|
235
|
+
resetPiiSubjectKmsForTests();
|
|
236
|
+
resetBlindIndexKeyForTests();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { expandScopes, type PatScopeConfig, parseGrant } from "../scopes";
|
|
3
|
+
|
|
4
|
+
const CONFIG: PatScopeConfig = {
|
|
5
|
+
credit: { label: "Kredite", read: ["credit:query:*"], write: ["credit:write:*"] },
|
|
6
|
+
miete: { label: "Mieten", read: ["ledger:query:*"] }, // read-only domain
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe("parseGrant", () => {
|
|
10
|
+
it("splits domain:level on the last colon", () => {
|
|
11
|
+
expect(parseGrant("credit:write")).toEqual({ domain: "credit", level: "write" });
|
|
12
|
+
});
|
|
13
|
+
it("rejects a bare token", () => {
|
|
14
|
+
expect(parseGrant("credit")).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("expandScopes", () => {
|
|
19
|
+
it("read grants only the read QNs", () => {
|
|
20
|
+
expect(expandScopes(CONFIG, ["credit:read"])).toEqual(["credit:query:*"]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("write grants read + write QNs", () => {
|
|
24
|
+
expect(expandScopes(CONFIG, ["credit:write"]).sort()).toEqual(
|
|
25
|
+
["credit:query:*", "credit:write:*"].sort(),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("read-only domain ignores a write grant's missing write set", () => {
|
|
30
|
+
expect(expandScopes(CONFIG, ["miete:write"])).toEqual(["ledger:query:*"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("unknown domain contributes nothing (fail-closed)", () => {
|
|
34
|
+
expect(expandScopes(CONFIG, ["ghost:write"])).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("unions across multiple grants without duplicates", () => {
|
|
38
|
+
expect(expandScopes(CONFIG, ["credit:read", "miete:read"]).sort()).toEqual(
|
|
39
|
+
["credit:query:*", "ledger:query:*"].sort(),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Pure constants — client-marked so the web screen (web/) may import screen-id
|
|
3
|
+
// and QN names without pulling the feature's server runtime barrel.
|
|
4
|
+
export const PAT_FEATURE = "personal-access-tokens";
|
|
5
|
+
|
|
6
|
+
// Snake_case reason strings (Error-Reasons guard: no colons/dashes).
|
|
7
|
+
export const PatErrors = {
|
|
8
|
+
ownershipDenied: "ownership_denied",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
// Dormant custom-screen id (r.screen) — the app places it via r.nav. The client
|
|
12
|
+
// maps it to the PatTokensScreen component.
|
|
13
|
+
export const PAT_SCREEN_ID = "api-tokens";
|
|
14
|
+
|
|
15
|
+
export const PatHandlers = {
|
|
16
|
+
create: "personal-access-tokens:write:create",
|
|
17
|
+
revoke: "personal-access-tokens:write:revoke",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export const PatQueries = {
|
|
21
|
+
mine: "personal-access-tokens:query:mine",
|
|
22
|
+
availableScopes: "personal-access-tokens:query:available-scopes",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
// Only the first chars of a minted token are stored (alongside the hash) so the
|
|
26
|
+
// UI can show "kpat_ab12…" for recognition without ever holding the secret.
|
|
27
|
+
export const PAT_PREFIX_DISPLAY_LENGTH = 12;
|
|
28
|
+
|
|
29
|
+
export type PatRateLimit = { readonly maxRequests: number; readonly windowMs: number };
|
|
30
|
+
|
|
31
|
+
// Default per-token request rate for PAT-authenticated calls. Generous enough
|
|
32
|
+
// for normal automation, low enough to blunt a leaked-token abuse spike.
|
|
33
|
+
export const PAT_DEFAULT_RATE_LIMIT: PatRateLimit = { maxRequests: 120, windowMs: 60_000 };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuthProviderPlugin,
|
|
3
|
+
EXT_TOKEN_VERIFIER,
|
|
4
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
5
|
+
import { PAT_TOKEN_PREFIX } from "@cosmicdrift/kumiko-framework/api";
|
|
6
|
+
import { buildEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
|
|
7
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { PAT_DEFAULT_RATE_LIMIT, PAT_FEATURE, PAT_SCREEN_ID, type PatRateLimit } from "./constants";
|
|
9
|
+
import { buildAvailableScopesQuery } from "./handlers/available-scopes.query";
|
|
10
|
+
import { createPatWrite } from "./handlers/create.write";
|
|
11
|
+
import { listPatQuery } from "./handlers/list.query";
|
|
12
|
+
import { revokePatWrite } from "./handlers/revoke.write";
|
|
13
|
+
import { PAT_FEATURE_I18N } from "./i18n";
|
|
14
|
+
import { createPatResolver } from "./resolver";
|
|
15
|
+
import { apiTokenEntity } from "./schema/api-token";
|
|
16
|
+
import type { PatScopeConfig } from "./scopes";
|
|
17
|
+
|
|
18
|
+
export type PersonalAccessTokensOptions = {
|
|
19
|
+
// The scopes this deployment offers. Each is a named bundle of QN globs a PAT
|
|
20
|
+
// may be granted (a scope can span features). Closed over by available-scopes
|
|
21
|
+
// (UI list) and exported so run-prod-app can build the resolver from the same
|
|
22
|
+
// single source.
|
|
23
|
+
readonly scopes: PatScopeConfig;
|
|
24
|
+
// Per-token request rate limit for PAT-authenticated calls. Defaults to
|
|
25
|
+
// PAT_DEFAULT_RATE_LIMIT (120/60s). run-prod-app builds the limiter from this.
|
|
26
|
+
readonly rateLimit?: PatRateLimit;
|
|
27
|
+
/** Make the whole feature tier-gatable via the tier-engine. Use
|
|
28
|
+
* { default: false } for fail-closed gating (feature off until a tier grants
|
|
29
|
+
* it). Omit to keep PAT always-on (default). */
|
|
30
|
+
readonly toggleable?: { readonly default: boolean };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type PatFeatureExports = {
|
|
34
|
+
readonly rateLimit: PatRateLimit;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Personal Access Tokens — long-lived, revocable bearer credentials for the
|
|
38
|
+
// HTTP API. Like `sessions`, the hot-path resolver is NOT a handler: it runs on
|
|
39
|
+
// every PAT-authenticated request and does a direct-DB point-read (see
|
|
40
|
+
// createPatResolver / the tokenVerifier extension-point registration below). The dispatcher-side handlers here
|
|
41
|
+
// only mint/list/revoke tokens and expose the scope catalog.
|
|
42
|
+
export function createPersonalAccessTokensFeature(
|
|
43
|
+
options: PersonalAccessTokensOptions,
|
|
44
|
+
): FeatureDefinition {
|
|
45
|
+
const { scopes } = options;
|
|
46
|
+
return defineFeature(PAT_FEATURE, (r) => {
|
|
47
|
+
r.describe(
|
|
48
|
+
"Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.",
|
|
49
|
+
);
|
|
50
|
+
r.uiHints({ displayLabel: "Personal Access Tokens", category: "identity", recommended: false });
|
|
51
|
+
// Opt-in tier-gating (mirrors ledger/tags): when set, the feature declares
|
|
52
|
+
// itself r.toggleable so the dispatcher gate + tier-engine can switch PAT
|
|
53
|
+
// on/off per tenant. { default: false } = fail-closed until a tier grants it.
|
|
54
|
+
if (options.toggleable !== undefined) r.toggleable(options.toggleable);
|
|
55
|
+
// Resolver reads memberships + users on every PAT request to build live
|
|
56
|
+
// roles — make both boot-time deps so a mis-wiring fails validateBoot.
|
|
57
|
+
// auth-foundation owns EXT_TOKEN_VERIFIER, which the useExtension below
|
|
58
|
+
// registers against.
|
|
59
|
+
r.requires("user", "tenant", "auth-foundation");
|
|
60
|
+
// Self-registers as a bearer-auth provider instead of the app wiring a
|
|
61
|
+
// dedicated patResolver callback — the middleware finds it generically
|
|
62
|
+
// via resolveTokenVerifier(), shape-matched by the PAT token prefix.
|
|
63
|
+
r.useExtension(EXT_TOKEN_VERIFIER, "pat", {
|
|
64
|
+
shape: { kind: "prefix", prefix: PAT_TOKEN_PREFIX },
|
|
65
|
+
build: (deps) => createPatResolver({ db: deps.db, scopes }),
|
|
66
|
+
} satisfies AuthProviderPlugin);
|
|
67
|
+
// Direct-write store like store_user_sessions: create/revoke write it, the
|
|
68
|
+
// resolver point-reads it. r.entity would make it a rebuildable projection
|
|
69
|
+
// whose replay (no token events) would wipe every live token (#498/#494).
|
|
70
|
+
r.storeTable(buildEntityTableMeta("api-token", apiTokenEntity, { source: "unmanaged" }), {
|
|
71
|
+
reason: "read_side.api_tokens_direct_write",
|
|
72
|
+
// create.write encrypts `name` via encryptForDirectWrite (#820).
|
|
73
|
+
piiEncryptedOnWrite: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const handlers = {
|
|
77
|
+
create: r.writeHandler(createPatWrite),
|
|
78
|
+
revoke: r.writeHandler(revokePatWrite),
|
|
79
|
+
};
|
|
80
|
+
const queries = {
|
|
81
|
+
mine: r.queryHandler(listPatQuery),
|
|
82
|
+
availableScopes: r.queryHandler(buildAvailableScopesQuery(scopes)),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Dormant custom-screen — the client maps PAT_SCREEN_ID to PatTokensScreen;
|
|
86
|
+
// the app places it via r.nav in its logged-in settings area.
|
|
87
|
+
r.screen({
|
|
88
|
+
id: PAT_SCREEN_ID,
|
|
89
|
+
type: "custom",
|
|
90
|
+
renderer: { react: { __component: "PatTokensScreen" } },
|
|
91
|
+
access: { openToAll: true },
|
|
92
|
+
});
|
|
93
|
+
r.translations({ keys: PAT_FEATURE_I18N });
|
|
94
|
+
|
|
95
|
+
// rateLimit flows into feature.exports so run-prod-app builds the
|
|
96
|
+
// limiter from the same declaration — single source of truth. `scopes`
|
|
97
|
+
// is closed over directly by the useExtension build() above and by
|
|
98
|
+
// available-scopes, so it doesn't need to round-trip through exports.
|
|
99
|
+
return {
|
|
100
|
+
handlers,
|
|
101
|
+
queries,
|
|
102
|
+
rateLimit: options.rateLimit ?? PAT_DEFAULT_RATE_LIMIT,
|
|
103
|
+
} satisfies { handlers: unknown; queries: unknown } & PatFeatureExports;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { PatScopeConfig } from "../scopes";
|
|
4
|
+
|
|
5
|
+
// Returns the app-declared scope domains ({name, label, canWrite}) so the mint
|
|
6
|
+
// UI can render a per-domain level picker (no access / read / read & write).
|
|
7
|
+
// canWrite is false for read-only domains → the UI hides the write option.
|
|
8
|
+
// Static per deployment, not per user.
|
|
9
|
+
export function buildAvailableScopesQuery(scopes: PatScopeConfig) {
|
|
10
|
+
return defineQueryHandler({
|
|
11
|
+
name: "available-scopes",
|
|
12
|
+
schema: z.object({}),
|
|
13
|
+
access: { openToAll: true },
|
|
14
|
+
handler: async () =>
|
|
15
|
+
Object.entries(scopes).map(([name, def]) => ({
|
|
16
|
+
name,
|
|
17
|
+
label: def.label,
|
|
18
|
+
canWrite: (def.write?.length ?? 0) > 0,
|
|
19
|
+
})),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
4
|
+
import { Temporal } from "temporal-polyfill";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { encryptForDirectWrite } from "../../shared";
|
|
7
|
+
import { mintPatToken } from "../hash";
|
|
8
|
+
import { apiTokenEntity, apiTokenTable } from "../schema/api-token";
|
|
9
|
+
|
|
10
|
+
// Mint a PAT for the calling user in their active tenant. The plaintext token
|
|
11
|
+
// is returned ONCE (data.token) and never again — only the hash is stored.
|
|
12
|
+
// `scopes` are granted scope names; unknown names simply grant nothing at
|
|
13
|
+
// resolve time (fail-closed), so no cross-check against the app config here.
|
|
14
|
+
export const createPatWrite = defineWriteHandler({
|
|
15
|
+
name: "create",
|
|
16
|
+
schema: z.object({
|
|
17
|
+
name: z.string().min(1).max(120),
|
|
18
|
+
scopes: z.array(z.string().min(1)).min(1),
|
|
19
|
+
expiresInDays: z.number().int().positive().max(3650).optional(),
|
|
20
|
+
}),
|
|
21
|
+
access: { openToAll: true },
|
|
22
|
+
handler: async (event, ctx) => {
|
|
23
|
+
const { raw, hash, prefix } = mintPatToken();
|
|
24
|
+
const now = Temporal.Now.instant();
|
|
25
|
+
const id = generateId();
|
|
26
|
+
const row = await encryptForDirectWrite(
|
|
27
|
+
apiTokenEntity,
|
|
28
|
+
{
|
|
29
|
+
id,
|
|
30
|
+
userId: event.user.id,
|
|
31
|
+
tenantId: event.user.tenantId,
|
|
32
|
+
name: event.payload.name,
|
|
33
|
+
tokenHash: hash,
|
|
34
|
+
prefix,
|
|
35
|
+
scopes: JSON.stringify(event.payload.scopes),
|
|
36
|
+
createdAt: now,
|
|
37
|
+
expiresAt: event.payload.expiresInDays
|
|
38
|
+
? now.add({ hours: 24 * event.payload.expiresInDays })
|
|
39
|
+
: null,
|
|
40
|
+
revokedAt: null,
|
|
41
|
+
},
|
|
42
|
+
"pat:create",
|
|
43
|
+
);
|
|
44
|
+
await insertOne(ctx.db, apiTokenTable, row);
|
|
45
|
+
return { isSuccess: true, data: { id, token: raw, prefix } };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { decryptStoredPii } from "../../shared";
|
|
5
|
+
import { apiTokenTable } from "../schema/api-token";
|
|
6
|
+
|
|
7
|
+
// The caller's own tokens — metadata only, never the hash or plaintext.
|
|
8
|
+
// Includes revoked/expired rows so the UI can show history; `prefix` is the
|
|
9
|
+
// only fragment of the secret ever exposed.
|
|
10
|
+
export const listPatQuery = defineQueryHandler({
|
|
11
|
+
name: "mine",
|
|
12
|
+
schema: z.object({}),
|
|
13
|
+
access: { openToAll: true },
|
|
14
|
+
handler: async (query, ctx) => {
|
|
15
|
+
const rows = await selectMany<{
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
prefix: string;
|
|
19
|
+
scopes: string;
|
|
20
|
+
createdAt: unknown;
|
|
21
|
+
expiresAt: unknown;
|
|
22
|
+
revokedAt: unknown;
|
|
23
|
+
}>(
|
|
24
|
+
ctx.db,
|
|
25
|
+
apiTokenTable,
|
|
26
|
+
{ userId: query.user.id },
|
|
27
|
+
{ orderBy: { col: "createdAt", direction: "desc" } },
|
|
28
|
+
);
|
|
29
|
+
return Promise.all(
|
|
30
|
+
rows.map(async (r) => ({
|
|
31
|
+
id: r.id,
|
|
32
|
+
name: await decryptStoredPii(r.name, "name", "pat:list"),
|
|
33
|
+
prefix: r.prefix,
|
|
34
|
+
scopes: parseScopeNames(r.scopes),
|
|
35
|
+
createdAt: r.createdAt,
|
|
36
|
+
expiresAt: r.expiresAt,
|
|
37
|
+
revokedAt: r.revokedAt,
|
|
38
|
+
})),
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function parseScopeNames(raw: string): string[] {
|
|
44
|
+
try {
|
|
45
|
+
const parsed: unknown = JSON.parse(raw);
|
|
46
|
+
return Array.isArray(parsed) ? parsed.filter((s): s is string => typeof s === "string") : [];
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { Temporal } from "temporal-polyfill";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { PatErrors } from "../constants";
|
|
7
|
+
import { apiTokenTable } from "../schema/api-token";
|
|
8
|
+
|
|
9
|
+
// Revoke one of the caller's own tokens. Ownership is enforced in the WHERE
|
|
10
|
+
// (userId = caller), so a caller can't revoke another user's token and a miss
|
|
11
|
+
// is a uniform ownership error with no existence oracle. isNull(revokedAt)
|
|
12
|
+
// keeps a double-revoke from overwriting the original timestamp.
|
|
13
|
+
export const revokePatWrite = defineWriteHandler({
|
|
14
|
+
name: "revoke",
|
|
15
|
+
schema: z.object({ id: z.uuid() }),
|
|
16
|
+
access: { openToAll: true },
|
|
17
|
+
handler: async (event, ctx) => {
|
|
18
|
+
const updated = await updateMany(
|
|
19
|
+
ctx.db,
|
|
20
|
+
apiTokenTable,
|
|
21
|
+
{ revokedAt: Temporal.Now.instant() },
|
|
22
|
+
{ id: event.payload.id, userId: event.user.id, revokedAt: null },
|
|
23
|
+
);
|
|
24
|
+
if (updated.length > 0) return { isSuccess: true, data: { id: event.payload.id } };
|
|
25
|
+
return writeFailure(
|
|
26
|
+
new UnprocessableError(PatErrors.ownershipDenied, {
|
|
27
|
+
i18nKey: "errors.ownershipDenied",
|
|
28
|
+
details: {
|
|
29
|
+
scope: "entity",
|
|
30
|
+
entityName: "api-token",
|
|
31
|
+
action: "revoke",
|
|
32
|
+
userId: event.user.id,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { generateToken, PAT_TOKEN_PREFIX } from "@cosmicdrift/kumiko-framework/api";
|
|
3
|
+
import { PAT_PREFIX_DISPLAY_LENGTH } from "./constants";
|
|
4
|
+
|
|
5
|
+
// A PAT is high-entropy (32 random bytes from generateToken). A single SHA-256
|
|
6
|
+
// is the right hash here: fast on the per-request auth path, and there's no
|
|
7
|
+
// brute-force surface that would justify argon2 (that's for low-entropy
|
|
8
|
+
// passwords). Only the hash is persisted — the plaintext exists once, in the
|
|
9
|
+
// create response.
|
|
10
|
+
export function hashPatToken(rawToken: string): string {
|
|
11
|
+
return createHash("sha256").update(rawToken).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mintPatToken(): { raw: string; hash: string; prefix: string } {
|
|
15
|
+
const raw = `${PAT_TOKEN_PREFIX}${generateToken()}`;
|
|
16
|
+
return { raw, hash: hashPatToken(raw), prefix: raw.slice(0, PAT_PREFIX_DISPLAY_LENGTH) };
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { PAT_DEFAULT_RATE_LIMIT, type PatRateLimit } from "./constants";
|
|
3
|
+
|
|
4
|
+
export type { PatRateLimit } from "./constants";
|
|
5
|
+
export { PAT_DEFAULT_RATE_LIMIT, PAT_FEATURE, PatHandlers, PatQueries } from "./constants";
|
|
6
|
+
export type { PersonalAccessTokensOptions } from "./feature";
|
|
7
|
+
export { createPersonalAccessTokensFeature } from "./feature";
|
|
8
|
+
export { hashPatToken, mintPatToken } from "./hash";
|
|
9
|
+
export { createPatResolver } from "./resolver";
|
|
10
|
+
export { apiTokenEntity, apiTokenTable } from "./schema/api-token";
|
|
11
|
+
export type { PatScopeConfig, PatScopeDef } from "./scopes";
|
|
12
|
+
export { expandScopes } from "./scopes";
|
|
13
|
+
|
|
14
|
+
// Reads the per-token rate-limit config off a mounted PAT feature's exports.
|
|
15
|
+
// Falls back to the default when absent. run-prod-app uses this to build the
|
|
16
|
+
// limiter from the same declaration the feature was given.
|
|
17
|
+
export function patRateLimitFromFeature(feature: FeatureDefinition): PatRateLimit {
|
|
18
|
+
const exports = feature.exports;
|
|
19
|
+
if (exports && typeof exports === "object" && "rateLimit" in exports) {
|
|
20
|
+
const { rateLimit } = exports as { rateLimit: unknown };
|
|
21
|
+
if (
|
|
22
|
+
rateLimit &&
|
|
23
|
+
typeof rateLimit === "object" &&
|
|
24
|
+
"maxRequests" in rateLimit &&
|
|
25
|
+
"windowMs" in rateLimit
|
|
26
|
+
) {
|
|
27
|
+
return rateLimit as PatRateLimit;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return PAT_DEFAULT_RATE_LIMIT;
|
|
31
|
+
}
|