@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,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL migration upgrade fixture (testcontainers).
|
|
3
|
+
*
|
|
4
|
+
* Proves the bundled PG migrations install the entire Fortress schema
|
|
5
|
+
* against a real, bare PostgreSQL server — catching dialect-specific
|
|
6
|
+
* problems (SERIAL, partial unique indexes, JSONB defaults, FK drop
|
|
7
|
+
* ordering) that the in-memory SQLite fixture can't. Mirrors the SQLite
|
|
8
|
+
* `upgrade-fixture.test.ts` but on a real engine.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Sql } from 'postgres';
|
|
12
|
+
import type { StartedTestContainer } from 'testcontainers';
|
|
13
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
14
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
15
|
+
import postgres from 'postgres';
|
|
16
|
+
import { GenericContainer, Wait } from 'testcontainers';
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
18
|
+
import { createDrizzleAdapter } from '../../drizzle/adapter';
|
|
19
|
+
import {
|
|
20
|
+
computeMigrationChecksum,
|
|
21
|
+
detectMigrationDrift,
|
|
22
|
+
getMigrationStatus,
|
|
23
|
+
hasMigrationDrift,
|
|
24
|
+
migrateDown,
|
|
25
|
+
migrateUp,
|
|
26
|
+
} from './engine';
|
|
27
|
+
import { FORTRESS_INDEXES, FORTRESS_TABLES, getFortressMigrations } from './migrations';
|
|
28
|
+
|
|
29
|
+
const SHA256_HEX_RE = /^[a-f0-9]{64}$/;
|
|
30
|
+
const INTEGRITY_FAILURE_RE = /integrity check failed/;
|
|
31
|
+
|
|
32
|
+
let container: StartedTestContainer;
|
|
33
|
+
let pgClient: Sql;
|
|
34
|
+
let connectionString: string;
|
|
35
|
+
|
|
36
|
+
function adapterFor(client: Sql): DatabaseAdapter {
|
|
37
|
+
return createDrizzleAdapter(drizzle(client) as any, { dialect: 'pg' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function adapter(): DatabaseAdapter {
|
|
41
|
+
return adapterFor(pgClient);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
container = await new GenericContainer('postgres:16-alpine')
|
|
46
|
+
.withEnvironment({
|
|
47
|
+
POSTGRES_USER: 'test',
|
|
48
|
+
POSTGRES_PASSWORD: 'test',
|
|
49
|
+
POSTGRES_DB: 'fortress_migrate_test',
|
|
50
|
+
})
|
|
51
|
+
.withExposedPorts(5432)
|
|
52
|
+
.withWaitStrategy(Wait.forListeningPorts())
|
|
53
|
+
.start();
|
|
54
|
+
|
|
55
|
+
connectionString = `postgres://test:test@${container.getHost()}:${container.getMappedPort(5432)}/fortress_migrate_test`;
|
|
56
|
+
pgClient = postgres(connectionString);
|
|
57
|
+
}, 60_000);
|
|
58
|
+
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
if (pgClient)
|
|
61
|
+
await pgClient.end();
|
|
62
|
+
if (container)
|
|
63
|
+
await container.stop();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('pg: migration upgrade fixture (bare postgres)', () => {
|
|
67
|
+
it('provisions a bare PG database end-to-end from the bundled migrations', async () => {
|
|
68
|
+
const db = adapter();
|
|
69
|
+
|
|
70
|
+
const initial = await getMigrationStatus(db, 'pg');
|
|
71
|
+
expect(initial.hasVersionTable).toBe(false);
|
|
72
|
+
expect(initial.currentVersion).toBe(0);
|
|
73
|
+
|
|
74
|
+
const initialDrift = await detectMigrationDrift(db, 'pg');
|
|
75
|
+
expect(hasMigrationDrift(initialDrift)).toBe(true);
|
|
76
|
+
expect(initialDrift.missingTables.length).toBe(FORTRESS_TABLES.length);
|
|
77
|
+
|
|
78
|
+
const baseline = await migrateUp(db, 'pg', 2);
|
|
79
|
+
expect(baseline.fromVersion).toBe(0);
|
|
80
|
+
expect(baseline.toVersion).toBe(2);
|
|
81
|
+
expect(baseline.applied.map(migration => migration.name)).toEqual(['schema_version', 'initial_schema']);
|
|
82
|
+
|
|
83
|
+
await db.rawQuery!(
|
|
84
|
+
`INSERT INTO fortress_user (email, name, password_hash, is_active, created_at, updated_at)
|
|
85
|
+
VALUES (?, ?, ?, true, ?, ?)`,
|
|
86
|
+
['legacy-pg@test.com', 'Legacy PG', 'h', '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z'],
|
|
87
|
+
);
|
|
88
|
+
const [legacyUser] = await db.rawQuery!<{ id: string }>(
|
|
89
|
+
'SELECT id FROM fortress_user WHERE email = ?',
|
|
90
|
+
['legacy-pg@test.com'],
|
|
91
|
+
);
|
|
92
|
+
await db.rawQuery!(
|
|
93
|
+
`INSERT INTO fortress_refresh_token
|
|
94
|
+
(user_id, token_hash, token_family, is_revoked, expires_at, created_at)
|
|
95
|
+
VALUES (?, ?, ?, false, ?, ?), (?, ?, ?, true, ?, ?)`,
|
|
96
|
+
[
|
|
97
|
+
legacyUser.id,
|
|
98
|
+
'legacy-pg-hash-1',
|
|
99
|
+
'legacy-pg-family',
|
|
100
|
+
'2030-01-01T00:00:00Z',
|
|
101
|
+
'2025-01-01T00:00:00Z',
|
|
102
|
+
legacyUser.id,
|
|
103
|
+
'legacy-pg-hash-2',
|
|
104
|
+
'legacy-pg-family',
|
|
105
|
+
'2030-01-01T00:00:00Z',
|
|
106
|
+
'2025-02-01T00:00:00Z',
|
|
107
|
+
],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Simulate a real pre-v4 installation: v2 did not originally include the
|
|
111
|
+
// partial index and could contain multiple defaults for one user.
|
|
112
|
+
await db.rawQuery!('DROP INDEX IF EXISTS fortress_tenant_user_one_default_idx');
|
|
113
|
+
await db.rawQuery!(
|
|
114
|
+
`INSERT INTO fortress_tenant (name, tax_id) VALUES (?, ?), (?, ?)`,
|
|
115
|
+
['Tenant A', 'tenant-a', 'Tenant B', 'tenant-b'],
|
|
116
|
+
);
|
|
117
|
+
const tenantRows = await db.rawQuery!<{ id: string }>(
|
|
118
|
+
`SELECT id FROM fortress_tenant WHERE tax_id IN (?, ?) ORDER BY tax_id`,
|
|
119
|
+
['tenant-a', 'tenant-b'],
|
|
120
|
+
);
|
|
121
|
+
await db.rawQuery!(
|
|
122
|
+
`INSERT INTO fortress_tenant_user (tenant_id, user_id, is_default) VALUES (?, ?, true), (?, ?, true)`,
|
|
123
|
+
[tenantRows[0].id, legacyUser.id, tenantRows[1].id, legacyUser.id],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Legacy case-variants are deterministically quarantined before the
|
|
127
|
+
// case-insensitive unique indexes are installed.
|
|
128
|
+
await db.rawQuery!(
|
|
129
|
+
`INSERT INTO fortress_user (email, name, is_active) VALUES (?, ?, true), (?, ?, true)`,
|
|
130
|
+
['Duplicate@Example.COM', 'Oldest duplicate', 'duplicate@example.com', 'Later duplicate'],
|
|
131
|
+
);
|
|
132
|
+
const duplicateUsers = await db.rawQuery!<{ id: string; email: string }>(
|
|
133
|
+
`SELECT id, email FROM fortress_user WHERE lower(email) = ? ORDER BY id`,
|
|
134
|
+
['duplicate@example.com'],
|
|
135
|
+
);
|
|
136
|
+
await db.rawQuery!(
|
|
137
|
+
`INSERT INTO fortress_login_identifier (user_id, type, value) VALUES (?, 'email', ?), (?, 'email', ?)`,
|
|
138
|
+
[duplicateUsers[0].id, duplicateUsers[0].email, duplicateUsers[1].id, duplicateUsers[1].email],
|
|
139
|
+
);
|
|
140
|
+
await db.rawQuery!(
|
|
141
|
+
`INSERT INTO fortress_refresh_token (user_id, token_hash, token_family, is_revoked, expires_at)
|
|
142
|
+
VALUES (?, ?, ?, false, ?)`,
|
|
143
|
+
[duplicateUsers[1].id, 'duplicate-session', 'duplicate-family', '2030-01-01T00:00:00Z'],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Conversion must not depend on the session timezone: historical
|
|
147
|
+
// timestamp-without-time-zone values are interpreted as UTC explicitly.
|
|
148
|
+
await db.rawQuery!('SET TIME ZONE \'America/Los_Angeles\'');
|
|
149
|
+
const up = await migrateUp(db, 'pg');
|
|
150
|
+
await db.rawQuery!('SET TIME ZONE \'UTC\'');
|
|
151
|
+
expect(up.fromVersion).toBe(2);
|
|
152
|
+
expect(up.toVersion).toBe(10);
|
|
153
|
+
expect(up.applied.map(migration => migration.name)).toEqual(['auth_continuation', 'tenant_default_unique', 'hot_indexes_timestamptz', 'canonical_email', 'audit_chain_anchor', 'two_factor_hardening', 'encrypt_totp_secrets', 'bigint_append_only_ids']);
|
|
154
|
+
const defaults = await db.rawQuery!<{ count: string }>(
|
|
155
|
+
`SELECT COUNT(*) AS count FROM fortress_tenant_user WHERE user_id = ? AND is_default = true`,
|
|
156
|
+
[legacyUser.id],
|
|
157
|
+
);
|
|
158
|
+
expect(Number(defaults[0].count)).toBe(1);
|
|
159
|
+
const widened = await db.rawQuery!<{ tableName: string; dataType: string }>(
|
|
160
|
+
`SELECT table_name AS "tableName", data_type AS "dataType"
|
|
161
|
+
FROM information_schema.columns
|
|
162
|
+
WHERE table_schema = current_schema()
|
|
163
|
+
AND column_name = 'id'
|
|
164
|
+
AND table_name IN ('fortress_refresh_token', 'fortress_audit_log', 'fortress_webhook_delivery')
|
|
165
|
+
ORDER BY table_name`,
|
|
166
|
+
);
|
|
167
|
+
expect(widened).toHaveLength(3);
|
|
168
|
+
expect(widened.every(column => column.dataType === 'bigint')).toBe(true);
|
|
169
|
+
|
|
170
|
+
const canonicalized = await db.rawQuery!<{ id: string; email: string; isActive: boolean }>(
|
|
171
|
+
`SELECT id, email, is_active AS "isActive" FROM fortress_user
|
|
172
|
+
WHERE id IN (?, ?) ORDER BY id`,
|
|
173
|
+
[duplicateUsers[0].id, duplicateUsers[1].id],
|
|
174
|
+
);
|
|
175
|
+
expect(canonicalized[0]).toMatchObject({
|
|
176
|
+
id: duplicateUsers[0].id,
|
|
177
|
+
email: 'duplicate@example.com',
|
|
178
|
+
isActive: true,
|
|
179
|
+
});
|
|
180
|
+
expect(canonicalized[1].email).toBe(`fortress-duplicate-${duplicateUsers[1].id}@invalid`);
|
|
181
|
+
expect(canonicalized[1].isActive).toBe(false);
|
|
182
|
+
const [duplicateSession] = await db.rawQuery!<{ isRevoked: boolean }>(
|
|
183
|
+
'SELECT is_revoked AS "isRevoked" FROM fortress_refresh_token WHERE token_hash = ?',
|
|
184
|
+
['duplicate-session'],
|
|
185
|
+
);
|
|
186
|
+
expect(duplicateSession.isRevoked).toBe(true);
|
|
187
|
+
await expect(db.create({
|
|
188
|
+
model: 'user',
|
|
189
|
+
data: { email: 'DUPLICATE@EXAMPLE.COM', name: 'Blocked duplicate', isActive: true },
|
|
190
|
+
})).rejects.toMatchObject({ code: 'CONFLICT', statusCode: 409 });
|
|
191
|
+
const emailIndexes = await db.rawQuery!<{ indexname: string }>(
|
|
192
|
+
`SELECT indexname FROM pg_indexes WHERE schemaname = current_schema()
|
|
193
|
+
AND indexname IN ('user_email_ci_unique', 'login_identifier_email_ci_unique') ORDER BY indexname`,
|
|
194
|
+
);
|
|
195
|
+
expect(emailIndexes.map(row => row.indexname)).toEqual([
|
|
196
|
+
'login_identifier_email_ci_unique',
|
|
197
|
+
'user_email_ci_unique',
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
const tenantIndexes = await db.rawQuery!<{ indexname: string }>(
|
|
201
|
+
`SELECT indexname FROM pg_indexes WHERE indexname = ?`,
|
|
202
|
+
['fortress_tenant_user_one_default_idx'],
|
|
203
|
+
);
|
|
204
|
+
expect(tenantIndexes).toHaveLength(1);
|
|
205
|
+
|
|
206
|
+
const familyRows = await db.rawQuery!<{ tokenHash: string; familyCreatedAt: string }>(
|
|
207
|
+
`SELECT token_hash AS "tokenHash", family_created_at AS "familyCreatedAt"
|
|
208
|
+
FROM fortress_refresh_token WHERE token_family = ? ORDER BY created_at`,
|
|
209
|
+
['legacy-pg-family'],
|
|
210
|
+
);
|
|
211
|
+
expect(familyRows.map(row => row.tokenHash)).toEqual([
|
|
212
|
+
'legacy-pg-hash-1',
|
|
213
|
+
'legacy-pg-hash-2',
|
|
214
|
+
]);
|
|
215
|
+
expect(familyRows[0].familyCreatedAt).toBe(familyRows[1].familyCreatedAt);
|
|
216
|
+
expect(familyRows[0].familyCreatedAt).toContain('2025-01-01');
|
|
217
|
+
|
|
218
|
+
await db.rawQuery!(
|
|
219
|
+
`INSERT INTO fortress_refresh_token
|
|
220
|
+
(user_id, token_hash, token_family, is_revoked, expires_at)
|
|
221
|
+
VALUES (?, ?, ?, false, ?)`,
|
|
222
|
+
[legacyUser.id, 'default-pg-hash', 'default-pg-family', '2030-01-01T00:00:00Z'],
|
|
223
|
+
);
|
|
224
|
+
const [defaulted] = await db.rawQuery!<{ recent: boolean }>(
|
|
225
|
+
`SELECT family_created_at >= now() - interval '5 seconds' AS recent
|
|
226
|
+
FROM fortress_refresh_token WHERE token_hash = ?`,
|
|
227
|
+
['default-pg-hash'],
|
|
228
|
+
);
|
|
229
|
+
expect(defaulted.recent).toBe(true);
|
|
230
|
+
|
|
231
|
+
const after = await getMigrationStatus(db, 'pg');
|
|
232
|
+
expect(after.currentVersion).toBe(10);
|
|
233
|
+
expect(after.upToDate).toBe(true);
|
|
234
|
+
|
|
235
|
+
// Real-engine schema check: no missing tables, no missing columns.
|
|
236
|
+
const afterDrift = await detectMigrationDrift(db, 'pg');
|
|
237
|
+
expect(hasMigrationDrift(afterDrift)).toBe(false);
|
|
238
|
+
expect(afterDrift.missingTables).toEqual([]);
|
|
239
|
+
expect(afterDrift.missingColumns).toEqual([]);
|
|
240
|
+
expect(afterDrift.missingIndexes).toEqual([]);
|
|
241
|
+
|
|
242
|
+
const timestampTypes = await db.rawQuery!<{ dataType: string; count: string }>(
|
|
243
|
+
`SELECT data_type AS "dataType", COUNT(*) AS count
|
|
244
|
+
FROM information_schema.columns
|
|
245
|
+
WHERE table_schema = current_schema()
|
|
246
|
+
AND table_name LIKE 'fortress_%'
|
|
247
|
+
AND table_name NOT IN ('fortress_migration_journal', 'fortress_audit_chain_state')
|
|
248
|
+
AND data_type LIKE 'timestamp%'
|
|
249
|
+
GROUP BY data_type`,
|
|
250
|
+
);
|
|
251
|
+
expect(timestampTypes).toEqual([{ dataType: 'timestamp with time zone', count: '62' }]);
|
|
252
|
+
|
|
253
|
+
const allIndexRows = await db.rawQuery!<{ indexname: string; indexdef: string }>(
|
|
254
|
+
`SELECT indexname, indexdef FROM pg_indexes
|
|
255
|
+
WHERE schemaname = current_schema() AND tablename LIKE 'fortress_%'`,
|
|
256
|
+
);
|
|
257
|
+
const requiredNames = new Set(FORTRESS_INDEXES.map(index => index.name));
|
|
258
|
+
const indexRows = allIndexRows.filter(row => requiredNames.has(row.indexname));
|
|
259
|
+
expect(indexRows.map(row => row.indexname).sort()).toEqual(
|
|
260
|
+
FORTRESS_INDEXES.map(index => index.name).sort(),
|
|
261
|
+
);
|
|
262
|
+
for (const expected of FORTRESS_INDEXES) {
|
|
263
|
+
const definition = indexRows.find(row => row.indexname === expected.name)?.indexdef.replaceAll('"', '');
|
|
264
|
+
expect(definition, expected.name).toContain(`(${expected.columns.join(', ')})`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The provisioned schema is usable: insert a row through the adapter,
|
|
268
|
+
// exercising SERIAL ids (stringified at the adapter boundary, see the
|
|
269
|
+
// v0.2.x `stringifyIds` change in CHANGELOG) and timestamp defaults.
|
|
270
|
+
const user = await db.create<{ id: string; createdAt: Date }>({
|
|
271
|
+
model: 'user',
|
|
272
|
+
data: { email: 'pg-migrate@test.com', name: 'PG Migrate', passwordHash: 'h', isActive: true },
|
|
273
|
+
});
|
|
274
|
+
expect(typeof user.id).toBe('string');
|
|
275
|
+
expect(user.id.length).toBeGreaterThan(0);
|
|
276
|
+
expect(user.createdAt).toBeInstanceOf(Date);
|
|
277
|
+
|
|
278
|
+
// Re-running is idempotent.
|
|
279
|
+
const reapply = await migrateUp(db, 'pg');
|
|
280
|
+
expect(reapply.applied).toEqual([]);
|
|
281
|
+
|
|
282
|
+
// A targeted rollback preserves refresh rows while removing v5 through v3.
|
|
283
|
+
const rollback = await migrateDown(db, 'pg', 2);
|
|
284
|
+
expect(rollback.rolledBack.map(migration => migration.name)).toEqual(['bigint_append_only_ids', 'encrypt_totp_secrets', 'two_factor_hardening', 'audit_chain_anchor', 'canonical_email', 'hot_indexes_timestamptz', 'tenant_default_unique', 'auth_continuation']);
|
|
285
|
+
expect(await db.count({ model: 'refresh_token' })).toBe(4);
|
|
286
|
+
const v3Columns = await db.rawQuery!<{ columnName: string }>(
|
|
287
|
+
`SELECT column_name AS "columnName" FROM information_schema.columns
|
|
288
|
+
WHERE table_schema = current_schema() AND table_name = 'fortress_refresh_token'
|
|
289
|
+
AND column_name IN ('family_created_at', 'successor_token_hash', 'rotated_at')`,
|
|
290
|
+
);
|
|
291
|
+
expect(v3Columns).toEqual([]);
|
|
292
|
+
const continuationTables = await db.rawQuery!<{ tableName: string }>(
|
|
293
|
+
`SELECT table_name AS "tableName" FROM information_schema.tables
|
|
294
|
+
WHERE table_schema = current_schema() AND table_name = 'fortress_auth_continuation'`,
|
|
295
|
+
);
|
|
296
|
+
expect(continuationTables).toEqual([]);
|
|
297
|
+
|
|
298
|
+
const restore = await migrateUp(db, 'pg');
|
|
299
|
+
expect(restore.applied.map(migration => migration.name)).toEqual(['auth_continuation', 'tenant_default_unique', 'hot_indexes_timestamptz', 'canonical_email', 'audit_chain_anchor', 'two_factor_hardening', 'encrypt_totp_secrets', 'bigint_append_only_ids']);
|
|
300
|
+
|
|
301
|
+
// Roll back drops every Fortress table (FK-safe via CASCADE ordering).
|
|
302
|
+
const down = await migrateDown(db, 'pg');
|
|
303
|
+
expect(down.toVersion).toBe(0);
|
|
304
|
+
const finalDrift = await detectMigrationDrift(db, 'pg');
|
|
305
|
+
expect(finalDrift.missingTables.length).toBe(FORTRESS_TABLES.length);
|
|
306
|
+
}, 60_000);
|
|
307
|
+
|
|
308
|
+
it('serializes concurrent migrators and rejects journal tampering', async () => {
|
|
309
|
+
const firstClient = postgres(connectionString);
|
|
310
|
+
const secondClient = postgres(connectionString);
|
|
311
|
+
const first = adapterFor(firstClient);
|
|
312
|
+
const second = adapterFor(secondClient);
|
|
313
|
+
try {
|
|
314
|
+
const upResults = await Promise.all([
|
|
315
|
+
migrateUp(first, 'pg'),
|
|
316
|
+
migrateUp(second, 'pg'),
|
|
317
|
+
]);
|
|
318
|
+
expect(upResults.map(result => result.applied.length).sort((a, b) => a - b)).toEqual([0, 10]);
|
|
319
|
+
expect(upResults.map(result => result.fromVersion).sort((a, b) => a - b)).toEqual([0, 10]);
|
|
320
|
+
|
|
321
|
+
const journal = await first.rawQuery!<{ version: number; checksum: string }>(
|
|
322
|
+
'SELECT version, checksum FROM fortress_migration_journal ORDER BY version',
|
|
323
|
+
);
|
|
324
|
+
expect(journal.map(row => Number(row.version))).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
325
|
+
expect(journal.every(row => SHA256_HEX_RE.test(row.checksum))).toBe(true);
|
|
326
|
+
|
|
327
|
+
const downResults = await Promise.all([
|
|
328
|
+
migrateDown(first, 'pg', 4),
|
|
329
|
+
migrateDown(second, 'pg', 4),
|
|
330
|
+
]);
|
|
331
|
+
expect(downResults.map(result => result.rolledBack.length).sort((a, b) => a - b)).toEqual([0, 6]);
|
|
332
|
+
expect((await getMigrationStatus(first, 'pg')).currentVersion).toBe(4);
|
|
333
|
+
|
|
334
|
+
await first.rawQuery!(
|
|
335
|
+
'UPDATE fortress_migration_journal SET checksum = ? WHERE version = 3',
|
|
336
|
+
['f'.repeat(64)],
|
|
337
|
+
);
|
|
338
|
+
await expect(migrateUp(second, 'pg')).rejects.toThrow(INTEGRITY_FAILURE_RE);
|
|
339
|
+
expect((await getMigrationStatus(first, 'pg')).currentVersion).toBe(4);
|
|
340
|
+
|
|
341
|
+
const migration3 = getFortressMigrations('pg').find(migration => migration.version === 3)!;
|
|
342
|
+
await first.rawQuery!(
|
|
343
|
+
'UPDATE fortress_migration_journal SET checksum = ? WHERE version = 3',
|
|
344
|
+
[await computeMigrationChecksum(migration3)],
|
|
345
|
+
);
|
|
346
|
+
await migrateDown(first, 'pg', 0);
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
await firstClient.end();
|
|
350
|
+
await secondClient.end();
|
|
351
|
+
}
|
|
352
|
+
}, 60_000);
|
|
353
|
+
});
|