@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,550 @@
|
|
|
1
|
+
import type { OpenAPISpec } from '../plugins/openapi/spec-builder';
|
|
2
|
+
import type { AuthEvent, AuthService } from './auth/auth-service';
|
|
3
|
+
import type { FortressConfig, ResolvedCookieConfig } from './config';
|
|
4
|
+
import type { EndpointDefinition } from './endpoint';
|
|
5
|
+
import type { AuthCookiePayload } from './http/cookie-serialize';
|
|
6
|
+
import type { PluginRequestContext } from './http/plugin-middleware';
|
|
7
|
+
import type { ResolvedPrincipal } from './http/principal';
|
|
8
|
+
import type { IamEvent, IamService, PermissionCheckEvent } from './iam/iam-service';
|
|
9
|
+
import type { PermissionSyncOptions, PermissionSyncResult } from './iam/permission-sync';
|
|
10
|
+
import type { RouteManifestEntry } from './manifest/route-manifest';
|
|
11
|
+
import type { MigrationApplyResult } from './migrations/engine';
|
|
12
|
+
import type { FortressLogger } from './observability/logger';
|
|
13
|
+
import type { TelemetryProvider } from './observability/types';
|
|
14
|
+
import type { ToOpenAPIOptions } from './openapi';
|
|
15
|
+
import type { FortressPlugin, MiddlewareDefinition } from './plugin';
|
|
16
|
+
import type { CallableForEndpoints, InferPluginCallMap, InferPlugins } from './plugin-methods-map';
|
|
17
|
+
import { authEndpoints } from './auth/auth-endpoints';
|
|
18
|
+
import { createAuthService } from './auth/auth-service';
|
|
19
|
+
import { resolveCookieConfig } from './config';
|
|
20
|
+
import { Errors } from './errors';
|
|
21
|
+
import { buildCall } from './http/call';
|
|
22
|
+
import { serializeAuthCookies as serializeAuthCookiesFn } from './http/cookie-serialize';
|
|
23
|
+
import { buildHandleRequest } from './http/handle-request';
|
|
24
|
+
import { canonicalizePath } from './http/match';
|
|
25
|
+
import { runPluginMiddleware as runPluginMiddlewareFn } from './http/plugin-middleware';
|
|
26
|
+
import { resolveRequestPrincipal as resolveRequestPrincipalFn } from './http/principal';
|
|
27
|
+
import { extractAccessToken as extractAccessTokenFn } from './http/token-extraction';
|
|
28
|
+
import { iamEndpoints } from './iam/iam-endpoints';
|
|
29
|
+
import { createIamService } from './iam/iam-service';
|
|
30
|
+
import { runPermissionSync } from './iam/permission-sync';
|
|
31
|
+
import { buildRouteManifest } from './manifest/route-manifest';
|
|
32
|
+
import { migrateUp } from './migrations/engine';
|
|
33
|
+
import { instrumentAdapter } from './observability/db-instrumentation';
|
|
34
|
+
import { SILENT_LOGGER } from './observability/logger';
|
|
35
|
+
import { NO_OP_TELEMETRY } from './observability/types';
|
|
36
|
+
import { toOpenAPI as endpointsToOpenAPI } from './openapi';
|
|
37
|
+
import { processPlugins } from './plugin-runner';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Typed in-process call surface. Composes auth + IAM endpoint callables
|
|
41
|
+
* with any plugin-contributed routes (derived from the plugins tuple).
|
|
42
|
+
*
|
|
43
|
+
* Each handler becomes a `(input, options?) => Promise<successBody>` where
|
|
44
|
+
* the input shape is the inferred intersection of the endpoint's body,
|
|
45
|
+
* query, and params schemas, and the output is the inferred 2xx response
|
|
46
|
+
* type. Non-2xx responses throw a `FortressError`.
|
|
47
|
+
*/
|
|
48
|
+
export type TypedCall<T extends readonly FortressPlugin[]>
|
|
49
|
+
= & CallableForEndpoints<typeof authEndpoints>
|
|
50
|
+
& CallableForEndpoints<typeof iamEndpoints>
|
|
51
|
+
& InferPluginCallMap<T>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configured fortress instance returned by {@link createFortress}. Holds the
|
|
55
|
+
* core auth and IAM services, the resolved config, every endpoint definition
|
|
56
|
+
* (auth + IAM + plugin routes), and the typed plugin method surface.
|
|
57
|
+
*
|
|
58
|
+
* Also exposes the framework-agnostic HTTP entry points
|
|
59
|
+
* ({@link Fortress.handleRequest}, {@link Fortress.runPluginMiddleware},
|
|
60
|
+
* {@link Fortress.extractAccessToken}, {@link Fortress.serializeAuthCookies})
|
|
61
|
+
* that adapters delegate to. See `src/core/http/`.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
export interface Fortress<
|
|
65
|
+
// eslint-disable-next-line ts/no-unsafe-function-type -- fallback type for untyped plugin access
|
|
66
|
+
TPlugins = Record<string, Record<string, Function>>,
|
|
67
|
+
TCall = Record<string, (input?: any, options?: any) => Promise<any>>,
|
|
68
|
+
> {
|
|
69
|
+
auth: AuthService;
|
|
70
|
+
iam: IamService;
|
|
71
|
+
plugins: TPlugins;
|
|
72
|
+
/**
|
|
73
|
+
* Typed in-process client. Each key is a handler name (from the core
|
|
74
|
+
* auth/IAM endpoint records or a plugin's `routes` record); each value is
|
|
75
|
+
* an async function whose input/output types are inferred from the
|
|
76
|
+
* endpoint's declared body/query/params/response schemas.
|
|
77
|
+
*
|
|
78
|
+
* Under the hood, each callable serializes its input to a `Request` and
|
|
79
|
+
* delegates to `fortress.handleRequest`, so middleware, token
|
|
80
|
+
* verification, RBAC, and validation all run — the same path a network
|
|
81
|
+
* client would eventually hit. See `src/core/http/call.ts`.
|
|
82
|
+
*/
|
|
83
|
+
call: TCall;
|
|
84
|
+
config: Readonly<FortressConfig>;
|
|
85
|
+
/** All endpoint definitions (auth + IAM + plugins) with JSON Schema metadata. */
|
|
86
|
+
endpoints: EndpointDefinition[];
|
|
87
|
+
/** Canonical generated route-security manifest derived from endpoint metadata. */
|
|
88
|
+
manifest: RouteManifestEntry[];
|
|
89
|
+
/** Resolved auth-cookie names and attributes (NODE_ENV-aware). */
|
|
90
|
+
cookies: ResolvedCookieConfig;
|
|
91
|
+
/** Resolved logger (defaults to a silent no-op when `config.logger` is unset). */
|
|
92
|
+
logger: FortressLogger;
|
|
93
|
+
/** Resolved telemetry provider (defaults to a no-op when `config.observability` is unset). */
|
|
94
|
+
telemetry: TelemetryProvider;
|
|
95
|
+
/**
|
|
96
|
+
* Handle a Fortress-managed request and return a web-standard `Response`.
|
|
97
|
+
* Composes plugin middleware, token verification, default-deny RBAC,
|
|
98
|
+
* validation, and dispatch.
|
|
99
|
+
*
|
|
100
|
+
* Adapters call this for paths owned by Fortress (e.g. `/auth/*`,
|
|
101
|
+
* `/iam/*`, plugin-registered routes). See `src/core/http/handle-request.ts`.
|
|
102
|
+
*/
|
|
103
|
+
handleRequest: (request: Request) => Promise<Response>;
|
|
104
|
+
/**
|
|
105
|
+
* Run plugin middleware at a given lifecycle phase, passing a duck-typed
|
|
106
|
+
* request context. Adapters call this on user-owned routes so plugins
|
|
107
|
+
* like rate-limit and audit-log still apply outside Fortress paths.
|
|
108
|
+
*/
|
|
109
|
+
runPluginMiddleware: (
|
|
110
|
+
phase: MiddlewareDefinition['position'],
|
|
111
|
+
ctx: PluginRequestContext,
|
|
112
|
+
) => Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Extract the access token from a request, preferring the configured
|
|
115
|
+
* cookie and falling back to `Authorization: Bearer`.
|
|
116
|
+
*/
|
|
117
|
+
extractAccessToken: (request: Request) => string | null;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the request principal by trying plugin `resolvePrincipal`
|
|
120
|
+
* hooks (e.g. `api-key`) first, then falling back to the configured JWT
|
|
121
|
+
* bearer token. Non-throwing — returns `null` when no credential is
|
|
122
|
+
* present or the JWT fails to verify.
|
|
123
|
+
*
|
|
124
|
+
* Adapter user-route middleware calls this so api-keys, cookies, and
|
|
125
|
+
* bearer tokens authenticate uniformly on user-owned routes — not just
|
|
126
|
+
* Fortress-owned routes dispatched through `handleRequest`.
|
|
127
|
+
*/
|
|
128
|
+
resolvePrincipal: (request: Request) => Promise<ResolvedPrincipal | null>;
|
|
129
|
+
/**
|
|
130
|
+
* Serialize an auth result (or pair of tokens) into one or two
|
|
131
|
+
* `Set-Cookie` header values using the resolved cookie config and the
|
|
132
|
+
* configured token expiries.
|
|
133
|
+
*/
|
|
134
|
+
serializeAuthCookies: (payload: AuthCookiePayload) => string[];
|
|
135
|
+
/**
|
|
136
|
+
* Run Fortress's pending schema migrations against the configured database
|
|
137
|
+
* and, optionally, an application-supplied migration step afterwards.
|
|
138
|
+
*
|
|
139
|
+
* Fortress migrations always run first because app schemas commonly
|
|
140
|
+
* reference fortress tables (foreign keys to `user.id`, etc.). The
|
|
141
|
+
* `migrateApp` callback (when provided) is awaited after Fortress
|
|
142
|
+
* migrations complete, so a host can do something like:
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* await fortress.migrate({
|
|
146
|
+
* migrateApp: () => migrate(db, { migrationsFolder: './drizzle' }),
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* Eliminates the two-step "forget to call `migrateUp` and silently
|
|
151
|
+
* have no auth tables" footgun.
|
|
152
|
+
*/
|
|
153
|
+
migrate: (opts?: MigrateOptions) => Promise<MigrateResult>;
|
|
154
|
+
/**
|
|
155
|
+
* Seed permissions discovered on registered endpoints (`fortress.endpoints`)
|
|
156
|
+
* into the IAM database and, optionally, bind them onto a set of default
|
|
157
|
+
* roles. Idempotent — re-running adds only what's missing and never
|
|
158
|
+
* revokes anything that was already there.
|
|
159
|
+
*
|
|
160
|
+
* Typical bootstrap sequence:
|
|
161
|
+
*
|
|
162
|
+
* ```ts
|
|
163
|
+
* await fortress.migrate({ migrateApp: () => migrate(db, '...') });
|
|
164
|
+
* await fortress.syncPermissionsFromManifest({
|
|
165
|
+
* defaultRoles: { admin: '*', member: ['school:read', 'school:list'] },
|
|
166
|
+
* });
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* Replaces the seed script every consumer used to write that walked
|
|
170
|
+
* `fortress.endpoints`, deduped `(resource, action)` pairs, and called
|
|
171
|
+
* `iam.createPermission(...)` for each.
|
|
172
|
+
*/
|
|
173
|
+
syncPermissionsFromManifest: (opts?: PermissionSyncOptions) => Promise<PermissionSyncResult>;
|
|
174
|
+
/**
|
|
175
|
+
* Emit a complete OpenAPI 3.1 spec from the endpoint definitions Fortress
|
|
176
|
+
* knows about: core auth/IAM routes, plugin routes, and any top-level
|
|
177
|
+
* host `routes` registered on {@link FortressConfig}.
|
|
178
|
+
*
|
|
179
|
+
* This is the programmatic companion to the OpenAPI plugin. Use it when
|
|
180
|
+
* you already own the docs route or when a build script needs to write
|
|
181
|
+
* `openapi.json` for client codegen:
|
|
182
|
+
*
|
|
183
|
+
* ```ts
|
|
184
|
+
* const spec = fortress.toOpenAPI({
|
|
185
|
+
* title: 'My API',
|
|
186
|
+
* version: '0.0.0',
|
|
187
|
+
* servers: [{ url: 'http://localhost:3001', description: 'Local' }],
|
|
188
|
+
* tags: [{ name: 'Schools' }],
|
|
189
|
+
* });
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* Defaults to endpoint `handler` names for `operationId`, matching most
|
|
193
|
+
* host route-contract files. Pass `operationId: 'methodPath'` to keep the
|
|
194
|
+
* historical OpenAPI-plugin ID style.
|
|
195
|
+
*/
|
|
196
|
+
toOpenAPI: (opts?: FortressToOpenAPIOptions) => OpenAPISpec;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Options accepted by {@link Fortress.toOpenAPI}. */
|
|
200
|
+
export interface FortressToOpenAPIOptions extends ToOpenAPIOptions {
|
|
201
|
+
/** Override the endpoints to emit. Defaults to `fortress.endpoints`. */
|
|
202
|
+
endpoints?: EndpointDefinition[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Options accepted by {@link Fortress.migrate}. */
|
|
206
|
+
export interface MigrateOptions {
|
|
207
|
+
/**
|
|
208
|
+
* Optional host-app migration step. Runs after Fortress migrations
|
|
209
|
+
* complete; any thrown error propagates after Fortress migrations have
|
|
210
|
+
* already been applied. Library-agnostic: pass any `() => Promise<void>`
|
|
211
|
+
* — e.g. drizzle's `migrate(db, { migrationsFolder })`, Knex's
|
|
212
|
+
* `db.migrate.latest()`, a hand-rolled SQL runner.
|
|
213
|
+
*/
|
|
214
|
+
migrateApp?: () => Promise<void>;
|
|
215
|
+
/**
|
|
216
|
+
* Override the migration dialect. Defaults to the adapter's `dialect`
|
|
217
|
+
* property (matches the behavior of {@link migrateUp}).
|
|
218
|
+
*/
|
|
219
|
+
dialect?: 'sqlite' | 'pg';
|
|
220
|
+
/** Stop applying Fortress migrations after this version. Defaults to the latest. */
|
|
221
|
+
targetVersion?: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Result returned by {@link Fortress.migrate}. */
|
|
225
|
+
export interface MigrateResult {
|
|
226
|
+
/** Result of the Fortress-managed migration step. */
|
|
227
|
+
fortress: MigrationApplyResult;
|
|
228
|
+
/** `true` if `migrateApp` was supplied and ran to completion. */
|
|
229
|
+
appRan: boolean;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Type-safe helper to retrieve a plugin's methods from a Fortress instance.
|
|
234
|
+
*
|
|
235
|
+
* Since plugin methods are dynamically typed at runtime, this helper lets
|
|
236
|
+
* consumers provide a known interface for type-safe access without casting.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```ts
|
|
240
|
+
* interface TwoFactorMethods {
|
|
241
|
+
* setup: (userId: string) => Promise<{ secret: string; qrCode: string }>;
|
|
242
|
+
* verify: (userId: string, code: string) => Promise<boolean>;
|
|
243
|
+
* }
|
|
244
|
+
*
|
|
245
|
+
* const twoFactor = getPluginMethods<TwoFactorMethods>(fortress, 'two-factor');
|
|
246
|
+
* const result = await twoFactor.setup(userId); // fully typed
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export function getPluginMethods<T>(fortress: Fortress, pluginName: string): T {
|
|
250
|
+
const methods = fortress.plugins[pluginName];
|
|
251
|
+
if (!methods) {
|
|
252
|
+
throw Errors.notFound(`Plugin '${pluginName}' is not registered`);
|
|
253
|
+
}
|
|
254
|
+
return methods as T;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const MIN_SECRET_BYTES = 32;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Build a configured {@link Fortress} instance from a {@link FortressConfig}.
|
|
261
|
+
*
|
|
262
|
+
* Validates the JWT secret strength, deduplicates and processes the plugin
|
|
263
|
+
* list, wires up auth/IAM services, and returns the assembled instance with
|
|
264
|
+
* type-safe plugin method access for any plugins listed in the config.
|
|
265
|
+
*/
|
|
266
|
+
export function createFortress<const T extends readonly FortressPlugin[]>(
|
|
267
|
+
config: FortressConfig & { plugins?: T },
|
|
268
|
+
): Fortress<InferPlugins<T>, TypedCall<T>> {
|
|
269
|
+
// Validate JWT key strength (HS256 shared-secret bytes).
|
|
270
|
+
const keys = Array.isArray(config.jwt.key) ? config.jwt.key : [config.jwt.key];
|
|
271
|
+
for (const key of keys) {
|
|
272
|
+
if (new TextEncoder().encode(key).length < MIN_SECRET_BYTES) {
|
|
273
|
+
throw Errors.badRequest(
|
|
274
|
+
`JWT key must be at least ${MIN_SECRET_BYTES} bytes for HS256 security. Got ${new TextEncoder().encode(key).length} bytes.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const session = config.jwt.session;
|
|
280
|
+
if (session) {
|
|
281
|
+
for (const [name, value] of Object.entries(session)) {
|
|
282
|
+
if (value != null && (!Number.isInteger(value) || value <= 0)) {
|
|
283
|
+
throw Errors.badRequest(`jwt.session.${name} must be a positive integer`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Synthesize a virtual plugin from any top-level `routes` field so host
|
|
289
|
+
// apps don't have to hand-roll a one-field plugin just to register their
|
|
290
|
+
// own endpoints. Prepended to the plugin list so its routes appear before
|
|
291
|
+
// explicit plugins in the manifest, matching the registration order a
|
|
292
|
+
// user would write themselves.
|
|
293
|
+
const HOST_ROUTES_PLUGIN_NAME = '__host';
|
|
294
|
+
const userPlugins = config.plugins ?? [];
|
|
295
|
+
if (config.routes) {
|
|
296
|
+
const collision = userPlugins.find(p => p.name === HOST_ROUTES_PLUGIN_NAME);
|
|
297
|
+
if (collision) {
|
|
298
|
+
throw Errors.badRequest(
|
|
299
|
+
`Plugin name '${HOST_ROUTES_PLUGIN_NAME}' is reserved for top-level \`routes\`; rename your plugin.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const hostRoutesPlugin: FortressPlugin | null = config.routes
|
|
304
|
+
? { name: HOST_ROUTES_PLUGIN_NAME, routes: config.routes }
|
|
305
|
+
: null;
|
|
306
|
+
const plugins = hostRoutesPlugin ? [hostRoutesPlugin, ...userPlugins] : userPlugins;
|
|
307
|
+
|
|
308
|
+
// Resolve observability defaults. SILENT_LOGGER and NO_OP_TELEMETRY are
|
|
309
|
+
// zero-allocation singletons — if the caller doesn't opt in, Fortress
|
|
310
|
+
// never writes to stderr and every metric/span call is a no-op.
|
|
311
|
+
const logger = config.logger ?? SILENT_LOGGER;
|
|
312
|
+
const telemetry = config.observability ?? NO_OP_TELEMETRY;
|
|
313
|
+
|
|
314
|
+
// Wrap the adapter with DB instrumentation. When `telemetry` is the
|
|
315
|
+
// no-op default, the wrapper records into a no-op histogram — essentially
|
|
316
|
+
// free. When a real OTel adapter is wired, every Fortress-internal query
|
|
317
|
+
// (and every plugin query that also uses this adapter) emits the stable
|
|
318
|
+
// `db.client.operation.duration` metric with standard attributes.
|
|
319
|
+
const db = instrumentAdapter(config.database, telemetry);
|
|
320
|
+
|
|
321
|
+
// Validate plugin name uniqueness
|
|
322
|
+
const pluginNames = new Set<string>();
|
|
323
|
+
for (const plugin of plugins) {
|
|
324
|
+
if (pluginNames.has(plugin.name)) {
|
|
325
|
+
throw Errors.badRequest(`Duplicate plugin name: '${plugin.name}'`);
|
|
326
|
+
}
|
|
327
|
+
pluginNames.add(plugin.name);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Token-verify histogram is built before the auth service so it can be
|
|
331
|
+
// passed in via deps. Kept here (not inside auth-service) so the metric
|
|
332
|
+
// catalog lives in one place.
|
|
333
|
+
const tokenVerifyDuration = telemetry.meter.createHistogram('fortress.auth.token_verify.duration', {
|
|
334
|
+
unit: 's',
|
|
335
|
+
description: 'JWT access token verification latency',
|
|
336
|
+
boundaries: [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const auth = createAuthService(db, config, plugins, { logger, telemetry, tokenVerifyDuration });
|
|
340
|
+
const iam = createIamService(db, config, { logger, telemetry });
|
|
341
|
+
const pluginMethods = processPlugins(plugins, db, config, auth, iam, logger);
|
|
342
|
+
|
|
343
|
+
// --- Wire built-in telemetry observers ------------------------------
|
|
344
|
+
//
|
|
345
|
+
// Translate AuthEvent / IamEvent / PermissionCheckEvent into metric
|
|
346
|
+
// updates. Attribute keys follow the Prometheus `.total` convention
|
|
347
|
+
// for counters and seconds for durations. User IDs are NEVER placed
|
|
348
|
+
// on metric attributes (cardinality bomb) — they go on spans/logs.
|
|
349
|
+
const authEventCounter = telemetry.meter.createCounter('fortress.auth.events.total', {
|
|
350
|
+
description: 'Auth lifecycle events (login, register, refresh, logout, token reuse)',
|
|
351
|
+
});
|
|
352
|
+
const iamEventCounter = telemetry.meter.createCounter('fortress.iam.events.total', {
|
|
353
|
+
description: 'IAM mutation events (role/permission/binding/group changes)',
|
|
354
|
+
});
|
|
355
|
+
const permissionCheckDuration = telemetry.meter.createHistogram('fortress.iam.permission_check.duration', {
|
|
356
|
+
unit: 's',
|
|
357
|
+
description: 'Permission check latency',
|
|
358
|
+
boundaries: [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
|
|
359
|
+
});
|
|
360
|
+
const cacheHitCounter = telemetry.meter.createCounter('fortress.iam.permission_check.cache.hits', {
|
|
361
|
+
description: 'Permission check resolved from cache',
|
|
362
|
+
});
|
|
363
|
+
const cacheMissCounter = telemetry.meter.createCounter('fortress.iam.permission_check.cache.misses', {
|
|
364
|
+
description: 'Permission check that hit the database',
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
auth.addAuthObserver((event: AuthEvent) => {
|
|
368
|
+
authEventCounter.add(1, {
|
|
369
|
+
event: event.eventType,
|
|
370
|
+
outcome: event.outcome ?? 'n/a',
|
|
371
|
+
method: event.method ?? 'n/a',
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
iam.addIamObserver((event: IamEvent) => {
|
|
376
|
+
iamEventCounter.add(1, { event: event.eventType });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
iam.addPermissionCheckObserver((event: PermissionCheckEvent) => {
|
|
380
|
+
permissionCheckDuration.record(event.durationSeconds, {
|
|
381
|
+
subject_type: event.subjectType,
|
|
382
|
+
result: event.allowed ? 'allow' : 'deny',
|
|
383
|
+
cached: event.cached ? 'true' : 'false',
|
|
384
|
+
});
|
|
385
|
+
if (event.cached) {
|
|
386
|
+
cacheHitCounter.add(1);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
cacheMissCounter.add(1);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Wire IAM events → audit log if the plugin is registered
|
|
394
|
+
if (pluginMethods['audit-log']?.logCustomEvent) {
|
|
395
|
+
const logCustomEvent = pluginMethods['audit-log'].logCustomEvent as (event: IamEvent) => Promise<void>;
|
|
396
|
+
iam.addIamObserver(event => logCustomEvent(event));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Assemble all endpoint definitions. A plugin may intentionally override a
|
|
400
|
+
// core route, but two plugins claiming the same method+path is ambiguous and
|
|
401
|
+
// therefore rejected instead of depending on registration order.
|
|
402
|
+
const endpointMap = new Map<string, EndpointDefinition>();
|
|
403
|
+
const endpointOwners = new Map<string, string>();
|
|
404
|
+
const coreEndpoints: EndpointDefinition[] = [
|
|
405
|
+
...Object.values(authEndpoints) as EndpointDefinition[],
|
|
406
|
+
...Object.values(iamEndpoints) as EndpointDefinition[],
|
|
407
|
+
];
|
|
408
|
+
for (const ep of coreEndpoints) {
|
|
409
|
+
const routeKey = `${ep.method.toUpperCase()} ${canonicalizePath(ep.path)}`;
|
|
410
|
+
endpointMap.set(routeKey, ep);
|
|
411
|
+
endpointOwners.set(routeKey, 'core');
|
|
412
|
+
}
|
|
413
|
+
for (const plugin of plugins) {
|
|
414
|
+
for (const ep of Object.values(plugin.routes ?? {}) as EndpointDefinition[]) {
|
|
415
|
+
const routeKey = `${ep.method.toUpperCase()} ${canonicalizePath(ep.path)}`;
|
|
416
|
+
const owner = endpointOwners.get(routeKey);
|
|
417
|
+
if (owner && owner !== 'core') {
|
|
418
|
+
throw Errors.badRequest(
|
|
419
|
+
`Duplicate endpoint ${routeKey} declared by plugins "${owner}" and "${plugin.name}"`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
endpointMap.set(routeKey, ep);
|
|
423
|
+
endpointOwners.set(routeKey, plugin.name);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const endpoints = Array.from(endpointMap.values());
|
|
427
|
+
|
|
428
|
+
// L-tier: fail-fast on `security: ['none']` + `permission` collisions.
|
|
429
|
+
// The two are mutually exclusive: an unauthenticated route has no subject
|
|
430
|
+
// to evaluate the permission against, so default-deny RBAC would always
|
|
431
|
+
// reject it. Catch the misconfiguration at startup, not at first request.
|
|
432
|
+
const oauthSelfAuthAllowlist = new Set([
|
|
433
|
+
'GET /oauth/authorize',
|
|
434
|
+
'POST /oauth/token',
|
|
435
|
+
'POST /oauth/introspect',
|
|
436
|
+
'POST /oauth/revoke',
|
|
437
|
+
'GET /oauth/userinfo',
|
|
438
|
+
'GET /oauth/.well-known/openid-configuration',
|
|
439
|
+
'GET /oauth/.well-known/jwks.json',
|
|
440
|
+
]);
|
|
441
|
+
for (const ep of endpoints) {
|
|
442
|
+
const security = ep.meta?.security ?? [];
|
|
443
|
+
if (security.includes('none') && ep.meta?.permission) {
|
|
444
|
+
throw Errors.badRequest(
|
|
445
|
+
`Endpoint ${ep.method} ${ep.path} declares security:['none'] AND a permission `
|
|
446
|
+
+ `(${ep.meta.permission.resource}:${ep.meta.permission.action}). These are mutually `
|
|
447
|
+
+ `exclusive — default-deny RBAC would reject every request.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// P3.6: `bearerKind: 'oauth'` means "this handler self-authenticates"
|
|
452
|
+
// and therefore skips the normal plugin/JWT/RBAC/JSON-validation
|
|
453
|
+
// pipeline. It is intentionally reserved for the OAuth protocol routes
|
|
454
|
+
// that parse form bodies or OAuth access tokens themselves. Any other
|
|
455
|
+
// route trying to use it is almost certainly a latent auth bypass.
|
|
456
|
+
if (ep.meta?.bearerKind === 'oauth' && !oauthSelfAuthAllowlist.has(`${ep.method} ${ep.path}`)) {
|
|
457
|
+
throw Errors.badRequest(
|
|
458
|
+
`Endpoint ${ep.method} ${ep.path} sets bearerKind:'oauth' but is not an approved self-auth OAuth protocol route.`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Resolve cookie config once at startup so all HTTP entry points share names.
|
|
464
|
+
const cookies = resolveCookieConfig(config.cookies);
|
|
465
|
+
|
|
466
|
+
// Build the framework-agnostic HTTP auxiliary closures upfront. `handleRequest`
|
|
467
|
+
// and `call` both get bound after the instance is constructed because
|
|
468
|
+
// they need the assembled `Fortress` object (route table is built from
|
|
469
|
+
// `endpoints`; `call` delegates to `handleRequest`).
|
|
470
|
+
let routeManifest: RouteManifestEntry[] | undefined;
|
|
471
|
+
|
|
472
|
+
const instance: Fortress<InferPlugins<T>, TypedCall<T>> = {
|
|
473
|
+
auth,
|
|
474
|
+
iam,
|
|
475
|
+
plugins: pluginMethods as InferPlugins<T>,
|
|
476
|
+
call: {} as TypedCall<T>,
|
|
477
|
+
config,
|
|
478
|
+
endpoints,
|
|
479
|
+
get manifest(): RouteManifestEntry[] {
|
|
480
|
+
routeManifest ??= buildRouteManifest(this as Fortress);
|
|
481
|
+
return routeManifest;
|
|
482
|
+
},
|
|
483
|
+
cookies,
|
|
484
|
+
logger,
|
|
485
|
+
telemetry,
|
|
486
|
+
handleRequest: undefined as unknown as (request: Request) => Promise<Response>,
|
|
487
|
+
runPluginMiddleware: (phase, ctx): Promise<void> => runPluginMiddlewareFn(plugins, config, phase, ctx),
|
|
488
|
+
extractAccessToken: (request: Request): string | null => extractAccessTokenFn(request, cookies),
|
|
489
|
+
resolvePrincipal: (request: Request): Promise<ResolvedPrincipal | null> =>
|
|
490
|
+
resolveRequestPrincipalFn(instance as Fortress, request),
|
|
491
|
+
serializeAuthCookies: (payload: AuthCookiePayload): string[] =>
|
|
492
|
+
serializeAuthCookiesFn(payload, cookies, {
|
|
493
|
+
access: config.jwt.accessTokenExpirySeconds ?? 900,
|
|
494
|
+
refresh: config.jwt.refreshTokenExpirySeconds ?? 604_800,
|
|
495
|
+
}),
|
|
496
|
+
async migrate(opts?: MigrateOptions): Promise<MigrateResult> {
|
|
497
|
+
// Fortress migrations first — app schemas commonly FK to fortress
|
|
498
|
+
// tables, so this order is the safe default. Unwrap the instrumented
|
|
499
|
+
// adapter so `db.dialect` reflects the underlying engine.
|
|
500
|
+
const fortressResult = await migrateUp(db, opts?.dialect, opts?.targetVersion);
|
|
501
|
+
let appRan = false;
|
|
502
|
+
if (opts?.migrateApp) {
|
|
503
|
+
await opts.migrateApp();
|
|
504
|
+
appRan = true;
|
|
505
|
+
}
|
|
506
|
+
return { fortress: fortressResult, appRan };
|
|
507
|
+
},
|
|
508
|
+
syncPermissionsFromManifest(opts?: PermissionSyncOptions): Promise<PermissionSyncResult> {
|
|
509
|
+
return runPermissionSync(iam, opts?.endpoints ?? instance.endpoints, opts);
|
|
510
|
+
},
|
|
511
|
+
toOpenAPI(opts?: FortressToOpenAPIOptions): OpenAPISpec {
|
|
512
|
+
return endpointsToOpenAPI(opts?.endpoints ?? instance.endpoints, opts);
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
instance.handleRequest = buildHandleRequest(instance as Fortress);
|
|
517
|
+
|
|
518
|
+
// Assemble the typed call map. Core auth/IAM handlers come first; plugin
|
|
519
|
+
// routes are layered on top so they can override a core handler that
|
|
520
|
+
// shares a key (matches the `endpointMap` dedup order).
|
|
521
|
+
const callEndpoints: Record<string, EndpointDefinition> = {
|
|
522
|
+
...(authEndpoints as unknown as Record<string, EndpointDefinition>),
|
|
523
|
+
...(iamEndpoints as unknown as Record<string, EndpointDefinition>),
|
|
524
|
+
};
|
|
525
|
+
const callOwners = new Map<string, string>();
|
|
526
|
+
for (const key of Object.keys(callEndpoints))
|
|
527
|
+
callOwners.set(key, 'core');
|
|
528
|
+
for (const plugin of plugins) {
|
|
529
|
+
// Top-level `routes` are metadata for manifest/OpenAPI/protected host
|
|
530
|
+
// routes; they do not come with plugin methods, so exposing them through
|
|
531
|
+
// `fortress.call.*` would create a runtime `NOT_FOUND` footgun. If a
|
|
532
|
+
// consumer wants typed in-process callables for custom routes, use a real
|
|
533
|
+
// plugin that supplies both `routes` and matching `methods`.
|
|
534
|
+
if (plugin.name === HOST_ROUTES_PLUGIN_NAME)
|
|
535
|
+
continue;
|
|
536
|
+
for (const [key, endpoint] of Object.entries(plugin.routes ?? {}) as [string, EndpointDefinition][]) {
|
|
537
|
+
const owner = callOwners.get(key);
|
|
538
|
+
if (owner && owner !== 'core') {
|
|
539
|
+
throw Errors.badRequest(
|
|
540
|
+
`Duplicate fortress.call key "${key}" declared by plugins "${owner}" and "${plugin.name}"`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
callEndpoints[key] = endpoint;
|
|
544
|
+
callOwners.set(key, plugin.name);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
instance.call = buildCall(instance as Fortress, callEndpoints) as TypedCall<T>;
|
|
548
|
+
|
|
549
|
+
return instance;
|
|
550
|
+
}
|