@bajustone/fortress 1.0.0-rc.1
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/CHANGELOG.md +1011 -0
- package/LICENSE +21 -0
- package/README.md +760 -0
- package/SECURITY.md +197 -0
- package/bin/fortress.ts +667 -0
- package/dist/auth-service-BcyQ2PuZ.d.cts +307 -0
- package/dist/auth-service-BkzZkWfH.d.ts +307 -0
- package/dist/config-B2366s_G.d.ts +665 -0
- package/dist/config-GtlVt6ZD.d.cts +665 -0
- package/dist/convert-routes-BLCQT57f.d.ts +72 -0
- package/dist/convert-routes-BdMvP2_X.d.cts +72 -0
- package/dist/crypto.cjs +61 -0
- package/dist/crypto.d.cts +36 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/crypto.js +35 -0
- package/dist/drift-BT1wtMDJ.d.cts +22 -0
- package/dist/drift-k9LiYpRP.d.ts +22 -0
- package/dist/drizzle/pg.cjs +481 -0
- package/dist/drizzle/pg.d.cts +15 -0
- package/dist/drizzle/pg.d.ts +15 -0
- package/dist/drizzle/pg.js +454 -0
- package/dist/drizzle.cjs +1442 -0
- package/dist/drizzle.d.cts +85 -0
- package/dist/drizzle.d.ts +85 -0
- package/dist/drizzle.js +1411 -0
- package/dist/express.cjs +1357 -0
- package/dist/express.d.cts +333 -0
- package/dist/express.d.ts +333 -0
- package/dist/express.js +1314 -0
- package/dist/fetcher.cjs +77 -0
- package/dist/fetcher.d.cts +7 -0
- package/dist/fetcher.d.ts +7 -0
- package/dist/fetcher.js +41 -0
- package/dist/fortress-BmSvFfqK.d.cts +1089 -0
- package/dist/fortress-uA-_cheR.d.ts +1089 -0
- package/dist/hono.cjs +1530 -0
- package/dist/hono.d.cts +514 -0
- package/dist/hono.d.ts +514 -0
- package/dist/hono.js +1483 -0
- package/dist/index-CxEkfCA0.d.cts +77 -0
- package/dist/index-CxEkfCA0.d.ts +77 -0
- package/dist/index-D054pcb-.d.cts +146 -0
- package/dist/index-jAMbbUAY.d.ts +146 -0
- package/dist/index.cjs +9046 -0
- package/dist/index.d.cts +717 -0
- package/dist/index.d.ts +717 -0
- package/dist/index.js +8935 -0
- package/dist/jwt.cjs +255 -0
- package/dist/jwt.d.cts +90 -0
- package/dist/jwt.d.ts +90 -0
- package/dist/jwt.js +227 -0
- package/dist/otel.cjs +108 -0
- package/dist/otel.d.cts +62 -0
- package/dist/otel.d.ts +62 -0
- package/dist/otel.js +73 -0
- package/dist/plugins/account-lockout.cjs +322 -0
- package/dist/plugins/account-lockout.d.cts +42 -0
- package/dist/plugins/account-lockout.d.ts +42 -0
- package/dist/plugins/account-lockout.js +295 -0
- package/dist/plugins/admin.cjs +2378 -0
- package/dist/plugins/admin.d.cts +72 -0
- package/dist/plugins/admin.d.ts +72 -0
- package/dist/plugins/admin.js +2351 -0
- package/dist/plugins/api-key.cjs +792 -0
- package/dist/plugins/api-key.d.cts +121 -0
- package/dist/plugins/api-key.d.ts +121 -0
- package/dist/plugins/api-key.js +765 -0
- package/dist/plugins/audit-log.cjs +493 -0
- package/dist/plugins/audit-log.d.cts +86 -0
- package/dist/plugins/audit-log.d.ts +86 -0
- package/dist/plugins/audit-log.js +468 -0
- package/dist/plugins/data-isolation.cjs +211 -0
- package/dist/plugins/data-isolation.d.cts +49 -0
- package/dist/plugins/data-isolation.d.ts +49 -0
- package/dist/plugins/data-isolation.js +186 -0
- package/dist/plugins/email-verification.cjs +273 -0
- package/dist/plugins/email-verification.d.cts +44 -0
- package/dist/plugins/email-verification.d.ts +44 -0
- package/dist/plugins/email-verification.js +246 -0
- package/dist/plugins/magic-link.cjs +231 -0
- package/dist/plugins/magic-link.d.cts +39 -0
- package/dist/plugins/magic-link.d.ts +39 -0
- package/dist/plugins/magic-link.js +204 -0
- package/dist/plugins/oauth.cjs +1696 -0
- package/dist/plugins/oauth.d.cts +406 -0
- package/dist/plugins/oauth.d.ts +406 -0
- package/dist/plugins/oauth.js +1669 -0
- package/dist/plugins/openapi.cjs +1185 -0
- package/dist/plugins/openapi.d.cts +6 -0
- package/dist/plugins/openapi.d.ts +6 -0
- package/dist/plugins/openapi.js +1158 -0
- package/dist/plugins/rate-limit/express.cjs +55 -0
- package/dist/plugins/rate-limit/express.d.cts +64 -0
- package/dist/plugins/rate-limit/express.d.ts +64 -0
- package/dist/plugins/rate-limit/express.js +30 -0
- package/dist/plugins/rate-limit/hono.cjs +51 -0
- package/dist/plugins/rate-limit/hono.d.cts +59 -0
- package/dist/plugins/rate-limit/hono.d.ts +59 -0
- package/dist/plugins/rate-limit/hono.js +26 -0
- package/dist/plugins/rate-limit/sveltekit.cjs +49 -0
- package/dist/plugins/rate-limit/sveltekit.d.cts +59 -0
- package/dist/plugins/rate-limit/sveltekit.d.ts +59 -0
- package/dist/plugins/rate-limit/sveltekit.js +24 -0
- package/dist/plugins/rate-limit.cjs +354 -0
- package/dist/plugins/rate-limit.d.cts +140 -0
- package/dist/plugins/rate-limit.d.ts +140 -0
- package/dist/plugins/rate-limit.js +325 -0
- package/dist/plugins/social-login.cjs +905 -0
- package/dist/plugins/social-login.d.cts +114 -0
- package/dist/plugins/social-login.d.ts +114 -0
- package/dist/plugins/social-login.js +880 -0
- package/dist/plugins/tenancy.cjs +780 -0
- package/dist/plugins/tenancy.d.cts +108 -0
- package/dist/plugins/tenancy.d.ts +108 -0
- package/dist/plugins/tenancy.js +753 -0
- package/dist/plugins/two-factor.cjs +555 -0
- package/dist/plugins/two-factor.d.cts +67 -0
- package/dist/plugins/two-factor.d.ts +67 -0
- package/dist/plugins/two-factor.js +526 -0
- package/dist/plugins/webauthn.cjs +786 -0
- package/dist/plugins/webauthn.d.cts +72 -0
- package/dist/plugins/webauthn.d.ts +72 -0
- package/dist/plugins/webauthn.js +766 -0
- package/dist/plugins/webhook.cjs +819 -0
- package/dist/plugins/webhook.d.cts +254 -0
- package/dist/plugins/webhook.d.ts +254 -0
- package/dist/plugins/webhook.js +789 -0
- package/dist/protect-D1v_0upK.d.cts +123 -0
- package/dist/protect-DsxV_-dJ.d.ts +123 -0
- package/dist/sveltekit.cjs +1258 -0
- package/dist/sveltekit.d.cts +420 -0
- package/dist/sveltekit.d.ts +420 -0
- package/dist/sveltekit.js +1207 -0
- package/dist/testing.cjs +4294 -0
- package/dist/testing.d.cts +197 -0
- package/dist/testing.d.ts +197 -0
- package/dist/testing.js +4264 -0
- package/dist/types-et0R8tsC.d.cts +217 -0
- package/dist/types-et0R8tsC.d.ts +217 -0
- package/docs/adapter-typed-helpers.md +192 -0
- package/docs/admin-recipes.md +227 -0
- package/docs/architecture.md +434 -0
- package/docs/ci/github-actions.yml +59 -0
- package/docs/ci.md +109 -0
- package/docs/compatibility.md +115 -0
- package/docs/deployment.md +305 -0
- package/docs/hardening.md +118 -0
- package/docs/host-owned-routes.md +154 -0
- package/docs/migrations/0001-schema-version.md +54 -0
- package/docs/migrations/0002-initial-schema.md +84 -0
- package/docs/migrations/upgrade-guide.md +160 -0
- package/docs/observability.md +394 -0
- package/docs/plugins/account-lockout.md +131 -0
- package/docs/plugins/admin.md +402 -0
- package/docs/plugins/api-key.md +327 -0
- package/docs/plugins/audit-log.md +261 -0
- package/docs/plugins/data-isolation.md +186 -0
- package/docs/plugins/email-verification.md +121 -0
- package/docs/plugins/magic-link.md +123 -0
- package/docs/plugins/oauth.md +544 -0
- package/docs/plugins/openapi.md +250 -0
- package/docs/plugins/rate-limit.md +223 -0
- package/docs/plugins/social-login.md +337 -0
- package/docs/plugins/tenancy.md +122 -0
- package/docs/plugins/two-factor.md +191 -0
- package/docs/plugins/webauthn.md +188 -0
- package/docs/plugins/webhook.md +242 -0
- package/docs/policy-as-code.md +156 -0
- package/docs/route-manifest.md +46 -0
- package/docs/security.md +261 -0
- package/docs/threat-model.md +161 -0
- package/examples/README.md +119 -0
- package/examples/express-app/index.ts +747 -0
- package/examples/hono-app/docker-compose.yml +14 -0
- package/examples/hono-app/index.ts +1009 -0
- package/examples/policy/fortress.policy.json +47 -0
- package/examples/sveltekit-app/README.md +125 -0
- package/examples/sveltekit-app/src/app.d.ts +16 -0
- package/examples/sveltekit-app/src/hooks.server.ts +23 -0
- package/examples/sveltekit-app/src/lib/server/fortress.ts +81 -0
- package/examples/sveltekit-app/src/routes/api/echo/[id]/+server.ts +32 -0
- package/examples/sveltekit-app/src/routes/api/fortress/[...path]/+server.ts +15 -0
- package/examples/sveltekit-app/src/routes/dashboard/+page.server.ts +20 -0
- package/examples/sveltekit-app/src/routes/login/+page.server.ts +48 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.server.ts +69 -0
- package/examples/sveltekit-app/src/routes/oauth/consent/+page.svelte +83 -0
- package/migrations/pg/0001_schema_version.down.sql +2 -0
- package/migrations/pg/0001_schema_version.sql +8 -0
- package/migrations/pg/0002_initial_schema.down.sql +36 -0
- package/migrations/pg/0002_initial_schema.sql +376 -0
- package/migrations/pg/0003_auth_continuation.down.sql +6 -0
- package/migrations/pg/0003_auth_continuation.sql +30 -0
- package/migrations/pg/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/pg/0004_tenant_default_unique.sql +14 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.down.sql +71 -0
- package/migrations/pg/0005_hot_indexes_timestamptz.sql +71 -0
- package/migrations/pg/0006_canonical_email.down.sql +3 -0
- package/migrations/pg/0006_canonical_email.sql +8 -0
- package/migrations/pg/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/pg/0007_audit_chain_anchor.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.down.sql +8 -0
- package/migrations/pg/0008_two_factor_hardening.sql +8 -0
- package/migrations/pg/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/pg/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/pg/0010_bigint_append_only_ids.down.sql +2 -0
- package/migrations/pg/0010_bigint_append_only_ids.sql +23 -0
- package/migrations/sqlite/0001_schema_version.down.sql +2 -0
- package/migrations/sqlite/0001_schema_version.sql +8 -0
- package/migrations/sqlite/0002_initial_schema.down.sql +36 -0
- package/migrations/sqlite/0002_initial_schema.sql +376 -0
- package/migrations/sqlite/0003_auth_continuation.down.sql +27 -0
- package/migrations/sqlite/0003_auth_continuation.sql +45 -0
- package/migrations/sqlite/0004_tenant_default_unique.down.sql +1 -0
- package/migrations/sqlite/0004_tenant_default_unique.sql +13 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.down.sql +10 -0
- package/migrations/sqlite/0005_hot_indexes_timestamptz.sql +10 -0
- package/migrations/sqlite/0006_canonical_email.down.sql +3 -0
- package/migrations/sqlite/0006_canonical_email.sql +8 -0
- package/migrations/sqlite/0007_audit_chain_anchor.down.sql +1 -0
- package/migrations/sqlite/0007_audit_chain_anchor.sql +8 -0
- package/migrations/sqlite/0008_two_factor_hardening.down.sql +14 -0
- package/migrations/sqlite/0008_two_factor_hardening.sql +7 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.down.sql +2 -0
- package/migrations/sqlite/0009_encrypt_totp_secrets.sql +5 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.down.sql +1 -0
- package/migrations/sqlite/0010_bigint_append_only_ids.sql +2 -0
- package/package.json +398 -0
- package/src/adapters/database/index.ts +63 -0
- package/src/adapters/database/types.ts +22 -0
- package/src/changelog-script.test.ts +21 -0
- package/src/cli.test.ts +79 -0
- package/src/core/auth/auth-admin.test.ts +247 -0
- package/src/core/auth/auth-endpoints.ts +558 -0
- package/src/core/auth/auth-observer.test.ts +191 -0
- package/src/core/auth/auth-service.ts +1464 -0
- package/src/core/auth/email.test.ts +17 -0
- package/src/core/auth/email.ts +11 -0
- package/src/core/auth/hooks.test.ts +251 -0
- package/src/core/auth/impersonation.test.ts +179 -0
- package/src/core/auth/jwt.test.ts +184 -0
- package/src/core/auth/jwt.ts +239 -0
- package/src/core/auth/login-timing.test.ts +77 -0
- package/src/core/auth/password-policy.test.ts +253 -0
- package/src/core/auth/password-policy.ts +220 -0
- package/src/core/auth/password.test.ts +42 -0
- package/src/core/auth/password.ts +62 -0
- package/src/core/auth/post-auth-gate.test.ts +258 -0
- package/src/core/auth/post-auth-gate.ts +228 -0
- package/src/core/auth/refresh-token.test.ts +86 -0
- package/src/core/auth/refresh-token.ts +105 -0
- package/src/core/auth/timing-safe.ts +23 -0
- package/src/core/config.ts +212 -0
- package/src/core/endpoint.ts +149 -0
- package/src/core/errors.ts +199 -0
- package/src/core/fetcher-interop.test.ts +106 -0
- package/src/core/fortress.test.ts +354 -0
- package/src/core/fortress.ts +550 -0
- package/src/core/http/call.test.ts +148 -0
- package/src/core/http/call.ts +149 -0
- package/src/core/http/cookie-serialize.test.ts +198 -0
- package/src/core/http/cookie-serialize.ts +107 -0
- package/src/core/http/csrf.test.ts +140 -0
- package/src/core/http/csrf.ts +173 -0
- package/src/core/http/dispatch.ts +652 -0
- package/src/core/http/error-response.test.ts +69 -0
- package/src/core/http/error-response.ts +93 -0
- package/src/core/http/fortress-rbac.test.ts +43 -0
- package/src/core/http/fortress-rbac.ts +127 -0
- package/src/core/http/handle-request.test.ts +441 -0
- package/src/core/http/handle-request.ts +354 -0
- package/src/core/http/match.test.ts +122 -0
- package/src/core/http/match.ts +126 -0
- package/src/core/http/outbound.test.ts +34 -0
- package/src/core/http/outbound.ts +105 -0
- package/src/core/http/plugin-middleware.ts +67 -0
- package/src/core/http/principal.test.ts +345 -0
- package/src/core/http/principal.ts +86 -0
- package/src/core/http/protect.test.ts +220 -0
- package/src/core/http/protect.ts +394 -0
- package/src/core/http/token-extraction.test.ts +59 -0
- package/src/core/http/token-extraction.ts +53 -0
- package/src/core/iam/explain.test.ts +177 -0
- package/src/core/iam/explain.ts +302 -0
- package/src/core/iam/iam-endpoints.ts +779 -0
- package/src/core/iam/iam-service.test.ts +829 -0
- package/src/core/iam/iam-service.ts +1137 -0
- package/src/core/iam/permission-cache.test.ts +115 -0
- package/src/core/iam/permission-cache.ts +71 -0
- package/src/core/iam/permission-check-observer.test.ts +90 -0
- package/src/core/iam/permission-evaluator.test.ts +291 -0
- package/src/core/iam/permission-evaluator.ts +198 -0
- package/src/core/iam/permission-sync.test.ts +166 -0
- package/src/core/iam/permission-sync.ts +192 -0
- package/src/core/iam/resource-sync.test.ts +70 -0
- package/src/core/iam/resource-sync.ts +182 -0
- package/src/core/iam/service-account-http.test.ts +529 -0
- package/src/core/iam/service-account.test.ts +282 -0
- package/src/core/internal-adapter.test.ts +389 -0
- package/src/core/internal-adapter.ts +435 -0
- package/src/core/json-schema.ts +50 -0
- package/src/core/manifest/__snapshots__/route-manifest.test.ts.snap +192 -0
- package/src/core/manifest/drift.ts +103 -0
- package/src/core/manifest/route-manifest.test.ts +142 -0
- package/src/core/manifest/route-manifest.ts +154 -0
- package/src/core/migrate.test.ts +72 -0
- package/src/core/migrations/engine.test.ts +308 -0
- package/src/core/migrations/engine.ts +548 -0
- package/src/core/migrations/migrations.ts +1771 -0
- package/src/core/migrations/pg-migration.integration-test.ts +353 -0
- package/src/core/migrations/upgrade-fixture.test.ts +378 -0
- package/src/core/observability/db-instrumentation.ts +205 -0
- package/src/core/observability/listener-list.test.ts +124 -0
- package/src/core/observability/listener-list.ts +73 -0
- package/src/core/observability/logger.test.ts +43 -0
- package/src/core/observability/logger.ts +49 -0
- package/src/core/observability/spans.test.ts +193 -0
- package/src/core/observability/types.ts +80 -0
- package/src/core/openapi-drift.test.ts +41 -0
- package/src/core/openapi.ts +47 -0
- package/src/core/plugin-methods-map.ts +75 -0
- package/src/core/plugin-runner.test.ts +233 -0
- package/src/core/plugin-runner.ts +276 -0
- package/src/core/plugin.ts +210 -0
- package/src/core/policy/apply.ts +272 -0
- package/src/core/policy/diff.ts +288 -0
- package/src/core/policy/loader.test.ts +63 -0
- package/src/core/policy/loader.ts +214 -0
- package/src/core/policy/policy.test.ts +232 -0
- package/src/core/policy/types.ts +105 -0
- package/src/core/schema-builder.test.ts +660 -0
- package/src/core/schema-builder.ts +881 -0
- package/src/core/standard-schema.ts +56 -0
- package/src/core/types.ts +258 -0
- package/src/core/validation.test.ts +195 -0
- package/src/core/validation.ts +192 -0
- package/src/drizzle/adapter.test.ts +425 -0
- package/src/drizzle/adapter.ts +474 -0
- package/src/drizzle/index.ts +31 -0
- package/src/drizzle/pg/adapter.integration-test.ts +456 -0
- package/src/drizzle/pg/index.ts +13 -0
- package/src/drizzle/pg/pg.integration-test.ts +1539 -0
- package/src/drizzle/pg/schema.ts +539 -0
- package/src/drizzle/pg-error-map.test.ts +218 -0
- package/src/drizzle/pg-error-map.ts +151 -0
- package/src/drizzle/schema.ts +544 -0
- package/src/drizzle/sqlite-serialization.test.ts +106 -0
- package/src/express/express-api-key-user-routes.test.ts +241 -0
- package/src/express/express.test.ts +442 -0
- package/src/express/handle.ts +191 -0
- package/src/express/index.ts +62 -0
- package/src/express/middleware.ts +551 -0
- package/src/express/protect.ts +154 -0
- package/src/express/validated.test.ts +86 -0
- package/src/express/validated.ts +99 -0
- package/src/fetcher/index.ts +81 -0
- package/src/hono/convert-routes.test.ts +220 -0
- package/src/hono/convert-routes.ts +196 -0
- package/src/hono/converters.test.ts +50 -0
- package/src/hono/converters.ts +43 -0
- package/src/hono/handle.ts +79 -0
- package/src/hono/helpers.ts +2 -0
- package/src/hono/hono-api-key-user-routes.test.ts +225 -0
- package/src/hono/hono-handlers.test.ts +861 -0
- package/src/hono/hono.test.ts +454 -0
- package/src/hono/index.ts +101 -0
- package/src/hono/middleware/auth.ts +236 -0
- package/src/hono/middleware/auth.types.test.ts +94 -0
- package/src/hono/middleware/csrf.test.ts +127 -0
- package/src/hono/middleware/csrf.ts +68 -0
- package/src/hono/middleware/error-handler.ts +12 -0
- package/src/hono/middleware/plugin-middleware.ts +35 -0
- package/src/hono/middleware/rbac.ts +131 -0
- package/src/hono/middleware/security-headers.test.ts +88 -0
- package/src/hono/middleware/security-headers.ts +68 -0
- package/src/hono/openapi.ts +237 -0
- package/src/hono/protect.ts +87 -0
- package/src/hono/validated.test.ts +123 -0
- package/src/hono/validated.ts +62 -0
- package/src/index.ts +309 -0
- package/src/integration.test.ts +718 -0
- package/src/internal/changelog.ts +56 -0
- package/src/otel/index.ts +158 -0
- package/src/otel/otel-types.test.ts +35 -0
- package/src/otel/otel.test.ts +235 -0
- package/src/plugins/account-lockout/account-lockout.test.ts +367 -0
- package/src/plugins/account-lockout/index.ts +267 -0
- package/src/plugins/admin/admin-api-key.test.ts +438 -0
- package/src/plugins/admin/index.ts +1612 -0
- package/src/plugins/api-key/api-key.test.ts +684 -0
- package/src/plugins/api-key/core.ts +336 -0
- package/src/plugins/api-key/index.ts +315 -0
- package/src/plugins/audit-log/audit-log.test.ts +749 -0
- package/src/plugins/audit-log/index.ts +680 -0
- package/src/plugins/data-isolation/data-isolation.test.ts +197 -0
- package/src/plugins/data-isolation/index.ts +157 -0
- package/src/plugins/email-verification/email-verification.test.ts +199 -0
- package/src/plugins/email-verification/index.ts +190 -0
- package/src/plugins/magic-link/index.ts +129 -0
- package/src/plugins/magic-link/magic-link.test.ts +156 -0
- package/src/plugins/oauth/id-token.ts +86 -0
- package/src/plugins/oauth/index.ts +2145 -0
- package/src/plugins/oauth/jwks.test.ts +32 -0
- package/src/plugins/oauth/jwks.ts +182 -0
- package/src/plugins/oauth/oauth.test.ts +2220 -0
- package/src/plugins/oauth/pkce.ts +58 -0
- package/src/plugins/openapi/index.ts +298 -0
- package/src/plugins/openapi/openapi.test.ts +425 -0
- package/src/plugins/openapi/spec-builder.ts +287 -0
- package/src/plugins/rate-limit/express.test.ts +89 -0
- package/src/plugins/rate-limit/express.ts +86 -0
- package/src/plugins/rate-limit/hono.test.ts +60 -0
- package/src/plugins/rate-limit/hono.ts +74 -0
- package/src/plugins/rate-limit/index.ts +383 -0
- package/src/plugins/rate-limit/memory-store.ts +61 -0
- package/src/plugins/rate-limit/rate-limit.test.ts +756 -0
- package/src/plugins/rate-limit/sveltekit.test.ts +58 -0
- package/src/plugins/rate-limit/sveltekit.ts +66 -0
- package/src/plugins/social-login/index.ts +715 -0
- package/src/plugins/social-login/providers/apple.ts +34 -0
- package/src/plugins/social-login/providers/discord.ts +26 -0
- package/src/plugins/social-login/providers/github.ts +54 -0
- package/src/plugins/social-login/providers/google.ts +23 -0
- package/src/plugins/social-login/providers/index.ts +22 -0
- package/src/plugins/social-login/providers/microsoft.ts +35 -0
- package/src/plugins/social-login/providers/oidc.ts +37 -0
- package/src/plugins/social-login/providers/providers.test.ts +211 -0
- package/src/plugins/social-login/social-login.test.ts +675 -0
- package/src/plugins/social-login/types.ts +80 -0
- package/src/plugins/tenancy/index.ts +544 -0
- package/src/plugins/tenancy/tenancy.test.ts +323 -0
- package/src/plugins/two-factor/index.ts +569 -0
- package/src/plugins/two-factor/two-factor.test.ts +235 -0
- package/src/plugins/webauthn/index.ts +554 -0
- package/src/plugins/webauthn/webauthn.test.ts +472 -0
- package/src/plugins/webhook/builtin-events.ts +29 -0
- package/src/plugins/webhook/delivery.test.ts +51 -0
- package/src/plugins/webhook/delivery.ts +154 -0
- package/src/plugins/webhook/hooks.ts +89 -0
- package/src/plugins/webhook/index.ts +445 -0
- package/src/plugins/webhook/queue/database.ts +67 -0
- package/src/plugins/webhook/queue/in-memory.test.ts +48 -0
- package/src/plugins/webhook/queue/in-memory.ts +71 -0
- package/src/plugins/webhook/queue/types.ts +51 -0
- package/src/plugins/webhook/registry.test.ts +48 -0
- package/src/plugins/webhook/registry.ts +64 -0
- package/src/plugins/webhook/signing.ts +45 -0
- package/src/plugins/webhook/ssrf.ts +198 -0
- package/src/plugins/webhook/types.ts +138 -0
- package/src/plugins/webhook/webhook.test.ts +324 -0
- package/src/sveltekit/actions.ts +233 -0
- package/src/sveltekit/catch-all.ts +43 -0
- package/src/sveltekit/cookies.ts +136 -0
- package/src/sveltekit/handle.ts +356 -0
- package/src/sveltekit/helpers.ts +69 -0
- package/src/sveltekit/index.ts +74 -0
- package/src/sveltekit/protect.ts +80 -0
- package/src/sveltekit/sveltekit-types.test.ts +31 -0
- package/src/sveltekit/sveltekit.test.ts +799 -0
- package/src/sveltekit/types.ts +134 -0
- package/src/sveltekit/validated.test.ts +117 -0
- package/src/sveltekit/validated.ts +78 -0
- package/src/testing/adapter-conformance.test.ts +408 -0
- package/src/testing/checks.test.ts +124 -0
- package/src/testing/checks.ts +304 -0
- package/src/testing/index.ts +107 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration upgrade fixture — proves the bundled migrations apply
|
|
3
|
+
* end-to-end against a freshly provisioned database, not just against the
|
|
4
|
+
* test adapter (which pre-creates every Fortress table).
|
|
5
|
+
*
|
|
6
|
+
* Uses a bare in-memory SQLite database so the starting state has no
|
|
7
|
+
* Fortress tables at all. As bundled migrations grow beyond the
|
|
8
|
+
* schema-version checkpoint, this fixture doubles as the regression
|
|
9
|
+
* guard that catches a migration that silently fails to install a table.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { createDrizzleAdapter } from '../../drizzle/adapter';
|
|
15
|
+
import {
|
|
16
|
+
detectMigrationDrift,
|
|
17
|
+
getMigrationStatus,
|
|
18
|
+
hasMigrationDrift,
|
|
19
|
+
migrateDown,
|
|
20
|
+
migrateUp,
|
|
21
|
+
} from './engine';
|
|
22
|
+
import { FORTRESS_TABLES } from './migrations';
|
|
23
|
+
|
|
24
|
+
const isBun = typeof (globalThis as Record<string, unknown>).Bun !== 'undefined';
|
|
25
|
+
|
|
26
|
+
function createBareSqliteAdapter(): DatabaseAdapter {
|
|
27
|
+
if (isBun) {
|
|
28
|
+
// eslint-disable-next-line ts/no-require-imports
|
|
29
|
+
const { Database } = require('bun:sqlite');
|
|
30
|
+
// eslint-disable-next-line ts/no-require-imports
|
|
31
|
+
const { drizzle } = require('drizzle-orm/bun-sqlite');
|
|
32
|
+
const sqlite = new Database(':memory:');
|
|
33
|
+
sqlite.exec('PRAGMA foreign_keys = ON;');
|
|
34
|
+
return createDrizzleAdapter(drizzle(sqlite));
|
|
35
|
+
}
|
|
36
|
+
// eslint-disable-next-line ts/no-require-imports
|
|
37
|
+
const BetterSqlite3 = require('better-sqlite3');
|
|
38
|
+
// eslint-disable-next-line ts/no-require-imports
|
|
39
|
+
const { drizzle } = require('drizzle-orm/better-sqlite3');
|
|
40
|
+
const sqlite = new BetterSqlite3(':memory:');
|
|
41
|
+
sqlite.pragma('foreign_keys = ON');
|
|
42
|
+
return createDrizzleAdapter(drizzle(sqlite));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('migration upgrade fixture (bare sqlite)', () => {
|
|
46
|
+
it('provisions a bare DB end-to-end from the bundled migrations', async () => {
|
|
47
|
+
const db = createBareSqliteAdapter();
|
|
48
|
+
|
|
49
|
+
const initial = await getMigrationStatus(db, 'sqlite');
|
|
50
|
+
expect(initial.hasVersionTable).toBe(false);
|
|
51
|
+
expect(initial.currentVersion).toBe(0);
|
|
52
|
+
|
|
53
|
+
const initialDrift = await detectMigrationDrift(db, 'sqlite');
|
|
54
|
+
expect(hasMigrationDrift(initialDrift)).toBe(true);
|
|
55
|
+
expect(initialDrift.missingVersionTable).toBe(true);
|
|
56
|
+
// A bare DB is missing every Fortress table.
|
|
57
|
+
expect(initialDrift.missingTables.length).toBe(FORTRESS_TABLES.length);
|
|
58
|
+
|
|
59
|
+
const up = await migrateUp(db, 'sqlite');
|
|
60
|
+
expect(up.fromVersion).toBe(0);
|
|
61
|
+
expect(up.toVersion).toBe(10);
|
|
62
|
+
expect(up.applied.map(migration => migration.name)).toEqual(['schema_version', 'initial_schema', 'auth_continuation', 'tenant_default_unique', 'hot_indexes_timestamptz', 'canonical_email', 'audit_chain_anchor', 'two_factor_hardening', 'encrypt_totp_secrets', 'bigint_append_only_ids']);
|
|
63
|
+
|
|
64
|
+
const after = await getMigrationStatus(db, 'sqlite');
|
|
65
|
+
expect(after.hasVersionTable).toBe(true);
|
|
66
|
+
expect(after.currentVersion).toBe(10);
|
|
67
|
+
expect(after.upToDate).toBe(true);
|
|
68
|
+
|
|
69
|
+
// The full schema is now installed: no missing tables, no missing columns.
|
|
70
|
+
const afterDrift = await detectMigrationDrift(db, 'sqlite');
|
|
71
|
+
expect(hasMigrationDrift(afterDrift)).toBe(false);
|
|
72
|
+
expect(afterDrift.missingTables).toEqual([]);
|
|
73
|
+
expect(afterDrift.missingColumns).toEqual([]);
|
|
74
|
+
expect(afterDrift.missingIndexes).toEqual([]);
|
|
75
|
+
|
|
76
|
+
// The provisioned schema is actually usable, not just present.
|
|
77
|
+
const user = await db.create<{ id: string }>({
|
|
78
|
+
model: 'user',
|
|
79
|
+
data: { email: 'fixture@test.com', name: 'Fixture', passwordHash: 'h', isActive: true },
|
|
80
|
+
});
|
|
81
|
+
expect(user.id).toBeTruthy();
|
|
82
|
+
|
|
83
|
+
const continuation = await db.create<{ id: string; consumedAt: Date | null }>({
|
|
84
|
+
model: 'auth_continuation',
|
|
85
|
+
data: {
|
|
86
|
+
userId: user.id,
|
|
87
|
+
tokenHash: 'fixture-continuation-hash',
|
|
88
|
+
reason: 'two-factor',
|
|
89
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
90
|
+
consumedAt: null,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
expect(continuation.id).toBeTruthy();
|
|
94
|
+
expect(continuation.consumedAt).toBeNull();
|
|
95
|
+
|
|
96
|
+
// Re-running is idempotent.
|
|
97
|
+
const reapply = await migrateUp(db, 'sqlite');
|
|
98
|
+
expect(reapply.applied).toEqual([]);
|
|
99
|
+
|
|
100
|
+
// Roll back below 0 drops every Fortress table again.
|
|
101
|
+
const down = await migrateDown(db, 'sqlite');
|
|
102
|
+
expect(down.toVersion).toBe(0);
|
|
103
|
+
expect(down.rolledBack.map(migration => migration.name)).toEqual(['bigint_append_only_ids', 'encrypt_totp_secrets', 'two_factor_hardening', 'audit_chain_anchor', 'canonical_email', 'hot_indexes_timestamptz', 'tenant_default_unique', 'auth_continuation', 'initial_schema', 'schema_version']);
|
|
104
|
+
const final = await getMigrationStatus(db, 'sqlite');
|
|
105
|
+
expect(final.hasVersionTable).toBe(false);
|
|
106
|
+
const finalDrift = await detectMigrationDrift(db, 'sqlite');
|
|
107
|
+
expect(finalDrift.missingTables.length).toBe(FORTRESS_TABLES.length);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('migration 0009 removes legacy plaintext TOTP enrolments irreversibly', async () => {
|
|
111
|
+
const db = createBareSqliteAdapter();
|
|
112
|
+
await migrateUp(db, 'sqlite', 8);
|
|
113
|
+
await db.rawQuery!('INSERT INTO fortress_user (email, name) VALUES (\'mfa@example.com\', \'MFA\')');
|
|
114
|
+
const [user] = await db.rawQuery!<{ id: string }>('SELECT id FROM fortress_user WHERE email = \'mfa@example.com\'');
|
|
115
|
+
await db.rawQuery!(
|
|
116
|
+
'INSERT INTO fortress_two_factor_secret (user_id, secret, is_enabled) VALUES (?, ?, 1)',
|
|
117
|
+
[user.id, 'PLAINTEXT-TOTP-SEED'],
|
|
118
|
+
);
|
|
119
|
+
await db.rawQuery!(
|
|
120
|
+
'INSERT INTO fortress_backup_code (user_id, code_hash, is_used) VALUES (?, ?, 0)',
|
|
121
|
+
[user.id, 'backup-hash'],
|
|
122
|
+
);
|
|
123
|
+
await db.rawQuery!(
|
|
124
|
+
'INSERT INTO fortress_trusted_device (user_id, device_hash, expires_at, last_used_at) VALUES (?, ?, ?, ?)',
|
|
125
|
+
[user.id, 'device-hash', 1_900_000_000, 1_800_000_000],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const result = await migrateUp(db, 'sqlite');
|
|
129
|
+
expect(result.applied.map(migration => migration.name)).toEqual(['encrypt_totp_secrets', 'bigint_append_only_ids']);
|
|
130
|
+
expect(await db.count({ model: 'two_factor_secret' })).toBe(0);
|
|
131
|
+
expect(await db.count({ model: 'backup_code' })).toBe(0);
|
|
132
|
+
expect(await db.count({ model: 'trusted_device' })).toBe(0);
|
|
133
|
+
|
|
134
|
+
await migrateDown(db, 'sqlite', 8);
|
|
135
|
+
expect(await db.count({ model: 'two_factor_secret' })).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('canonicalizes legacy emails and quarantines duplicate accounts before v6 indexes', async () => {
|
|
139
|
+
const db = createBareSqliteAdapter();
|
|
140
|
+
await migrateUp(db, 'sqlite', 5);
|
|
141
|
+
|
|
142
|
+
await db.rawQuery!(
|
|
143
|
+
`INSERT INTO fortress_user (email, name, is_active) VALUES (?, ?, 1), (?, ?, 1)`,
|
|
144
|
+
['É@Example.COM', 'Oldest duplicate', 'E\u0301@example.com', 'Later duplicate'],
|
|
145
|
+
);
|
|
146
|
+
const users = await db.rawQuery!<{ id: string; email: string }>(
|
|
147
|
+
'SELECT id, email FROM fortress_user ORDER BY id',
|
|
148
|
+
);
|
|
149
|
+
await db.rawQuery!(
|
|
150
|
+
`INSERT INTO fortress_user (email, name, is_active) VALUES (?, ?, 1)`,
|
|
151
|
+
[`FORTRESS-DUPLICATE-${users[1].id}@INVALID`, 'Tombstone collision'],
|
|
152
|
+
);
|
|
153
|
+
await db.rawQuery!(
|
|
154
|
+
`INSERT INTO fortress_login_identifier (user_id, type, value)
|
|
155
|
+
VALUES (?, 'email', ?), (?, 'email', 'Alias@Example.COM'), (?, 'email', ?)`,
|
|
156
|
+
[users[0].id, users[0].email, users[0].id, users[1].id, users[1].email],
|
|
157
|
+
);
|
|
158
|
+
await db.rawQuery!(
|
|
159
|
+
`INSERT INTO fortress_login_identifier (user_id, type, value)
|
|
160
|
+
SELECT id, 'email', 'É@EXAMPLE.COM' FROM fortress_user WHERE name = 'Tombstone collision'`,
|
|
161
|
+
);
|
|
162
|
+
await db.rawQuery!(
|
|
163
|
+
`INSERT INTO fortress_login_identifier (user_id, type, value)
|
|
164
|
+
VALUES (?, 'email', 'CROSS@EXAMPLE.COM')`,
|
|
165
|
+
[users[0].id],
|
|
166
|
+
);
|
|
167
|
+
await db.rawQuery!(
|
|
168
|
+
`INSERT INTO fortress_login_identifier (user_id, type, value)
|
|
169
|
+
SELECT id, 'username', 'cross@example.com' FROM fortress_user WHERE name = 'Tombstone collision'`,
|
|
170
|
+
);
|
|
171
|
+
await db.rawQuery!(
|
|
172
|
+
`INSERT INTO fortress_refresh_token (user_id, token_hash, token_family, is_revoked, expires_at)
|
|
173
|
+
VALUES (?, ?, ?, 0, ?)`,
|
|
174
|
+
[users[1].id, 'duplicate-session', 'duplicate-family', 1_900_000_000],
|
|
175
|
+
);
|
|
176
|
+
await db.rawQuery!(
|
|
177
|
+
`INSERT INTO fortress_oauth_refresh_token
|
|
178
|
+
(token, family_id, client_id, user_id, issued_at, expires_at)
|
|
179
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
180
|
+
['oauth-duplicate-session', 'oauth-duplicate-family', 'client', users[1].id, 1_700_000_000, 1_900_000_000],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const upgrade = await migrateUp(db, 'sqlite');
|
|
184
|
+
expect(upgrade.applied.map(migration => migration.name)).toEqual(['canonical_email', 'audit_chain_anchor', 'two_factor_hardening', 'encrypt_totp_secrets', 'bigint_append_only_ids']);
|
|
185
|
+
|
|
186
|
+
const migrated = await db.rawQuery!<{ id: string; email: string; is_active: number }>(
|
|
187
|
+
'SELECT id, email, is_active FROM fortress_user ORDER BY id',
|
|
188
|
+
);
|
|
189
|
+
expect(migrated[0]).toMatchObject({ email: 'é@example.com', is_active: 1 });
|
|
190
|
+
expect(migrated[1]).toMatchObject({
|
|
191
|
+
email: `fortress-duplicate-${users[1].id}-1@invalid`,
|
|
192
|
+
is_active: 0,
|
|
193
|
+
});
|
|
194
|
+
const identifiers = await db.rawQuery!<{ value: string }>(
|
|
195
|
+
`SELECT value FROM fortress_login_identifier ORDER BY id`,
|
|
196
|
+
);
|
|
197
|
+
expect(identifiers.map(row => row.value)).toEqual([
|
|
198
|
+
'é@example.com',
|
|
199
|
+
'alias@example.com',
|
|
200
|
+
'cross@example.com',
|
|
201
|
+
]);
|
|
202
|
+
const [session] = await db.rawQuery!<{ is_revoked: number }>(
|
|
203
|
+
'SELECT is_revoked FROM fortress_refresh_token WHERE token_hash = ?',
|
|
204
|
+
['duplicate-session'],
|
|
205
|
+
);
|
|
206
|
+
expect(session.is_revoked).toBe(1);
|
|
207
|
+
expect(await db.count({ model: 'oauth_refresh_token' })).toBe(0);
|
|
208
|
+
|
|
209
|
+
await expect(db.create({
|
|
210
|
+
model: 'user',
|
|
211
|
+
data: { email: 'é@EXAMPLE.COM', name: 'Blocked duplicate', isActive: true },
|
|
212
|
+
})).rejects.toMatchObject({ code: 'CONFLICT', statusCode: 409 });
|
|
213
|
+
|
|
214
|
+
const other = await db.create<{ id: string }>({
|
|
215
|
+
model: 'user',
|
|
216
|
+
data: { email: 'other@example.com', name: 'Other', isActive: true },
|
|
217
|
+
});
|
|
218
|
+
await expect(db.create({
|
|
219
|
+
model: 'login_identifier',
|
|
220
|
+
data: { userId: other.id, type: 'email', value: 'é@EXAMPLE.COM' },
|
|
221
|
+
})).rejects.toMatchObject({ code: 'CONFLICT', statusCode: 409 });
|
|
222
|
+
const indexes = await db.rawQuery!<{ name: string }>(
|
|
223
|
+
`SELECT name FROM sqlite_master WHERE type = 'index'
|
|
224
|
+
AND name IN ('user_email_ci_unique', 'login_identifier_email_ci_unique') ORDER BY name`,
|
|
225
|
+
);
|
|
226
|
+
expect(indexes.map(row => row.name)).toEqual([
|
|
227
|
+
'login_identifier_email_ci_unique',
|
|
228
|
+
'user_email_ci_unique',
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('rolls back email cleanup atomically when a v6 constraint cannot be installed', async () => {
|
|
233
|
+
const db = createBareSqliteAdapter();
|
|
234
|
+
await migrateUp(db, 'sqlite', 5);
|
|
235
|
+
await db.rawQuery!(
|
|
236
|
+
`INSERT INTO fortress_user (email, name, is_active)
|
|
237
|
+
VALUES ('Dup@Example.com', 'Winner', 1), ('dup@example.com', 'Loser', 1)`,
|
|
238
|
+
);
|
|
239
|
+
const users = await db.rawQuery!<{ id: string; email: string }>('SELECT id, email FROM fortress_user ORDER BY id');
|
|
240
|
+
await db.rawQuery!(
|
|
241
|
+
`INSERT INTO fortress_login_identifier (user_id, type, value)
|
|
242
|
+
VALUES (?, 'email', ?), (?, 'email', ?)`,
|
|
243
|
+
[users[0].id, users[0].email, users[1].id, users[1].email],
|
|
244
|
+
);
|
|
245
|
+
// Force the DDL step to fail after cleanup by occupying its index name.
|
|
246
|
+
await db.rawQuery!('CREATE INDEX user_email_ci_unique ON fortress_user (name)');
|
|
247
|
+
|
|
248
|
+
await expect(migrateUp(db, 'sqlite')).rejects.toThrow();
|
|
249
|
+
const after = await db.rawQuery!<{ email: string; is_active: number }>(
|
|
250
|
+
'SELECT email, is_active FROM fortress_user ORDER BY id',
|
|
251
|
+
);
|
|
252
|
+
expect(after).toEqual([
|
|
253
|
+
{ email: 'Dup@Example.com', is_active: 1 },
|
|
254
|
+
{ email: 'dup@example.com', is_active: 1 },
|
|
255
|
+
]);
|
|
256
|
+
expect((await getMigrationStatus(db, 'sqlite')).currentVersion).toBe(5);
|
|
257
|
+
const columns = await db.rawQuery!<{ name: string }>('PRAGMA index_info(user_email_ci_unique)');
|
|
258
|
+
expect(columns.map(column => column.name)).toEqual(['name']);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('upgrades v2 refresh families without resetting their age', async () => {
|
|
262
|
+
const db = createBareSqliteAdapter();
|
|
263
|
+
await migrateUp(db, 'sqlite', 2);
|
|
264
|
+
|
|
265
|
+
await db.rawQuery!(
|
|
266
|
+
`INSERT INTO fortress_user (email, name, password_hash, is_active, created_at, updated_at)
|
|
267
|
+
VALUES (?, ?, ?, 1, ?, ?)`,
|
|
268
|
+
['legacy@test.com', 'Legacy', 'h', 1_700_000_000, 1_700_000_000],
|
|
269
|
+
);
|
|
270
|
+
const [user] = await db.rawQuery!<{ id: string }>(
|
|
271
|
+
'SELECT id FROM fortress_user WHERE email = ?',
|
|
272
|
+
['legacy@test.com'],
|
|
273
|
+
);
|
|
274
|
+
await db.rawQuery!(
|
|
275
|
+
`INSERT INTO fortress_refresh_token
|
|
276
|
+
(user_id, token_hash, token_family, is_revoked, expires_at, created_at)
|
|
277
|
+
VALUES (?, ?, ?, 0, ?, ?), (?, ?, ?, 1, ?, ?)`,
|
|
278
|
+
[
|
|
279
|
+
user.id,
|
|
280
|
+
'legacy-hash-1',
|
|
281
|
+
'legacy-family',
|
|
282
|
+
1_900_000_000,
|
|
283
|
+
1_700_000_000,
|
|
284
|
+
user.id,
|
|
285
|
+
'legacy-hash-2',
|
|
286
|
+
'legacy-family',
|
|
287
|
+
1_900_000_000,
|
|
288
|
+
1_750_000_000,
|
|
289
|
+
],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Simulate a real pre-v4 database, including duplicate defaults that the
|
|
293
|
+
// new migration must repair before adding its partial unique index.
|
|
294
|
+
await db.rawQuery!('DROP INDEX IF EXISTS fortress_tenant_user_one_default_idx');
|
|
295
|
+
await db.rawQuery!(
|
|
296
|
+
`INSERT INTO fortress_tenant (name, tax_id) VALUES (?, ?), (?, ?)`,
|
|
297
|
+
['Tenant A', 'tenant-a', 'Tenant B', 'tenant-b'],
|
|
298
|
+
);
|
|
299
|
+
const tenants = await db.rawQuery!<{ id: string }>(
|
|
300
|
+
`SELECT id FROM fortress_tenant WHERE tax_id IN (?, ?) ORDER BY tax_id`,
|
|
301
|
+
['tenant-a', 'tenant-b'],
|
|
302
|
+
);
|
|
303
|
+
await db.rawQuery!(
|
|
304
|
+
`INSERT INTO fortress_tenant_user (tenant_id, user_id, is_default) VALUES (?, ?, 1), (?, ?, 1)`,
|
|
305
|
+
[tenants[0].id, user.id, tenants[1].id, user.id],
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const upgrade = await migrateUp(db, 'sqlite');
|
|
309
|
+
expect(upgrade.applied.map(migration => migration.name)).toEqual(['auth_continuation', 'tenant_default_unique', 'hot_indexes_timestamptz', 'canonical_email', 'audit_chain_anchor', 'two_factor_hardening', 'encrypt_totp_secrets', 'bigint_append_only_ids']);
|
|
310
|
+
const [defaultCount] = await db.rawQuery!<{ count: number }>(
|
|
311
|
+
`SELECT COUNT(*) AS count FROM fortress_tenant_user WHERE user_id = ? AND is_default = 1`,
|
|
312
|
+
[user.id],
|
|
313
|
+
);
|
|
314
|
+
expect(Number(defaultCount.count)).toBe(1);
|
|
315
|
+
const tenantIndexes = await db.rawQuery!<{ name: string }>(
|
|
316
|
+
`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`,
|
|
317
|
+
['fortress_tenant_user_one_default_idx'],
|
|
318
|
+
);
|
|
319
|
+
expect(tenantIndexes).toHaveLength(1);
|
|
320
|
+
|
|
321
|
+
const tokens = await db.rawQuery!<{
|
|
322
|
+
token_hash: string;
|
|
323
|
+
family_created_at: number;
|
|
324
|
+
successor_token_hash: string | null;
|
|
325
|
+
rotated_at: number | null;
|
|
326
|
+
}>(`SELECT token_hash, family_created_at, successor_token_hash, rotated_at
|
|
327
|
+
FROM fortress_refresh_token WHERE token_family = ? ORDER BY created_at`, ['legacy-family']);
|
|
328
|
+
expect(tokens).toEqual([
|
|
329
|
+
{
|
|
330
|
+
token_hash: 'legacy-hash-1',
|
|
331
|
+
family_created_at: 1_700_000_000,
|
|
332
|
+
successor_token_hash: null,
|
|
333
|
+
rotated_at: null,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
token_hash: 'legacy-hash-2',
|
|
337
|
+
family_created_at: 1_700_000_000,
|
|
338
|
+
successor_token_hash: null,
|
|
339
|
+
rotated_at: null,
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
const beforeDefaultInsert = Math.floor(Date.now() / 1000) - 5;
|
|
344
|
+
await db.rawQuery!(
|
|
345
|
+
`INSERT INTO fortress_refresh_token
|
|
346
|
+
(user_id, token_hash, token_family, is_revoked, expires_at)
|
|
347
|
+
VALUES (?, ?, ?, 0, ?)`,
|
|
348
|
+
[user.id, 'default-hash', 'default-family', 1_900_000_000],
|
|
349
|
+
);
|
|
350
|
+
const [defaulted] = await db.rawQuery!<{ family_created_at: number }>(
|
|
351
|
+
'SELECT family_created_at FROM fortress_refresh_token WHERE token_hash = ?',
|
|
352
|
+
['default-hash'],
|
|
353
|
+
);
|
|
354
|
+
expect(defaulted.family_created_at).toBeGreaterThanOrEqual(beforeDefaultInsert);
|
|
355
|
+
|
|
356
|
+
const drift = await detectMigrationDrift(db, 'sqlite');
|
|
357
|
+
expect(hasMigrationDrift(drift)).toBe(false);
|
|
358
|
+
|
|
359
|
+
const rollback = await migrateDown(db, 'sqlite', 2);
|
|
360
|
+
expect(rollback.rolledBack.map(migration => migration.name)).toEqual(['bigint_append_only_ids', 'encrypt_totp_secrets', 'two_factor_hardening', 'audit_chain_anchor', 'canonical_email', 'hot_indexes_timestamptz', 'tenant_default_unique', 'auth_continuation']);
|
|
361
|
+
expect(await db.count({ model: 'refresh_token' })).toBe(3);
|
|
362
|
+
const columns = await db.rawQuery!<{ name: string }>('PRAGMA table_info(fortress_refresh_token)');
|
|
363
|
+
expect(columns.map(column => column.name)).not.toEqual(expect.arrayContaining([
|
|
364
|
+
'family_created_at',
|
|
365
|
+
'successor_token_hash',
|
|
366
|
+
'rotated_at',
|
|
367
|
+
]));
|
|
368
|
+
const continuationTables = await db.rawQuery!<{ name: string }>(
|
|
369
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'fortress_auth_continuation'`,
|
|
370
|
+
);
|
|
371
|
+
expect(continuationTables).toEqual([]);
|
|
372
|
+
const indexesAfterRollback = await db.rawQuery!<{ name: string }>(
|
|
373
|
+
`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`,
|
|
374
|
+
['fortress_tenant_user_one_default_idx'],
|
|
375
|
+
);
|
|
376
|
+
expect(indexesAfterRollback).toEqual([]);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
2
|
+
import type { Histogram, Span, TelemetryProvider } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a {@link DatabaseAdapter} so every operation records its duration
|
|
6
|
+
* into the standard OpenTelemetry semantic-convention histogram
|
|
7
|
+
* `db.client.operation.duration`, **and** emits a matching span for the
|
|
8
|
+
* operation. Attributes follow the stable database spec:
|
|
9
|
+
* - `db.system.name` — adapter dialect (`sqlite` / `pg` / `unknown`)
|
|
10
|
+
* - `db.operation.name` — verb (`create` / `findOne` / `findMany` / `update` / `delete` / `count` / `rawQuery` / `transaction`)
|
|
11
|
+
* - `db.collection.name` — model name (only when known)
|
|
12
|
+
*
|
|
13
|
+
* Span names follow the OTel DB convention `{operation} {collection}` when
|
|
14
|
+
* the model is known (e.g. `findOne user`), or just `{operation}` for
|
|
15
|
+
* `rawQuery` / `transaction` where there's no single collection.
|
|
16
|
+
*
|
|
17
|
+
* When the adapter's `transaction` method is called, the child adapter
|
|
18
|
+
* passed to the callback is recursively wrapped so queries inside a
|
|
19
|
+
* transaction are also instrumented.
|
|
20
|
+
*
|
|
21
|
+
* When the telemetry provider is the no-op default, both
|
|
22
|
+
* `createHistogram` and `startSpan` return shared no-op singletons, so
|
|
23
|
+
* the wrapper imposes essentially zero overhead beyond the monotonic
|
|
24
|
+
* `performance.now()` pair.
|
|
25
|
+
*/
|
|
26
|
+
export function instrumentAdapter(
|
|
27
|
+
adapter: DatabaseAdapter,
|
|
28
|
+
telemetry: TelemetryProvider,
|
|
29
|
+
): DatabaseAdapter {
|
|
30
|
+
const histogram: Histogram = telemetry.meter.createHistogram(
|
|
31
|
+
'db.client.operation.duration',
|
|
32
|
+
{
|
|
33
|
+
unit: 's',
|
|
34
|
+
description: 'Duration of database client operations',
|
|
35
|
+
boundaries: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10],
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
const tracer = telemetry.tracer;
|
|
39
|
+
const dbSystem = adapter.dialect ?? 'unknown';
|
|
40
|
+
|
|
41
|
+
function spanAttrs(
|
|
42
|
+
operation: string,
|
|
43
|
+
model: string | undefined,
|
|
44
|
+
): Record<string, string> {
|
|
45
|
+
const attrs: Record<string, string> = {
|
|
46
|
+
'db.system.name': dbSystem,
|
|
47
|
+
'db.operation.name': operation,
|
|
48
|
+
};
|
|
49
|
+
if (model !== undefined) {
|
|
50
|
+
attrs['db.collection.name'] = model;
|
|
51
|
+
}
|
|
52
|
+
return attrs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function startDbSpan(operation: string, model: string | undefined): Span {
|
|
56
|
+
const name = model !== undefined ? `${operation} ${model}` : operation;
|
|
57
|
+
return tracer.startSpan(name, spanAttrs(operation, model));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function record(
|
|
61
|
+
operation: string,
|
|
62
|
+
model: string | undefined,
|
|
63
|
+
start: number,
|
|
64
|
+
): void {
|
|
65
|
+
const durationSeconds = (performance.now() - start) / 1000;
|
|
66
|
+
histogram.record(durationSeconds, spanAttrs(operation, model));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function finishErr(span: Span, err: unknown): void {
|
|
70
|
+
span.recordException(err);
|
|
71
|
+
span.setStatus({
|
|
72
|
+
code: 'error',
|
|
73
|
+
message: err instanceof Error ? err.message : String(err),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const wrapped: DatabaseAdapter = {
|
|
78
|
+
dialect: adapter.dialect,
|
|
79
|
+
async create<T>(params: { model: string; data: Record<string, unknown> }): Promise<T> {
|
|
80
|
+
const start = performance.now();
|
|
81
|
+
const span = startDbSpan('create', params.model);
|
|
82
|
+
try {
|
|
83
|
+
return await adapter.create<T>(params);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
finishErr(span, err);
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
record('create', params.model, start);
|
|
91
|
+
span.end();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async findOne<T>(params: Parameters<DatabaseAdapter['findOne']>[0]): Promise<T | null> {
|
|
95
|
+
const start = performance.now();
|
|
96
|
+
const span = startDbSpan('findOne', params.model);
|
|
97
|
+
try {
|
|
98
|
+
return await adapter.findOne<T>(params);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
finishErr(span, err);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
record('findOne', params.model, start);
|
|
106
|
+
span.end();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async findMany<T>(params: Parameters<DatabaseAdapter['findMany']>[0]): Promise<T[]> {
|
|
110
|
+
const start = performance.now();
|
|
111
|
+
const span = startDbSpan('findMany', params.model);
|
|
112
|
+
try {
|
|
113
|
+
return await adapter.findMany<T>(params);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
finishErr(span, err);
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
record('findMany', params.model, start);
|
|
121
|
+
span.end();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async update<T>(params: Parameters<DatabaseAdapter['update']>[0]): Promise<T | null> {
|
|
125
|
+
const start = performance.now();
|
|
126
|
+
const span = startDbSpan('update', params.model);
|
|
127
|
+
try {
|
|
128
|
+
return await adapter.update<T>(params);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
finishErr(span, err);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
record('update', params.model, start);
|
|
136
|
+
span.end();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async delete(params: Parameters<DatabaseAdapter['delete']>[0]): Promise<void> {
|
|
140
|
+
const start = performance.now();
|
|
141
|
+
const span = startDbSpan('delete', params.model);
|
|
142
|
+
try {
|
|
143
|
+
await adapter.delete(params);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
finishErr(span, err);
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
record('delete', params.model, start);
|
|
151
|
+
span.end();
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
async count(params: Parameters<DatabaseAdapter['count']>[0]): Promise<number> {
|
|
155
|
+
const start = performance.now();
|
|
156
|
+
const span = startDbSpan('count', params.model);
|
|
157
|
+
try {
|
|
158
|
+
return await adapter.count(params);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
finishErr(span, err);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
record('count', params.model, start);
|
|
166
|
+
span.end();
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
async transaction<T>(fn: (tx: DatabaseAdapter) => Promise<T>): Promise<T> {
|
|
170
|
+
const start = performance.now();
|
|
171
|
+
const span = startDbSpan('transaction', undefined);
|
|
172
|
+
try {
|
|
173
|
+
return await adapter.transaction(tx => fn(instrumentAdapter(tx, telemetry)));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
finishErr(span, err);
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
record('transaction', undefined, start);
|
|
181
|
+
span.end();
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (adapter.rawQuery) {
|
|
187
|
+
wrapped.rawQuery = async <T>(sql: string, params?: unknown[]): Promise<T[]> => {
|
|
188
|
+
const start = performance.now();
|
|
189
|
+
const span = startDbSpan('rawQuery', undefined);
|
|
190
|
+
try {
|
|
191
|
+
return await adapter.rawQuery!<T>(sql, params);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
finishErr(span, err);
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
record('rawQuery', undefined, start);
|
|
199
|
+
span.end();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return wrapped;
|
|
205
|
+
}
|