@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,118 @@
|
|
|
1
|
+
# Hardening
|
|
2
|
+
|
|
3
|
+
A focused checklist for operators running Fortress in production. Pair
|
|
4
|
+
with [deployment.md](./deployment.md) (configuration), [security.md](./security.md) (architecture),
|
|
5
|
+
and [threat-model.md](./threat-model.md) (attacker model).
|
|
6
|
+
|
|
7
|
+
Each item is "what to do" + "why" + "how to verify".
|
|
8
|
+
|
|
9
|
+
## Identity and credentials
|
|
10
|
+
|
|
11
|
+
### JWT secret
|
|
12
|
+
|
|
13
|
+
- **Do:** Generate via `fortress generate-secret` (64 bytes hex, exceeds the 32-byte HS256 minimum). Store in a secret manager. Rotate at least annually and immediately on suspected compromise.
|
|
14
|
+
- **Why:** A leaked HS256 secret lets an attacker forge any access token. Fortress validates secret length at boot — anything shorter throws.
|
|
15
|
+
- **Verify:** `createFortress` accepts an array for rotation (`jwt.key: [new, old]`) — make sure your rotation playbook adds `new` to position 0, redeploys, waits past `accessTokenExpirySeconds`, removes `old`.
|
|
16
|
+
|
|
17
|
+
### Cookie posture
|
|
18
|
+
|
|
19
|
+
- **Do:** Keep the `__Host-` prefix defaults. Only set `cookies: { secure: false }` for localhost HTTP dev.
|
|
20
|
+
- **Why:** `__Host-` cookies are scoped to the origin, have no `Domain`, and refuse to be set without HTTPS — the strongest browser-enforced isolation.
|
|
21
|
+
- **Verify:** `curl -I https://your-domain/auth/login` returns `Set-Cookie: __Host-fortress_*` with `Secure; HttpOnly; SameSite=Lax; Path=/`.
|
|
22
|
+
|
|
23
|
+
### Password policy
|
|
24
|
+
|
|
25
|
+
- **Do:** Keep the default `passwordPolicy.minLength` of 15 and enable `checkBreached`; choose `breachedFailureMode: 'closed'` when assurance is more important than registration availability.
|
|
26
|
+
- **Why:** Length plus breached-password screening provides useful resistance without brittle composition rules. Every HIBP outage emits `PASSWORD_BREACH_CHECK_DEGRADED` for alerting.
|
|
27
|
+
- **Verify:** Try registering with `"abc"` — it should reject with `BAD_REQUEST`; simulate an HIBP outage and verify your chosen open/closed behavior.
|
|
28
|
+
|
|
29
|
+
### Refresh-token posture
|
|
30
|
+
|
|
31
|
+
- **Do:** Keep `refreshTokenExpirySeconds` ≤ 30 days. Enforce per-device refresh family rotation (Fortress default). Treat any `TOKEN_REUSE_DETECTED` event as a paging incident.
|
|
32
|
+
- **Why:** Refresh-token reuse detection turns a stolen refresh token into a same-day revocation event. Without rotation, the attacker has the full token lifetime.
|
|
33
|
+
|
|
34
|
+
## Authorization
|
|
35
|
+
|
|
36
|
+
### Default-deny everywhere
|
|
37
|
+
|
|
38
|
+
- **Do:** Every Fortress-managed route should carry either `.security('none')` (intentional public) or `.permission(resource, action)` (default-deny). The mutual-exclusion check at boot will throw if you mix them.
|
|
39
|
+
- **Why:** The plan's stated goal — operators should never have to remember to add a guard.
|
|
40
|
+
- **Verify:** `fortress check:public-routes` in CI catches a stray `.security('none')`; the bootstrap allow-list covers only Fortress's intentional public surface.
|
|
41
|
+
|
|
42
|
+
### RBAC drift
|
|
43
|
+
|
|
44
|
+
- **Do:** Add `fortress manifest:check` to CI; also run `runFortressChecks({ fortress })` in a test that constructs your real `Fortress` instance.
|
|
45
|
+
- **Why:** Manifest drift catches "I added a route and forgot the `.permission` decorator" before it ships.
|
|
46
|
+
|
|
47
|
+
### Policy as code
|
|
48
|
+
|
|
49
|
+
- **Do:** Manage roles/groups/service accounts via `fortress.policy.json` instead of imperative `iam.createRole(...)` from production code. Reconcile in CI with `applyPolicyPlan(...)`.
|
|
50
|
+
- **Why:** Reviewable Git history for permission grants; no "who gave Bob role X?" mystery.
|
|
51
|
+
|
|
52
|
+
## Transport and edge
|
|
53
|
+
|
|
54
|
+
### HTTPS
|
|
55
|
+
|
|
56
|
+
- **Do:** Terminate TLS at the edge; redirect HTTP to HTTPS. Set HSTS via your edge (`Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`).
|
|
57
|
+
- **Why:** All security guarantees in this guide assume HTTPS in the browser.
|
|
58
|
+
|
|
59
|
+
### CSRF
|
|
60
|
+
|
|
61
|
+
- **Do:** Keep the default pipeline check on for cookie-authenticated browser sessions. Add `csrf: { rejectSameSite: true }` if your app is single-host (`example.com` only). Skip the check for routes that absolutely must accept third-party POSTs (webhooks) and replace it with signature verification per-route.
|
|
62
|
+
- **Why:** Cookies are ambient credentials; without a CSRF check, any malicious page can trigger a state change.
|
|
63
|
+
- **Verify:** A POST from a third-party origin (no `X-Fortress-CSRF` header, `Sec-Fetch-Site: cross-site`) returns 403.
|
|
64
|
+
|
|
65
|
+
### Rate limiting
|
|
66
|
+
|
|
67
|
+
- **Do:** Mount the `rate-limit` plugin with `login`, `register`, `refresh`, `oauthToken`, and `apiKeyIssue` blocks. In multi-replica deployments, bring your own `RateLimitStore` (Redis, Memcached) — the in-memory default does not coordinate across replicas.
|
|
68
|
+
- **Why:** Per-IP and per-account limits stop credential-stuffing and account-enumeration before they reach the password hasher.
|
|
69
|
+
|
|
70
|
+
## Data isolation
|
|
71
|
+
|
|
72
|
+
### Tenancy
|
|
73
|
+
|
|
74
|
+
- **Do:** Use the verified `tenantId` JWT claim (set by `enrichTokenClaims`); never an `X-Tenant-Code` header. Use numeric tenant IDs in schema names (`tenant_<id>`). For PostgreSQL, the adapter pins `search_path` per transaction via `set_config('search_path', ?, true)`.
|
|
75
|
+
- **Why:** The hardening plan documented in [docs/plugins/tenancy.md](./plugins/tenancy.md) closes a class of cross-tenant data-leak bugs.
|
|
76
|
+
|
|
77
|
+
### Multi-tenant scope rules
|
|
78
|
+
|
|
79
|
+
- **Do:** For row-level isolation across databases (not just PG), use the `data-isolation` plugin with explicit `scopes` per model.
|
|
80
|
+
- **Why:** Tenancy-via-schema only works on PG; everywhere else, scope rules are the safety net.
|
|
81
|
+
|
|
82
|
+
## Audit and logging
|
|
83
|
+
|
|
84
|
+
### Audit chain
|
|
85
|
+
|
|
86
|
+
- **Do:** Mount `auditLog({ hashChain: true })`. Schedule a nightly `verifyChain()` job. Mirror auth events into the audit log via an `addAuthObserver` (see [observability.md §6](./observability.md)).
|
|
87
|
+
- **Why:** Hash-chaining detects after-the-fact deletions; verification turns it into an enforced invariant.
|
|
88
|
+
|
|
89
|
+
### Structured logging
|
|
90
|
+
|
|
91
|
+
- **Do:** Pass a structured logger to `FortressConfig.logger` (`pino`, `fastify.log`, etc.). Default is silent — production without a logger means you'll never see token reuse or RBAC denials.
|
|
92
|
+
- **Why:** Security events are useless if no one sees them.
|
|
93
|
+
|
|
94
|
+
## Cryptographic posture
|
|
95
|
+
|
|
96
|
+
- **Do:** Stick with Argon2id (the default). Don't downgrade to PBKDF2/bcrypt without a measured reason.
|
|
97
|
+
- **Do:** Use the bundled OAuth signing key rotation (`oauth_signing_key` table); rotate quarterly.
|
|
98
|
+
- **Do:** When using social-login, pin provider issuer URLs to HTTPS; the bundled providers do this.
|
|
99
|
+
|
|
100
|
+
## Deployment hygiene
|
|
101
|
+
|
|
102
|
+
- **Do:** Run `bun run lint` + `bun run typecheck` + `bun run test` + `fortress migrate:check` + `fortress manifest:check` + `fortress check:public-routes` on every CI build. The shipped GitHub Actions workflow does all six (see [docs/ci/github-actions.yml](./ci/github-actions.yml)).
|
|
103
|
+
- **Do:** Take a database backup before every deploy that bumps a migration version; test restores quarterly.
|
|
104
|
+
- **Do:** Pin to a minor version (`@bajustone/fortress@~0.1`). Pre-1.0 Fortress reserves the right to ship breaking changes in any minor.
|
|
105
|
+
|
|
106
|
+
## Release notes discipline
|
|
107
|
+
|
|
108
|
+
The maintainer commits to:
|
|
109
|
+
|
|
110
|
+
- Every change that affects a security-relevant code path lands with a `### Security` or `### Changed` entry in the CHANGELOG noting the impact.
|
|
111
|
+
- Migration-bearing changes ship with a `docs/migrations/<version>.md` note (forward, rollback, backfill).
|
|
112
|
+
- Findings from external reviews land in `docs/security-review-<date>.md` with a remediation plan.
|
|
113
|
+
|
|
114
|
+
If a CHANGELOG entry for a security-affecting change is missing in a release, file an issue — it's a process bug.
|
|
115
|
+
|
|
116
|
+
## Reporting a vulnerability
|
|
117
|
+
|
|
118
|
+
See [SECURITY.md](../SECURITY.md). Email `security@bajustone.dev`. Acknowledgement within 48 hours, fix targeted within 7 days for criticals.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Host-owned routes
|
|
2
|
+
|
|
3
|
+
Fortress has two HTTP surfaces:
|
|
4
|
+
|
|
5
|
+
1. **Fortress-managed routes** — endpoints registered in `fortress.endpoints` and served through `fortress.handleRequest()` by `mountFortress()` / `createSvelteKitHandle()`.
|
|
6
|
+
2. **Host-owned routes** — routes your app registers in Hono, Express, SvelteKit, `Bun.serve`, etc. These often call `fortress.auth.*`, `fortress.iam.*`, or plugin methods directly.
|
|
7
|
+
|
|
8
|
+
Use `protect()` / `protectedRoute()` when a host-owned route should receive the same security controls as a Fortress-managed route.
|
|
9
|
+
|
|
10
|
+
## What `protect()` runs
|
|
11
|
+
|
|
12
|
+
Given an endpoint definition (or handler name) present in the route manifest, Fortress applies:
|
|
13
|
+
|
|
14
|
+
- plugin middleware: `before-auth`, `after-auth`, `after-rbac`;
|
|
15
|
+
- pipeline CSRF protection for unsafe cookie-authenticated requests;
|
|
16
|
+
- principal resolution via plugin credentials first, then Fortress JWT bearer/cookie;
|
|
17
|
+
- default-deny RBAC from `endpoint.meta.permission`;
|
|
18
|
+
- request validation from endpoint body/query/params schemas;
|
|
19
|
+
- auth-cookie attachment when the handler returns `{ accessToken, refreshToken? }`.
|
|
20
|
+
|
|
21
|
+
The route's endpoint metadata remains the source of truth. Do not duplicate permissions in a separate route map unless you intentionally want a different policy.
|
|
22
|
+
|
|
23
|
+
## Core API
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { protect } from '@bajustone/fortress';
|
|
27
|
+
|
|
28
|
+
const handler = protect(fortress, endpointDefinition, async (ctx) => {
|
|
29
|
+
// ctx.subject, ctx.userId, ctx.input, ctx.params, ctx.query, ctx.body
|
|
30
|
+
return { ok: true };
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const response = await handler(request);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`target` may be an `EndpointDefinition` or a unique endpoint `handler` name. If a handler name maps to multiple routes, pass the definition directly or set `method`.
|
|
37
|
+
|
|
38
|
+
### Typed input inference
|
|
39
|
+
|
|
40
|
+
`protect()` is generic over the endpoint you pass it. When `target` is the value returned by `endpoint(...).build()`, its phantom `<TBody, TQuery, TParams, TResponses>` generics flow into the `ProtectedRouteContext`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const createThing = endpoint('POST', '/things/:id')
|
|
44
|
+
.summary('Create thing')
|
|
45
|
+
.security('bearer')
|
|
46
|
+
.permission('thing', 'write')
|
|
47
|
+
.body(obj({ name: str() }, 'name'))
|
|
48
|
+
.params(obj({ id: int() }, 'id'))
|
|
49
|
+
.response(201, 'Created', obj({ ok: str() }, 'ok'))
|
|
50
|
+
.handler('createThing')
|
|
51
|
+
.build();
|
|
52
|
+
|
|
53
|
+
const handler = protect(fortress, createThing, async (ctx) => {
|
|
54
|
+
// ctx.body is { name: string } — non-optional, because a body schema is
|
|
55
|
+
// declared and validation already ran before the handler executes.
|
|
56
|
+
// ctx.params is { id: string }
|
|
57
|
+
// ctx.input is { name: string; id: string }
|
|
58
|
+
return { ok: ctx.body.name };
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
When the endpoint declares a body schema, `ctx.body` is narrowed to a non-optional `T` — use it directly, no `!` and no detour through `ctx.input`. Endpoints with no declared body schema keep the loose `unknown` body.
|
|
63
|
+
|
|
64
|
+
Passing a string handler name keeps the looser `Record<string, unknown>` / `unknown` typing (no inference source available). Runtime validation and coercion are identical in both cases — this is purely a typing improvement.
|
|
65
|
+
|
|
66
|
+
### Typed status returns with `ctx.respond`
|
|
67
|
+
|
|
68
|
+
For non-2xx returns, use `ctx.respond(status, body)` instead of hand-rolling a `Response`. When the target is a typed endpoint, both arguments narrow against the response declarations:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const getThing = endpoint('GET', '/things/:id')
|
|
72
|
+
.summary('Get thing')
|
|
73
|
+
.security('bearer')
|
|
74
|
+
.params(obj({ id: int() }, 'id'))
|
|
75
|
+
.response(200, 'OK', obj({ data: str() }, 'data'))
|
|
76
|
+
.errorResponse(404, 'Not found')
|
|
77
|
+
.handler('getThing')
|
|
78
|
+
.build();
|
|
79
|
+
|
|
80
|
+
const handler = protect(fortress, getThing, async (ctx) => {
|
|
81
|
+
const thing = await load(ctx.params.id);
|
|
82
|
+
if (!thing)
|
|
83
|
+
return ctx.respond(404, { code: 'NOT_FOUND', message: 'no such thing', statusCode: 404 });
|
|
84
|
+
// ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
85
|
+
// must be a declared status typed against responses[404].schema (ErrorEnvelope)
|
|
86
|
+
return { data: thing };
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
String-target endpoints fall back to `(status: number, body: unknown) => Response` — same ergonomics as before, no narrowing.
|
|
91
|
+
|
|
92
|
+
## Hono
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { endpoint, obj, str } from '@bajustone/fortress';
|
|
96
|
+
import { protectedRoute } from '@bajustone/fortress/hono';
|
|
97
|
+
|
|
98
|
+
const statsEndpoint = endpoint('GET', '/api/stats')
|
|
99
|
+
.summary('Stats')
|
|
100
|
+
.security('bearer')
|
|
101
|
+
.permission('stats', 'read')
|
|
102
|
+
.response(200, 'OK', obj({ ok: str() }, 'ok'))
|
|
103
|
+
.handler('stats')
|
|
104
|
+
.build();
|
|
105
|
+
|
|
106
|
+
// Include the endpoint so it appears in
|
|
107
|
+
// fortress.endpoints / fortress.manifest / OpenAPI. Either pass it via the
|
|
108
|
+
// top-level `routes` shorthand (preferred) or as a one-field plugin.
|
|
109
|
+
const fortress = createFortress({
|
|
110
|
+
// ...
|
|
111
|
+
routes: { stats: statsEndpoint },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Equivalent long-form (use this if you want to colocate host endpoints with
|
|
115
|
+
// other plugin metadata in the same record):
|
|
116
|
+
// plugins: [{ name: 'host-routes', routes: { stats: statsEndpoint } }],
|
|
117
|
+
|
|
118
|
+
app.get('/api/stats', protectedRoute(fortress, statsEndpoint, async (_c, ctx) => {
|
|
119
|
+
return { ok: ctx.subject?.type ?? 'unknown' };
|
|
120
|
+
}));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Register the Hono route before `mountFortress(app, fortress)` so the host handler wins over the manifest-owned fallback route.
|
|
124
|
+
|
|
125
|
+
## Express
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { protectedRoute } from '@bajustone/fortress/express';
|
|
129
|
+
|
|
130
|
+
app.get('/api/stats', protectedRoute(fortress, statsEndpoint, async (_req, _res, ctx) => {
|
|
131
|
+
return { ok: ctx.subject?.type ?? 'unknown' };
|
|
132
|
+
}));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## SvelteKit
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { protectedRoute } from '@bajustone/fortress/sveltekit';
|
|
139
|
+
|
|
140
|
+
const GET = protectedRoute(fortress, statsEndpoint, async (_event, ctx) => {
|
|
141
|
+
return new Response(JSON.stringify({ ok: ctx.subject?.type }), {
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export { GET };
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Boundary checklist
|
|
150
|
+
|
|
151
|
+
- `fortress.handleRequest()` remains responsible for Fortress-managed routes.
|
|
152
|
+
- `protectedRoute()` is responsible for host-owned routes that opt in.
|
|
153
|
+
- Public host routes can remain unwrapped.
|
|
154
|
+
- If a host route is mounted at a different path than its endpoint metadata, pass `options.path` / `options.params` so plugin middleware and validation use the intended canonical route.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Migration 0001 — Schema version tracking
|
|
2
|
+
|
|
3
|
+
Introduced: 2026-06-08
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
Adds the `fortress_schema_version` singleton table. Fortress migration tooling uses this table to compare a live database against bundled migrations.
|
|
8
|
+
|
|
9
|
+
## Forward
|
|
10
|
+
|
|
11
|
+
SQLite:
|
|
12
|
+
|
|
13
|
+
```sql
|
|
14
|
+
CREATE TABLE IF NOT EXISTS fortress_schema_version (
|
|
15
|
+
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
16
|
+
version INTEGER NOT NULL,
|
|
17
|
+
applied_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
18
|
+
);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
PostgreSQL:
|
|
22
|
+
|
|
23
|
+
```sql
|
|
24
|
+
CREATE TABLE IF NOT EXISTS fortress_schema_version (
|
|
25
|
+
id integer PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
26
|
+
version integer NOT NULL,
|
|
27
|
+
applied_at timestamp NOT NULL DEFAULT now()
|
|
28
|
+
);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The singleton version row (`id = 1`) is **not** written by this SQL — the
|
|
32
|
+
migration runner (`migrateUp`) stamps it after each migration applies, so
|
|
33
|
+
version tracking has a single source of truth. Applying the raw SQL by
|
|
34
|
+
hand creates the table at version 0; run `migrateUp` (or the CLI) to stamp
|
|
35
|
+
the current version.
|
|
36
|
+
|
|
37
|
+
## Rollback
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
DROP TABLE IF EXISTS fortress_schema_version;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Backfill / cleanup
|
|
44
|
+
|
|
45
|
+
No data backfill is required. Existing Fortress tables are not modified by this migration.
|
|
46
|
+
|
|
47
|
+
## Validation
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { getMigrationStatus } from '@bajustone/fortress';
|
|
51
|
+
|
|
52
|
+
const status = await getMigrationStatus(fortress.config.database);
|
|
53
|
+
console.log(status.currentVersion, status.latestVersion);
|
|
54
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Migration 0002 — Initial Fortress schema
|
|
2
|
+
|
|
3
|
+
Introduced: 2026-06-08
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
Creates every Fortress-owned table, index, and constraint — everything
|
|
8
|
+
except the `fortress_schema_version` checkpoint installed by `0001`. This
|
|
9
|
+
is the baseline that lets `migrateUp` provision a brand-new database
|
|
10
|
+
(SQLite or PostgreSQL) end-to-end through the adapter's `rawQuery`, with
|
|
11
|
+
no Drizzle or `drizzle-kit` dependency at runtime.
|
|
12
|
+
|
|
13
|
+
The migration is the SQL-first source of truth for the schema:
|
|
14
|
+
|
|
15
|
+
- `src/testing/index.ts` builds the in-memory test adapter from this DDL.
|
|
16
|
+
- The column-drift checker (`detectMigrationDrift`) parses expected
|
|
17
|
+
columns out of this DDL.
|
|
18
|
+
|
|
19
|
+
So the test adapter and a production `migrateUp` cannot drift.
|
|
20
|
+
|
|
21
|
+
## Forward
|
|
22
|
+
|
|
23
|
+
Run via the migration runner (recommended — it also stamps the version
|
|
24
|
+
row):
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { migrateUp } from '@bajustone/fortress';
|
|
28
|
+
|
|
29
|
+
await migrateUp(adapter); // applies 0001 + 0002, stamps version 2
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or apply the bundled SQL directly:
|
|
33
|
+
|
|
34
|
+
- SQLite: `migrations/sqlite/0002_initial_schema.sql`
|
|
35
|
+
- PostgreSQL: `migrations/pg/0002_initial_schema.sql`
|
|
36
|
+
|
|
37
|
+
All statements are `CREATE TABLE IF NOT EXISTS` / `CREATE [UNIQUE] INDEX
|
|
38
|
+
IF NOT EXISTS`, so applying it to a database that already has the Fortress
|
|
39
|
+
tables (e.g. provisioned from the Drizzle schema) is a no-op.
|
|
40
|
+
|
|
41
|
+
## Rollback
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { migrateDown } from '@bajustone/fortress';
|
|
45
|
+
|
|
46
|
+
await migrateDown(adapter, undefined, 1); // roll back to version 1
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or apply `migrations/{sqlite,pg}/0002_initial_schema.down.sql`.
|
|
50
|
+
|
|
51
|
+
> **Destructive.** The down step drops every Fortress table (PostgreSQL
|
|
52
|
+
> uses `CASCADE`). It deletes all users, tokens, roles, and audit history.
|
|
53
|
+
> Back up first and only do this on a database you intend to re-provision.
|
|
54
|
+
|
|
55
|
+
## Backfill / cleanup
|
|
56
|
+
|
|
57
|
+
None. On a fresh database this is the initial install. On a database whose
|
|
58
|
+
tables were already provisioned from the Drizzle schema, the forward step
|
|
59
|
+
is a no-op and only the version row changes.
|
|
60
|
+
|
|
61
|
+
## Migrating from a pre-0002 database
|
|
62
|
+
|
|
63
|
+
If you deployed an earlier Fortress version and provisioned tables from
|
|
64
|
+
the Drizzle schema (the previous documented path):
|
|
65
|
+
|
|
66
|
+
1. Pull this version.
|
|
67
|
+
2. Run `migrateUp(adapter)`. The `IF NOT EXISTS` guards mean existing
|
|
68
|
+
tables are left untouched; the version row advances to 2.
|
|
69
|
+
3. Run `detectMigrationDrift(adapter)` and confirm both `missingTables`
|
|
70
|
+
and `missingColumns` are empty. If `missingColumns` is non-empty, your
|
|
71
|
+
tables predate a column added in a later schema change — apply the
|
|
72
|
+
listed columns (the bundled DDL is the reference) and re-check.
|
|
73
|
+
|
|
74
|
+
## Validation
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { detectMigrationDrift, getMigrationStatus, hasMigrationDrift } from '@bajustone/fortress';
|
|
78
|
+
|
|
79
|
+
const status = await getMigrationStatus(adapter);
|
|
80
|
+
console.log(status.currentVersion, status.latestVersion); // 2 2
|
|
81
|
+
|
|
82
|
+
const drift = await detectMigrationDrift(adapter);
|
|
83
|
+
console.log(hasMigrationDrift(drift)); // false
|
|
84
|
+
```
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Fortress migration upgrade guide
|
|
2
|
+
|
|
3
|
+
This guide is the operational counterpart to the per-release notes under
|
|
4
|
+
`docs/migrations/<version>.md`. It describes how to run, verify, and roll
|
|
5
|
+
back Fortress migrations across releases.
|
|
6
|
+
|
|
7
|
+
## Migration sources of truth
|
|
8
|
+
|
|
9
|
+
- **Bundled migrations:** `migrations/{sqlite,pg}/<version>_<name>.sql`
|
|
10
|
+
and `<version>_<name>.down.sql`. Committed to the repo for review and
|
|
11
|
+
auditability. Mirrored as string constants in
|
|
12
|
+
`src/core/migrations/migrations.ts` so the runtime works without disk
|
|
13
|
+
access (serverless, bundled CLIs).
|
|
14
|
+
- **Expected table list:** `FORTRESS_TABLES` in
|
|
15
|
+
`src/core/migrations/migrations.ts`. Drives the live-DB drift checker.
|
|
16
|
+
Kept in sync with `src/drizzle/schema.ts`, `src/drizzle/pg/schema.ts`,
|
|
17
|
+
and `src/testing/index.ts`; the engine test fails if the test adapter
|
|
18
|
+
ever stops creating one of these tables.
|
|
19
|
+
- **Schema-version table:** `fortress_schema_version` (single row,
|
|
20
|
+
`id = 1`). The applied baseline is whatever `version` holds; missing
|
|
21
|
+
table means version 0.
|
|
22
|
+
|
|
23
|
+
## Runtime API
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import {
|
|
27
|
+
detectMigrationDrift,
|
|
28
|
+
getMigrationStatus,
|
|
29
|
+
hasMigrationDrift,
|
|
30
|
+
migrateDown,
|
|
31
|
+
migrateUp,
|
|
32
|
+
} from '@bajustone/fortress';
|
|
33
|
+
|
|
34
|
+
const status = await getMigrationStatus(adapter);
|
|
35
|
+
// { dialect, currentVersion, latestVersion, pending, applied, upToDate, hasVersionTable }
|
|
36
|
+
|
|
37
|
+
const up = await migrateUp(adapter);
|
|
38
|
+
// { fromVersion, toVersion, applied: FortressMigration[] }
|
|
39
|
+
|
|
40
|
+
const down = await migrateDown(adapter, 'sqlite', 0);
|
|
41
|
+
// rolls back to version 0
|
|
42
|
+
|
|
43
|
+
const drift = await detectMigrationDrift(adapter);
|
|
44
|
+
// { currentVersion, latestVersion, missingVersionTable, unknownFutureVersion,
|
|
45
|
+
// pendingVersions, missingTables, missingColumns, missingIndexes }
|
|
46
|
+
if (hasMigrationDrift(drift)) throw new Error('Schema drift detected');
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Pass an explicit `dialect` when the adapter does not advertise one (most
|
|
50
|
+
non-Drizzle adapters). Both `migrateUp` and `migrateDown` are idempotent and safe to run on every
|
|
51
|
+
deploy. Mutation runs acquire a database-backed lock, re-read the checkpoint
|
|
52
|
+
only after locking, and execute transactionally. The engine maintains
|
|
53
|
+
`fortress_migration_journal` with one SHA-256 checksum per applied migration;
|
|
54
|
+
legacy checkpoints are backfilled once, while later missing or changed rows
|
|
55
|
+
fail closed before any schema mutation.
|
|
56
|
+
|
|
57
|
+
## CLI
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
fortress migrate:status
|
|
61
|
+
fortress migrate:down
|
|
62
|
+
fortress migrate:diff
|
|
63
|
+
fortress migrate:check # exits non-zero on drift; suitable for CI
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The CLI commands operate on the bundled migration catalog and do **not**
|
|
67
|
+
connect to a database. Migration v6 includes a Unicode-aware data step, so
|
|
68
|
+
`fortress migrate:up` intentionally refuses to emit SQL-only output that would
|
|
69
|
+
skip cleanup. Run the runtime API against your live database from application
|
|
70
|
+
code; it executes data steps and constraints atomically using the configured
|
|
71
|
+
adapter.
|
|
72
|
+
|
|
73
|
+
## Drift signals (`detectMigrationDrift`)
|
|
74
|
+
|
|
75
|
+
| Field | Meaning | Recommended action |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `missingVersionTable` | `fortress_schema_version` does not exist | Run `migrateUp` |
|
|
78
|
+
| `pendingVersions` | Migrations the runtime has but the DB has not applied | Run `migrateUp` |
|
|
79
|
+
| `unknownFutureVersion` | DB is at a higher version than the bundled migrations | Upgrade the Fortress version before deploying |
|
|
80
|
+
| `missingTables` | Any `FORTRESS_TABLES` entry not present in the DB | Run `migrateUp`, or finish the interrupted migration run |
|
|
81
|
+
| `missingColumns` | A table that exists but is missing a column the bundled DDL defines | Apply the migration that adds the column (or recreate the table from the bundled DDL) |
|
|
82
|
+
| `missingIndexes` | A required hot-path index is absent | Apply the owning migration or restore the named index |
|
|
83
|
+
|
|
84
|
+
`missingTables`, `missingColumns`, and `missingIndexes` are the deep checks — they catch the
|
|
85
|
+
case where the version table claims everything is applied but the schema
|
|
86
|
+
is genuinely incomplete (manual DROP, partial restore, a table created
|
|
87
|
+
from an older schema dump). Expected columns are parsed straight out of
|
|
88
|
+
the bundled migration DDL, so the check works for any adapter — not just
|
|
89
|
+
Drizzle. Wire `hasMigrationDrift()` into your deploy preflight and your CI
|
|
90
|
+
smoke tests.
|
|
91
|
+
|
|
92
|
+
## Current migration catalog
|
|
93
|
+
|
|
94
|
+
Ten migrations ship today:
|
|
95
|
+
|
|
96
|
+
- **`0001_schema_version`** — installs the schema checkpoint.
|
|
97
|
+
- **`0002_initial_schema`** — creates the original Fortress schema.
|
|
98
|
+
- **`0003_auth_continuation`** — adds pending-auth/session metadata.
|
|
99
|
+
- **`0004_tenant_default_unique`** — enforces one default tenant membership.
|
|
100
|
+
- **`0005_hot_indexes_timestamptz`** — adds hot indexes and UTC-safe PostgreSQL timestamps.
|
|
101
|
+
- **`0006_canonical_email`** — performs runtime Unicode email cleanup and installs case-insensitive uniqueness.
|
|
102
|
+
- **`0007_audit_chain_anchor`** — installs the permanent zero-entry sentinel and persists the terminal audit hash/count so tail or full-chain deletion is detectable.
|
|
103
|
+
- **`0008_two_factor_hardening`** — adds durable continuation failure counters, invalidation/cooldown metadata, and the indexed per-account failure lookup used by MFA throttling.
|
|
104
|
+
- **`0009_encrypt_totp_secrets`** — removes legacy plaintext TOTP enrolments, backup codes, and trusted devices. SQL cannot encrypt legacy seeds with the application-held key, so affected users must re-enrol; new seeds are stored as AES-256-GCM envelopes.
|
|
105
|
+
- **`0010_bigint_append_only_ids`** — widens PostgreSQL identifiers and sequences on unbounded/churn-heavy token, audit, webhook-delivery, and WebAuthn-challenge tables to bigint. SQLite identifiers are already signed 64-bit. Narrowing rollback is intentionally unsupported.
|
|
106
|
+
|
|
107
|
+
If `fortress_audit_log` already contains rows written without hash chaining, migration 0007 intentionally leaves the anchor at zero; SQL cannot establish a trustworthy cryptographic history. Before serving auth traffic with `auditLog({ hashChain: true })`, archive and externally attest those rows, create the hash-chain-enabled Fortress instance during a maintenance window, and call `await fortress.plugins['audit-log'].rebaselineChain()`. The method transactionally accepts only an uninitialized zero anchor and fully unchained rows, links them deterministically, advances the anchor, and verifies the result. See [Audit Log Plugin](../plugins/audit-log.md#enabling-hash-chaining-on-an-existing-log).
|
|
108
|
+
|
|
109
|
+
The migrations are the SQL-first source of truth: `src/testing/index.ts`
|
|
110
|
+
derives the test adapter's schema from them, and the column-drift checker
|
|
111
|
+
parses expected columns from them, so the test adapter and a production
|
|
112
|
+
`migrateUp` cannot diverge.
|
|
113
|
+
|
|
114
|
+
### Provisioning a new database
|
|
115
|
+
|
|
116
|
+
1. Point Fortress at an empty database (any adapter with `rawQuery`).
|
|
117
|
+
2. Call `migrateUp(adapter)` — this creates the checkpoint table, all
|
|
118
|
+
Fortress tables, and stamps `fortress_schema_version` at the latest
|
|
119
|
+
version.
|
|
120
|
+
3. Call `detectMigrationDrift(adapter)` to confirm `missingTables` and
|
|
121
|
+
`missingColumns` are empty.
|
|
122
|
+
|
|
123
|
+
> Drizzle users may still provision tables directly from
|
|
124
|
+
> `src/drizzle/{,pg/}schema.ts` if they prefer to own schema management;
|
|
125
|
+
> `migrateUp` is idempotent (`CREATE TABLE IF NOT EXISTS`) and will simply
|
|
126
|
+
> stamp the version row in that case.
|
|
127
|
+
|
|
128
|
+
### Upgrading an existing database
|
|
129
|
+
|
|
130
|
+
1. Pull the new Fortress version.
|
|
131
|
+
2. Read the version-specific notes in `docs/migrations/<version>.md` for
|
|
132
|
+
forward/rollback/backfill steps.
|
|
133
|
+
3. Call `migrateUp(adapter)` on application start (or in a one-shot
|
|
134
|
+
deploy job).
|
|
135
|
+
4. Confirm via `migrate:check` / `hasMigrationDrift()` that the
|
|
136
|
+
database is at the expected version with no missing tables.
|
|
137
|
+
|
|
138
|
+
## Rollback
|
|
139
|
+
|
|
140
|
+
`migrateDown(adapter, dialect, targetVersion)` rolls the schema back to
|
|
141
|
+
the supplied version (default `0`). Each migration's `.down.sql` is
|
|
142
|
+
applied in reverse order. The version row is updated last, so a failed
|
|
143
|
+
rollback leaves the previous version recorded — re-run after fixing the
|
|
144
|
+
underlying issue.
|
|
145
|
+
|
|
146
|
+
Always take a backup before rolling back across an irreversible change
|
|
147
|
+
(table drops, column drops, type narrowing). The `0001_schema_version`
|
|
148
|
+
down step only removes the checkpoint table. **`0002_initial_schema`'s
|
|
149
|
+
down step drops every Fortress table** (PostgreSQL uses `CASCADE`), so
|
|
150
|
+
rolling back below version 2 is destructive — it deletes all users,
|
|
151
|
+
tokens, roles, and audit history. Only do this against a database you
|
|
152
|
+
intend to re-provision, and back up first.
|
|
153
|
+
|
|
154
|
+
## Custom / additional migrations
|
|
155
|
+
|
|
156
|
+
The bundled catalog covers Fortress-owned tables. Application-owned
|
|
157
|
+
tables (e.g. tenant-specific business tables, audit-log partitions) are
|
|
158
|
+
out of scope — keep them in your own migration tool of choice. The
|
|
159
|
+
adapter's `rawQuery` is available if you need to call into the same DB
|
|
160
|
+
connection from your own scripts.
|