@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,316 @@
|
|
|
1
|
+
import { computeRevisionEtag } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
+
import {
|
|
3
|
+
defineFeature,
|
|
4
|
+
type FeatureDefinition,
|
|
5
|
+
SYSTEM_TENANT_ID,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { LEGAL_ROUTES } from "../legal-pages";
|
|
8
|
+
import { cachedSecurePageResponse } from "../page-render";
|
|
9
|
+
import type { SystemQueryFn } from "../shared";
|
|
10
|
+
import { SEO_CONFIG_KEYS, SEO_DEFAULT_PATHS } from "./constants";
|
|
11
|
+
import { seoConfigQuery } from "./handlers/seo-config.query";
|
|
12
|
+
import { buildLlmsTxt } from "./llms-txt";
|
|
13
|
+
import { buildRobotsTxt, type RobotsPolicy } from "./robots-txt";
|
|
14
|
+
import { buildSitemapXml, type SitemapEntry } from "./sitemap";
|
|
15
|
+
|
|
16
|
+
// 300s-shared-cache: sitemap/llms.txt change rarely (new page publish, not
|
|
17
|
+
// per-request), longer than legal-pages/managed-pages' 60s content cache.
|
|
18
|
+
const SEO_CACHE = { kind: "revalidate", maxAgeSeconds: 300 } as const;
|
|
19
|
+
|
|
20
|
+
const MANAGED_PAGES_BY_TENANT_PUBLISHED_QN = "managed-pages:query:by-tenant-published";
|
|
21
|
+
const SEO_CONFIG_QUERY_QN = "seo:query:config";
|
|
22
|
+
|
|
23
|
+
export type ManagedPagesDiscoveryOptions = {
|
|
24
|
+
/** Same per-host tenant resolver the app already passes to
|
|
25
|
+
* createManagedPagesFeature — needed to X-Tenant-scope the anonymous
|
|
26
|
+
* by-tenant-published query. */
|
|
27
|
+
readonly resolveApexTenant: (host: string) => Promise<string | null> | string | null;
|
|
28
|
+
/** Must match the basePath passed to createManagedPagesFeature. Default "/p". */
|
|
29
|
+
readonly basePath?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type SeoOptions = {
|
|
33
|
+
/** Primary page inventory for sitemap.xml/llms.txt. App-authored, trusted
|
|
34
|
+
* (same trust boundary as ApexCta.href) — return fully-qualified absolute
|
|
35
|
+
* URLs. Called once per request with the resolved request Host. */
|
|
36
|
+
readonly sitemapEntries: (
|
|
37
|
+
host: string,
|
|
38
|
+
) => Promise<readonly SitemapEntry[]> | readonly SitemapEntry[];
|
|
39
|
+
/** Merge legal-pages' 4 fixed routes into sitemap.xml/llms.txt. Set true
|
|
40
|
+
* only when the app ALSO mounts createLegalPagesFeature() — this does not
|
|
41
|
+
* detect it automatically (no such feature-presence hook exists at the
|
|
42
|
+
* httpRoute layer). Default false. */
|
|
43
|
+
readonly includeLegalPages?: boolean;
|
|
44
|
+
/** Merge managed-pages' published slugs (via its anonymous
|
|
45
|
+
* by-tenant-published query) into sitemap.xml/llms.txt. Omit if the app
|
|
46
|
+
* doesn't mount managed-pages, or already includes those URLs in
|
|
47
|
+
* `sitemapEntries` itself. */
|
|
48
|
+
readonly managedPages?: ManagedPagesDiscoveryOptions;
|
|
49
|
+
/** Serves GET /robots.txt with per-host policy. Default: no route — the
|
|
50
|
+
* static `public/robots.txt` dev-server already ships covers the common
|
|
51
|
+
* case (no per-host logic needed). Only opt in for staging/preview hosts
|
|
52
|
+
* that need a different (e.g. Disallow: /) policy at runtime. */
|
|
53
|
+
readonly robotsPolicy?: (host: string) => Promise<RobotsPolicy> | RobotsPolicy;
|
|
54
|
+
/** Override the 3 mount paths. Default: /sitemap.xml, /llms.txt, /robots.txt. */
|
|
55
|
+
readonly basePaths?: Partial<typeof SEO_DEFAULT_PATHS>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type SeoConfigValues = {
|
|
59
|
+
readonly organizationName: string;
|
|
60
|
+
readonly organizationLogoUrl: string;
|
|
61
|
+
readonly twitterSite: string;
|
|
62
|
+
readonly llmsSummary: string;
|
|
63
|
+
readonly defaultOgImage: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const EMPTY_SEO_CONFIG: SeoConfigValues = {
|
|
67
|
+
organizationName: "",
|
|
68
|
+
organizationLogoUrl: "",
|
|
69
|
+
twitterSite: "",
|
|
70
|
+
llmsSummary: "",
|
|
71
|
+
defaultOgImage: "",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Config is decoration, not a hard dependency — a failed/unreachable read
|
|
75
|
+
// degrades to empty strings (same posture as managed-pages' readBrandingResponse).
|
|
76
|
+
// systemQuery forces tenantId in-process — no internal X-Tenant self-fetch,
|
|
77
|
+
// which a host-based anonymousAccess resolver in "authoritative" mode would
|
|
78
|
+
// reject as a forged client override.
|
|
79
|
+
async function readSeoConfig(
|
|
80
|
+
systemQuery: SystemQueryFn,
|
|
81
|
+
tenantId: string,
|
|
82
|
+
): Promise<SeoConfigValues> {
|
|
83
|
+
try {
|
|
84
|
+
const data = (await systemQuery(SEO_CONFIG_QUERY_QN, {}, tenantId)) as Partial<SeoConfigValues>;
|
|
85
|
+
return { ...EMPTY_SEO_CONFIG, ...data };
|
|
86
|
+
} catch {
|
|
87
|
+
return EMPTY_SEO_CONFIG;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Merges the app callback with the optional legal-pages/managed-pages
|
|
92
|
+
// sources. legal-pages routes are static (no per-tenant data) — merged
|
|
93
|
+
// directly from the public LEGAL_ROUTES constant. managed-pages entries need
|
|
94
|
+
// a live query (per-tenant published slugs) — merged via systemQuery, same
|
|
95
|
+
// cross-feature decoupling as legal-pages' own text-content calls; a
|
|
96
|
+
// failed/unreachable read degrades to "no managed-pages entries" rather than
|
|
97
|
+
// failing the whole route.
|
|
98
|
+
async function gatherEntries(
|
|
99
|
+
opts: SeoOptions,
|
|
100
|
+
systemQuery: SystemQueryFn,
|
|
101
|
+
origin: string,
|
|
102
|
+
host: string,
|
|
103
|
+
): Promise<SitemapEntry[]> {
|
|
104
|
+
const entries: SitemapEntry[] = [...(await opts.sitemapEntries(host))];
|
|
105
|
+
|
|
106
|
+
if (opts.includeLegalPages) {
|
|
107
|
+
for (const route of LEGAL_ROUTES) {
|
|
108
|
+
entries.push({ loc: `${origin}${route.path}` });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.managedPages) {
|
|
113
|
+
const tenantId = await opts.managedPages.resolveApexTenant(host);
|
|
114
|
+
if (tenantId) {
|
|
115
|
+
try {
|
|
116
|
+
const data = (await systemQuery(MANAGED_PAGES_BY_TENANT_PUBLISHED_QN, {}, tenantId)) as {
|
|
117
|
+
pages?: readonly { slug: string; title: string; updatedAt: string }[];
|
|
118
|
+
};
|
|
119
|
+
const basePath = opts.managedPages.basePath ?? "/p";
|
|
120
|
+
for (const page of data.pages ?? []) {
|
|
121
|
+
entries.push({
|
|
122
|
+
loc: `${origin}${basePath}/${page.slug}`,
|
|
123
|
+
title: page.title,
|
|
124
|
+
lastmod: page.updatedAt,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// managed-pages unreachable/not mounted — degrade to callback-only entries.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return entries;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// TLS-terminating reverse proxies (ingress-nginx et al.) forward the
|
|
137
|
+
// original request over plain HTTP internally, so `c.req.url`'s scheme
|
|
138
|
+
// reflects the proxy hop, not what the client actually used — trusting it
|
|
139
|
+
// downgraded every legal-pages/managed-pages URL in sitemap.xml/llms.txt to
|
|
140
|
+
// http:// in production. `x-forwarded-proto` carries the real scheme;
|
|
141
|
+
// fall back to the raw URL only when the header is absent (plain local dev).
|
|
142
|
+
function requestHost(c: { req: { header: (name: string) => string | undefined; url: string } }): {
|
|
143
|
+
origin: string;
|
|
144
|
+
host: string;
|
|
145
|
+
} {
|
|
146
|
+
const url = new URL(c.req.url);
|
|
147
|
+
const host = c.req.header("host") ?? url.host;
|
|
148
|
+
const forwardedProto = c.req.header("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
149
|
+
const protocol =
|
|
150
|
+
forwardedProto === "https" || forwardedProto === "http"
|
|
151
|
+
? forwardedProto
|
|
152
|
+
: url.protocol.replace(":", "");
|
|
153
|
+
return { origin: `${protocol}://${host}`, host };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// seo — site-discovery routes (sitemap.xml/llms.txt/robots.txt) plus the
|
|
157
|
+
// wrapInLayout OG/JSON-LD extension (page-render/layout.ts's `seo` opt) and
|
|
158
|
+
// the organizationSchema/webPageSchema/faqPageSchema JSON-LD builders
|
|
159
|
+
// (schema-builders.ts, imported directly by apps — not wired into this
|
|
160
|
+
// feature's own routes). Complements the apex ApexHead/renderApexPage
|
|
161
|
+
// renderer, which already ships full OG/JSON-LD/canonical head handling —
|
|
162
|
+
// this feature only fills the site-discovery + CMS-page-head gaps.
|
|
163
|
+
export function createSeoFeature(opts: SeoOptions): FeatureDefinition {
|
|
164
|
+
const paths = { ...SEO_DEFAULT_PATHS, ...opts.basePaths };
|
|
165
|
+
|
|
166
|
+
return defineFeature("seo", (r) => {
|
|
167
|
+
r.describe(
|
|
168
|
+
`Site-discovery + SEO/AEO/GEO surface for apex/content pages. Serves GET ${paths.sitemap} and GET ${paths.llmsTxt} (both anonymous, revalidate-cached), merging the app-supplied sitemapEntries() callback with legal-pages' fixed routes (includeLegalPages) and/or managed-pages' published slugs (managedPages.resolveApexTenant) when opted in. Serves GET ${paths.robots} only when robotsPolicy is supplied (default off — the static public/robots.txt already covers the common case). Tenant-scoped config keys (seo:config:seo-organization-{name,logo-url}, seo:config:seo-twitter-site, seo:config:seo-llms-summary, seo:config:seo-default-og-image) feed the Organization JSON-LD helper + llms.txt summary. Also exports pure schema.org JSON-LD builders (organizationSchema/webPageSchema/faqPageSchema) for apps to pass into ApexHead.schemaJson or wrapInLayout({ seo: { schemaJson } }) directly — this feature does not inject JSON-LD on its own routes.`,
|
|
169
|
+
);
|
|
170
|
+
r.uiHints({
|
|
171
|
+
displayLabel: "SEO / Site Discovery",
|
|
172
|
+
category: "content",
|
|
173
|
+
recommended: false,
|
|
174
|
+
});
|
|
175
|
+
// config keys need the config feature's write/read machinery to actually
|
|
176
|
+
// function (same hard dependency managed-pages has) — only legal-pages/
|
|
177
|
+
// managed-pages are genuinely optional (merged only when opted in).
|
|
178
|
+
r.requires("config");
|
|
179
|
+
r.optionalRequires("legal-pages", "managed-pages");
|
|
180
|
+
|
|
181
|
+
r.config({ keys: SEO_CONFIG_KEYS });
|
|
182
|
+
r.queryHandler(seoConfigQuery);
|
|
183
|
+
|
|
184
|
+
r.httpRoute({
|
|
185
|
+
method: "GET",
|
|
186
|
+
path: paths.sitemap,
|
|
187
|
+
anonymous: true,
|
|
188
|
+
handler: async (c, { systemQuery }) => {
|
|
189
|
+
const { origin, host } = requestHost(c);
|
|
190
|
+
const entries = await gatherEntries(opts, systemQuery, origin, host);
|
|
191
|
+
const xml = buildSitemapXml(entries);
|
|
192
|
+
const etag = computeRevisionEtag([host, xml]);
|
|
193
|
+
return cachedSecurePageResponse(c.req.raw, {
|
|
194
|
+
body: xml,
|
|
195
|
+
etag,
|
|
196
|
+
cache: SEO_CACHE,
|
|
197
|
+
extra: { "content-type": "application/xml; charset=utf-8" },
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
r.httpRoute({
|
|
203
|
+
method: "GET",
|
|
204
|
+
path: paths.llmsTxt,
|
|
205
|
+
anonymous: true,
|
|
206
|
+
handler: async (c, { systemQuery }) => {
|
|
207
|
+
const { origin, host } = requestHost(c);
|
|
208
|
+
const tenantId = opts.managedPages
|
|
209
|
+
? ((await opts.managedPages.resolveApexTenant(host)) ?? SYSTEM_TENANT_ID)
|
|
210
|
+
: SYSTEM_TENANT_ID;
|
|
211
|
+
const [entries, seoConfig] = await Promise.all([
|
|
212
|
+
gatherEntries(opts, systemQuery, origin, host),
|
|
213
|
+
readSeoConfig(systemQuery, tenantId),
|
|
214
|
+
]);
|
|
215
|
+
const sections =
|
|
216
|
+
entries.length > 0
|
|
217
|
+
? [
|
|
218
|
+
{
|
|
219
|
+
heading: "Pages",
|
|
220
|
+
links: entries.map((e) => ({ title: e.title ?? e.loc, url: e.loc })),
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
: [];
|
|
224
|
+
const text = buildLlmsTxt({
|
|
225
|
+
title: seoConfig.organizationName || host,
|
|
226
|
+
summary: seoConfig.llmsSummary,
|
|
227
|
+
sections,
|
|
228
|
+
});
|
|
229
|
+
const etag = computeRevisionEtag([host, text]);
|
|
230
|
+
return cachedSecurePageResponse(c.req.raw, {
|
|
231
|
+
body: text,
|
|
232
|
+
etag,
|
|
233
|
+
cache: SEO_CACHE,
|
|
234
|
+
extra: { "content-type": "text/plain; charset=utf-8" },
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (opts.robotsPolicy) {
|
|
240
|
+
const robotsPolicy = opts.robotsPolicy;
|
|
241
|
+
r.httpRoute({
|
|
242
|
+
method: "GET",
|
|
243
|
+
path: paths.robots,
|
|
244
|
+
anonymous: true,
|
|
245
|
+
handler: async (c) => {
|
|
246
|
+
const { host } = requestHost(c);
|
|
247
|
+
const policy = await robotsPolicy(host);
|
|
248
|
+
const text = buildRobotsTxt(policy);
|
|
249
|
+
const etag = computeRevisionEtag([host, text]);
|
|
250
|
+
return cachedSecurePageResponse(c.req.raw, {
|
|
251
|
+
body: text,
|
|
252
|
+
etag,
|
|
253
|
+
cache: SEO_CACHE,
|
|
254
|
+
extra: { "content-type": "text/plain; charset=utf-8" },
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Boot-Check body als named function extrahiert — direkt unit-testbar,
|
|
261
|
+
// gleiches "thin job-shell ruft testable function"-Pattern wie
|
|
262
|
+
// legal-pages' runLegalPagesBootCheck.
|
|
263
|
+
r.job(
|
|
264
|
+
"seo-boot-check",
|
|
265
|
+
{ trigger: { manual: true }, runOnBoot: true, runIn: "api" },
|
|
266
|
+
async (_payload, ctx) =>
|
|
267
|
+
runSeoBootCheck({
|
|
268
|
+
sitemapEntries: opts.sitemapEntries,
|
|
269
|
+
includeLegalPages: opts.includeLegalPages ?? false,
|
|
270
|
+
hasManagedPages: opts.managedPages !== undefined,
|
|
271
|
+
log: ctx.log,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
return {};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export type SeoBootCheckCtx = {
|
|
280
|
+
readonly sitemapEntries: (
|
|
281
|
+
host: string,
|
|
282
|
+
) => Promise<readonly SitemapEntry[]> | readonly SitemapEntry[];
|
|
283
|
+
readonly includeLegalPages: boolean;
|
|
284
|
+
/** managedPages entries are only resolvable per-request (need a live
|
|
285
|
+
* tenant + query) — the boot-check can't probe them, so their mere
|
|
286
|
+
* presence counts as "has a source" rather than sampling actual rows. */
|
|
287
|
+
readonly hasManagedPages: boolean;
|
|
288
|
+
readonly log?: {
|
|
289
|
+
readonly info?: (msg: string) => void;
|
|
290
|
+
readonly warn?: (msg: string) => void;
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Exported for direct tests. Throws in NODE_ENV=production when NONE of the
|
|
295
|
+
// three entry sources (sitemapEntries() probe, includeLegalPages,
|
|
296
|
+
// managedPages) can supply anything — the routes would otherwise serve a
|
|
297
|
+
// permanently empty document. Otherwise log.warn. Logs log.info when at
|
|
298
|
+
// least one source is present.
|
|
299
|
+
export async function runSeoBootCheck(ctx: SeoBootCheckCtx): Promise<void> {
|
|
300
|
+
const sample = await ctx.sitemapEntries("boot-check.invalid");
|
|
301
|
+
const hasSource = sample.length > 0 || ctx.includeLegalPages || ctx.hasManagedPages;
|
|
302
|
+
|
|
303
|
+
if (hasSource) {
|
|
304
|
+
ctx.log?.info?.("seo boot-check: sitemap/llms.txt have at least one entry source");
|
|
305
|
+
} else {
|
|
306
|
+
const message =
|
|
307
|
+
"seo: sitemapEntries() returned no entries and neither includeLegalPages " +
|
|
308
|
+
"nor managedPages is set — sitemap.xml/llms.txt will serve an empty " +
|
|
309
|
+
"document. Wire real entries, set includeLegalPages:true, or pass managedPages.";
|
|
310
|
+
|
|
311
|
+
if (process.env["NODE_ENV"] === "production") {
|
|
312
|
+
throw new Error(`Boot-Validation failed: ${message}`);
|
|
313
|
+
}
|
|
314
|
+
ctx.log?.warn?.(message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SEO_CONFIG_QN } from "../constants";
|
|
4
|
+
|
|
5
|
+
// Public read of the seo feature's own tenant-config values, for the
|
|
6
|
+
// anonymous sitemap.xml/llms.txt routes (a Response is per-request; those
|
|
7
|
+
// routes only get `{ app }`, not `ctx.config` directly, so they reach this
|
|
8
|
+
// the same way managed-pages' render route reaches its branding query — an
|
|
9
|
+
// internal app.fetch with X-Tenant set to the resolved tenant).
|
|
10
|
+
//
|
|
11
|
+
// config:query:values (the generic config feature query) is NOT usable here:
|
|
12
|
+
// it requires an authenticated caller (any role) even though its per-key
|
|
13
|
+
// access defaults to read:all — a genuinely anonymous visitor gets 403 before
|
|
14
|
+
// per-key filtering ever runs. This mirrors managed-pages' branding query
|
|
15
|
+
// instead (`access: { roles: ["anonymous", ...] }`), the pattern actually
|
|
16
|
+
// proven to work for anonymous-served pages.
|
|
17
|
+
export const seoConfigQuery = defineQueryHandler({
|
|
18
|
+
name: "config",
|
|
19
|
+
schema: z.object({}),
|
|
20
|
+
access: { roles: ["anonymous", "User", "TenantAdmin", "SystemAdmin"] },
|
|
21
|
+
handler: async (_query, ctx) => {
|
|
22
|
+
const read = async (qualifiedKey: string): Promise<string> => {
|
|
23
|
+
if (!ctx.config) return "";
|
|
24
|
+
const value = await ctx.config(qualifiedKey);
|
|
25
|
+
return typeof value === "string" ? value : "";
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
organizationName: await read(SEO_CONFIG_QN.organizationName),
|
|
29
|
+
organizationLogoUrl: await read(SEO_CONFIG_QN.organizationLogoUrl),
|
|
30
|
+
twitterSite: await read(SEO_CONFIG_QN.twitterSite),
|
|
31
|
+
llmsSummary: await read(SEO_CONFIG_QN.llmsSummary),
|
|
32
|
+
defaultOgImage: await read(SEO_CONFIG_QN.defaultOgImage),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
package/src/seo/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { SEO_CONFIG_KEYS, SEO_CONFIG_QN, SEO_DEFAULT_PATHS, SEO_FEATURE } from "./constants";
|
|
2
|
+
export {
|
|
3
|
+
createSeoFeature,
|
|
4
|
+
type ManagedPagesDiscoveryOptions,
|
|
5
|
+
runSeoBootCheck,
|
|
6
|
+
type SeoBootCheckCtx,
|
|
7
|
+
type SeoOptions,
|
|
8
|
+
} from "./feature";
|
|
9
|
+
export { buildLlmsTxt, type LlmsTxtInput, type LlmsTxtLink, type LlmsTxtSection } from "./llms-txt";
|
|
10
|
+
export { buildRobotsTxt, type RobotsPolicy } from "./robots-txt";
|
|
11
|
+
export {
|
|
12
|
+
type FaqItem,
|
|
13
|
+
faqPageSchema,
|
|
14
|
+
type OrganizationSchemaInput,
|
|
15
|
+
organizationSchema,
|
|
16
|
+
type WebPageSchemaInput,
|
|
17
|
+
webPageSchema,
|
|
18
|
+
} from "./schema-builders";
|
|
19
|
+
export { buildSitemapXml, type SitemapEntry } from "./sitemap";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type LlmsTxtLink = {
|
|
2
|
+
readonly title: string;
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly desc?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type LlmsTxtSection = {
|
|
8
|
+
readonly heading: string;
|
|
9
|
+
readonly links: readonly LlmsTxtLink[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type LlmsTxtInput = {
|
|
13
|
+
readonly title: string;
|
|
14
|
+
readonly summary: string;
|
|
15
|
+
readonly sections?: readonly LlmsTxtSection[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Plain-text builder for the community llms.txt convention (H1 title,
|
|
19
|
+
// blockquote summary, `## `-headed link lists). No HTML escaping needed —
|
|
20
|
+
// Markdown link syntax, plain-text output.
|
|
21
|
+
export function buildLlmsTxt(input: LlmsTxtInput): string {
|
|
22
|
+
const sections = (input.sections ?? [])
|
|
23
|
+
.filter((s) => s.links.length > 0)
|
|
24
|
+
.map((s) => {
|
|
25
|
+
const links = s.links
|
|
26
|
+
.map((l) => `- [${l.title}](${l.url})${l.desc ? `: ${l.desc}` : ""}`)
|
|
27
|
+
.join("\n");
|
|
28
|
+
return `## ${s.heading}\n\n${links}`;
|
|
29
|
+
})
|
|
30
|
+
.join("\n\n");
|
|
31
|
+
const summary = input.summary ? `\n\n> ${input.summary}` : "";
|
|
32
|
+
return `# ${input.title}${summary}${sections ? `\n\n${sections}` : ""}\n`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type RobotsPolicy = {
|
|
2
|
+
readonly allow: boolean;
|
|
3
|
+
readonly sitemapUrl?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// Only used when an app opts into GET /robots.txt (SeoOptions.robotsPolicy) —
|
|
7
|
+
// the common case (no per-host logic) is already covered by dev-server's
|
|
8
|
+
// static public/robots.txt passthrough.
|
|
9
|
+
export function buildRobotsTxt(policy: RobotsPolicy): string {
|
|
10
|
+
const rule = policy.allow ? "Disallow:" : "Disallow: /";
|
|
11
|
+
const sitemap = policy.sitemapUrl ? `\nSitemap: ${policy.sitemapUrl}` : "";
|
|
12
|
+
return `User-agent: *\n${rule}${sitemap}\n`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Pure schema.org JSON-LD builders — plain data, no HTML/escaping (the
|
|
2
|
+
// serialization boundary is `renderApexHeadTags`'s `scriptSafeJsonHtml`,
|
|
3
|
+
// which already escapes `<` so a value can't break out of the
|
|
4
|
+
// `<script type="application/ld+json">` block). Feed the result into
|
|
5
|
+
// `ApexHead.schemaJson` (renderApexPage) or `wrapInLayout({ seo: { schemaJson
|
|
6
|
+
// } })` (CMS pages) — this feature does not inject JSON-LD on its own routes.
|
|
7
|
+
|
|
8
|
+
export type OrganizationSchemaInput = {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly url?: string;
|
|
11
|
+
readonly logoUrl?: string;
|
|
12
|
+
readonly sameAs?: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function organizationSchema(input: OrganizationSchemaInput): Record<string, unknown> {
|
|
16
|
+
return {
|
|
17
|
+
"@context": "https://schema.org",
|
|
18
|
+
"@type": "Organization",
|
|
19
|
+
name: input.name,
|
|
20
|
+
...(input.url ? { url: input.url } : {}),
|
|
21
|
+
...(input.logoUrl ? { logo: input.logoUrl } : {}),
|
|
22
|
+
...(input.sameAs && input.sameAs.length > 0 ? { sameAs: input.sameAs } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type WebPageSchemaInput = {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly url: string;
|
|
29
|
+
readonly description?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function webPageSchema(input: WebPageSchemaInput): Record<string, unknown> {
|
|
33
|
+
return {
|
|
34
|
+
"@context": "https://schema.org",
|
|
35
|
+
"@type": "WebPage",
|
|
36
|
+
name: input.name,
|
|
37
|
+
url: input.url,
|
|
38
|
+
...(input.description ? { description: input.description } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FaqItem = { readonly question: string; readonly answer: string };
|
|
43
|
+
|
|
44
|
+
// Feeds Google's FAQ rich-result + is the most directly LLM-answer-engine-
|
|
45
|
+
// legible JSON-LD shape (question/answer pairs) — the "AEO/GEO" half of the
|
|
46
|
+
// schema-builder set, same emission path as organizationSchema/webPageSchema.
|
|
47
|
+
export function faqPageSchema(items: readonly FaqItem[]): Record<string, unknown> {
|
|
48
|
+
return {
|
|
49
|
+
"@context": "https://schema.org",
|
|
50
|
+
"@type": "FAQPage",
|
|
51
|
+
mainEntity: items.map((item) => ({
|
|
52
|
+
"@type": "Question",
|
|
53
|
+
name: item.question,
|
|
54
|
+
acceptedAnswer: { "@type": "Answer", text: item.answer },
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { escapeXml } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
|
|
3
|
+
export type SitemapEntry = {
|
|
4
|
+
readonly loc: string;
|
|
5
|
+
/** ISO-8601 date/datetime. */
|
|
6
|
+
readonly lastmod?: string;
|
|
7
|
+
readonly changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
8
|
+
/** hreflang alternates for the same logical page (multilingual sitemaps). */
|
|
9
|
+
readonly alternates?: readonly { readonly hreflang: string; readonly href: string }[];
|
|
10
|
+
/** Human-/LLM-readable page title, used by llms.txt link lines. Falls back
|
|
11
|
+
* to `loc` when absent (callback-only entries rarely set this). */
|
|
12
|
+
readonly title?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Pure XML builder — sitemaps.org urlset + xhtml:link alternates. No
|
|
16
|
+
// dedup/sort: callers control ordering, entries are trusted app/feature data
|
|
17
|
+
// (see feature.ts's trust boundary — same posture as ApexCta.href).
|
|
18
|
+
export function buildSitemapXml(entries: readonly SitemapEntry[]): string {
|
|
19
|
+
const urls = entries
|
|
20
|
+
.map((e) => {
|
|
21
|
+
const lastmod = e.lastmod ? `\n <lastmod>${escapeXml(e.lastmod)}</lastmod>` : "";
|
|
22
|
+
const changefreq = e.changefreq ? `\n <changefreq>${e.changefreq}</changefreq>` : "";
|
|
23
|
+
const alternates = (e.alternates ?? [])
|
|
24
|
+
.map(
|
|
25
|
+
(a) =>
|
|
26
|
+
`\n <xhtml:link rel="alternate" hreflang="${escapeXml(a.hreflang)}" href="${escapeXml(a.href)}" />`,
|
|
27
|
+
)
|
|
28
|
+
.join("");
|
|
29
|
+
return ` <url>\n <loc>${escapeXml(e.loc)}</loc>${lastmod}${changefreq}${alternates}\n </url>`;
|
|
30
|
+
})
|
|
31
|
+
.join("\n");
|
|
32
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${urls}\n</urlset>\n`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import type { AppContext, SaveContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { bindAutoRevokeFromFeature, createSessionsFeature } from "../feature";
|
|
4
|
+
|
|
5
|
+
// The postSave hook is registered unconditionally; the revoker arrives either
|
|
6
|
+
// as the explicit constructor option or late-bound by run{Prod,Dev}App via
|
|
7
|
+
// bindAutoRevokeOnPasswordChange. These tests pin the binding + precedence
|
|
8
|
+
// semantics at the hook level — the full DB sweep is covered by
|
|
9
|
+
// password-auto-revoke.integration.test.ts.
|
|
10
|
+
|
|
11
|
+
const passwordChange: SaveContext = {
|
|
12
|
+
kind: "save",
|
|
13
|
+
id: "user-1",
|
|
14
|
+
data: {},
|
|
15
|
+
changes: { passwordHash: "new-hash" },
|
|
16
|
+
previous: {},
|
|
17
|
+
isNew: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// @cast-boundary test fixture — the hook never touches AppContext
|
|
21
|
+
const appContext = {} as unknown as AppContext;
|
|
22
|
+
|
|
23
|
+
function userPostSaveHook(feature: ReturnType<typeof createSessionsFeature>) {
|
|
24
|
+
const hook = feature.entityHooks?.postSave?.["user"]?.[0];
|
|
25
|
+
if (!hook) throw new Error("sessions feature did not register the user postSave hook");
|
|
26
|
+
return (ctx: SaveContext) => hook.fn(ctx, appContext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("sessions auto-revoke binding", () => {
|
|
30
|
+
test("late-bound revoker fires on password change", async () => {
|
|
31
|
+
const revoker = mock(async (_userId: string) => 1);
|
|
32
|
+
const feature = createSessionsFeature();
|
|
33
|
+
|
|
34
|
+
const bind = bindAutoRevokeFromFeature(feature);
|
|
35
|
+
expect(bind).toBeDefined();
|
|
36
|
+
bind?.(revoker);
|
|
37
|
+
|
|
38
|
+
await userPostSaveHook(feature)(passwordChange);
|
|
39
|
+
expect(revoker).toHaveBeenCalledTimes(1);
|
|
40
|
+
expect(revoker).toHaveBeenCalledWith("user-1");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("unbound hook is a silent no-op", async () => {
|
|
44
|
+
const feature = createSessionsFeature();
|
|
45
|
+
// must resolve without throwing — stateless-JWT deployments have no revoker
|
|
46
|
+
const result = await userPostSaveHook(feature)(passwordChange);
|
|
47
|
+
expect(result).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("skips isNew and non-passwordHash changes", async () => {
|
|
51
|
+
const revoker = mock(async (_userId: string) => 1);
|
|
52
|
+
const feature = createSessionsFeature();
|
|
53
|
+
bindAutoRevokeFromFeature(feature)?.(revoker);
|
|
54
|
+
|
|
55
|
+
await userPostSaveHook(feature)({ ...passwordChange, isNew: true });
|
|
56
|
+
await userPostSaveHook(feature)({ ...passwordChange, changes: { displayName: "x" } });
|
|
57
|
+
expect(revoker).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("explicit constructor option wins over the runtime binding", async () => {
|
|
61
|
+
const explicit = mock(async (_userId: string) => 1);
|
|
62
|
+
const lateBound = mock(async (_userId: string) => 1);
|
|
63
|
+
const feature = createSessionsFeature({ autoRevokeOnPasswordChange: explicit });
|
|
64
|
+
|
|
65
|
+
bindAutoRevokeFromFeature(feature)?.(lateBound);
|
|
66
|
+
await userPostSaveHook(feature)(passwordChange);
|
|
67
|
+
|
|
68
|
+
expect(explicit).toHaveBeenCalledWith("user-1");
|
|
69
|
+
expect(lateBound).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// expired/revoked rows go, live rows stay, batching + signal work.
|
|
6
6
|
|
|
7
7
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
8
9
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
10
|
import { sql } from "@cosmicdrift/kumiko-framework/db";
|
|
10
11
|
import type { AppContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -40,7 +41,7 @@ let stack: TestStack;
|
|
|
40
41
|
|
|
41
42
|
beforeAll(async () => {
|
|
42
43
|
stack = await setupTestStack({
|
|
43
|
-
features: [createSessionsFeature(), createUserFeature()],
|
|
44
|
+
features: [authFoundationFeature, createSessionsFeature(), createUserFeature()],
|
|
44
45
|
});
|
|
45
46
|
await unsafeCreateEntityTable(stack.db, userSessionEntity);
|
|
46
47
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
3
4
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
|
-
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
5
5
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
6
|
import {
|
|
7
7
|
setupTestStack,
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
unsafeCreateEntityTable,
|
|
11
11
|
unsafePushTables,
|
|
12
12
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createLateBoundHolder,
|
|
15
|
+
createTestEnvelopeCipher,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
14
17
|
import { AuthHandlers } from "../../auth-email-password/constants";
|
|
15
18
|
import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
|
|
16
19
|
import { createConfigFeature } from "../../config";
|
|
@@ -49,8 +52,8 @@ const encryptionKey = randomBytes(32).toString("base64");
|
|
|
49
52
|
const TENANT: TenantId = testTenantId(1);
|
|
50
53
|
|
|
51
54
|
beforeAll(async () => {
|
|
52
|
-
const encryption =
|
|
53
|
-
const resolver = createConfigResolver({ encryption });
|
|
55
|
+
const encryption = createTestEnvelopeCipher(encryptionKey);
|
|
56
|
+
const resolver = createConfigResolver({ cipher: encryption });
|
|
54
57
|
const bound = sessionCallbacksFromLateBound(callbacks);
|
|
55
58
|
const baseRevoker = bound.asMassRevoker();
|
|
56
59
|
|
|
@@ -64,6 +67,7 @@ beforeAll(async () => {
|
|
|
64
67
|
createUserFeature(),
|
|
65
68
|
createTenantFeature(),
|
|
66
69
|
createAuthEmailPasswordFeature(),
|
|
70
|
+
authFoundationFeature,
|
|
67
71
|
createSessionsFeature({
|
|
68
72
|
autoRevokeOnPasswordChange: massRevokeSpy,
|
|
69
73
|
}),
|
|
@@ -76,7 +80,7 @@ beforeAll(async () => {
|
|
|
76
80
|
},
|
|
77
81
|
});
|
|
78
82
|
callbacks.set(createSessionCallbacks({ db: stack.db }));
|
|
79
|
-
h = makeSessionHelpers(stack, TENANT);
|
|
83
|
+
h = makeSessionHelpers(stack, TENANT, bound.asAuthConfig().sessionCreator);
|
|
80
84
|
|
|
81
85
|
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
82
86
|
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
@@ -97,7 +101,7 @@ beforeEach(async () => {
|
|
|
97
101
|
|
|
98
102
|
describe("password change mass-revokes every live session", () => {
|
|
99
103
|
test("changing the password revokes ALL sessions including the caller's", async () => {
|
|
100
|
-
await h.seedUser("rotate@example.com", "first-password");
|
|
104
|
+
const { userId } = await h.seedUser("rotate@example.com", "first-password");
|
|
101
105
|
|
|
102
106
|
const a = await h.login("rotate@example.com", "first-password");
|
|
103
107
|
const b = await h.login("rotate@example.com", "first-password");
|
|
@@ -145,7 +149,7 @@ describe("password change mass-revokes every live session", () => {
|
|
|
145
149
|
|
|
146
150
|
// DB state confirms: zero live rows for this user
|
|
147
151
|
const liveRows = await selectMany(stack.db, userSessionTable);
|
|
148
|
-
const stillLive = liveRows.filter((r) => r["revokedAt"] === null);
|
|
152
|
+
const stillLive = liveRows.filter((r) => r["userId"] === userId && r["revokedAt"] === null);
|
|
149
153
|
expect(stillLive).toHaveLength(0);
|
|
150
154
|
|
|
151
155
|
// And logging in again with the NEW password works
|