@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,548 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
2
|
+
import type { FortressMigration, MigrationDialect } from './migrations';
|
|
3
|
+
import { Errors } from '../errors';
|
|
4
|
+
import { FORTRESS_INDEXES, FORTRESS_TABLES, getExpectedColumns, getFortressMigrations, getLatestMigrationVersion } from './migrations';
|
|
5
|
+
|
|
6
|
+
export interface MigrationStatus {
|
|
7
|
+
dialect: MigrationDialect;
|
|
8
|
+
currentVersion: number;
|
|
9
|
+
latestVersion: number;
|
|
10
|
+
pending: FortressMigration[];
|
|
11
|
+
applied: FortressMigration[];
|
|
12
|
+
upToDate: boolean;
|
|
13
|
+
hasVersionTable: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MigrationApplyResult {
|
|
17
|
+
dialect: MigrationDialect;
|
|
18
|
+
fromVersion: number;
|
|
19
|
+
toVersion: number;
|
|
20
|
+
applied: FortressMigration[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MigrationDownResult {
|
|
24
|
+
dialect: MigrationDialect;
|
|
25
|
+
fromVersion: number;
|
|
26
|
+
toVersion: number;
|
|
27
|
+
rolledBack: FortressMigration[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MigrationDrift {
|
|
31
|
+
dialect: MigrationDialect;
|
|
32
|
+
currentVersion: number;
|
|
33
|
+
latestVersion: number;
|
|
34
|
+
missingVersionTable: boolean;
|
|
35
|
+
unknownFutureVersion: boolean;
|
|
36
|
+
pendingVersions: number[];
|
|
37
|
+
/**
|
|
38
|
+
* Fortress-owned tables expected by the runtime but missing from the
|
|
39
|
+
* live database. A non-empty list almost always means an upgrade was
|
|
40
|
+
* skipped or the database was provisioned from an out-of-date schema
|
|
41
|
+
* dump. Compared against {@link FORTRESS_TABLES}.
|
|
42
|
+
*/
|
|
43
|
+
missingTables: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Per-table columns that the bundled migration DDL defines but the live
|
|
46
|
+
* database is missing. Surfaces a partially-applied or hand-patched
|
|
47
|
+
* schema where the table exists but is missing a column added by a later
|
|
48
|
+
* migration. Expected columns are parsed from the migration SQL itself
|
|
49
|
+
* (see `getExpectedColumns`), so this works for any adapter. Only tables
|
|
50
|
+
* that are present in the live DB are inspected — fully missing tables are
|
|
51
|
+
* reported via {@link missingTables} instead.
|
|
52
|
+
*/
|
|
53
|
+
missingColumns: { table: string; columns: string[] }[];
|
|
54
|
+
/** Required hot-path indexes absent from the live database. */
|
|
55
|
+
missingIndexes: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertRawQuery(db: DatabaseAdapter): NonNullable<DatabaseAdapter['rawQuery']> {
|
|
59
|
+
if (!db.rawQuery) {
|
|
60
|
+
throw Errors.badRequest('Migration tooling requires a database adapter with rawQuery support');
|
|
61
|
+
}
|
|
62
|
+
return db.rawQuery.bind(db);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function splitSqlStatements(sql: string): string[] {
|
|
66
|
+
const withoutLineComments = sql
|
|
67
|
+
.split('\n')
|
|
68
|
+
.filter(line => !line.trimStart().startsWith('--'))
|
|
69
|
+
.join('\n');
|
|
70
|
+
return withoutLineComments
|
|
71
|
+
.split(';')
|
|
72
|
+
.map(statement => statement.trim())
|
|
73
|
+
.filter(statement => statement.length > 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function executeSql(db: DatabaseAdapter, sql: string): Promise<void> {
|
|
77
|
+
const rawQuery = assertRawQuery(db);
|
|
78
|
+
for (const statement of splitSqlStatements(sql)) {
|
|
79
|
+
await rawQuery(statement);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function tableExists(db: DatabaseAdapter, dialect: MigrationDialect): Promise<boolean> {
|
|
84
|
+
const rawQuery = assertRawQuery(db);
|
|
85
|
+
if (dialect === 'sqlite') {
|
|
86
|
+
const rows = await rawQuery<{ name: string }>(
|
|
87
|
+
'SELECT name FROM sqlite_master WHERE type = \'table\' AND name = \'fortress_schema_version\'',
|
|
88
|
+
);
|
|
89
|
+
return rows.length > 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rows = await rawQuery<{ table_name: string }>(
|
|
93
|
+
'SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\' AND table_name = \'fortress_schema_version\'',
|
|
94
|
+
);
|
|
95
|
+
return rows.length > 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Enumerate every Fortress-owned table present in the live database. Used
|
|
100
|
+
* by drift detection to surface missing tables (e.g. an incomplete
|
|
101
|
+
* migration run or a freshly provisioned DB that never had Fortress
|
|
102
|
+
* tables created).
|
|
103
|
+
*/
|
|
104
|
+
async function listFortressTables(
|
|
105
|
+
db: DatabaseAdapter,
|
|
106
|
+
dialect: MigrationDialect,
|
|
107
|
+
): Promise<Set<string>> {
|
|
108
|
+
const rawQuery = assertRawQuery(db);
|
|
109
|
+
if (dialect === 'sqlite') {
|
|
110
|
+
const rows = await rawQuery<{ name: string }>(
|
|
111
|
+
'SELECT name FROM sqlite_master WHERE type = \'table\' AND name LIKE \'fortress_%\'',
|
|
112
|
+
);
|
|
113
|
+
return new Set(rows.map(row => row.name));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rows = await rawQuery<{ table_name: string }>(
|
|
117
|
+
'SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\' AND table_name LIKE \'fortress_%\'',
|
|
118
|
+
);
|
|
119
|
+
return new Set(rows.map(row => row.table_name));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* List the columns of a single Fortress table from the live database.
|
|
124
|
+
* Returns an empty set if the table does not exist. Uses catalog
|
|
125
|
+
* introspection (`PRAGMA table_info` / `information_schema.columns`) through
|
|
126
|
+
* `rawQuery`, so it stays adapter-agnostic. The table name is sourced from
|
|
127
|
+
* Fortress's own `FORTRESS_TABLES`, never user input.
|
|
128
|
+
*/
|
|
129
|
+
async function listColumns(
|
|
130
|
+
db: DatabaseAdapter,
|
|
131
|
+
dialect: MigrationDialect,
|
|
132
|
+
table: string,
|
|
133
|
+
): Promise<Set<string>> {
|
|
134
|
+
const rawQuery = assertRawQuery(db);
|
|
135
|
+
if (dialect === 'sqlite') {
|
|
136
|
+
const rows = await rawQuery<{ name: string }>(`PRAGMA table_info(${table})`);
|
|
137
|
+
return new Set(rows.map(row => row.name.toLowerCase()));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const rows = await rawQuery<{ column_name: string }>(
|
|
141
|
+
'SELECT column_name FROM information_schema.columns WHERE table_schema = \'public\' AND table_name = ?',
|
|
142
|
+
[table],
|
|
143
|
+
);
|
|
144
|
+
return new Set(rows.map(row => row.column_name.toLowerCase()));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function listFortressIndexes(
|
|
148
|
+
db: DatabaseAdapter,
|
|
149
|
+
dialect: MigrationDialect,
|
|
150
|
+
): Promise<Set<string>> {
|
|
151
|
+
const rawQuery = assertRawQuery(db);
|
|
152
|
+
if (dialect === 'sqlite') {
|
|
153
|
+
const rows = await rawQuery<{ name: string }>(
|
|
154
|
+
'SELECT name FROM sqlite_master WHERE type = \'index\' AND name NOT LIKE \'sqlite_autoindex_%\'',
|
|
155
|
+
);
|
|
156
|
+
return new Set(rows.map(row => row.name));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const rows = await rawQuery<{ indexname: string }>(
|
|
160
|
+
'SELECT indexname FROM pg_indexes WHERE schemaname = \'public\' AND tablename LIKE \'fortress_%\'',
|
|
161
|
+
);
|
|
162
|
+
return new Set(rows.map(row => row.indexname));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function readCurrentVersion(db: DatabaseAdapter, dialect: MigrationDialect): Promise<{ version: number; hasVersionTable: boolean }> {
|
|
166
|
+
const hasVersionTable = await tableExists(db, dialect);
|
|
167
|
+
if (!hasVersionTable)
|
|
168
|
+
return { version: 0, hasVersionTable };
|
|
169
|
+
|
|
170
|
+
const rawQuery = assertRawQuery(db);
|
|
171
|
+
const rows = await rawQuery<{ version: number | string }>('SELECT version FROM fortress_schema_version WHERE id = 1');
|
|
172
|
+
return { version: Number(rows[0]?.version ?? 0), hasVersionTable };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function recordVersion(db: DatabaseAdapter, dialect: MigrationDialect, version: number): Promise<void> {
|
|
176
|
+
if (version <= 0)
|
|
177
|
+
return;
|
|
178
|
+
const rawQuery = assertRawQuery(db);
|
|
179
|
+
if (dialect === 'sqlite') {
|
|
180
|
+
await rawQuery(
|
|
181
|
+
'INSERT INTO fortress_schema_version (id, version, applied_at) VALUES (1, ?, unixepoch()) ON CONFLICT(id) DO UPDATE SET version = excluded.version, applied_at = excluded.applied_at',
|
|
182
|
+
[version],
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await rawQuery(
|
|
188
|
+
'INSERT INTO fortress_schema_version (id, version, applied_at) VALUES (1, ?, now()) ON CONFLICT(id) DO UPDATE SET version = excluded.version, applied_at = excluded.applied_at',
|
|
189
|
+
[version],
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface MigrationJournalRow {
|
|
194
|
+
version: number | string;
|
|
195
|
+
name: string;
|
|
196
|
+
dialect: string;
|
|
197
|
+
checksum: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** SHA-256 of every immutable migration input, including runtime data-step identity. */
|
|
201
|
+
export async function computeMigrationChecksum(migration: FortressMigration): Promise<string> {
|
|
202
|
+
const payload = JSON.stringify([
|
|
203
|
+
migration.dialect,
|
|
204
|
+
migration.version,
|
|
205
|
+
migration.name,
|
|
206
|
+
migration.up,
|
|
207
|
+
migration.down,
|
|
208
|
+
migration.freshUp ?? null,
|
|
209
|
+
migration.dataStep ?? null,
|
|
210
|
+
migration.beforeUp?.toString() ?? null,
|
|
211
|
+
]);
|
|
212
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload));
|
|
213
|
+
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function ensureMigrationMetadata(db: DatabaseAdapter, dialect: MigrationDialect): Promise<void> {
|
|
217
|
+
const rawQuery = assertRawQuery(db);
|
|
218
|
+
if (dialect === 'sqlite') {
|
|
219
|
+
// The singleton write forces a database-level writer lock even for
|
|
220
|
+
// adapters whose transaction begins deferred. Separate processes then
|
|
221
|
+
// serialize before the checkpoint is re-read.
|
|
222
|
+
await rawQuery(`CREATE TABLE IF NOT EXISTS fortress_migration_lock (
|
|
223
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
224
|
+
touched_at INTEGER NOT NULL
|
|
225
|
+
)`);
|
|
226
|
+
await rawQuery(
|
|
227
|
+
`INSERT INTO fortress_migration_lock (id, touched_at) VALUES (1, unixepoch())
|
|
228
|
+
ON CONFLICT(id) DO UPDATE SET touched_at = excluded.touched_at`,
|
|
229
|
+
);
|
|
230
|
+
await rawQuery(`CREATE TABLE IF NOT EXISTS fortress_migration_journal (
|
|
231
|
+
version INTEGER PRIMARY KEY,
|
|
232
|
+
name TEXT NOT NULL,
|
|
233
|
+
dialect TEXT NOT NULL,
|
|
234
|
+
checksum TEXT NOT NULL,
|
|
235
|
+
applied_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
236
|
+
)`);
|
|
237
|
+
await rawQuery(`CREATE TABLE IF NOT EXISTS fortress_migration_state (
|
|
238
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
239
|
+
journal_initialized INTEGER NOT NULL DEFAULT 0
|
|
240
|
+
)`);
|
|
241
|
+
await rawQuery(
|
|
242
|
+
`INSERT INTO fortress_migration_state (id, journal_initialized) VALUES (1, 0)
|
|
243
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Transaction-scoped: releases automatically on commit and rollback and
|
|
249
|
+
// stays bound to the transaction adapter's one pooled connection.
|
|
250
|
+
await rawQuery('SELECT pg_advisory_xact_lock(117993, 0)');
|
|
251
|
+
const journalTable = await rawQuery<{ name: string | null }>(
|
|
252
|
+
`SELECT to_regclass('public.fortress_migration_journal') AS name`,
|
|
253
|
+
);
|
|
254
|
+
if (!journalTable[0]?.name) {
|
|
255
|
+
await rawQuery(`CREATE TABLE fortress_migration_journal (
|
|
256
|
+
version INTEGER PRIMARY KEY,
|
|
257
|
+
name TEXT NOT NULL,
|
|
258
|
+
dialect TEXT NOT NULL,
|
|
259
|
+
checksum VARCHAR(64) NOT NULL,
|
|
260
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
261
|
+
)`);
|
|
262
|
+
}
|
|
263
|
+
const stateTable = await rawQuery<{ name: string | null }>(
|
|
264
|
+
`SELECT to_regclass('public.fortress_migration_state') AS name`,
|
|
265
|
+
);
|
|
266
|
+
if (!stateTable[0]?.name) {
|
|
267
|
+
await rawQuery(`CREATE TABLE fortress_migration_state (
|
|
268
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
269
|
+
journal_initialized BOOLEAN NOT NULL DEFAULT false
|
|
270
|
+
)`);
|
|
271
|
+
}
|
|
272
|
+
await rawQuery(
|
|
273
|
+
`INSERT INTO fortress_migration_state (id, journal_initialized) VALUES (1, false)
|
|
274
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function readJournal(db: DatabaseAdapter): Promise<MigrationJournalRow[]> {
|
|
279
|
+
return assertRawQuery(db)<MigrationJournalRow>(
|
|
280
|
+
'SELECT version, name, dialect, checksum FROM fortress_migration_journal ORDER BY version',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function isJournalInitialized(db: DatabaseAdapter): Promise<boolean> {
|
|
285
|
+
const rows = await assertRawQuery(db)<{ journal_initialized: boolean | number | string }>(
|
|
286
|
+
'SELECT journal_initialized FROM fortress_migration_state WHERE id = 1',
|
|
287
|
+
);
|
|
288
|
+
const value = rows[0]?.journal_initialized;
|
|
289
|
+
return value === true || value === 1 || value === '1' || value === 'true';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function markJournalInitialized(db: DatabaseAdapter, dialect: MigrationDialect): Promise<void> {
|
|
293
|
+
await assertRawQuery(db)(
|
|
294
|
+
`UPDATE fortress_migration_state SET journal_initialized = ${dialect === 'pg' ? 'true' : '1'} WHERE id = 1`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function insertJournal(
|
|
299
|
+
db: DatabaseAdapter,
|
|
300
|
+
dialect: MigrationDialect,
|
|
301
|
+
migration: FortressMigration,
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
const rawQuery = assertRawQuery(db);
|
|
304
|
+
const checksum = await computeMigrationChecksum(migration);
|
|
305
|
+
if (dialect === 'sqlite') {
|
|
306
|
+
await rawQuery(
|
|
307
|
+
`INSERT INTO fortress_migration_journal (version, name, dialect, checksum, applied_at)
|
|
308
|
+
VALUES (?, ?, ?, ?, unixepoch())`,
|
|
309
|
+
[migration.version, migration.name, dialect, checksum],
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
await rawQuery(
|
|
314
|
+
`INSERT INTO fortress_migration_journal (version, name, dialect, checksum, applied_at)
|
|
315
|
+
VALUES (?, ?, ?, ?, now())`,
|
|
316
|
+
[migration.version, migration.name, dialect, checksum],
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function verifyOrBackfillJournal(
|
|
321
|
+
db: DatabaseAdapter,
|
|
322
|
+
dialect: MigrationDialect,
|
|
323
|
+
currentVersion: number,
|
|
324
|
+
): Promise<void> {
|
|
325
|
+
const migrations = getFortressMigrations(dialect);
|
|
326
|
+
const latestVersion = getLatestMigrationVersion(dialect);
|
|
327
|
+
if (currentVersion > latestVersion) {
|
|
328
|
+
throw Errors.badRequest(
|
|
329
|
+
`Database schema version ${currentVersion} is newer than bundled version ${latestVersion}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let rows = await readJournal(db);
|
|
334
|
+
const initialized = await isJournalInitialized(db);
|
|
335
|
+
// Compatibility bridge for installations created before the journal. The
|
|
336
|
+
// persistent state marker makes this a one-time transition: once initialized,
|
|
337
|
+
// even complete row loss is corruption and must not be silently re-certified.
|
|
338
|
+
if (!initialized && rows.length === 0 && currentVersion > 0) {
|
|
339
|
+
for (const migration of migrations.filter(item => item.version <= currentVersion))
|
|
340
|
+
await insertJournal(db, dialect, migration);
|
|
341
|
+
rows = await readJournal(db);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const byVersion = new Map(rows.map(row => [Number(row.version), row]));
|
|
345
|
+
for (const row of rows) {
|
|
346
|
+
const version = Number(row.version);
|
|
347
|
+
const migration = migrations.find(item => item.version === version);
|
|
348
|
+
if (!migration || version > currentVersion) {
|
|
349
|
+
throw Errors.badRequest(`Migration journal contains unexpected version ${version}`);
|
|
350
|
+
}
|
|
351
|
+
const checksum = await computeMigrationChecksum(migration);
|
|
352
|
+
if (row.name !== migration.name || row.dialect !== dialect || row.checksum !== checksum) {
|
|
353
|
+
throw Errors.badRequest(`Migration journal integrity check failed for version ${version}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const migration of migrations.filter(item => item.version <= currentVersion)) {
|
|
357
|
+
if (!byVersion.has(migration.version)) {
|
|
358
|
+
throw Errors.badRequest(`Migration journal is missing version ${migration.version}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!initialized)
|
|
362
|
+
await markJournalInitialized(db, dialect);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function deleteJournalVersion(db: DatabaseAdapter, version: number): Promise<void> {
|
|
366
|
+
await assertRawQuery(db)(
|
|
367
|
+
'DELETE FROM fortress_migration_journal WHERE version = ?',
|
|
368
|
+
[version],
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function dropMigrationMetadata(db: DatabaseAdapter, dialect: MigrationDialect): Promise<void> {
|
|
373
|
+
const rawQuery = assertRawQuery(db);
|
|
374
|
+
await rawQuery('DROP TABLE IF EXISTS fortress_migration_journal');
|
|
375
|
+
await rawQuery('DROP TABLE IF EXISTS fortress_migration_state');
|
|
376
|
+
if (dialect === 'sqlite')
|
|
377
|
+
await rawQuery('DROP TABLE IF EXISTS fortress_migration_lock');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function assertTargetVersion(targetVersion: number): void {
|
|
381
|
+
if (!Number.isSafeInteger(targetVersion) || targetVersion < 0)
|
|
382
|
+
throw Errors.badRequest('Migration target version must be a non-negative safe integer');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// better-sqlite3 waits synchronously on a cross-connection writer lock, which
|
|
386
|
+
// can starve the first transaction's Promise continuation in the same process.
|
|
387
|
+
// Serialize local migration callers as well; the database write lock remains
|
|
388
|
+
// the cross-process authority.
|
|
389
|
+
let sqliteMigrationChain: Promise<void> = Promise.resolve();
|
|
390
|
+
|
|
391
|
+
function withMigrationTransaction<T>(
|
|
392
|
+
db: DatabaseAdapter,
|
|
393
|
+
dialect: MigrationDialect,
|
|
394
|
+
fn: (tx: DatabaseAdapter) => Promise<T>,
|
|
395
|
+
): Promise<T> {
|
|
396
|
+
if (dialect === 'pg')
|
|
397
|
+
return db.transaction(fn);
|
|
398
|
+
const result = sqliteMigrationChain.then(
|
|
399
|
+
() => db.transaction(fn),
|
|
400
|
+
() => db.transaction(fn),
|
|
401
|
+
);
|
|
402
|
+
sqliteMigrationChain = result.then(() => undefined, () => undefined);
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function getMigrationStatus(
|
|
407
|
+
db: DatabaseAdapter,
|
|
408
|
+
dialect: MigrationDialect = db.dialect === 'pg' ? 'pg' : 'sqlite',
|
|
409
|
+
): Promise<MigrationStatus> {
|
|
410
|
+
const migrations = getFortressMigrations(dialect);
|
|
411
|
+
const latestVersion = getLatestMigrationVersion(dialect);
|
|
412
|
+
const { version: currentVersion, hasVersionTable } = await readCurrentVersion(db, dialect);
|
|
413
|
+
return {
|
|
414
|
+
dialect,
|
|
415
|
+
currentVersion,
|
|
416
|
+
latestVersion,
|
|
417
|
+
pending: migrations.filter(migration => migration.version > currentVersion),
|
|
418
|
+
applied: migrations.filter(migration => migration.version <= currentVersion),
|
|
419
|
+
upToDate: currentVersion >= latestVersion,
|
|
420
|
+
hasVersionTable,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export async function migrateUp(
|
|
425
|
+
db: DatabaseAdapter,
|
|
426
|
+
dialect: MigrationDialect = db.dialect === 'pg' ? 'pg' : 'sqlite',
|
|
427
|
+
targetVersion: number = getLatestMigrationVersion(dialect),
|
|
428
|
+
): Promise<MigrationApplyResult> {
|
|
429
|
+
assertTargetVersion(targetVersion);
|
|
430
|
+
return withMigrationTransaction(db, dialect, async (tx) => {
|
|
431
|
+
await ensureMigrationMetadata(tx, dialect);
|
|
432
|
+
// Mandatory post-lock read: no plan computed before this point is trusted.
|
|
433
|
+
const { version: currentVersion } = await readCurrentVersion(tx, dialect);
|
|
434
|
+
await verifyOrBackfillJournal(tx, dialect, currentVersion);
|
|
435
|
+
|
|
436
|
+
const toApply = getFortressMigrations(dialect)
|
|
437
|
+
.filter(migration => migration.version > currentVersion && migration.version <= targetVersion);
|
|
438
|
+
for (const migration of toApply) {
|
|
439
|
+
await migration.beforeUp?.(tx);
|
|
440
|
+
await executeSql(tx, migration.up);
|
|
441
|
+
await insertJournal(tx, dialect, migration);
|
|
442
|
+
await recordVersion(tx, dialect, migration.version);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const toVersion = toApply.at(-1)?.version ?? currentVersion;
|
|
446
|
+
if (toVersion === 0)
|
|
447
|
+
await dropMigrationMetadata(tx, dialect);
|
|
448
|
+
return {
|
|
449
|
+
dialect,
|
|
450
|
+
fromVersion: currentVersion,
|
|
451
|
+
toVersion,
|
|
452
|
+
applied: toApply,
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export async function migrateDown(
|
|
458
|
+
db: DatabaseAdapter,
|
|
459
|
+
dialect: MigrationDialect = db.dialect === 'pg' ? 'pg' : 'sqlite',
|
|
460
|
+
targetVersion: number = 0,
|
|
461
|
+
): Promise<MigrationDownResult> {
|
|
462
|
+
assertTargetVersion(targetVersion);
|
|
463
|
+
return withMigrationTransaction(db, dialect, async (tx) => {
|
|
464
|
+
await ensureMigrationMetadata(tx, dialect);
|
|
465
|
+
const { version: currentVersion } = await readCurrentVersion(tx, dialect);
|
|
466
|
+
await verifyOrBackfillJournal(tx, dialect, currentVersion);
|
|
467
|
+
|
|
468
|
+
// A target above current is an idempotent no-op, never a version advance.
|
|
469
|
+
const effectiveTarget = Math.min(targetVersion, currentVersion);
|
|
470
|
+
const migrations = getFortressMigrations(dialect);
|
|
471
|
+
if (
|
|
472
|
+
effectiveTarget > 0
|
|
473
|
+
&& effectiveTarget < currentVersion
|
|
474
|
+
&& !migrations.some(migration => migration.version === effectiveTarget)
|
|
475
|
+
) {
|
|
476
|
+
throw Errors.badRequest(`Unknown migration target version ${effectiveTarget}`);
|
|
477
|
+
}
|
|
478
|
+
const toRollback = migrations
|
|
479
|
+
.filter(migration => migration.version <= currentVersion && migration.version > effectiveTarget)
|
|
480
|
+
.sort((a, b) => b.version - a.version);
|
|
481
|
+
|
|
482
|
+
for (const migration of toRollback) {
|
|
483
|
+
await executeSql(tx, migration.down);
|
|
484
|
+
await deleteJournalVersion(tx, migration.version);
|
|
485
|
+
}
|
|
486
|
+
if (effectiveTarget === 0)
|
|
487
|
+
await dropMigrationMetadata(tx, dialect);
|
|
488
|
+
else if (toRollback.length > 0)
|
|
489
|
+
await recordVersion(tx, dialect, effectiveTarget);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
dialect,
|
|
493
|
+
fromVersion: currentVersion,
|
|
494
|
+
toVersion: effectiveTarget,
|
|
495
|
+
rolledBack: toRollback,
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export async function detectMigrationDrift(
|
|
501
|
+
db: DatabaseAdapter,
|
|
502
|
+
dialect: MigrationDialect = db.dialect === 'pg' ? 'pg' : 'sqlite',
|
|
503
|
+
): Promise<MigrationDrift> {
|
|
504
|
+
const status = await getMigrationStatus(db, dialect);
|
|
505
|
+
const present = await listFortressTables(db, dialect);
|
|
506
|
+
const missingTables = FORTRESS_TABLES.filter(name => !present.has(name));
|
|
507
|
+
|
|
508
|
+
// Column-level drift: inspect only tables that actually exist (fully
|
|
509
|
+
// missing tables are already reported above). Expected columns come from
|
|
510
|
+
// the bundled migration DDL, so this catches a table that was created
|
|
511
|
+
// from an older schema and never got a later migration's new column.
|
|
512
|
+
const expectedColumns = getExpectedColumns(dialect);
|
|
513
|
+
const missingColumns: { table: string; columns: string[] }[] = [];
|
|
514
|
+
for (const [table, columns] of Object.entries(expectedColumns)) {
|
|
515
|
+
if (!present.has(table))
|
|
516
|
+
continue;
|
|
517
|
+
const live = await listColumns(db, dialect, table);
|
|
518
|
+
const missing = columns.filter(column => !live.has(column));
|
|
519
|
+
if (missing.length > 0)
|
|
520
|
+
missingColumns.push({ table, columns: missing });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const presentIndexes = await listFortressIndexes(db, dialect);
|
|
524
|
+
const missingIndexes = FORTRESS_INDEXES
|
|
525
|
+
.map(index => index.name)
|
|
526
|
+
.filter(name => !presentIndexes.has(name));
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
dialect,
|
|
530
|
+
currentVersion: status.currentVersion,
|
|
531
|
+
latestVersion: status.latestVersion,
|
|
532
|
+
missingVersionTable: !status.hasVersionTable,
|
|
533
|
+
unknownFutureVersion: status.currentVersion > status.latestVersion,
|
|
534
|
+
pendingVersions: status.pending.map(migration => migration.version),
|
|
535
|
+
missingTables,
|
|
536
|
+
missingColumns,
|
|
537
|
+
missingIndexes,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function hasMigrationDrift(drift: MigrationDrift): boolean {
|
|
542
|
+
return drift.missingVersionTable
|
|
543
|
+
|| drift.unknownFutureVersion
|
|
544
|
+
|| drift.pendingVersions.length > 0
|
|
545
|
+
|| drift.missingTables.length > 0
|
|
546
|
+
|| drift.missingColumns.length > 0
|
|
547
|
+
|| drift.missingIndexes.length > 0;
|
|
548
|
+
}
|