@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
package/README.md
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
# Fortress
|
|
2
|
+
|
|
3
|
+
Authentication, sessions, IAM, and security plugins for TypeScript.
|
|
4
|
+
|
|
5
|
+
- Web-standard core: `Request → Response`
|
|
6
|
+
- Hono, Express, and SvelteKit adapters
|
|
7
|
+
- PostgreSQL and SQLite through Drizzle
|
|
8
|
+
- JWT access tokens and rotating refresh-token families
|
|
9
|
+
- Roles, groups, direct permissions, conditions, and service accounts
|
|
10
|
+
- Typed endpoint schemas, in-process calls, OpenAPI, migrations, and CI checks
|
|
11
|
+
- 15 optional plugins
|
|
12
|
+
|
|
13
|
+
Fortress runs on Bun, Deno, Node.js 20.19+, and edge runtimes. File-based helpers and the Bun CLI require a filesystem runtime.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# npm
|
|
19
|
+
npx jsr add @bajustone/fortress
|
|
20
|
+
|
|
21
|
+
# Bun
|
|
22
|
+
bunx jsr add @bajustone/fortress
|
|
23
|
+
|
|
24
|
+
# Deno
|
|
25
|
+
deno add jsr:@bajustone/fortress
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Install the integrations you use:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun add drizzle-orm hono # Hono + Drizzle
|
|
32
|
+
bun add drizzle-orm express # Express + Drizzle
|
|
33
|
+
bun add drizzle-orm @sveltejs/kit # SvelteKit + Drizzle
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Start with Hono and SQLite
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Hono } from 'hono';
|
|
40
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
41
|
+
import { createFortress } from '@bajustone/fortress';
|
|
42
|
+
import { createDrizzleAdapter } from '@bajustone/fortress/drizzle';
|
|
43
|
+
import { mountFortress } from '@bajustone/fortress/hono';
|
|
44
|
+
|
|
45
|
+
const fortress = createFortress({
|
|
46
|
+
database: createDrizzleAdapter(drizzle('app.db')),
|
|
47
|
+
jwt: {
|
|
48
|
+
key: process.env.FORTRESS_JWT_SECRET!, // at least 32 UTF-8 bytes
|
|
49
|
+
issuer: 'my-app',
|
|
50
|
+
},
|
|
51
|
+
// Required only for plain-HTTP local development.
|
|
52
|
+
cookies: { secure: false },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await fortress.migrate();
|
|
56
|
+
|
|
57
|
+
const app = new Hono();
|
|
58
|
+
mountFortress(app, fortress);
|
|
59
|
+
|
|
60
|
+
export default app;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`mountFortress()` serves core auth, IAM, and registered plugin routes. Login and refresh responses set access and refresh cookies.
|
|
64
|
+
|
|
65
|
+
Generate a key:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
fortress generate-secret
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `fortress` CLI uses Bun. Run database-bound operations from application code with your configured adapter.
|
|
72
|
+
|
|
73
|
+
## Use the service API
|
|
74
|
+
|
|
75
|
+
Direct service calls are useful in jobs, tests, scripts, and application services. They run auth/IAM hooks and observers but skip the HTTP middleware, CSRF, route RBAC, and request-validation pipeline.
|
|
76
|
+
|
|
77
|
+
### Register and sign in
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const user = await fortress.auth.createUser({
|
|
81
|
+
email: 'alice@example.com',
|
|
82
|
+
name: 'Alice',
|
|
83
|
+
password: 'correct-horse-battery-staple',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await fortress.auth.login(
|
|
87
|
+
'alice@example.com',
|
|
88
|
+
'correct-horse-battery-staple',
|
|
89
|
+
{
|
|
90
|
+
ipAddress: requestIp,
|
|
91
|
+
userAgent: request.headers.get('user-agent') ?? undefined,
|
|
92
|
+
deviceName: 'Alice laptop',
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (result.status === 'success') {
|
|
97
|
+
result.user;
|
|
98
|
+
result.accessToken;
|
|
99
|
+
result.refreshToken;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (result.status === 'pending') {
|
|
103
|
+
// Complete the factor through the plugin that requested it. Two-factor:
|
|
104
|
+
const completed = await fortress.plugins['two-factor'].verify(
|
|
105
|
+
result.pending.continuationToken,
|
|
106
|
+
code,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`AuthResult` has three variants:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
switch (result.status) {
|
|
115
|
+
case 'success':
|
|
116
|
+
result.refreshToken; // string
|
|
117
|
+
break;
|
|
118
|
+
case 'pending':
|
|
119
|
+
result.pending.reason;
|
|
120
|
+
result.pending.continuationToken;
|
|
121
|
+
break;
|
|
122
|
+
case 'impersonation':
|
|
123
|
+
result.refreshToken; // null
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Rotate and revoke sessions
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const next = await fortress.auth.refresh(refreshToken, {
|
|
132
|
+
ipAddress: requestIp,
|
|
133
|
+
userAgent: request.headers.get('user-agent') ?? undefined,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await fortress.auth.logout(next.refreshToken);
|
|
137
|
+
|
|
138
|
+
const sessions = await fortress.auth.listSessions(user.id);
|
|
139
|
+
await fortress.auth.revokeSession(user.id, sessions[0].id);
|
|
140
|
+
await fortress.auth.revokeAllOtherSessions(user.id, currentSessionId);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every refresh rotates the token. Reuse of a revoked token invalidates its family. Configure retry grace, inactivity limits, absolute lifetime, and per-user caps under `jwt.session`.
|
|
144
|
+
|
|
145
|
+
Add phone or username sign-in without replacing the primary email:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
await fortress.auth.addLoginIdentifier(user.id, 'phone', '+15551234567');
|
|
149
|
+
await fortress.auth.addLoginIdentifier(user.id, 'username', 'alice');
|
|
150
|
+
await fortress.auth.login('alice', password);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Issue a short-lived, access-only impersonation token. The caller must hold `fortress:impersonate`:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const impersonation = await fortress.auth.impersonate(admin.id, user.id, {
|
|
157
|
+
reason: 'Support case 1234',
|
|
158
|
+
expirySeconds: 5 * 60,
|
|
159
|
+
});
|
|
160
|
+
// impersonation.status === 'impersonation'
|
|
161
|
+
// JWT `sub` is user.id; `act.sub` is admin.id; refreshToken is null.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Grant and check permissions
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const editor = await fortress.iam.createRole('editor', [
|
|
168
|
+
{ resource: 'post', action: 'read' },
|
|
169
|
+
{ resource: 'post', action: 'update' },
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
await fortress.iam.bindRoleToUser(user.id, editor.id);
|
|
173
|
+
|
|
174
|
+
const allowed = await fortress.iam.checkPermission(
|
|
175
|
+
{ type: 'USER', id: user.id },
|
|
176
|
+
'post',
|
|
177
|
+
'update',
|
|
178
|
+
{ resource: { ownerId: user.id } },
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Permissions can come from roles, groups, or direct bindings:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const team = await fortress.iam.createGroup('engineering');
|
|
186
|
+
await fortress.iam.addUserToGroup(team.id, user.id);
|
|
187
|
+
await fortress.iam.bindRoleToGroup(team.id, editor.id);
|
|
188
|
+
|
|
189
|
+
await fortress.iam.bindPermissionToUser(user.id, {
|
|
190
|
+
resource: 'report',
|
|
191
|
+
action: 'export',
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Use `deny-overrides` to enforce explicit denies:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const fortress = createFortress({
|
|
199
|
+
database,
|
|
200
|
+
jwt: { key },
|
|
201
|
+
rbac: { evaluationMode: 'deny-overrides' },
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Add ABAC conditions to permissions:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
await fortress.iam.createRole('owner', [{
|
|
209
|
+
resource: 'post',
|
|
210
|
+
action: 'update',
|
|
211
|
+
conditions: [{
|
|
212
|
+
field: 'resource.ownerId',
|
|
213
|
+
operator: 'eq',
|
|
214
|
+
value: { ref: 'user.id' },
|
|
215
|
+
}],
|
|
216
|
+
}]);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Credential scopes only narrow IAM access. An empty `credentialScopes` array denies every permission:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
await fortress.iam.checkPermission(subject, 'post', 'read', {
|
|
223
|
+
credentialScopes: ['post:read'],
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Use service accounts
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const ci = await fortress.iam.createServiceAccount({
|
|
231
|
+
name: 'ci-deploy',
|
|
232
|
+
displayName: 'CI deployer',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await fortress.iam.bindRoleToServiceAccount(ci.id, editor.id);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Register the API-key plugin to authenticate the account:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { apiKey } from '@bajustone/fortress/plugins/api-key';
|
|
242
|
+
|
|
243
|
+
const fortress = createFortress({
|
|
244
|
+
database,
|
|
245
|
+
jwt: { key },
|
|
246
|
+
plugins: [apiKey()] as const,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const credential = await fortress.plugins['api-key'].createKey({
|
|
250
|
+
subject: { type: 'SERVICE_ACCOUNT', id: ci.id },
|
|
251
|
+
name: 'production',
|
|
252
|
+
scopes: ['post:read'],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
console.log(credential.key); // returned once; only its hash is stored
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Clients send `Authorization: ApiKey <key>` or `X-API-Key: <key>`.
|
|
259
|
+
|
|
260
|
+
## Call routes in process
|
|
261
|
+
|
|
262
|
+
`fortress.call` infers request and success-response types from endpoint definitions. It enters the same HTTP pipeline as a network request.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const login = await fortress.call.login({
|
|
266
|
+
identifier: 'alice@example.com',
|
|
267
|
+
password: 'correct-horse-battery-staple',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await fortress.call.revokeSession(
|
|
271
|
+
{ id: sessionId },
|
|
272
|
+
{ headers: { authorization: `Bearer ${login.accessToken}` } },
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Non-2xx responses throw `FortressError`:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { FortressError } from '@bajustone/fortress';
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await fortress.call.login({ identifier: 'alice@example.com', password: 'wrong' });
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error instanceof FortressError && error.code === 'UNAUTHORIZED') {
|
|
286
|
+
// Return your application's login error.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Mount a framework adapter
|
|
292
|
+
|
|
293
|
+
### Hono
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { Hono } from 'hono';
|
|
297
|
+
import {
|
|
298
|
+
createHonoMiddleware,
|
|
299
|
+
getSubject,
|
|
300
|
+
mountFortress,
|
|
301
|
+
} from '@bajustone/fortress/hono';
|
|
302
|
+
|
|
303
|
+
const app = new Hono();
|
|
304
|
+
mountFortress(app, fortress, { prefix: '/api' });
|
|
305
|
+
|
|
306
|
+
const { authMiddleware, rbacMiddleware, errorHandler } = createHonoMiddleware(fortress, {
|
|
307
|
+
routeMap: {
|
|
308
|
+
'GET /posts': { resource: 'post', action: 'read' },
|
|
309
|
+
'POST /posts': { resource: 'post', action: 'create' },
|
|
310
|
+
},
|
|
311
|
+
defaultDeny: true,
|
|
312
|
+
skipPaths: ['/health'],
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
app.onError(errorHandler);
|
|
316
|
+
app.use('/posts/*', authMiddleware, rbacMiddleware);
|
|
317
|
+
app.get('/posts', (c) => c.json({ subject: getSubject(c) }));
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Set HSTS, CSP, frame, content-type, referrer, and cross-domain-policy headers:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import { createSecurityHeadersMiddleware } from '@bajustone/fortress/hono';
|
|
324
|
+
|
|
325
|
+
app.use('*', createSecurityHeadersMiddleware());
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Express
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import express from 'express';
|
|
332
|
+
import {
|
|
333
|
+
createExpressMiddleware,
|
|
334
|
+
getSubject,
|
|
335
|
+
mountFortress,
|
|
336
|
+
} from '@bajustone/fortress/express';
|
|
337
|
+
|
|
338
|
+
const app = express();
|
|
339
|
+
app.use(express.json());
|
|
340
|
+
app.use(express.urlencoded({ extended: false })); // OAuth form endpoints
|
|
341
|
+
mountFortress(app, fortress, { prefix: '/api' });
|
|
342
|
+
|
|
343
|
+
const { authMiddleware, rbacMiddleware, errorHandler } = createExpressMiddleware(fortress, {
|
|
344
|
+
routeMap: { 'GET /posts': { resource: 'post', action: 'read' } },
|
|
345
|
+
unmappedRoutes: 'deny',
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
app.use('/posts', authMiddleware, rbacMiddleware);
|
|
349
|
+
app.get('/posts', (req, res) => res.json({ subject: getSubject(req) }));
|
|
350
|
+
app.use(errorHandler);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### SvelteKit
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
// src/hooks.server.ts
|
|
357
|
+
import { createSvelteKitHandle } from '@bajustone/fortress/sveltekit';
|
|
358
|
+
import { fortress } from '$lib/server/fortress';
|
|
359
|
+
|
|
360
|
+
export const handle = createSvelteKitHandle(fortress, { basePath: '/api' });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
// src/app.d.ts
|
|
365
|
+
import type { FortressLocals } from '@bajustone/fortress/sveltekit';
|
|
366
|
+
|
|
367
|
+
declare global {
|
|
368
|
+
namespace App {
|
|
369
|
+
interface Locals extends FortressLocals {}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export {};
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// src/routes/login/+page.server.ts
|
|
378
|
+
import { fortressActions } from '@bajustone/fortress/sveltekit';
|
|
379
|
+
import { fortress } from '$lib/server/fortress';
|
|
380
|
+
|
|
381
|
+
export const actions = {
|
|
382
|
+
default: fortressActions.login(fortress, { redirectTo: '/dashboard' }),
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The handle hook mounts Fortress routes, resolves `event.locals.fortress`, and refreshes expired access cookies on safe requests.
|
|
387
|
+
|
|
388
|
+
### Any `Request`/`Response` runtime
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
export default {
|
|
392
|
+
fetch(request: Request) {
|
|
393
|
+
return fortress.handleRequest(request);
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Protect application-owned routes
|
|
399
|
+
|
|
400
|
+
Define route metadata once, then use the adapter-specific `protectedRoute()` wrapper. The metadata drives authentication, CSRF, RBAC, validation, the route manifest, and OpenAPI.
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { endpoint, obj, str } from '@bajustone/fortress';
|
|
404
|
+
import { protectedRoute } from '@bajustone/fortress/hono';
|
|
405
|
+
|
|
406
|
+
const createPost = endpoint('POST', '/posts')
|
|
407
|
+
.security('bearer')
|
|
408
|
+
.permission('post', 'create')
|
|
409
|
+
.body(obj({ title: str({ min: 1 }) }, 'title'))
|
|
410
|
+
.response(201, 'Created', obj({ id: str() }, 'id'))
|
|
411
|
+
.handler('createPost')
|
|
412
|
+
.build();
|
|
413
|
+
|
|
414
|
+
const fortress = createFortress({
|
|
415
|
+
database,
|
|
416
|
+
jwt: { key },
|
|
417
|
+
routes: { createPost }, // manifest/OpenAPI metadata; host router owns dispatch
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
app.post('/posts', protectedRoute(fortress, createPost, async (_c, ctx) => {
|
|
421
|
+
const post = await savePost(ctx.body.title, ctx.subject!);
|
|
422
|
+
return ctx.respond(201, { id: post.id });
|
|
423
|
+
}));
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
See [Host-owned routes](docs/host-owned-routes.md).
|
|
427
|
+
|
|
428
|
+
## Validate custom handlers
|
|
429
|
+
|
|
430
|
+
Fortress schemas provide JSON Schema, Standard Schema V1 validation, and inferred TypeScript types:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { email, int, obj, strict, type Infer } from '@bajustone/fortress';
|
|
434
|
+
|
|
435
|
+
const CreateUser = strict(obj({
|
|
436
|
+
email: email(),
|
|
437
|
+
age: int({ min: 18 }),
|
|
438
|
+
}, 'email', 'age'));
|
|
439
|
+
|
|
440
|
+
type CreateUser = Infer<typeof CreateUser>;
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Use `vBody`, `vParam`, and `vQuery` in framework-owned handlers:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { vBody } from '@bajustone/fortress/hono';
|
|
447
|
+
|
|
448
|
+
app.post('/users', async (c) => {
|
|
449
|
+
const input = await vBody(c, CreateUser);
|
|
450
|
+
return c.json(await createUser(input), 201);
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
For other runtimes, validate all endpoint inputs at once:
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { validateRequest } from '@bajustone/fortress';
|
|
458
|
+
|
|
459
|
+
await validateRequest(endpoint.input, {
|
|
460
|
+
body: await request.json(),
|
|
461
|
+
query: Object.fromEntries(new URL(request.url).searchParams),
|
|
462
|
+
params,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Continue with the original data after validation.
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`endpoint()` accepts any Standard Schema V1 implementation, including Zod, Valibot, and ArkType. Fetcher's richer schema builder is available at `@bajustone/fortress/fetcher`.
|
|
469
|
+
|
|
470
|
+
## Configure Fortress
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
const fortress = createFortress({
|
|
474
|
+
database,
|
|
475
|
+
jwt: {
|
|
476
|
+
key: [currentSecret, previousSecret], // first signs; all verify
|
|
477
|
+
issuer: 'my-app',
|
|
478
|
+
audience: 'my-api',
|
|
479
|
+
accessTokenExpirySeconds: 15 * 60,
|
|
480
|
+
refreshTokenExpirySeconds: 7 * 24 * 60 * 60,
|
|
481
|
+
validateRefreshFingerprint: 'warn', // false | 'warn' | true
|
|
482
|
+
session: {
|
|
483
|
+
refreshGraceSeconds: 5,
|
|
484
|
+
idleTimeoutSeconds: 7 * 24 * 60 * 60,
|
|
485
|
+
absoluteTimeoutSeconds: 30 * 24 * 60 * 60,
|
|
486
|
+
maxSessionsPerUser: 10,
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
rbac: {
|
|
490
|
+
evaluationMode: 'deny-overrides',
|
|
491
|
+
resourceFile: './fortress.resources.json',
|
|
492
|
+
cache: { ttlSeconds: 30, maxEntries: 1000 }, // opt in
|
|
493
|
+
},
|
|
494
|
+
passwordPolicy: {
|
|
495
|
+
minLength: 15,
|
|
496
|
+
maxLength: 128,
|
|
497
|
+
checkBreached: true,
|
|
498
|
+
breachedFailureMode: 'open',
|
|
499
|
+
},
|
|
500
|
+
impersonation: { maxTtlSeconds: 15 * 60 },
|
|
501
|
+
cookies: {
|
|
502
|
+
secure: true,
|
|
503
|
+
sameSite: 'lax',
|
|
504
|
+
path: '/',
|
|
505
|
+
},
|
|
506
|
+
csrf: {
|
|
507
|
+
enabled: true,
|
|
508
|
+
skipPaths: ['/webhooks/incoming'],
|
|
509
|
+
},
|
|
510
|
+
plugins: [],
|
|
511
|
+
routes: {},
|
|
512
|
+
logger,
|
|
513
|
+
observability,
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Defaults:
|
|
518
|
+
|
|
519
|
+
- JWT issuer: `fortress`
|
|
520
|
+
- Access token: 15 minutes
|
|
521
|
+
- Refresh token: 7 days
|
|
522
|
+
- Permission evaluation: `allow-only`
|
|
523
|
+
- Permission cache: disabled until `rbac.cache` is supplied
|
|
524
|
+
- Password length: 15–128 characters; breach lookup disabled
|
|
525
|
+
- Cookies: `Secure`, `HttpOnly`, `SameSite=Lax`, `Path=/`, with `__Host-` names when possible
|
|
526
|
+
- CSRF: enabled for unsafe browser requests; cookie-authenticated requests require `X-Fortress-CSRF`
|
|
527
|
+
- Logger and telemetry: silent/no-op
|
|
528
|
+
|
|
529
|
+
Set `cookies: { secure: false }` only for plain-HTTP local development. `SameSite=None` requires `Secure`.
|
|
530
|
+
|
|
531
|
+
### Replace password hashing
|
|
532
|
+
|
|
533
|
+
The default hasher is Argon2id through WASM. Supply a native implementation when needed:
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
import type { PasswordHasher } from '@bajustone/fortress';
|
|
537
|
+
|
|
538
|
+
const passwordHasher: PasswordHasher = {
|
|
539
|
+
hash: password => Bun.password.hash(password, { algorithm: 'argon2id' }),
|
|
540
|
+
verify: (hash, password) => Bun.password.verify(password, hash),
|
|
541
|
+
};
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## Add plugins
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { accountLockout } from '@bajustone/fortress/plugins/account-lockout';
|
|
548
|
+
import { auditLog } from '@bajustone/fortress/plugins/audit-log';
|
|
549
|
+
import { rateLimit } from '@bajustone/fortress/plugins/rate-limit';
|
|
550
|
+
|
|
551
|
+
const fortress = createFortress({
|
|
552
|
+
database,
|
|
553
|
+
jwt: { key },
|
|
554
|
+
plugins: [
|
|
555
|
+
rateLimit(),
|
|
556
|
+
accountLockout(),
|
|
557
|
+
auditLog({ hashChain: true }),
|
|
558
|
+
] as const,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await fortress.plugins['account-lockout'].resetLockout('alice@example.com');
|
|
562
|
+
const events = await fortress.plugins['audit-log'].getAuditLog({ userId: user.id });
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Hooks run in registration order. Put rejection/gate plugins before side-effect plugins. A failing `afterLogin`, `afterRegister`, or `afterTokenRefresh` side-effect is logged and does not invalidate an already committed auth result. Use `beforeLogin` or `postAuthGate` to block token issuance.
|
|
566
|
+
|
|
567
|
+
| Need | Plugin | Guide |
|
|
568
|
+
|---|---|---|
|
|
569
|
+
| Protected admin CRUD and first-admin bootstrap | `admin` | [Admin](docs/plugins/admin.md) |
|
|
570
|
+
| API keys for users and service accounts | `api-key` | [API key](docs/plugins/api-key.md) |
|
|
571
|
+
| Progressive login lockouts | `account-lockout` | [Account lockout](docs/plugins/account-lockout.md) |
|
|
572
|
+
| Sliding-window limits for auth and app routes | `rate-limit` | [Rate limit](docs/plugins/rate-limit.md) |
|
|
573
|
+
| Verify email before issuing a session | `email-verification` | [Email verification](docs/plugins/email-verification.md) |
|
|
574
|
+
| TOTP, backup codes, and trusted devices | `two-factor` | [Two-factor](docs/plugins/two-factor.md) |
|
|
575
|
+
| Passkeys or WebAuthn as a second factor | `webauthn` | [WebAuthn](docs/plugins/webauthn.md) |
|
|
576
|
+
| Passwordless email login | `magic-link` | [Magic link](docs/plugins/magic-link.md) |
|
|
577
|
+
| Google, GitHub, Apple, Microsoft, Discord, or OIDC login | `social-login` | [Social login](docs/plugins/social-login.md) |
|
|
578
|
+
| OAuth 2.0/OIDC authorization server | `oauth` | [OAuth server](docs/plugins/oauth.md) |
|
|
579
|
+
| PostgreSQL schema-per-tenant isolation | `tenancy` | [Tenancy](docs/plugins/tenancy.md) |
|
|
580
|
+
| Database-independent row-level scoping | `data-isolation` | [Data isolation](docs/plugins/data-isolation.md) |
|
|
581
|
+
| Queryable or hash-chained audit events | `audit-log` | [Audit log](docs/plugins/audit-log.md) |
|
|
582
|
+
| Signed, queued outbound events | `webhook` | [Webhook](docs/plugins/webhook.md) |
|
|
583
|
+
| OpenAPI 3.1 JSON and Scalar UI | `openapi` | [OpenAPI](docs/plugins/openapi.md) |
|
|
584
|
+
|
|
585
|
+
## Migrate and bootstrap
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
await fortress.migrate({
|
|
589
|
+
migrateApp: () => migrate(appDb, { migrationsFolder: './drizzle' }),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
await fortress.syncPermissionsFromManifest({
|
|
593
|
+
defaultRoles: {
|
|
594
|
+
admin: '*',
|
|
595
|
+
member: ['post:read'],
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Fortress migrations run before `migrateApp`. Permission sync is idempotent and only adds missing manifest permissions and bindings.
|
|
601
|
+
|
|
602
|
+
Check live schema drift:
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
import { detectMigrationDrift, hasMigrationDrift } from '@bajustone/fortress';
|
|
606
|
+
|
|
607
|
+
const drift = await detectMigrationDrift(fortress.config.database);
|
|
608
|
+
if (hasMigrationDrift(drift))
|
|
609
|
+
throw new Error(JSON.stringify(drift));
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
See the [migration upgrade guide](docs/migrations/upgrade-guide.md).
|
|
613
|
+
|
|
614
|
+
## Use PostgreSQL
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
618
|
+
import { createDrizzleAdapter } from '@bajustone/fortress/drizzle';
|
|
619
|
+
import { fortressPgSchema } from '@bajustone/fortress/drizzle/pg';
|
|
620
|
+
|
|
621
|
+
const drizzleDb = drizzle(connectionString, { schema: fortressPgSchema });
|
|
622
|
+
const database = createDrizzleAdapter(drizzleDb, { dialect: 'pg' });
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
`@bajustone/fortress/drizzle/pg` exports the schema. Import `createDrizzleAdapter` from `@bajustone/fortress/drizzle`.
|
|
626
|
+
|
|
627
|
+
To use another datastore, implement `DatabaseAdapter`:
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
import type { DatabaseAdapter } from '@bajustone/fortress';
|
|
631
|
+
|
|
632
|
+
const database: DatabaseAdapter = {
|
|
633
|
+
create: async ({ model, data }) => insert(model, data),
|
|
634
|
+
findOne: async ({ model, where }) => findOne(model, where),
|
|
635
|
+
findMany: async ({ model, where, limit, offset, sortBy }) => findMany(model, { where, limit, offset, sortBy }),
|
|
636
|
+
update: async ({ model, where, data }) => update(model, where, data),
|
|
637
|
+
delete: async ({ model, where }) => remove(model, where),
|
|
638
|
+
count: async ({ model, where }) => count(model, where),
|
|
639
|
+
transaction: async fn => runTransaction(fn),
|
|
640
|
+
rawQuery: async (sql, params) => query(sql, params), // optional
|
|
641
|
+
dialect: 'pg', // optional
|
|
642
|
+
};
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
## Generate OpenAPI
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
const spec = fortress.toOpenAPI({
|
|
649
|
+
title: 'My API',
|
|
650
|
+
version: '1.0.0',
|
|
651
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
await Bun.write('openapi.json', JSON.stringify(spec, null, 2));
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Or mount the plugin:
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
import { openapi } from '@bajustone/fortress/plugins/openapi';
|
|
661
|
+
|
|
662
|
+
const fortress = createFortress({
|
|
663
|
+
database,
|
|
664
|
+
jwt: { key },
|
|
665
|
+
plugins: [openapi({ title: 'My API', version: '1.0.0' })] as const,
|
|
666
|
+
});
|
|
667
|
+
// GET /openapi.json
|
|
668
|
+
// GET /openapi
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## Test and check drift
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
# Node tests need the optional SQLite driver.
|
|
675
|
+
npm install --save-dev better-sqlite3
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
import { createFortress } from '@bajustone/fortress';
|
|
680
|
+
import { createTestAdapter, runFortressChecks } from '@bajustone/fortress/testing';
|
|
681
|
+
|
|
682
|
+
const fortress = createFortress({
|
|
683
|
+
database: createTestAdapter(),
|
|
684
|
+
jwt: { key: 'test-secret-that-is-at-least-32-bytes' },
|
|
685
|
+
plugins,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const result = await runFortressChecks({ fortress });
|
|
689
|
+
if (!result.ok)
|
|
690
|
+
throw new Error(result.messages.join('\n'));
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
The testing entrypoint also exports `smokeTestAuth`, `checkMigrationDrift`, `checkRouteManifestDrift`, and `checkPublicRoutes`. See [CI checks](docs/ci.md).
|
|
694
|
+
|
|
695
|
+
## Observe auth and IAM
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
const stopAuth = fortress.auth.addAuthObserver(async (event) => {
|
|
699
|
+
await securityEvents.write(event);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const stopIam = fortress.iam.addIamObserver(async (event) => {
|
|
703
|
+
await audit.write(event);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const stopChecks = fortress.iam.addPermissionCheckObserver((event) => {
|
|
707
|
+
permissionLatency.record(event.durationSeconds);
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Auth and IAM observers may return promises; failures are logged and contained. Permission-check observers are synchronous and run on the authorization hot path.
|
|
712
|
+
|
|
713
|
+
Use OpenTelemetry through the optional entrypoint:
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
import { createOtelTelemetry } from '@bajustone/fortress/otel';
|
|
717
|
+
|
|
718
|
+
const observability = await createOtelTelemetry({ name: 'my-app' });
|
|
719
|
+
const fortress = createFortress({ database, jwt: { key }, observability });
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
See [Observability](docs/observability.md).
|
|
723
|
+
|
|
724
|
+
## CLI
|
|
725
|
+
|
|
726
|
+
```bash
|
|
727
|
+
fortress init
|
|
728
|
+
fortress generate-secret
|
|
729
|
+
fortress sync:types
|
|
730
|
+
fortress openapi --out openapi.json
|
|
731
|
+
fortress schemas --format json-schema --out schemas.json
|
|
732
|
+
fortress manifest --out route-manifest.json
|
|
733
|
+
fortress manifest:check
|
|
734
|
+
fortress check:public-routes
|
|
735
|
+
fortress migrate:status --dialect pg
|
|
736
|
+
fortress migrate:check --dialect pg
|
|
737
|
+
fortress policy:summary
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
`sync:push`, `sync:pull`, `policy:diff`, `policy:apply`, `policy:check`, and live migration checks print code for the database-bound operation. Run that code inside your application so Fortress can use its configured adapter.
|
|
741
|
+
|
|
742
|
+
## Guides
|
|
743
|
+
|
|
744
|
+
- [Examples](examples/README.md)
|
|
745
|
+
- [Host-owned routes](docs/host-owned-routes.md)
|
|
746
|
+
- [Typed framework helpers](docs/adapter-typed-helpers.md)
|
|
747
|
+
- [Policy as code](docs/policy-as-code.md)
|
|
748
|
+
- [Admin recipes](docs/admin-recipes.md)
|
|
749
|
+
- [Deployment](docs/deployment.md)
|
|
750
|
+
- [Security](docs/security.md) and [hardening](docs/hardening.md)
|
|
751
|
+
- [Threat model](docs/threat-model.md)
|
|
752
|
+
- [Observability](docs/observability.md)
|
|
753
|
+
- [Route manifest](docs/route-manifest.md)
|
|
754
|
+
- [CI checks](docs/ci.md)
|
|
755
|
+
- [Compatibility](docs/compatibility.md)
|
|
756
|
+
- [Architecture and plugin authoring](docs/architecture.md)
|
|
757
|
+
|
|
758
|
+
## License
|
|
759
|
+
|
|
760
|
+
MIT
|