@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,1771 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
2
|
+
import { normalizeEmail } from '../auth/email';
|
|
3
|
+
|
|
4
|
+
export type MigrationDialect = 'sqlite' | 'pg';
|
|
5
|
+
|
|
6
|
+
export interface FortressMigration {
|
|
7
|
+
version: number;
|
|
8
|
+
name: string;
|
|
9
|
+
dialect: MigrationDialect;
|
|
10
|
+
up: string;
|
|
11
|
+
down: string;
|
|
12
|
+
/** SQL used only when provisioning an empty database from concatenated DDL. */
|
|
13
|
+
freshUp?: string;
|
|
14
|
+
/** Stable identity included in the migration checksum for a JS data step. */
|
|
15
|
+
dataStep?: string;
|
|
16
|
+
/** Runs immediately before `up` while the migration lock/transaction is held. */
|
|
17
|
+
beforeUp?: (db: DatabaseAdapter) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// NOTE: the version row itself is written by the migration runner
|
|
21
|
+
// (`recordVersion` in engine.ts) after each migration applies, so the
|
|
22
|
+
// forward SQL only creates the checkpoint table. This keeps a single
|
|
23
|
+
// source of version truth and lets `src/testing/index.ts` provision the
|
|
24
|
+
// schema from this DDL without pre-stamping a version.
|
|
25
|
+
const SQLITE_0001_UP = `
|
|
26
|
+
CREATE TABLE IF NOT EXISTS fortress_schema_version (
|
|
27
|
+
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
28
|
+
version INTEGER NOT NULL,
|
|
29
|
+
applied_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
30
|
+
);
|
|
31
|
+
`.trim();
|
|
32
|
+
|
|
33
|
+
const SQLITE_0001_DOWN = `
|
|
34
|
+
DROP TABLE IF EXISTS fortress_schema_version;
|
|
35
|
+
`.trim();
|
|
36
|
+
|
|
37
|
+
const PG_0001_UP = `
|
|
38
|
+
CREATE TABLE IF NOT EXISTS fortress_schema_version (
|
|
39
|
+
id integer PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
40
|
+
version integer NOT NULL,
|
|
41
|
+
applied_at timestamp NOT NULL DEFAULT now()
|
|
42
|
+
);
|
|
43
|
+
`.trim();
|
|
44
|
+
|
|
45
|
+
const PG_0001_DOWN = `
|
|
46
|
+
DROP TABLE IF EXISTS fortress_schema_version;
|
|
47
|
+
`.trim();
|
|
48
|
+
|
|
49
|
+
// --- 0002: initial Fortress schema ---
|
|
50
|
+
//
|
|
51
|
+
// Creates every Fortress-owned table (everything except the
|
|
52
|
+
// `fortress_schema_version` checkpoint installed by 0001). This is the
|
|
53
|
+
// SQL-first source of truth for provisioning a brand-new database: it runs
|
|
54
|
+
// through any `DatabaseAdapter.rawQuery`, so a custom (non-Drizzle) adapter
|
|
55
|
+
// installs the exact same schema. `src/testing/index.ts` derives its
|
|
56
|
+
// in-memory schema from these migrations, and the column-drift checker
|
|
57
|
+
// parses expected columns out of this DDL — keep this the canonical copy.
|
|
58
|
+
|
|
59
|
+
const SQLITE_0002_UP = `
|
|
60
|
+
CREATE TABLE IF NOT EXISTS fortress_user (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
|
+
email TEXT NOT NULL UNIQUE,
|
|
63
|
+
name TEXT NOT NULL,
|
|
64
|
+
password_hash TEXT,
|
|
65
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
66
|
+
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
67
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
68
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS fortress_login_identifier (
|
|
72
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
74
|
+
type TEXT NOT NULL,
|
|
75
|
+
value TEXT NOT NULL UNIQUE
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS fortress_refresh_token (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
81
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
82
|
+
token_family TEXT NOT NULL,
|
|
83
|
+
is_revoked INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
expires_at INTEGER NOT NULL,
|
|
85
|
+
ip_address TEXT,
|
|
86
|
+
user_agent TEXT,
|
|
87
|
+
device_name TEXT,
|
|
88
|
+
last_active_at INTEGER,
|
|
89
|
+
fingerprint_hash TEXT,
|
|
90
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
CREATE TABLE IF NOT EXISTS fortress_group (
|
|
94
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
95
|
+
name TEXT NOT NULL UNIQUE,
|
|
96
|
+
description TEXT
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE TABLE IF NOT EXISTS fortress_group_user (
|
|
100
|
+
group_id INTEGER NOT NULL REFERENCES fortress_group(id) ON DELETE CASCADE,
|
|
101
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
102
|
+
PRIMARY KEY (group_id, user_id)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE TABLE IF NOT EXISTS fortress_service_account (
|
|
106
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
107
|
+
name TEXT NOT NULL UNIQUE,
|
|
108
|
+
display_name TEXT,
|
|
109
|
+
description TEXT,
|
|
110
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
111
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
112
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE TABLE IF NOT EXISTS fortress_resource (
|
|
116
|
+
name TEXT PRIMARY KEY,
|
|
117
|
+
description TEXT
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
CREATE TABLE IF NOT EXISTS fortress_permission (
|
|
121
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
122
|
+
resource TEXT NOT NULL REFERENCES fortress_resource(name) ON DELETE CASCADE,
|
|
123
|
+
action TEXT NOT NULL,
|
|
124
|
+
effect TEXT NOT NULL DEFAULT 'ALLOW',
|
|
125
|
+
conditions TEXT,
|
|
126
|
+
description TEXT
|
|
127
|
+
);
|
|
128
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_no_conditions
|
|
129
|
+
ON fortress_permission (resource, action, effect)
|
|
130
|
+
WHERE conditions IS NULL;
|
|
131
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_with_conditions
|
|
132
|
+
ON fortress_permission (resource, action, effect, conditions)
|
|
133
|
+
WHERE conditions IS NOT NULL;
|
|
134
|
+
|
|
135
|
+
CREATE TABLE IF NOT EXISTS fortress_role (
|
|
136
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
137
|
+
name TEXT NOT NULL UNIQUE,
|
|
138
|
+
description TEXT,
|
|
139
|
+
is_system INTEGER NOT NULL DEFAULT 0
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
CREATE TABLE IF NOT EXISTS fortress_role_permission (
|
|
143
|
+
role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
|
|
144
|
+
permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
|
|
145
|
+
PRIMARY KEY (role_id, permission_id)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE TABLE IF NOT EXISTS fortress_role_binding (
|
|
149
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
150
|
+
role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
|
|
151
|
+
subject_type TEXT NOT NULL,
|
|
152
|
+
subject_id INTEGER NOT NULL,
|
|
153
|
+
tenant_id TEXT,
|
|
154
|
+
UNIQUE (role_id, subject_type, subject_id, tenant_id)
|
|
155
|
+
);
|
|
156
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_global
|
|
157
|
+
ON fortress_role_binding (role_id, subject_type, subject_id)
|
|
158
|
+
WHERE tenant_id IS NULL;
|
|
159
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_tenant
|
|
160
|
+
ON fortress_role_binding (role_id, subject_type, subject_id, tenant_id)
|
|
161
|
+
WHERE tenant_id IS NOT NULL;
|
|
162
|
+
|
|
163
|
+
CREATE TABLE IF NOT EXISTS fortress_direct_permission_binding (
|
|
164
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
165
|
+
permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
|
|
166
|
+
subject_type TEXT NOT NULL,
|
|
167
|
+
subject_id INTEGER NOT NULL,
|
|
168
|
+
tenant_id TEXT,
|
|
169
|
+
UNIQUE (permission_id, subject_type, subject_id, tenant_id)
|
|
170
|
+
);
|
|
171
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_global
|
|
172
|
+
ON fortress_direct_permission_binding (permission_id, subject_type, subject_id)
|
|
173
|
+
WHERE tenant_id IS NULL;
|
|
174
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_tenant
|
|
175
|
+
ON fortress_direct_permission_binding (permission_id, subject_type, subject_id, tenant_id)
|
|
176
|
+
WHERE tenant_id IS NOT NULL;
|
|
177
|
+
|
|
178
|
+
CREATE TABLE IF NOT EXISTS fortress_email_verification_token (
|
|
179
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
180
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
181
|
+
token TEXT NOT NULL,
|
|
182
|
+
email TEXT NOT NULL,
|
|
183
|
+
expires_at INTEGER NOT NULL,
|
|
184
|
+
used_at INTEGER,
|
|
185
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
CREATE TABLE IF NOT EXISTS fortress_magic_link_token (
|
|
189
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
190
|
+
email TEXT NOT NULL,
|
|
191
|
+
token TEXT NOT NULL,
|
|
192
|
+
expires_at INTEGER NOT NULL,
|
|
193
|
+
used_at INTEGER,
|
|
194
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
CREATE TABLE IF NOT EXISTS fortress_api_key (
|
|
198
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
199
|
+
subject_type TEXT NOT NULL,
|
|
200
|
+
subject_id INTEGER NOT NULL,
|
|
201
|
+
name TEXT NOT NULL,
|
|
202
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
203
|
+
key_prefix TEXT NOT NULL,
|
|
204
|
+
scopes TEXT,
|
|
205
|
+
expires_at INTEGER,
|
|
206
|
+
last_used_at INTEGER,
|
|
207
|
+
is_revoked INTEGER NOT NULL DEFAULT 0,
|
|
208
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
209
|
+
);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS api_key_subject_idx ON fortress_api_key (subject_type, subject_id);
|
|
211
|
+
|
|
212
|
+
CREATE TABLE IF NOT EXISTS fortress_two_factor_secret (
|
|
213
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
214
|
+
user_id INTEGER NOT NULL UNIQUE REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
215
|
+
secret TEXT NOT NULL,
|
|
216
|
+
is_enabled INTEGER NOT NULL DEFAULT 0,
|
|
217
|
+
last_used_counter INTEGER,
|
|
218
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
CREATE TABLE IF NOT EXISTS fortress_backup_code (
|
|
222
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
223
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
224
|
+
code_hash TEXT NOT NULL,
|
|
225
|
+
is_used INTEGER NOT NULL DEFAULT 0,
|
|
226
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
CREATE TABLE IF NOT EXISTS fortress_trusted_device (
|
|
230
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
231
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
232
|
+
device_hash TEXT NOT NULL,
|
|
233
|
+
expires_at INTEGER NOT NULL,
|
|
234
|
+
last_used_at INTEGER NOT NULL,
|
|
235
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
CREATE TABLE IF NOT EXISTS fortress_social_account (
|
|
239
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
240
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
241
|
+
provider TEXT NOT NULL,
|
|
242
|
+
provider_account_id TEXT NOT NULL,
|
|
243
|
+
email TEXT,
|
|
244
|
+
access_token TEXT,
|
|
245
|
+
refresh_token TEXT,
|
|
246
|
+
token_expires_at INTEGER,
|
|
247
|
+
profile TEXT,
|
|
248
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
249
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
250
|
+
UNIQUE (user_id, provider),
|
|
251
|
+
UNIQUE (provider, provider_account_id)
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
CREATE TABLE IF NOT EXISTS fortress_tenant (
|
|
255
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
256
|
+
name TEXT NOT NULL,
|
|
257
|
+
tax_id TEXT NOT NULL UNIQUE,
|
|
258
|
+
description TEXT,
|
|
259
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
260
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
CREATE TABLE IF NOT EXISTS fortress_tenant_user (
|
|
264
|
+
tenant_id INTEGER NOT NULL REFERENCES fortress_tenant(id) ON DELETE CASCADE,
|
|
265
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
266
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
267
|
+
PRIMARY KEY (tenant_id, user_id)
|
|
268
|
+
);
|
|
269
|
+
CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
|
|
270
|
+
ON fortress_tenant_user (user_id) WHERE is_default = 1;
|
|
271
|
+
|
|
272
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_client (
|
|
273
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
274
|
+
client_id TEXT NOT NULL UNIQUE,
|
|
275
|
+
client_secret_hash TEXT NOT NULL,
|
|
276
|
+
name TEXT NOT NULL,
|
|
277
|
+
redirect_uris TEXT NOT NULL,
|
|
278
|
+
grant_types TEXT NOT NULL,
|
|
279
|
+
allowed_scopes TEXT,
|
|
280
|
+
token_endpoint_auth_method TEXT,
|
|
281
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_authorization_code (
|
|
285
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
286
|
+
code TEXT NOT NULL UNIQUE,
|
|
287
|
+
client_id TEXT NOT NULL,
|
|
288
|
+
user_id INTEGER NOT NULL,
|
|
289
|
+
redirect_uri TEXT NOT NULL,
|
|
290
|
+
scope TEXT,
|
|
291
|
+
code_challenge TEXT,
|
|
292
|
+
code_challenge_method TEXT,
|
|
293
|
+
nonce TEXT,
|
|
294
|
+
auth_time INTEGER,
|
|
295
|
+
expires_at INTEGER NOT NULL,
|
|
296
|
+
used_at INTEGER,
|
|
297
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_access_token (
|
|
301
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
302
|
+
token TEXT NOT NULL UNIQUE,
|
|
303
|
+
client_id TEXT NOT NULL,
|
|
304
|
+
user_id INTEGER,
|
|
305
|
+
scope TEXT,
|
|
306
|
+
expires_at INTEGER NOT NULL,
|
|
307
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_refresh_token (
|
|
311
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
312
|
+
token TEXT NOT NULL UNIQUE,
|
|
313
|
+
family_id TEXT NOT NULL,
|
|
314
|
+
client_id TEXT NOT NULL,
|
|
315
|
+
user_id INTEGER NOT NULL,
|
|
316
|
+
scope TEXT,
|
|
317
|
+
issued_at INTEGER NOT NULL,
|
|
318
|
+
expires_at INTEGER NOT NULL,
|
|
319
|
+
used_at INTEGER,
|
|
320
|
+
parent_id INTEGER,
|
|
321
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_pending_flow (
|
|
325
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
326
|
+
flow_id TEXT NOT NULL UNIQUE,
|
|
327
|
+
client_id TEXT NOT NULL,
|
|
328
|
+
redirect_uri TEXT NOT NULL,
|
|
329
|
+
scope TEXT,
|
|
330
|
+
state TEXT NOT NULL,
|
|
331
|
+
code_challenge TEXT,
|
|
332
|
+
code_challenge_method TEXT,
|
|
333
|
+
nonce TEXT,
|
|
334
|
+
user_id INTEGER,
|
|
335
|
+
used_at INTEGER,
|
|
336
|
+
expires_at INTEGER NOT NULL,
|
|
337
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_signing_key (
|
|
341
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
342
|
+
kid TEXT NOT NULL UNIQUE,
|
|
343
|
+
alg TEXT NOT NULL,
|
|
344
|
+
public_jwk TEXT NOT NULL,
|
|
345
|
+
private_jwk TEXT NOT NULL,
|
|
346
|
+
rotated_at INTEGER,
|
|
347
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
CREATE TABLE IF NOT EXISTS fortress_user_scope_assignment (
|
|
351
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
352
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
353
|
+
scope_name TEXT NOT NULL,
|
|
354
|
+
scope_value TEXT NOT NULL,
|
|
355
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
CREATE TABLE IF NOT EXISTS fortress_account_lockout (
|
|
359
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
360
|
+
identifier TEXT NOT NULL UNIQUE,
|
|
361
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
362
|
+
last_failed_at INTEGER,
|
|
363
|
+
locked_until INTEGER,
|
|
364
|
+
lockout_count INTEGER NOT NULL DEFAULT 0,
|
|
365
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
CREATE TABLE IF NOT EXISTS fortress_audit_log (
|
|
369
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
370
|
+
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
371
|
+
event_type TEXT NOT NULL,
|
|
372
|
+
actor_id INTEGER,
|
|
373
|
+
actor_type TEXT NOT NULL DEFAULT 'USER',
|
|
374
|
+
target_id INTEGER,
|
|
375
|
+
target_type TEXT,
|
|
376
|
+
ip_address TEXT,
|
|
377
|
+
user_agent TEXT,
|
|
378
|
+
outcome TEXT NOT NULL DEFAULT 'SUCCESS',
|
|
379
|
+
metadata TEXT,
|
|
380
|
+
previous_hash TEXT,
|
|
381
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
CREATE TABLE IF NOT EXISTS fortress_webhook_endpoint (
|
|
385
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
386
|
+
url TEXT NOT NULL,
|
|
387
|
+
events TEXT NOT NULL,
|
|
388
|
+
secret TEXT NOT NULL,
|
|
389
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
390
|
+
deactivated_reason TEXT,
|
|
391
|
+
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
|
392
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
CREATE TABLE IF NOT EXISTS fortress_webhook_delivery (
|
|
396
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
397
|
+
endpoint_id INTEGER NOT NULL REFERENCES fortress_webhook_endpoint(id) ON DELETE CASCADE,
|
|
398
|
+
event_type TEXT NOT NULL,
|
|
399
|
+
payload TEXT NOT NULL,
|
|
400
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
401
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
402
|
+
idempotency_key TEXT,
|
|
403
|
+
last_attempt_at INTEGER,
|
|
404
|
+
next_retry_at INTEGER,
|
|
405
|
+
response_status INTEGER,
|
|
406
|
+
response_body TEXT,
|
|
407
|
+
error_kind TEXT,
|
|
408
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_webhook_delivery_idempotency
|
|
412
|
+
ON fortress_webhook_delivery (endpoint_id, idempotency_key)
|
|
413
|
+
WHERE idempotency_key IS NOT NULL;
|
|
414
|
+
|
|
415
|
+
CREATE TABLE IF NOT EXISTS fortress_webauthn_credential (
|
|
416
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
417
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
418
|
+
credential_id TEXT NOT NULL UNIQUE,
|
|
419
|
+
public_key TEXT NOT NULL,
|
|
420
|
+
counter INTEGER NOT NULL DEFAULT 0,
|
|
421
|
+
device_type TEXT NOT NULL,
|
|
422
|
+
backed_up INTEGER NOT NULL DEFAULT 0,
|
|
423
|
+
transports TEXT,
|
|
424
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
CREATE TABLE IF NOT EXISTS fortress_webauthn_challenge (
|
|
428
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
429
|
+
challenge TEXT NOT NULL UNIQUE,
|
|
430
|
+
user_id INTEGER,
|
|
431
|
+
expires_at INTEGER NOT NULL,
|
|
432
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
433
|
+
);
|
|
434
|
+
`.trim();
|
|
435
|
+
|
|
436
|
+
// DROP order is the reverse of creation so child tables go before the
|
|
437
|
+
// parents they reference (matters for PostgreSQL FK enforcement; SQLite is
|
|
438
|
+
// permissive but ordering keeps the two dialects symmetric).
|
|
439
|
+
const SQLITE_0002_DOWN = `
|
|
440
|
+
DROP TABLE IF EXISTS fortress_webauthn_challenge;
|
|
441
|
+
DROP TABLE IF EXISTS fortress_webauthn_credential;
|
|
442
|
+
DROP TABLE IF EXISTS fortress_webhook_delivery;
|
|
443
|
+
DROP TABLE IF EXISTS fortress_webhook_endpoint;
|
|
444
|
+
DROP TABLE IF EXISTS fortress_audit_log;
|
|
445
|
+
DROP TABLE IF EXISTS fortress_account_lockout;
|
|
446
|
+
DROP TABLE IF EXISTS fortress_user_scope_assignment;
|
|
447
|
+
DROP TABLE IF EXISTS fortress_oauth_signing_key;
|
|
448
|
+
DROP TABLE IF EXISTS fortress_oauth_pending_flow;
|
|
449
|
+
DROP TABLE IF EXISTS fortress_oauth_refresh_token;
|
|
450
|
+
DROP TABLE IF EXISTS fortress_oauth_access_token;
|
|
451
|
+
DROP TABLE IF EXISTS fortress_oauth_authorization_code;
|
|
452
|
+
DROP TABLE IF EXISTS fortress_oauth_client;
|
|
453
|
+
DROP TABLE IF EXISTS fortress_tenant_user;
|
|
454
|
+
DROP TABLE IF EXISTS fortress_tenant;
|
|
455
|
+
DROP TABLE IF EXISTS fortress_social_account;
|
|
456
|
+
DROP TABLE IF EXISTS fortress_trusted_device;
|
|
457
|
+
DROP TABLE IF EXISTS fortress_backup_code;
|
|
458
|
+
DROP TABLE IF EXISTS fortress_two_factor_secret;
|
|
459
|
+
DROP TABLE IF EXISTS fortress_api_key;
|
|
460
|
+
DROP TABLE IF EXISTS fortress_magic_link_token;
|
|
461
|
+
DROP TABLE IF EXISTS fortress_email_verification_token;
|
|
462
|
+
DROP TABLE IF EXISTS fortress_direct_permission_binding;
|
|
463
|
+
DROP TABLE IF EXISTS fortress_role_binding;
|
|
464
|
+
DROP TABLE IF EXISTS fortress_role_permission;
|
|
465
|
+
DROP TABLE IF EXISTS fortress_role;
|
|
466
|
+
DROP TABLE IF EXISTS fortress_permission;
|
|
467
|
+
DROP TABLE IF EXISTS fortress_resource;
|
|
468
|
+
DROP TABLE IF EXISTS fortress_service_account;
|
|
469
|
+
DROP TABLE IF EXISTS fortress_group_user;
|
|
470
|
+
DROP TABLE IF EXISTS fortress_group;
|
|
471
|
+
DROP TABLE IF EXISTS fortress_refresh_token;
|
|
472
|
+
DROP TABLE IF EXISTS fortress_login_identifier;
|
|
473
|
+
DROP TABLE IF EXISTS fortress_user;
|
|
474
|
+
`.trim();
|
|
475
|
+
|
|
476
|
+
const PG_0002_UP = `
|
|
477
|
+
CREATE TABLE IF NOT EXISTS fortress_user (
|
|
478
|
+
id SERIAL PRIMARY KEY,
|
|
479
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
480
|
+
name VARCHAR(255) NOT NULL,
|
|
481
|
+
password_hash TEXT,
|
|
482
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
483
|
+
email_verified BOOLEAN NOT NULL DEFAULT false,
|
|
484
|
+
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
485
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
CREATE TABLE IF NOT EXISTS fortress_login_identifier (
|
|
489
|
+
id SERIAL PRIMARY KEY,
|
|
490
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
491
|
+
type VARCHAR(20) NOT NULL,
|
|
492
|
+
value VARCHAR(255) NOT NULL UNIQUE
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
CREATE TABLE IF NOT EXISTS fortress_refresh_token (
|
|
496
|
+
id BIGSERIAL PRIMARY KEY,
|
|
497
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
498
|
+
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
|
499
|
+
token_family VARCHAR(64) NOT NULL,
|
|
500
|
+
is_revoked BOOLEAN NOT NULL DEFAULT false,
|
|
501
|
+
expires_at TIMESTAMP NOT NULL,
|
|
502
|
+
ip_address VARCHAR(45),
|
|
503
|
+
user_agent TEXT,
|
|
504
|
+
device_name TEXT,
|
|
505
|
+
last_active_at TIMESTAMP,
|
|
506
|
+
fingerprint_hash VARCHAR(64),
|
|
507
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
CREATE TABLE IF NOT EXISTS fortress_group (
|
|
511
|
+
id SERIAL PRIMARY KEY,
|
|
512
|
+
name VARCHAR(100) NOT NULL UNIQUE,
|
|
513
|
+
description TEXT
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
CREATE TABLE IF NOT EXISTS fortress_group_user (
|
|
517
|
+
group_id INTEGER NOT NULL REFERENCES fortress_group(id) ON DELETE CASCADE,
|
|
518
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
519
|
+
PRIMARY KEY (group_id, user_id)
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
CREATE TABLE IF NOT EXISTS fortress_service_account (
|
|
523
|
+
id SERIAL PRIMARY KEY,
|
|
524
|
+
name VARCHAR(100) NOT NULL UNIQUE,
|
|
525
|
+
display_name VARCHAR(255),
|
|
526
|
+
description TEXT,
|
|
527
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
528
|
+
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
529
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
CREATE TABLE IF NOT EXISTS fortress_resource (
|
|
533
|
+
name VARCHAR(100) PRIMARY KEY,
|
|
534
|
+
description TEXT
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
CREATE TABLE IF NOT EXISTS fortress_permission (
|
|
538
|
+
id SERIAL PRIMARY KEY,
|
|
539
|
+
resource VARCHAR(100) NOT NULL REFERENCES fortress_resource(name) ON DELETE CASCADE,
|
|
540
|
+
action VARCHAR(100) NOT NULL,
|
|
541
|
+
effect VARCHAR(10) NOT NULL DEFAULT 'ALLOW',
|
|
542
|
+
conditions JSONB,
|
|
543
|
+
description TEXT
|
|
544
|
+
);
|
|
545
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_no_conditions
|
|
546
|
+
ON fortress_permission (resource, action, effect)
|
|
547
|
+
WHERE conditions IS NULL;
|
|
548
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_permission_with_conditions
|
|
549
|
+
ON fortress_permission (resource, action, effect, conditions)
|
|
550
|
+
WHERE conditions IS NOT NULL;
|
|
551
|
+
|
|
552
|
+
CREATE TABLE IF NOT EXISTS fortress_role (
|
|
553
|
+
id SERIAL PRIMARY KEY,
|
|
554
|
+
name VARCHAR(100) NOT NULL UNIQUE,
|
|
555
|
+
description TEXT,
|
|
556
|
+
is_system BOOLEAN NOT NULL DEFAULT false
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
CREATE TABLE IF NOT EXISTS fortress_role_permission (
|
|
560
|
+
role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
|
|
561
|
+
permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
|
|
562
|
+
PRIMARY KEY (role_id, permission_id)
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
CREATE TABLE IF NOT EXISTS fortress_role_binding (
|
|
566
|
+
id SERIAL PRIMARY KEY,
|
|
567
|
+
role_id INTEGER NOT NULL REFERENCES fortress_role(id) ON DELETE CASCADE,
|
|
568
|
+
subject_type VARCHAR(20) NOT NULL,
|
|
569
|
+
subject_id INTEGER NOT NULL,
|
|
570
|
+
tenant_id VARCHAR(100),
|
|
571
|
+
UNIQUE (role_id, subject_type, subject_id, tenant_id)
|
|
572
|
+
);
|
|
573
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_global
|
|
574
|
+
ON fortress_role_binding (role_id, subject_type, subject_id)
|
|
575
|
+
WHERE tenant_id IS NULL;
|
|
576
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_role_binding_tenant
|
|
577
|
+
ON fortress_role_binding (role_id, subject_type, subject_id, tenant_id)
|
|
578
|
+
WHERE tenant_id IS NOT NULL;
|
|
579
|
+
|
|
580
|
+
CREATE TABLE IF NOT EXISTS fortress_direct_permission_binding (
|
|
581
|
+
id SERIAL PRIMARY KEY,
|
|
582
|
+
permission_id INTEGER NOT NULL REFERENCES fortress_permission(id) ON DELETE CASCADE,
|
|
583
|
+
subject_type VARCHAR(20) NOT NULL,
|
|
584
|
+
subject_id INTEGER NOT NULL,
|
|
585
|
+
tenant_id VARCHAR(100),
|
|
586
|
+
UNIQUE (permission_id, subject_type, subject_id, tenant_id)
|
|
587
|
+
);
|
|
588
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_global
|
|
589
|
+
ON fortress_direct_permission_binding (permission_id, subject_type, subject_id)
|
|
590
|
+
WHERE tenant_id IS NULL;
|
|
591
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_direct_permission_binding_tenant
|
|
592
|
+
ON fortress_direct_permission_binding (permission_id, subject_type, subject_id, tenant_id)
|
|
593
|
+
WHERE tenant_id IS NOT NULL;
|
|
594
|
+
|
|
595
|
+
CREATE TABLE IF NOT EXISTS fortress_email_verification_token (
|
|
596
|
+
id BIGSERIAL PRIMARY KEY,
|
|
597
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
598
|
+
token TEXT NOT NULL,
|
|
599
|
+
email VARCHAR(255) NOT NULL,
|
|
600
|
+
expires_at TIMESTAMP NOT NULL,
|
|
601
|
+
used_at TIMESTAMP,
|
|
602
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
CREATE TABLE IF NOT EXISTS fortress_magic_link_token (
|
|
606
|
+
id BIGSERIAL PRIMARY KEY,
|
|
607
|
+
email VARCHAR(255) NOT NULL,
|
|
608
|
+
token VARCHAR(64) NOT NULL,
|
|
609
|
+
expires_at TIMESTAMP NOT NULL,
|
|
610
|
+
used_at TIMESTAMP,
|
|
611
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
CREATE TABLE IF NOT EXISTS fortress_api_key (
|
|
615
|
+
id SERIAL PRIMARY KEY,
|
|
616
|
+
subject_type VARCHAR(20) NOT NULL,
|
|
617
|
+
subject_id INTEGER NOT NULL,
|
|
618
|
+
name VARCHAR(255) NOT NULL,
|
|
619
|
+
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
|
620
|
+
key_prefix VARCHAR(20) NOT NULL,
|
|
621
|
+
scopes TEXT,
|
|
622
|
+
expires_at TIMESTAMP,
|
|
623
|
+
last_used_at TIMESTAMP,
|
|
624
|
+
is_revoked BOOLEAN NOT NULL DEFAULT false,
|
|
625
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
626
|
+
);
|
|
627
|
+
CREATE INDEX IF NOT EXISTS api_key_subject_idx ON fortress_api_key (subject_type, subject_id);
|
|
628
|
+
|
|
629
|
+
CREATE TABLE IF NOT EXISTS fortress_two_factor_secret (
|
|
630
|
+
id SERIAL PRIMARY KEY,
|
|
631
|
+
user_id INTEGER NOT NULL UNIQUE REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
632
|
+
secret TEXT NOT NULL,
|
|
633
|
+
is_enabled BOOLEAN NOT NULL DEFAULT false,
|
|
634
|
+
last_used_counter INTEGER,
|
|
635
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
CREATE TABLE IF NOT EXISTS fortress_backup_code (
|
|
639
|
+
id SERIAL PRIMARY KEY,
|
|
640
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
641
|
+
code_hash TEXT NOT NULL,
|
|
642
|
+
is_used BOOLEAN NOT NULL DEFAULT false,
|
|
643
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
CREATE TABLE IF NOT EXISTS fortress_trusted_device (
|
|
647
|
+
id SERIAL PRIMARY KEY,
|
|
648
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
649
|
+
device_hash VARCHAR(64) NOT NULL,
|
|
650
|
+
expires_at TIMESTAMP NOT NULL,
|
|
651
|
+
last_used_at TIMESTAMP NOT NULL,
|
|
652
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
CREATE TABLE IF NOT EXISTS fortress_social_account (
|
|
656
|
+
id SERIAL PRIMARY KEY,
|
|
657
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
658
|
+
provider VARCHAR(50) NOT NULL,
|
|
659
|
+
provider_account_id VARCHAR(255) NOT NULL,
|
|
660
|
+
email VARCHAR(255),
|
|
661
|
+
access_token TEXT,
|
|
662
|
+
refresh_token TEXT,
|
|
663
|
+
token_expires_at TIMESTAMP,
|
|
664
|
+
profile JSONB,
|
|
665
|
+
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
666
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
667
|
+
UNIQUE (user_id, provider),
|
|
668
|
+
UNIQUE (provider, provider_account_id)
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
CREATE TABLE IF NOT EXISTS fortress_tenant (
|
|
672
|
+
id SERIAL PRIMARY KEY,
|
|
673
|
+
name VARCHAR(255) NOT NULL,
|
|
674
|
+
tax_id VARCHAR(100) NOT NULL UNIQUE,
|
|
675
|
+
description TEXT,
|
|
676
|
+
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
677
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
CREATE TABLE IF NOT EXISTS fortress_tenant_user (
|
|
681
|
+
tenant_id INTEGER NOT NULL REFERENCES fortress_tenant(id) ON DELETE CASCADE,
|
|
682
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
683
|
+
is_default BOOLEAN NOT NULL DEFAULT false,
|
|
684
|
+
PRIMARY KEY (tenant_id, user_id)
|
|
685
|
+
);
|
|
686
|
+
CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
|
|
687
|
+
ON fortress_tenant_user (user_id) WHERE is_default = true;
|
|
688
|
+
|
|
689
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_client (
|
|
690
|
+
id SERIAL PRIMARY KEY,
|
|
691
|
+
client_id VARCHAR(255) NOT NULL UNIQUE,
|
|
692
|
+
client_secret_hash TEXT NOT NULL,
|
|
693
|
+
name VARCHAR(255) NOT NULL,
|
|
694
|
+
redirect_uris TEXT NOT NULL,
|
|
695
|
+
grant_types TEXT NOT NULL,
|
|
696
|
+
allowed_scopes TEXT,
|
|
697
|
+
token_endpoint_auth_method TEXT,
|
|
698
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_authorization_code (
|
|
702
|
+
id BIGSERIAL PRIMARY KEY,
|
|
703
|
+
code VARCHAR(255) NOT NULL UNIQUE,
|
|
704
|
+
client_id VARCHAR(255) NOT NULL,
|
|
705
|
+
user_id INTEGER NOT NULL,
|
|
706
|
+
redirect_uri TEXT NOT NULL,
|
|
707
|
+
scope TEXT,
|
|
708
|
+
code_challenge TEXT,
|
|
709
|
+
code_challenge_method VARCHAR(10),
|
|
710
|
+
nonce TEXT,
|
|
711
|
+
auth_time INTEGER,
|
|
712
|
+
expires_at TIMESTAMP NOT NULL,
|
|
713
|
+
used_at TIMESTAMP,
|
|
714
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_access_token (
|
|
718
|
+
id BIGSERIAL PRIMARY KEY,
|
|
719
|
+
token VARCHAR(255) NOT NULL UNIQUE,
|
|
720
|
+
client_id VARCHAR(255) NOT NULL,
|
|
721
|
+
user_id INTEGER,
|
|
722
|
+
scope TEXT,
|
|
723
|
+
expires_at TIMESTAMP NOT NULL,
|
|
724
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_refresh_token (
|
|
728
|
+
id BIGSERIAL PRIMARY KEY,
|
|
729
|
+
token VARCHAR(255) NOT NULL UNIQUE,
|
|
730
|
+
family_id VARCHAR(64) NOT NULL,
|
|
731
|
+
client_id VARCHAR(255) NOT NULL,
|
|
732
|
+
user_id INTEGER NOT NULL,
|
|
733
|
+
scope TEXT,
|
|
734
|
+
issued_at TIMESTAMP NOT NULL,
|
|
735
|
+
expires_at TIMESTAMP NOT NULL,
|
|
736
|
+
used_at TIMESTAMP,
|
|
737
|
+
parent_id INTEGER,
|
|
738
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_pending_flow (
|
|
742
|
+
id BIGSERIAL PRIMARY KEY,
|
|
743
|
+
flow_id TEXT NOT NULL UNIQUE,
|
|
744
|
+
client_id VARCHAR(255) NOT NULL,
|
|
745
|
+
redirect_uri TEXT NOT NULL,
|
|
746
|
+
scope TEXT,
|
|
747
|
+
state VARCHAR(255) NOT NULL,
|
|
748
|
+
code_challenge TEXT,
|
|
749
|
+
code_challenge_method VARCHAR(10),
|
|
750
|
+
nonce TEXT,
|
|
751
|
+
user_id INTEGER,
|
|
752
|
+
used_at TIMESTAMP,
|
|
753
|
+
expires_at TIMESTAMP NOT NULL,
|
|
754
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
CREATE TABLE IF NOT EXISTS fortress_oauth_signing_key (
|
|
758
|
+
id SERIAL PRIMARY KEY,
|
|
759
|
+
kid VARCHAR(64) NOT NULL UNIQUE,
|
|
760
|
+
alg VARCHAR(16) NOT NULL,
|
|
761
|
+
public_jwk TEXT NOT NULL,
|
|
762
|
+
private_jwk TEXT NOT NULL,
|
|
763
|
+
rotated_at TIMESTAMP,
|
|
764
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
CREATE TABLE IF NOT EXISTS fortress_user_scope_assignment (
|
|
768
|
+
id SERIAL PRIMARY KEY,
|
|
769
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
770
|
+
scope_name VARCHAR(100) NOT NULL,
|
|
771
|
+
scope_value VARCHAR(255) NOT NULL,
|
|
772
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
CREATE TABLE IF NOT EXISTS fortress_account_lockout (
|
|
776
|
+
id SERIAL PRIMARY KEY,
|
|
777
|
+
identifier VARCHAR(255) NOT NULL UNIQUE,
|
|
778
|
+
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
779
|
+
last_failed_at TIMESTAMP,
|
|
780
|
+
locked_until TIMESTAMP,
|
|
781
|
+
lockout_count INTEGER NOT NULL DEFAULT 0,
|
|
782
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
CREATE TABLE IF NOT EXISTS fortress_audit_log (
|
|
786
|
+
id BIGSERIAL PRIMARY KEY,
|
|
787
|
+
timestamp TIMESTAMP NOT NULL DEFAULT now(),
|
|
788
|
+
event_type VARCHAR(100) NOT NULL,
|
|
789
|
+
actor_id INTEGER,
|
|
790
|
+
actor_type VARCHAR(20) NOT NULL DEFAULT 'USER',
|
|
791
|
+
target_id INTEGER,
|
|
792
|
+
target_type VARCHAR(50),
|
|
793
|
+
ip_address VARCHAR(45),
|
|
794
|
+
user_agent TEXT,
|
|
795
|
+
outcome VARCHAR(20) NOT NULL DEFAULT 'SUCCESS',
|
|
796
|
+
metadata JSONB,
|
|
797
|
+
previous_hash TEXT,
|
|
798
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
CREATE TABLE IF NOT EXISTS fortress_webhook_endpoint (
|
|
802
|
+
id SERIAL PRIMARY KEY,
|
|
803
|
+
url TEXT NOT NULL,
|
|
804
|
+
events TEXT NOT NULL,
|
|
805
|
+
secret TEXT NOT NULL,
|
|
806
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
807
|
+
deactivated_reason TEXT,
|
|
808
|
+
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
|
809
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
CREATE TABLE IF NOT EXISTS fortress_webhook_delivery (
|
|
813
|
+
id BIGSERIAL PRIMARY KEY,
|
|
814
|
+
endpoint_id INTEGER NOT NULL REFERENCES fortress_webhook_endpoint(id) ON DELETE CASCADE,
|
|
815
|
+
event_type VARCHAR(100) NOT NULL,
|
|
816
|
+
payload TEXT NOT NULL,
|
|
817
|
+
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
818
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
819
|
+
idempotency_key TEXT,
|
|
820
|
+
last_attempt_at TIMESTAMP,
|
|
821
|
+
next_retry_at TIMESTAMP,
|
|
822
|
+
response_status INTEGER,
|
|
823
|
+
response_body TEXT,
|
|
824
|
+
error_kind TEXT,
|
|
825
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_webhook_delivery_idempotency
|
|
829
|
+
ON fortress_webhook_delivery (endpoint_id, idempotency_key)
|
|
830
|
+
WHERE idempotency_key IS NOT NULL;
|
|
831
|
+
|
|
832
|
+
CREATE TABLE IF NOT EXISTS fortress_webauthn_credential (
|
|
833
|
+
id SERIAL PRIMARY KEY,
|
|
834
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
835
|
+
credential_id TEXT NOT NULL UNIQUE,
|
|
836
|
+
public_key TEXT NOT NULL,
|
|
837
|
+
counter INTEGER NOT NULL DEFAULT 0,
|
|
838
|
+
device_type VARCHAR(20) NOT NULL,
|
|
839
|
+
backed_up BOOLEAN NOT NULL DEFAULT false,
|
|
840
|
+
transports TEXT,
|
|
841
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
CREATE TABLE IF NOT EXISTS fortress_webauthn_challenge (
|
|
845
|
+
id BIGSERIAL PRIMARY KEY,
|
|
846
|
+
challenge TEXT NOT NULL UNIQUE,
|
|
847
|
+
user_id INTEGER,
|
|
848
|
+
expires_at TIMESTAMP NOT NULL,
|
|
849
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
850
|
+
);
|
|
851
|
+
`.trim();
|
|
852
|
+
|
|
853
|
+
const PG_0002_DOWN = `
|
|
854
|
+
DROP TABLE IF EXISTS fortress_webauthn_challenge CASCADE;
|
|
855
|
+
DROP TABLE IF EXISTS fortress_webauthn_credential CASCADE;
|
|
856
|
+
DROP TABLE IF EXISTS fortress_webhook_delivery CASCADE;
|
|
857
|
+
DROP TABLE IF EXISTS fortress_webhook_endpoint CASCADE;
|
|
858
|
+
DROP TABLE IF EXISTS fortress_audit_log CASCADE;
|
|
859
|
+
DROP TABLE IF EXISTS fortress_account_lockout CASCADE;
|
|
860
|
+
DROP TABLE IF EXISTS fortress_user_scope_assignment CASCADE;
|
|
861
|
+
DROP TABLE IF EXISTS fortress_oauth_signing_key CASCADE;
|
|
862
|
+
DROP TABLE IF EXISTS fortress_oauth_pending_flow CASCADE;
|
|
863
|
+
DROP TABLE IF EXISTS fortress_oauth_refresh_token CASCADE;
|
|
864
|
+
DROP TABLE IF EXISTS fortress_oauth_access_token CASCADE;
|
|
865
|
+
DROP TABLE IF EXISTS fortress_oauth_authorization_code CASCADE;
|
|
866
|
+
DROP TABLE IF EXISTS fortress_oauth_client CASCADE;
|
|
867
|
+
DROP TABLE IF EXISTS fortress_tenant_user CASCADE;
|
|
868
|
+
DROP TABLE IF EXISTS fortress_tenant CASCADE;
|
|
869
|
+
DROP TABLE IF EXISTS fortress_social_account CASCADE;
|
|
870
|
+
DROP TABLE IF EXISTS fortress_trusted_device CASCADE;
|
|
871
|
+
DROP TABLE IF EXISTS fortress_backup_code CASCADE;
|
|
872
|
+
DROP TABLE IF EXISTS fortress_two_factor_secret CASCADE;
|
|
873
|
+
DROP TABLE IF EXISTS fortress_api_key CASCADE;
|
|
874
|
+
DROP TABLE IF EXISTS fortress_magic_link_token CASCADE;
|
|
875
|
+
DROP TABLE IF EXISTS fortress_email_verification_token CASCADE;
|
|
876
|
+
DROP TABLE IF EXISTS fortress_direct_permission_binding CASCADE;
|
|
877
|
+
DROP TABLE IF EXISTS fortress_role_binding CASCADE;
|
|
878
|
+
DROP TABLE IF EXISTS fortress_role_permission CASCADE;
|
|
879
|
+
DROP TABLE IF EXISTS fortress_role CASCADE;
|
|
880
|
+
DROP TABLE IF EXISTS fortress_permission CASCADE;
|
|
881
|
+
DROP TABLE IF EXISTS fortress_resource CASCADE;
|
|
882
|
+
DROP TABLE IF EXISTS fortress_service_account CASCADE;
|
|
883
|
+
DROP TABLE IF EXISTS fortress_group_user CASCADE;
|
|
884
|
+
DROP TABLE IF EXISTS fortress_group CASCADE;
|
|
885
|
+
DROP TABLE IF EXISTS fortress_refresh_token CASCADE;
|
|
886
|
+
DROP TABLE IF EXISTS fortress_login_identifier CASCADE;
|
|
887
|
+
DROP TABLE IF EXISTS fortress_user CASCADE;
|
|
888
|
+
`.trim();
|
|
889
|
+
|
|
890
|
+
// --- 0003: auth continuation + refresh-session metadata ---
|
|
891
|
+
//
|
|
892
|
+
// Adds the single-use state carrier used by the post-auth gate and the
|
|
893
|
+
// refresh-family metadata needed by grace-window rotation and session caps.
|
|
894
|
+
// Existing refresh families are backfilled from their original row creation
|
|
895
|
+
// time so an upgrade does not reset their absolute age.
|
|
896
|
+
|
|
897
|
+
const SQLITE_0003_UP = `
|
|
898
|
+
CREATE TABLE fortress_refresh_token_v3 (
|
|
899
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
900
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
901
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
902
|
+
token_family TEXT NOT NULL,
|
|
903
|
+
family_created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
904
|
+
successor_token_hash TEXT,
|
|
905
|
+
rotated_at INTEGER,
|
|
906
|
+
is_revoked INTEGER NOT NULL DEFAULT 0,
|
|
907
|
+
expires_at INTEGER NOT NULL,
|
|
908
|
+
ip_address TEXT,
|
|
909
|
+
user_agent TEXT,
|
|
910
|
+
device_name TEXT,
|
|
911
|
+
last_active_at INTEGER,
|
|
912
|
+
fingerprint_hash TEXT,
|
|
913
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
914
|
+
);
|
|
915
|
+
INSERT INTO fortress_refresh_token_v3 (
|
|
916
|
+
id, user_id, token_hash, token_family, family_created_at,
|
|
917
|
+
successor_token_hash, rotated_at, is_revoked, expires_at, ip_address,
|
|
918
|
+
user_agent, device_name, last_active_at, fingerprint_hash, created_at
|
|
919
|
+
)
|
|
920
|
+
SELECT
|
|
921
|
+
token.id, token.user_id, token.token_hash, token.token_family,
|
|
922
|
+
(SELECT MIN(family.created_at)
|
|
923
|
+
FROM fortress_refresh_token AS family
|
|
924
|
+
WHERE family.token_family = token.token_family),
|
|
925
|
+
NULL, NULL, token.is_revoked, token.expires_at, token.ip_address,
|
|
926
|
+
token.user_agent, token.device_name, token.last_active_at,
|
|
927
|
+
token.fingerprint_hash, token.created_at
|
|
928
|
+
FROM fortress_refresh_token AS token;
|
|
929
|
+
DROP TABLE fortress_refresh_token;
|
|
930
|
+
ALTER TABLE fortress_refresh_token_v3 RENAME TO fortress_refresh_token;
|
|
931
|
+
|
|
932
|
+
CREATE TABLE fortress_auth_continuation (
|
|
933
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
934
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
935
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
936
|
+
reason TEXT NOT NULL,
|
|
937
|
+
expires_at INTEGER NOT NULL,
|
|
938
|
+
consumed_at INTEGER,
|
|
939
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
940
|
+
);
|
|
941
|
+
`.trim();
|
|
942
|
+
|
|
943
|
+
const SQLITE_0003_DOWN = `
|
|
944
|
+
DROP TABLE IF EXISTS fortress_auth_continuation;
|
|
945
|
+
CREATE TABLE fortress_refresh_token_v2 (
|
|
946
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
947
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
948
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
949
|
+
token_family TEXT NOT NULL,
|
|
950
|
+
is_revoked INTEGER NOT NULL DEFAULT 0,
|
|
951
|
+
expires_at INTEGER NOT NULL,
|
|
952
|
+
ip_address TEXT,
|
|
953
|
+
user_agent TEXT,
|
|
954
|
+
device_name TEXT,
|
|
955
|
+
last_active_at INTEGER,
|
|
956
|
+
fingerprint_hash TEXT,
|
|
957
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
958
|
+
);
|
|
959
|
+
INSERT INTO fortress_refresh_token_v2 (
|
|
960
|
+
id, user_id, token_hash, token_family, is_revoked, expires_at,
|
|
961
|
+
ip_address, user_agent, device_name, last_active_at, fingerprint_hash, created_at
|
|
962
|
+
)
|
|
963
|
+
SELECT
|
|
964
|
+
id, user_id, token_hash, token_family, is_revoked, expires_at,
|
|
965
|
+
ip_address, user_agent, device_name, last_active_at, fingerprint_hash, created_at
|
|
966
|
+
FROM fortress_refresh_token;
|
|
967
|
+
DROP TABLE fortress_refresh_token;
|
|
968
|
+
ALTER TABLE fortress_refresh_token_v2 RENAME TO fortress_refresh_token;
|
|
969
|
+
`.trim();
|
|
970
|
+
|
|
971
|
+
const PG_0003_UP = `
|
|
972
|
+
ALTER TABLE fortress_refresh_token
|
|
973
|
+
ADD COLUMN family_created_at TIMESTAMP;
|
|
974
|
+
UPDATE fortress_refresh_token AS token
|
|
975
|
+
SET family_created_at = family.created_at
|
|
976
|
+
FROM (
|
|
977
|
+
SELECT token_family, MIN(created_at) AS created_at
|
|
978
|
+
FROM fortress_refresh_token
|
|
979
|
+
GROUP BY token_family
|
|
980
|
+
) AS family
|
|
981
|
+
WHERE token.token_family = family.token_family;
|
|
982
|
+
ALTER TABLE fortress_refresh_token
|
|
983
|
+
ALTER COLUMN family_created_at SET NOT NULL;
|
|
984
|
+
ALTER TABLE fortress_refresh_token
|
|
985
|
+
ALTER COLUMN family_created_at SET DEFAULT now();
|
|
986
|
+
ALTER TABLE fortress_refresh_token
|
|
987
|
+
ADD COLUMN successor_token_hash VARCHAR(64);
|
|
988
|
+
ALTER TABLE fortress_refresh_token
|
|
989
|
+
ADD COLUMN rotated_at TIMESTAMP;
|
|
990
|
+
|
|
991
|
+
CREATE TABLE fortress_auth_continuation (
|
|
992
|
+
id BIGSERIAL PRIMARY KEY,
|
|
993
|
+
user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
994
|
+
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
|
995
|
+
reason VARCHAR(32) NOT NULL,
|
|
996
|
+
expires_at TIMESTAMP NOT NULL,
|
|
997
|
+
consumed_at TIMESTAMP,
|
|
998
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
999
|
+
);
|
|
1000
|
+
`.trim();
|
|
1001
|
+
|
|
1002
|
+
const PG_0003_DOWN = `
|
|
1003
|
+
DROP TABLE IF EXISTS fortress_auth_continuation CASCADE;
|
|
1004
|
+
ALTER TABLE fortress_refresh_token DROP COLUMN rotated_at;
|
|
1005
|
+
ALTER TABLE fortress_refresh_token DROP COLUMN successor_token_hash;
|
|
1006
|
+
ALTER TABLE fortress_refresh_token DROP COLUMN family_created_at;
|
|
1007
|
+
`.trim();
|
|
1008
|
+
|
|
1009
|
+
const SQLITE_0004_UP = `
|
|
1010
|
+
UPDATE fortress_tenant_user
|
|
1011
|
+
SET is_default = 0
|
|
1012
|
+
WHERE is_default = 1
|
|
1013
|
+
AND rowid NOT IN (
|
|
1014
|
+
SELECT MIN(rowid)
|
|
1015
|
+
FROM fortress_tenant_user
|
|
1016
|
+
WHERE is_default = 1
|
|
1017
|
+
GROUP BY user_id
|
|
1018
|
+
);
|
|
1019
|
+
CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
|
|
1020
|
+
ON fortress_tenant_user (user_id)
|
|
1021
|
+
WHERE is_default = 1;
|
|
1022
|
+
`.trim();
|
|
1023
|
+
|
|
1024
|
+
const SQLITE_0004_DOWN = `
|
|
1025
|
+
DROP INDEX IF EXISTS fortress_tenant_user_one_default_idx;
|
|
1026
|
+
`.trim();
|
|
1027
|
+
|
|
1028
|
+
const PG_0004_UP = `
|
|
1029
|
+
WITH ranked_defaults AS (
|
|
1030
|
+
SELECT ctid, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY tenant_id) AS position
|
|
1031
|
+
FROM fortress_tenant_user
|
|
1032
|
+
WHERE is_default = true
|
|
1033
|
+
)
|
|
1034
|
+
UPDATE fortress_tenant_user AS membership
|
|
1035
|
+
SET is_default = false
|
|
1036
|
+
FROM ranked_defaults
|
|
1037
|
+
WHERE membership.ctid = ranked_defaults.ctid
|
|
1038
|
+
AND ranked_defaults.position > 1;
|
|
1039
|
+
CREATE UNIQUE INDEX IF NOT EXISTS fortress_tenant_user_one_default_idx
|
|
1040
|
+
ON fortress_tenant_user (user_id)
|
|
1041
|
+
WHERE is_default = true;
|
|
1042
|
+
`.trim();
|
|
1043
|
+
|
|
1044
|
+
const PG_0004_DOWN = `
|
|
1045
|
+
DROP INDEX IF EXISTS fortress_tenant_user_one_default_idx;
|
|
1046
|
+
`.trim();
|
|
1047
|
+
|
|
1048
|
+
// --- 0005: hot-path indexes and timezone-safe PostgreSQL timestamps ---
|
|
1049
|
+
|
|
1050
|
+
const SQLITE_0005_UP = `
|
|
1051
|
+
CREATE INDEX refresh_token_family_idx ON fortress_refresh_token (token_family);
|
|
1052
|
+
CREATE INDEX refresh_token_user_idx ON fortress_refresh_token (user_id);
|
|
1053
|
+
CREATE INDEX email_verification_token_token_idx ON fortress_email_verification_token (token);
|
|
1054
|
+
CREATE INDEX magic_link_token_token_idx ON fortress_magic_link_token (token);
|
|
1055
|
+
CREATE INDEX role_binding_subject_idx ON fortress_role_binding (subject_type, subject_id);
|
|
1056
|
+
CREATE INDEX direct_permission_binding_subject_idx ON fortress_direct_permission_binding (subject_type, subject_id);
|
|
1057
|
+
CREATE INDEX backup_code_user_idx ON fortress_backup_code (user_id);
|
|
1058
|
+
CREATE INDEX trusted_device_user_idx ON fortress_trusted_device (user_id);
|
|
1059
|
+
CREATE INDEX webhook_delivery_retry_idx ON fortress_webhook_delivery (status, next_retry_at);
|
|
1060
|
+
CREATE INDEX audit_log_timestamp_idx ON fortress_audit_log (timestamp);
|
|
1061
|
+
`.trim();
|
|
1062
|
+
|
|
1063
|
+
const SQLITE_0005_DOWN = `
|
|
1064
|
+
DROP INDEX IF EXISTS audit_log_timestamp_idx;
|
|
1065
|
+
DROP INDEX IF EXISTS webhook_delivery_retry_idx;
|
|
1066
|
+
DROP INDEX IF EXISTS trusted_device_user_idx;
|
|
1067
|
+
DROP INDEX IF EXISTS backup_code_user_idx;
|
|
1068
|
+
DROP INDEX IF EXISTS direct_permission_binding_subject_idx;
|
|
1069
|
+
DROP INDEX IF EXISTS role_binding_subject_idx;
|
|
1070
|
+
DROP INDEX IF EXISTS magic_link_token_token_idx;
|
|
1071
|
+
DROP INDEX IF EXISTS email_verification_token_token_idx;
|
|
1072
|
+
DROP INDEX IF EXISTS refresh_token_user_idx;
|
|
1073
|
+
DROP INDEX IF EXISTS refresh_token_family_idx;
|
|
1074
|
+
`.trim();
|
|
1075
|
+
|
|
1076
|
+
const PG_0005_UP = `
|
|
1077
|
+
CREATE INDEX refresh_token_family_idx ON fortress_refresh_token (token_family);
|
|
1078
|
+
CREATE INDEX refresh_token_user_idx ON fortress_refresh_token (user_id);
|
|
1079
|
+
CREATE INDEX email_verification_token_token_idx ON fortress_email_verification_token (token);
|
|
1080
|
+
CREATE INDEX magic_link_token_token_idx ON fortress_magic_link_token (token);
|
|
1081
|
+
CREATE INDEX role_binding_subject_idx ON fortress_role_binding (subject_type, subject_id);
|
|
1082
|
+
CREATE INDEX direct_permission_binding_subject_idx ON fortress_direct_permission_binding (subject_type, subject_id);
|
|
1083
|
+
CREATE INDEX backup_code_user_idx ON fortress_backup_code (user_id);
|
|
1084
|
+
CREATE INDEX trusted_device_user_idx ON fortress_trusted_device (user_id);
|
|
1085
|
+
CREATE INDEX webhook_delivery_retry_idx ON fortress_webhook_delivery (status, next_retry_at);
|
|
1086
|
+
CREATE INDEX audit_log_timestamp_idx ON fortress_audit_log (timestamp);
|
|
1087
|
+
|
|
1088
|
+
ALTER TABLE fortress_schema_version ALTER COLUMN applied_at TYPE TIMESTAMPTZ USING applied_at AT TIME ZONE 'UTC';
|
|
1089
|
+
ALTER TABLE fortress_user ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1090
|
+
ALTER TABLE fortress_user ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
|
|
1091
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN family_created_at TYPE TIMESTAMPTZ USING family_created_at AT TIME ZONE 'UTC';
|
|
1092
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN rotated_at TYPE TIMESTAMPTZ USING rotated_at AT TIME ZONE 'UTC';
|
|
1093
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1094
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN last_active_at TYPE TIMESTAMPTZ USING last_active_at AT TIME ZONE 'UTC';
|
|
1095
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1096
|
+
ALTER TABLE fortress_auth_continuation ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1097
|
+
ALTER TABLE fortress_auth_continuation ALTER COLUMN consumed_at TYPE TIMESTAMPTZ USING consumed_at AT TIME ZONE 'UTC';
|
|
1098
|
+
ALTER TABLE fortress_auth_continuation ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1099
|
+
ALTER TABLE fortress_service_account ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1100
|
+
ALTER TABLE fortress_service_account ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
|
|
1101
|
+
ALTER TABLE fortress_email_verification_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1102
|
+
ALTER TABLE fortress_email_verification_token ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
|
|
1103
|
+
ALTER TABLE fortress_email_verification_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1104
|
+
ALTER TABLE fortress_magic_link_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1105
|
+
ALTER TABLE fortress_magic_link_token ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
|
|
1106
|
+
ALTER TABLE fortress_magic_link_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1107
|
+
ALTER TABLE fortress_api_key ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1108
|
+
ALTER TABLE fortress_api_key ALTER COLUMN last_used_at TYPE TIMESTAMPTZ USING last_used_at AT TIME ZONE 'UTC';
|
|
1109
|
+
ALTER TABLE fortress_api_key ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1110
|
+
ALTER TABLE fortress_two_factor_secret ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1111
|
+
ALTER TABLE fortress_backup_code ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1112
|
+
ALTER TABLE fortress_trusted_device ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1113
|
+
ALTER TABLE fortress_trusted_device ALTER COLUMN last_used_at TYPE TIMESTAMPTZ USING last_used_at AT TIME ZONE 'UTC';
|
|
1114
|
+
ALTER TABLE fortress_trusted_device ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1115
|
+
ALTER TABLE fortress_social_account ALTER COLUMN token_expires_at TYPE TIMESTAMPTZ USING token_expires_at AT TIME ZONE 'UTC';
|
|
1116
|
+
ALTER TABLE fortress_social_account ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1117
|
+
ALTER TABLE fortress_social_account ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
|
|
1118
|
+
ALTER TABLE fortress_tenant ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1119
|
+
ALTER TABLE fortress_tenant ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC';
|
|
1120
|
+
ALTER TABLE fortress_oauth_client ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1121
|
+
ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1122
|
+
ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
|
|
1123
|
+
ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1124
|
+
ALTER TABLE fortress_oauth_access_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1125
|
+
ALTER TABLE fortress_oauth_access_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1126
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN issued_at TYPE TIMESTAMPTZ USING issued_at AT TIME ZONE 'UTC';
|
|
1127
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1128
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
|
|
1129
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1130
|
+
ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN used_at TYPE TIMESTAMPTZ USING used_at AT TIME ZONE 'UTC';
|
|
1131
|
+
ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1132
|
+
ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1133
|
+
ALTER TABLE fortress_oauth_signing_key ALTER COLUMN rotated_at TYPE TIMESTAMPTZ USING rotated_at AT TIME ZONE 'UTC';
|
|
1134
|
+
ALTER TABLE fortress_oauth_signing_key ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1135
|
+
ALTER TABLE fortress_user_scope_assignment ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1136
|
+
ALTER TABLE fortress_account_lockout ALTER COLUMN last_failed_at TYPE TIMESTAMPTZ USING last_failed_at AT TIME ZONE 'UTC';
|
|
1137
|
+
ALTER TABLE fortress_account_lockout ALTER COLUMN locked_until TYPE TIMESTAMPTZ USING locked_until AT TIME ZONE 'UTC';
|
|
1138
|
+
ALTER TABLE fortress_account_lockout ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1139
|
+
ALTER TABLE fortress_audit_log ALTER COLUMN timestamp TYPE TIMESTAMPTZ USING timestamp AT TIME ZONE 'UTC';
|
|
1140
|
+
ALTER TABLE fortress_audit_log ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1141
|
+
ALTER TABLE fortress_webhook_endpoint ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1142
|
+
ALTER TABLE fortress_webhook_delivery ALTER COLUMN last_attempt_at TYPE TIMESTAMPTZ USING last_attempt_at AT TIME ZONE 'UTC';
|
|
1143
|
+
ALTER TABLE fortress_webhook_delivery ALTER COLUMN next_retry_at TYPE TIMESTAMPTZ USING next_retry_at AT TIME ZONE 'UTC';
|
|
1144
|
+
ALTER TABLE fortress_webhook_delivery ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1145
|
+
ALTER TABLE fortress_webauthn_credential ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1146
|
+
ALTER TABLE fortress_webauthn_challenge ALTER COLUMN expires_at TYPE TIMESTAMPTZ USING expires_at AT TIME ZONE 'UTC';
|
|
1147
|
+
ALTER TABLE fortress_webauthn_challenge ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC';
|
|
1148
|
+
`.trim();
|
|
1149
|
+
|
|
1150
|
+
const PG_0005_DOWN = `
|
|
1151
|
+
ALTER TABLE fortress_schema_version ALTER COLUMN applied_at TYPE TIMESTAMP USING applied_at AT TIME ZONE 'UTC';
|
|
1152
|
+
ALTER TABLE fortress_user ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1153
|
+
ALTER TABLE fortress_user ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
|
|
1154
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN family_created_at TYPE TIMESTAMP USING family_created_at AT TIME ZONE 'UTC';
|
|
1155
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN rotated_at TYPE TIMESTAMP USING rotated_at AT TIME ZONE 'UTC';
|
|
1156
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1157
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN last_active_at TYPE TIMESTAMP USING last_active_at AT TIME ZONE 'UTC';
|
|
1158
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1159
|
+
ALTER TABLE fortress_auth_continuation ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1160
|
+
ALTER TABLE fortress_auth_continuation ALTER COLUMN consumed_at TYPE TIMESTAMP USING consumed_at AT TIME ZONE 'UTC';
|
|
1161
|
+
ALTER TABLE fortress_auth_continuation ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1162
|
+
ALTER TABLE fortress_service_account ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1163
|
+
ALTER TABLE fortress_service_account ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
|
|
1164
|
+
ALTER TABLE fortress_email_verification_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1165
|
+
ALTER TABLE fortress_email_verification_token ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
|
|
1166
|
+
ALTER TABLE fortress_email_verification_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1167
|
+
ALTER TABLE fortress_magic_link_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1168
|
+
ALTER TABLE fortress_magic_link_token ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
|
|
1169
|
+
ALTER TABLE fortress_magic_link_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1170
|
+
ALTER TABLE fortress_api_key ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1171
|
+
ALTER TABLE fortress_api_key ALTER COLUMN last_used_at TYPE TIMESTAMP USING last_used_at AT TIME ZONE 'UTC';
|
|
1172
|
+
ALTER TABLE fortress_api_key ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1173
|
+
ALTER TABLE fortress_two_factor_secret ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1174
|
+
ALTER TABLE fortress_backup_code ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1175
|
+
ALTER TABLE fortress_trusted_device ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1176
|
+
ALTER TABLE fortress_trusted_device ALTER COLUMN last_used_at TYPE TIMESTAMP USING last_used_at AT TIME ZONE 'UTC';
|
|
1177
|
+
ALTER TABLE fortress_trusted_device ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1178
|
+
ALTER TABLE fortress_social_account ALTER COLUMN token_expires_at TYPE TIMESTAMP USING token_expires_at AT TIME ZONE 'UTC';
|
|
1179
|
+
ALTER TABLE fortress_social_account ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1180
|
+
ALTER TABLE fortress_social_account ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
|
|
1181
|
+
ALTER TABLE fortress_tenant ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1182
|
+
ALTER TABLE fortress_tenant ALTER COLUMN updated_at TYPE TIMESTAMP USING updated_at AT TIME ZONE 'UTC';
|
|
1183
|
+
ALTER TABLE fortress_oauth_client ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1184
|
+
ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1185
|
+
ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
|
|
1186
|
+
ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1187
|
+
ALTER TABLE fortress_oauth_access_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1188
|
+
ALTER TABLE fortress_oauth_access_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1189
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN issued_at TYPE TIMESTAMP USING issued_at AT TIME ZONE 'UTC';
|
|
1190
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1191
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
|
|
1192
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1193
|
+
ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN used_at TYPE TIMESTAMP USING used_at AT TIME ZONE 'UTC';
|
|
1194
|
+
ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1195
|
+
ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1196
|
+
ALTER TABLE fortress_oauth_signing_key ALTER COLUMN rotated_at TYPE TIMESTAMP USING rotated_at AT TIME ZONE 'UTC';
|
|
1197
|
+
ALTER TABLE fortress_oauth_signing_key ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1198
|
+
ALTER TABLE fortress_user_scope_assignment ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1199
|
+
ALTER TABLE fortress_account_lockout ALTER COLUMN last_failed_at TYPE TIMESTAMP USING last_failed_at AT TIME ZONE 'UTC';
|
|
1200
|
+
ALTER TABLE fortress_account_lockout ALTER COLUMN locked_until TYPE TIMESTAMP USING locked_until AT TIME ZONE 'UTC';
|
|
1201
|
+
ALTER TABLE fortress_account_lockout ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1202
|
+
ALTER TABLE fortress_audit_log ALTER COLUMN timestamp TYPE TIMESTAMP USING timestamp AT TIME ZONE 'UTC';
|
|
1203
|
+
ALTER TABLE fortress_audit_log ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1204
|
+
ALTER TABLE fortress_webhook_endpoint ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1205
|
+
ALTER TABLE fortress_webhook_delivery ALTER COLUMN last_attempt_at TYPE TIMESTAMP USING last_attempt_at AT TIME ZONE 'UTC';
|
|
1206
|
+
ALTER TABLE fortress_webhook_delivery ALTER COLUMN next_retry_at TYPE TIMESTAMP USING next_retry_at AT TIME ZONE 'UTC';
|
|
1207
|
+
ALTER TABLE fortress_webhook_delivery ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1208
|
+
ALTER TABLE fortress_webauthn_credential ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1209
|
+
ALTER TABLE fortress_webauthn_challenge ALTER COLUMN expires_at TYPE TIMESTAMP USING expires_at AT TIME ZONE 'UTC';
|
|
1210
|
+
ALTER TABLE fortress_webauthn_challenge ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC';
|
|
1211
|
+
|
|
1212
|
+
DROP INDEX IF EXISTS audit_log_timestamp_idx;
|
|
1213
|
+
DROP INDEX IF EXISTS webhook_delivery_retry_idx;
|
|
1214
|
+
DROP INDEX IF EXISTS trusted_device_user_idx;
|
|
1215
|
+
DROP INDEX IF EXISTS backup_code_user_idx;
|
|
1216
|
+
DROP INDEX IF EXISTS direct_permission_binding_subject_idx;
|
|
1217
|
+
DROP INDEX IF EXISTS role_binding_subject_idx;
|
|
1218
|
+
DROP INDEX IF EXISTS magic_link_token_token_idx;
|
|
1219
|
+
DROP INDEX IF EXISTS email_verification_token_token_idx;
|
|
1220
|
+
DROP INDEX IF EXISTS refresh_token_user_idx;
|
|
1221
|
+
DROP INDEX IF EXISTS refresh_token_family_idx;
|
|
1222
|
+
`.trim();
|
|
1223
|
+
|
|
1224
|
+
// --- 0006: canonical, case-insensitive email identity ---
|
|
1225
|
+
|
|
1226
|
+
interface MigrationUserEmail {
|
|
1227
|
+
id: string;
|
|
1228
|
+
email: string;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
interface MigrationEmailIdentifier {
|
|
1232
|
+
id: string;
|
|
1233
|
+
userId: string;
|
|
1234
|
+
type: string;
|
|
1235
|
+
value: string;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Canonicalize legacy emails before the case-insensitive indexes are created.
|
|
1240
|
+
* If multiple accounts collapse to one identity, the oldest numeric ID wins;
|
|
1241
|
+
* later accounts are disabled, assigned a non-routable unique tombstone, and
|
|
1242
|
+
* have all renewable OAuth/core credentials removed. Accounts are never
|
|
1243
|
+
* silently merged. Email aliases are normalized independently; if aliases
|
|
1244
|
+
* collide, the oldest identifier row wins and later duplicates are removed.
|
|
1245
|
+
*/
|
|
1246
|
+
async function normalizeExistingUserEmails(db: DatabaseAdapter): Promise<void> {
|
|
1247
|
+
const users = await db.findMany<MigrationUserEmail>({
|
|
1248
|
+
model: 'user',
|
|
1249
|
+
sortBy: { field: 'id', direction: 'asc' },
|
|
1250
|
+
});
|
|
1251
|
+
const identifiers = await db.findMany<MigrationEmailIdentifier>({
|
|
1252
|
+
model: 'login_identifier',
|
|
1253
|
+
sortBy: { field: 'id', direction: 'asc' },
|
|
1254
|
+
});
|
|
1255
|
+
const groups = new Map<string, MigrationUserEmail[]>();
|
|
1256
|
+
const usedCanonicalEmails = new Set([
|
|
1257
|
+
...users.map(user => normalizeEmail(user.email)),
|
|
1258
|
+
...identifiers.map(identifier => normalizeEmail(identifier.value)),
|
|
1259
|
+
]);
|
|
1260
|
+
for (const user of users) {
|
|
1261
|
+
const canonical = normalizeEmail(user.email);
|
|
1262
|
+
const group = groups.get(canonical) ?? [];
|
|
1263
|
+
group.push(user);
|
|
1264
|
+
groups.set(canonical, group);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const tombstoneFor = (id: string): string => {
|
|
1268
|
+
let attempt = 0;
|
|
1269
|
+
let value = `fortress-duplicate-${id}@invalid`;
|
|
1270
|
+
while (usedCanonicalEmails.has(normalizeEmail(value))) {
|
|
1271
|
+
attempt++;
|
|
1272
|
+
value = `fortress-duplicate-${id}-${attempt}@invalid`;
|
|
1273
|
+
}
|
|
1274
|
+
usedCanonicalEmails.add(normalizeEmail(value));
|
|
1275
|
+
return value;
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const duplicateUserIds = new Set<string>();
|
|
1279
|
+
const primaryOwnerByCanonical = new Map<string, string>();
|
|
1280
|
+
for (const [canonical, matches] of groups) {
|
|
1281
|
+
const [survivor, ...duplicates] = matches;
|
|
1282
|
+
primaryOwnerByCanonical.set(canonical, survivor.id);
|
|
1283
|
+
for (const duplicate of duplicates) {
|
|
1284
|
+
duplicateUserIds.add(duplicate.id);
|
|
1285
|
+
const tombstone = tombstoneFor(duplicate.id);
|
|
1286
|
+
await db.update({
|
|
1287
|
+
model: 'refresh_token',
|
|
1288
|
+
where: [{ field: 'userId', operator: '=', value: duplicate.id }],
|
|
1289
|
+
data: { isRevoked: true },
|
|
1290
|
+
});
|
|
1291
|
+
// OAuth refresh tokens have no revoked flag; remove every OAuth bearer
|
|
1292
|
+
// artifact that could renew or continue authorization for the loser.
|
|
1293
|
+
for (const model of [
|
|
1294
|
+
'oauth_refresh_token',
|
|
1295
|
+
'oauth_access_token',
|
|
1296
|
+
'oauth_authorization_code',
|
|
1297
|
+
'oauth_pending_flow',
|
|
1298
|
+
'auth_continuation',
|
|
1299
|
+
]) {
|
|
1300
|
+
await db.delete({
|
|
1301
|
+
model,
|
|
1302
|
+
where: [{ field: 'userId', operator: '=', value: duplicate.id }],
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
await db.update({
|
|
1306
|
+
model: 'user',
|
|
1307
|
+
where: [{ field: 'id', operator: '=', value: duplicate.id }],
|
|
1308
|
+
data: { email: tombstone, isActive: false },
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (survivor.email !== canonical) {
|
|
1313
|
+
await db.update({
|
|
1314
|
+
model: 'user',
|
|
1315
|
+
where: [{ field: 'id', operator: '=', value: survivor.id }],
|
|
1316
|
+
data: { email: canonical },
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const identifierGroups = new Map<string, MigrationEmailIdentifier[]>();
|
|
1322
|
+
const occupiedNonEmailValues = new Set<string>();
|
|
1323
|
+
for (const identifier of identifiers) {
|
|
1324
|
+
if (duplicateUserIds.has(identifier.userId)) {
|
|
1325
|
+
await db.delete({
|
|
1326
|
+
model: 'login_identifier',
|
|
1327
|
+
where: [{ field: 'id', operator: '=', value: identifier.id }],
|
|
1328
|
+
});
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
if (identifier.type !== 'email') {
|
|
1332
|
+
// A primary email must not be shadowed by exact-first lookup of a
|
|
1333
|
+
// cross-type identifier with the same final canonical value.
|
|
1334
|
+
if (primaryOwnerByCanonical.has(identifier.value)) {
|
|
1335
|
+
await db.delete({
|
|
1336
|
+
model: 'login_identifier',
|
|
1337
|
+
where: [{ field: 'id', operator: '=', value: identifier.id }],
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
occupiedNonEmailValues.add(identifier.value);
|
|
1342
|
+
}
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
const canonical = normalizeEmail(identifier.value);
|
|
1346
|
+
const group = identifierGroups.get(canonical) ?? [];
|
|
1347
|
+
group.push(identifier);
|
|
1348
|
+
identifierGroups.set(canonical, group);
|
|
1349
|
+
}
|
|
1350
|
+
for (const [canonical, matches] of identifierGroups) {
|
|
1351
|
+
const primaryOwner = primaryOwnerByCanonical.get(canonical);
|
|
1352
|
+
// A user's primary email always outranks another account's alias. Without
|
|
1353
|
+
// this rule exact-first identifier lookup could route the primary address
|
|
1354
|
+
// to the alias owner after migration. Otherwise an existing case-sensitive
|
|
1355
|
+
// non-email identifier keeps its exact value and the conflicting email
|
|
1356
|
+
// alias is removed.
|
|
1357
|
+
const survivor = occupiedNonEmailValues.has(canonical)
|
|
1358
|
+
? undefined
|
|
1359
|
+
: primaryOwner
|
|
1360
|
+
? matches.find(identifier => identifier.userId === primaryOwner)
|
|
1361
|
+
: matches[0];
|
|
1362
|
+
for (const duplicate of matches) {
|
|
1363
|
+
if (duplicate.id === survivor?.id)
|
|
1364
|
+
continue;
|
|
1365
|
+
await db.delete({
|
|
1366
|
+
model: 'login_identifier',
|
|
1367
|
+
where: [{ field: 'id', operator: '=', value: duplicate.id }],
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
if (survivor && survivor.value !== canonical) {
|
|
1371
|
+
await db.update({
|
|
1372
|
+
model: 'login_identifier',
|
|
1373
|
+
where: [{ field: 'id', operator: '=', value: survivor.id }],
|
|
1374
|
+
data: { value: canonical },
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (!db.rawQuery)
|
|
1380
|
+
throw new Error('Email data migration requires rawQuery support');
|
|
1381
|
+
await db.rawQuery('CREATE TABLE fortress_email_migration_ready (id INTEGER PRIMARY KEY)');
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const SQLITE_0006_UP = `
|
|
1385
|
+
-- This sentinel is created only by the Fortress migration engine after its
|
|
1386
|
+
-- Unicode-aware data step. Applying this SQL directly fails closed.
|
|
1387
|
+
INSERT INTO fortress_email_migration_ready (id) VALUES (1);
|
|
1388
|
+
CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (email COLLATE NOCASE);
|
|
1389
|
+
CREATE UNIQUE INDEX login_identifier_email_ci_unique
|
|
1390
|
+
ON fortress_login_identifier (value COLLATE NOCASE)
|
|
1391
|
+
WHERE type = 'email';
|
|
1392
|
+
DROP TABLE fortress_email_migration_ready;
|
|
1393
|
+
`.trim();
|
|
1394
|
+
|
|
1395
|
+
const SQLITE_0006_FRESH_UP = `
|
|
1396
|
+
CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (email COLLATE NOCASE);
|
|
1397
|
+
CREATE UNIQUE INDEX login_identifier_email_ci_unique
|
|
1398
|
+
ON fortress_login_identifier (value COLLATE NOCASE)
|
|
1399
|
+
WHERE type = 'email';
|
|
1400
|
+
`.trim();
|
|
1401
|
+
|
|
1402
|
+
const SQLITE_0006_DOWN = `
|
|
1403
|
+
-- Canonicalization and duplicate-account quarantine are intentionally irreversible.
|
|
1404
|
+
DROP INDEX IF EXISTS login_identifier_email_ci_unique;
|
|
1405
|
+
DROP INDEX IF EXISTS user_email_ci_unique;
|
|
1406
|
+
`.trim();
|
|
1407
|
+
|
|
1408
|
+
const PG_0006_UP = `
|
|
1409
|
+
-- This sentinel is created only by the Fortress migration engine after its
|
|
1410
|
+
-- Unicode-aware data step. Applying this SQL directly fails closed.
|
|
1411
|
+
INSERT INTO fortress_email_migration_ready (id) VALUES (1);
|
|
1412
|
+
CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (lower(email));
|
|
1413
|
+
CREATE UNIQUE INDEX login_identifier_email_ci_unique
|
|
1414
|
+
ON fortress_login_identifier (lower(value))
|
|
1415
|
+
WHERE type = 'email';
|
|
1416
|
+
DROP TABLE fortress_email_migration_ready;
|
|
1417
|
+
`.trim();
|
|
1418
|
+
|
|
1419
|
+
const PG_0006_FRESH_UP = `
|
|
1420
|
+
CREATE UNIQUE INDEX user_email_ci_unique ON fortress_user (lower(email));
|
|
1421
|
+
CREATE UNIQUE INDEX login_identifier_email_ci_unique
|
|
1422
|
+
ON fortress_login_identifier (lower(value))
|
|
1423
|
+
WHERE type = 'email';
|
|
1424
|
+
`.trim();
|
|
1425
|
+
|
|
1426
|
+
const PG_0006_DOWN = `
|
|
1427
|
+
-- Canonicalization and duplicate-account quarantine are intentionally irreversible.
|
|
1428
|
+
DROP INDEX IF EXISTS login_identifier_email_ci_unique;
|
|
1429
|
+
DROP INDEX IF EXISTS user_email_ci_unique;
|
|
1430
|
+
`.trim();
|
|
1431
|
+
|
|
1432
|
+
// --- 0007: persistent audit-chain terminal anchor ---
|
|
1433
|
+
|
|
1434
|
+
const SQLITE_0007_UP = `
|
|
1435
|
+
CREATE TABLE fortress_audit_chain_state (
|
|
1436
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1437
|
+
last_hash TEXT,
|
|
1438
|
+
entry_count INTEGER NOT NULL CHECK (entry_count >= 0),
|
|
1439
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
1440
|
+
CHECK ((entry_count = 0 AND last_hash IS NULL) OR (entry_count > 0 AND last_hash IS NOT NULL))
|
|
1441
|
+
);
|
|
1442
|
+
INSERT INTO fortress_audit_chain_state (id, last_hash, entry_count) VALUES (1, NULL, 0);
|
|
1443
|
+
`.trim();
|
|
1444
|
+
|
|
1445
|
+
const SQLITE_0007_DOWN = 'DROP TABLE IF EXISTS fortress_audit_chain_state;';
|
|
1446
|
+
|
|
1447
|
+
const PG_0007_UP = `
|
|
1448
|
+
CREATE TABLE fortress_audit_chain_state (
|
|
1449
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1450
|
+
last_hash VARCHAR(64),
|
|
1451
|
+
entry_count INTEGER NOT NULL CHECK (entry_count >= 0),
|
|
1452
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
1453
|
+
CHECK ((entry_count = 0 AND last_hash IS NULL) OR (entry_count > 0 AND last_hash IS NOT NULL))
|
|
1454
|
+
);
|
|
1455
|
+
INSERT INTO fortress_audit_chain_state (id, last_hash, entry_count) VALUES (1, NULL, 0);
|
|
1456
|
+
`.trim();
|
|
1457
|
+
|
|
1458
|
+
const PG_0007_DOWN = 'DROP TABLE IF EXISTS fortress_audit_chain_state;';
|
|
1459
|
+
|
|
1460
|
+
// --- 0008: bounded failed-proof accounting ---
|
|
1461
|
+
|
|
1462
|
+
const SQLITE_0008_UP = `
|
|
1463
|
+
ALTER TABLE fortress_auth_continuation ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0;
|
|
1464
|
+
ALTER TABLE fortress_auth_continuation ADD COLUMN last_failed_at INTEGER;
|
|
1465
|
+
ALTER TABLE fortress_auth_continuation ADD COLUMN invalidated_at INTEGER;
|
|
1466
|
+
ALTER TABLE fortress_auth_continuation ADD COLUMN max_attempts INTEGER NOT NULL DEFAULT 5;
|
|
1467
|
+
ALTER TABLE fortress_auth_continuation ADD COLUMN cooldown_seconds INTEGER NOT NULL DEFAULT 1;
|
|
1468
|
+
CREATE INDEX auth_continuation_failure_idx ON fortress_auth_continuation (user_id, reason, last_failed_at);
|
|
1469
|
+
`.trim();
|
|
1470
|
+
const SQLITE_0008_DOWN = `
|
|
1471
|
+
CREATE TABLE fortress_auth_continuation_old AS SELECT id, user_id, token_hash, reason, expires_at, consumed_at, created_at FROM fortress_auth_continuation;
|
|
1472
|
+
DROP TABLE fortress_auth_continuation;
|
|
1473
|
+
CREATE TABLE fortress_auth_continuation (
|
|
1474
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES fortress_user(id) ON DELETE CASCADE,
|
|
1475
|
+
token_hash TEXT NOT NULL UNIQUE, reason TEXT NOT NULL, expires_at INTEGER NOT NULL, consumed_at INTEGER,
|
|
1476
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1477
|
+
);
|
|
1478
|
+
INSERT INTO fortress_auth_continuation SELECT * FROM fortress_auth_continuation_old;
|
|
1479
|
+
DROP TABLE fortress_auth_continuation_old;
|
|
1480
|
+
`.trim();
|
|
1481
|
+
const PG_0008_UP = `
|
|
1482
|
+
ALTER TABLE fortress_auth_continuation
|
|
1483
|
+
ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
1484
|
+
ADD COLUMN last_failed_at TIMESTAMPTZ,
|
|
1485
|
+
ADD COLUMN invalidated_at TIMESTAMPTZ,
|
|
1486
|
+
ADD COLUMN max_attempts INTEGER NOT NULL DEFAULT 5,
|
|
1487
|
+
ADD COLUMN cooldown_seconds INTEGER NOT NULL DEFAULT 1;
|
|
1488
|
+
CREATE INDEX auth_continuation_failure_idx ON fortress_auth_continuation (user_id, reason, last_failed_at);
|
|
1489
|
+
`.trim();
|
|
1490
|
+
const PG_0008_DOWN = `
|
|
1491
|
+
DROP INDEX IF EXISTS auth_continuation_failure_idx;
|
|
1492
|
+
ALTER TABLE fortress_auth_continuation DROP COLUMN cooldown_seconds, DROP COLUMN max_attempts, DROP COLUMN invalidated_at, DROP COLUMN last_failed_at, DROP COLUMN failed_attempts;
|
|
1493
|
+
`.trim();
|
|
1494
|
+
|
|
1495
|
+
// --- 0009: remove legacy plaintext TOTP enrolments ---
|
|
1496
|
+
// SQL cannot encrypt existing seeds without the application-held key. Delete
|
|
1497
|
+
// the enrolment atomically so upgraded databases cannot retain usable plaintext
|
|
1498
|
+
// credentials. Users re-enrol and all newly stored seeds are AES-GCM envelopes.
|
|
1499
|
+
const SQLITE_0009_UP = `
|
|
1500
|
+
DELETE FROM fortress_backup_code;
|
|
1501
|
+
DELETE FROM fortress_trusted_device;
|
|
1502
|
+
DELETE FROM fortress_two_factor_secret;
|
|
1503
|
+
`.trim();
|
|
1504
|
+
const SQLITE_0009_DOWN = 'SELECT 1;';
|
|
1505
|
+
const PG_0009_UP = SQLITE_0009_UP;
|
|
1506
|
+
const PG_0009_DOWN = SQLITE_0009_DOWN;
|
|
1507
|
+
|
|
1508
|
+
// --- 0010: widen unbounded PostgreSQL append-only identifiers ---
|
|
1509
|
+
const SQLITE_0010_UP = 'SELECT 1;';
|
|
1510
|
+
const SQLITE_0010_DOWN = 'SELECT 1;';
|
|
1511
|
+
const PG_0010_UP = `
|
|
1512
|
+
ALTER SEQUENCE fortress_refresh_token_id_seq AS BIGINT;
|
|
1513
|
+
ALTER TABLE fortress_refresh_token ALTER COLUMN id TYPE BIGINT;
|
|
1514
|
+
ALTER SEQUENCE fortress_auth_continuation_id_seq AS BIGINT;
|
|
1515
|
+
ALTER TABLE fortress_auth_continuation ALTER COLUMN id TYPE BIGINT;
|
|
1516
|
+
ALTER SEQUENCE fortress_email_verification_token_id_seq AS BIGINT;
|
|
1517
|
+
ALTER TABLE fortress_email_verification_token ALTER COLUMN id TYPE BIGINT;
|
|
1518
|
+
ALTER SEQUENCE fortress_magic_link_token_id_seq AS BIGINT;
|
|
1519
|
+
ALTER TABLE fortress_magic_link_token ALTER COLUMN id TYPE BIGINT;
|
|
1520
|
+
ALTER SEQUENCE fortress_oauth_authorization_code_id_seq AS BIGINT;
|
|
1521
|
+
ALTER TABLE fortress_oauth_authorization_code ALTER COLUMN id TYPE BIGINT;
|
|
1522
|
+
ALTER SEQUENCE fortress_oauth_access_token_id_seq AS BIGINT;
|
|
1523
|
+
ALTER TABLE fortress_oauth_access_token ALTER COLUMN id TYPE BIGINT;
|
|
1524
|
+
ALTER SEQUENCE fortress_oauth_refresh_token_id_seq AS BIGINT;
|
|
1525
|
+
ALTER TABLE fortress_oauth_refresh_token ALTER COLUMN id TYPE BIGINT;
|
|
1526
|
+
ALTER SEQUENCE fortress_oauth_pending_flow_id_seq AS BIGINT;
|
|
1527
|
+
ALTER TABLE fortress_oauth_pending_flow ALTER COLUMN id TYPE BIGINT;
|
|
1528
|
+
ALTER SEQUENCE fortress_audit_log_id_seq AS BIGINT;
|
|
1529
|
+
ALTER TABLE fortress_audit_log ALTER COLUMN id TYPE BIGINT;
|
|
1530
|
+
ALTER SEQUENCE fortress_webhook_delivery_id_seq AS BIGINT;
|
|
1531
|
+
ALTER TABLE fortress_webhook_delivery ALTER COLUMN id TYPE BIGINT;
|
|
1532
|
+
ALTER SEQUENCE fortress_webauthn_challenge_id_seq AS BIGINT;
|
|
1533
|
+
ALTER TABLE fortress_webauthn_challenge ALTER COLUMN id TYPE BIGINT;
|
|
1534
|
+
`.trim();
|
|
1535
|
+
const PG_0010_DOWN = 'SELECT 1;'; // Narrowing can lose data and is intentionally irreversible.
|
|
1536
|
+
|
|
1537
|
+
export const fortressMigrations: Record<MigrationDialect, FortressMigration[]> = {
|
|
1538
|
+
sqlite: [
|
|
1539
|
+
{ version: 1, name: 'schema_version', dialect: 'sqlite', up: SQLITE_0001_UP, down: SQLITE_0001_DOWN },
|
|
1540
|
+
{ version: 2, name: 'initial_schema', dialect: 'sqlite', up: SQLITE_0002_UP, down: SQLITE_0002_DOWN },
|
|
1541
|
+
{ version: 3, name: 'auth_continuation', dialect: 'sqlite', up: SQLITE_0003_UP, down: SQLITE_0003_DOWN },
|
|
1542
|
+
{ version: 4, name: 'tenant_default_unique', dialect: 'sqlite', up: SQLITE_0004_UP, down: SQLITE_0004_DOWN },
|
|
1543
|
+
{ version: 5, name: 'hot_indexes_timestamptz', dialect: 'sqlite', up: SQLITE_0005_UP, down: SQLITE_0005_DOWN },
|
|
1544
|
+
{ version: 6, name: 'canonical_email', dialect: 'sqlite', up: SQLITE_0006_UP, down: SQLITE_0006_DOWN, freshUp: SQLITE_0006_FRESH_UP, dataStep: 'normalize-email-v2', beforeUp: normalizeExistingUserEmails },
|
|
1545
|
+
{ version: 7, name: 'audit_chain_anchor', dialect: 'sqlite', up: SQLITE_0007_UP, down: SQLITE_0007_DOWN },
|
|
1546
|
+
{ version: 8, name: 'two_factor_hardening', dialect: 'sqlite', up: SQLITE_0008_UP, down: SQLITE_0008_DOWN },
|
|
1547
|
+
{ version: 9, name: 'encrypt_totp_secrets', dialect: 'sqlite', up: SQLITE_0009_UP, down: SQLITE_0009_DOWN },
|
|
1548
|
+
{ version: 10, name: 'bigint_append_only_ids', dialect: 'sqlite', up: SQLITE_0010_UP, down: SQLITE_0010_DOWN },
|
|
1549
|
+
],
|
|
1550
|
+
pg: [
|
|
1551
|
+
{ version: 1, name: 'schema_version', dialect: 'pg', up: PG_0001_UP, down: PG_0001_DOWN },
|
|
1552
|
+
{ version: 2, name: 'initial_schema', dialect: 'pg', up: PG_0002_UP, down: PG_0002_DOWN },
|
|
1553
|
+
{ version: 3, name: 'auth_continuation', dialect: 'pg', up: PG_0003_UP, down: PG_0003_DOWN },
|
|
1554
|
+
{ version: 4, name: 'tenant_default_unique', dialect: 'pg', up: PG_0004_UP, down: PG_0004_DOWN },
|
|
1555
|
+
{ version: 5, name: 'hot_indexes_timestamptz', dialect: 'pg', up: PG_0005_UP, down: PG_0005_DOWN },
|
|
1556
|
+
{ version: 6, name: 'canonical_email', dialect: 'pg', up: PG_0006_UP, down: PG_0006_DOWN, freshUp: PG_0006_FRESH_UP, dataStep: 'normalize-email-v2', beforeUp: normalizeExistingUserEmails },
|
|
1557
|
+
{ version: 7, name: 'audit_chain_anchor', dialect: 'pg', up: PG_0007_UP, down: PG_0007_DOWN },
|
|
1558
|
+
{ version: 8, name: 'two_factor_hardening', dialect: 'pg', up: PG_0008_UP, down: PG_0008_DOWN },
|
|
1559
|
+
{ version: 9, name: 'encrypt_totp_secrets', dialect: 'pg', up: PG_0009_UP, down: PG_0009_DOWN },
|
|
1560
|
+
{ version: 10, name: 'bigint_append_only_ids', dialect: 'pg', up: PG_0010_UP, down: PG_0010_DOWN },
|
|
1561
|
+
],
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
export function getFortressMigrations(dialect: MigrationDialect): FortressMigration[] {
|
|
1565
|
+
return [...fortressMigrations[dialect]].sort((a, b) => a.version - b.version);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
export function getLatestMigrationVersion(dialect: MigrationDialect): number {
|
|
1569
|
+
return getFortressMigrations(dialect).at(-1)?.version ?? 0;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Concatenated forward SQL for every bundled migration of a dialect, in
|
|
1574
|
+
* version order. `src/testing/index.ts` provisions its in-memory schema
|
|
1575
|
+
* from this so the test adapter and production migrations can never drift.
|
|
1576
|
+
*/
|
|
1577
|
+
export function getMigrationUpSql(dialect: MigrationDialect): string {
|
|
1578
|
+
return getFortressMigrations(dialect)
|
|
1579
|
+
.map(migration => migration.freshUp ?? migration.up)
|
|
1580
|
+
.join('\n\n');
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Canonical list of every Fortress-owned table. Used by
|
|
1585
|
+
* {@link detectMigrationDrift} to surface live-DB drift beyond just the
|
|
1586
|
+
* `fortress_schema_version` checkpoint — any of these tables missing in
|
|
1587
|
+
* the target database signals an incomplete or stale migration state.
|
|
1588
|
+
*
|
|
1589
|
+
* Keep in sync with `src/drizzle/{schema,pg/schema}.ts` and the bundled
|
|
1590
|
+
* migrations. The list is asserted by `src/core/migrations/engine.test.ts`
|
|
1591
|
+
* against the test adapter so an accidental drop/add is caught at test time.
|
|
1592
|
+
*/
|
|
1593
|
+
export const FORTRESS_TABLES: readonly string[] = [
|
|
1594
|
+
'fortress_schema_version',
|
|
1595
|
+
'fortress_user',
|
|
1596
|
+
'fortress_login_identifier',
|
|
1597
|
+
'fortress_refresh_token',
|
|
1598
|
+
'fortress_auth_continuation',
|
|
1599
|
+
'fortress_group',
|
|
1600
|
+
'fortress_group_user',
|
|
1601
|
+
'fortress_service_account',
|
|
1602
|
+
'fortress_resource',
|
|
1603
|
+
'fortress_permission',
|
|
1604
|
+
'fortress_role',
|
|
1605
|
+
'fortress_role_permission',
|
|
1606
|
+
'fortress_role_binding',
|
|
1607
|
+
'fortress_direct_permission_binding',
|
|
1608
|
+
'fortress_email_verification_token',
|
|
1609
|
+
'fortress_magic_link_token',
|
|
1610
|
+
'fortress_api_key',
|
|
1611
|
+
'fortress_two_factor_secret',
|
|
1612
|
+
'fortress_backup_code',
|
|
1613
|
+
'fortress_trusted_device',
|
|
1614
|
+
'fortress_social_account',
|
|
1615
|
+
'fortress_tenant',
|
|
1616
|
+
'fortress_tenant_user',
|
|
1617
|
+
'fortress_oauth_client',
|
|
1618
|
+
'fortress_oauth_authorization_code',
|
|
1619
|
+
'fortress_oauth_access_token',
|
|
1620
|
+
'fortress_oauth_refresh_token',
|
|
1621
|
+
'fortress_oauth_pending_flow',
|
|
1622
|
+
'fortress_oauth_signing_key',
|
|
1623
|
+
'fortress_user_scope_assignment',
|
|
1624
|
+
'fortress_account_lockout',
|
|
1625
|
+
'fortress_audit_log',
|
|
1626
|
+
'fortress_audit_chain_state',
|
|
1627
|
+
'fortress_webhook_endpoint',
|
|
1628
|
+
'fortress_webhook_delivery',
|
|
1629
|
+
'fortress_webauthn_credential',
|
|
1630
|
+
'fortress_webauthn_challenge',
|
|
1631
|
+
] as const;
|
|
1632
|
+
|
|
1633
|
+
/** Hot-path indexes whose presence is part of the runtime schema contract. */
|
|
1634
|
+
export const FORTRESS_INDEXES: ReadonlyArray<{
|
|
1635
|
+
name: string;
|
|
1636
|
+
table: string;
|
|
1637
|
+
columns: readonly string[];
|
|
1638
|
+
}> = [
|
|
1639
|
+
{ name: 'refresh_token_family_idx', table: 'fortress_refresh_token', columns: ['token_family'] },
|
|
1640
|
+
{ name: 'refresh_token_user_idx', table: 'fortress_refresh_token', columns: ['user_id'] },
|
|
1641
|
+
{ name: 'email_verification_token_token_idx', table: 'fortress_email_verification_token', columns: ['token'] },
|
|
1642
|
+
{ name: 'magic_link_token_token_idx', table: 'fortress_magic_link_token', columns: ['token'] },
|
|
1643
|
+
{ name: 'role_binding_subject_idx', table: 'fortress_role_binding', columns: ['subject_type', 'subject_id'] },
|
|
1644
|
+
{ name: 'direct_permission_binding_subject_idx', table: 'fortress_direct_permission_binding', columns: ['subject_type', 'subject_id'] },
|
|
1645
|
+
{ name: 'backup_code_user_idx', table: 'fortress_backup_code', columns: ['user_id'] },
|
|
1646
|
+
{ name: 'trusted_device_user_idx', table: 'fortress_trusted_device', columns: ['user_id'] },
|
|
1647
|
+
{ name: 'webhook_delivery_retry_idx', table: 'fortress_webhook_delivery', columns: ['status', 'next_retry_at'] },
|
|
1648
|
+
{ name: 'audit_log_timestamp_idx', table: 'fortress_audit_log', columns: ['timestamp'] },
|
|
1649
|
+
] as const;
|
|
1650
|
+
|
|
1651
|
+
const CONSTRAINT_KEYWORDS = new Set(['primary', 'unique', 'foreign', 'constraint', 'check']);
|
|
1652
|
+
|
|
1653
|
+
const CREATE_TABLE_RE = /create\s+table\s+(?:if\s+not\s+exists\s+)?(\w+)\s*\(/gi;
|
|
1654
|
+
const ADD_COLUMN_RE = /alter\s+table\s+(\w+)\s+add\s+column\s+(\w+)/gi;
|
|
1655
|
+
const RENAME_TABLE_RE = /alter\s+table\s+(\w+)\s+rename\s+to\s+(\w+)/gi;
|
|
1656
|
+
const WHITESPACE_RE = /\s+/;
|
|
1657
|
+
const QUOTE_CHARS_RE = /["'`]/g;
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* Parse the expected column set of every Fortress table directly out of the
|
|
1661
|
+
* bundled migration DDL. Because the migrations are the SQL-first source of
|
|
1662
|
+
* truth, this needs no schema description maintained on the side and no
|
|
1663
|
+
* Drizzle-specific tooling — it works for any adapter. Powers the
|
|
1664
|
+
* column-level half of {@link detectMigrationDrift}.
|
|
1665
|
+
*
|
|
1666
|
+
* The parser is intentionally narrow: it understands the controlled DDL
|
|
1667
|
+
* Fortress emits (`CREATE TABLE [IF NOT EXISTS] name ( ... );` with
|
|
1668
|
+
* comma-separated definitions, plus the controlled `ALTER TABLE` add-column
|
|
1669
|
+
* and rename forms used by incremental migrations). Constraint-only lines
|
|
1670
|
+
* (PRIMARY KEY (...), UNIQUE (...), etc.) and
|
|
1671
|
+
* standalone CREATE INDEX statements are skipped.
|
|
1672
|
+
*/
|
|
1673
|
+
export function getExpectedColumns(dialect: MigrationDialect): Record<string, string[]> {
|
|
1674
|
+
const sql = getMigrationUpSql(dialect)
|
|
1675
|
+
// Strip line comments so `-- ...` never leaks into a column name.
|
|
1676
|
+
.split('\n')
|
|
1677
|
+
.filter(line => !line.trimStart().startsWith('--'))
|
|
1678
|
+
.join('\n');
|
|
1679
|
+
|
|
1680
|
+
const result: Record<string, string[]> = {};
|
|
1681
|
+
// CREATE_TABLE_RE carries the global flag (stateful lastIndex); reset it so
|
|
1682
|
+
// repeated calls always scan from the start.
|
|
1683
|
+
CREATE_TABLE_RE.lastIndex = 0;
|
|
1684
|
+
|
|
1685
|
+
let match = CREATE_TABLE_RE.exec(sql);
|
|
1686
|
+
while (match !== null) {
|
|
1687
|
+
const tableName = match[1];
|
|
1688
|
+
const bodyStart = match.index + match[0].length;
|
|
1689
|
+
|
|
1690
|
+
// Walk forward tracking paren depth to find the matching close paren.
|
|
1691
|
+
let depth = 1;
|
|
1692
|
+
let i = bodyStart;
|
|
1693
|
+
for (; i < sql.length && depth > 0; i++) {
|
|
1694
|
+
if (sql[i] === '(')
|
|
1695
|
+
depth++;
|
|
1696
|
+
else if (sql[i] === ')')
|
|
1697
|
+
depth--;
|
|
1698
|
+
}
|
|
1699
|
+
const body = sql.slice(bodyStart, i - 1);
|
|
1700
|
+
|
|
1701
|
+
result[tableName] = extractColumnNames(body);
|
|
1702
|
+
CREATE_TABLE_RE.lastIndex = i;
|
|
1703
|
+
match = CREATE_TABLE_RE.exec(sql);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Later migrations add columns to tables created by an earlier version.
|
|
1707
|
+
// Include those additions in drift expectations without rewriting the
|
|
1708
|
+
// immutable baseline migration.
|
|
1709
|
+
ADD_COLUMN_RE.lastIndex = 0;
|
|
1710
|
+
let addMatch = ADD_COLUMN_RE.exec(sql);
|
|
1711
|
+
while (addMatch !== null) {
|
|
1712
|
+
const [, tableName, columnName] = addMatch;
|
|
1713
|
+
const columns = result[tableName] ?? (result[tableName] = []);
|
|
1714
|
+
const normalized = columnName.toLowerCase();
|
|
1715
|
+
if (!columns.includes(normalized))
|
|
1716
|
+
columns.push(normalized);
|
|
1717
|
+
addMatch = ADD_COLUMN_RE.exec(sql);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// SQLite table rebuilds create a versioned replacement and rename it over
|
|
1721
|
+
// the prior table. Reflect that final name/shape in drift expectations.
|
|
1722
|
+
RENAME_TABLE_RE.lastIndex = 0;
|
|
1723
|
+
let renameMatch = RENAME_TABLE_RE.exec(sql);
|
|
1724
|
+
while (renameMatch !== null) {
|
|
1725
|
+
const [, fromName, toName] = renameMatch;
|
|
1726
|
+
if (result[fromName]) {
|
|
1727
|
+
result[toName] = result[fromName];
|
|
1728
|
+
delete result[fromName];
|
|
1729
|
+
}
|
|
1730
|
+
renameMatch = RENAME_TABLE_RE.exec(sql);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
return result;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function extractColumnNames(body: string): string[] {
|
|
1737
|
+
const columns: string[] = [];
|
|
1738
|
+
let depth = 0;
|
|
1739
|
+
let current = '';
|
|
1740
|
+
|
|
1741
|
+
const flush = (): void => {
|
|
1742
|
+
const trimmed = current.trim();
|
|
1743
|
+
current = '';
|
|
1744
|
+
if (!trimmed)
|
|
1745
|
+
return;
|
|
1746
|
+
const firstWord = trimmed.split(WHITESPACE_RE)[0].replace(QUOTE_CHARS_RE, '').toLowerCase();
|
|
1747
|
+
if (!firstWord || CONSTRAINT_KEYWORDS.has(firstWord))
|
|
1748
|
+
return;
|
|
1749
|
+
columns.push(firstWord);
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
for (const char of body) {
|
|
1753
|
+
if (char === '(') {
|
|
1754
|
+
depth++;
|
|
1755
|
+
current += char;
|
|
1756
|
+
}
|
|
1757
|
+
else if (char === ')') {
|
|
1758
|
+
depth--;
|
|
1759
|
+
current += char;
|
|
1760
|
+
}
|
|
1761
|
+
else if (char === ',' && depth === 0) {
|
|
1762
|
+
flush();
|
|
1763
|
+
}
|
|
1764
|
+
else {
|
|
1765
|
+
current += char;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
flush();
|
|
1769
|
+
|
|
1770
|
+
return columns;
|
|
1771
|
+
}
|