@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,551 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../adapters/database';
|
|
2
|
+
import type { Fortress } from '../core/fortress';
|
|
3
|
+
import type { PluginRequestContext } from '../core/http/plugin-middleware';
|
|
4
|
+
import type { MiddlewareDefinition, PluginContext } from '../core/plugin';
|
|
5
|
+
import type { Subject, TokenClaims } from '../core/types';
|
|
6
|
+
import { FortressError } from '../core/errors';
|
|
7
|
+
import {
|
|
8
|
+
chainAdapterWrappers,
|
|
9
|
+
collectScopeRules,
|
|
10
|
+
executePluginMiddleware,
|
|
11
|
+
wrapAdapterWithScopeRules,
|
|
12
|
+
} from '../core/plugin-runner';
|
|
13
|
+
|
|
14
|
+
// Minimal Express-compatible types so users bring their own express version
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fortress-specific fields attached to the Express `Request` by
|
|
18
|
+
* {@link createAuthMiddleware}. Exported so host apps can declaration-merge
|
|
19
|
+
* them into express's native `Request` type for typed access without casts.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // src/types/express.d.ts
|
|
24
|
+
* import type { FortressExpressFields } from '@bajustone/fortress/express';
|
|
25
|
+
*
|
|
26
|
+
* declare module 'express-serve-static-core' {
|
|
27
|
+
* interface Request extends FortressExpressFields {}
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export interface FortressExpressFields {
|
|
32
|
+
/**
|
|
33
|
+
* The resolved principal — set for every authenticated request regardless
|
|
34
|
+
* of credential type (JWT, api-key, future OAuth client_credentials, mTLS).
|
|
35
|
+
*/
|
|
36
|
+
fortressSubject?: Subject;
|
|
37
|
+
/**
|
|
38
|
+
* Convenience alias — populated **only** when the subject is a `USER`.
|
|
39
|
+
* Non-USER subjects (e.g. `SERVICE_ACCOUNT` via api-key) leave this
|
|
40
|
+
* undefined; fall back to {@link fortressSubject}.
|
|
41
|
+
*/
|
|
42
|
+
fortressUserId?: string;
|
|
43
|
+
fortressClaims?: TokenClaims;
|
|
44
|
+
fortressScopes?: string[] | null;
|
|
45
|
+
fortressDb?: DatabaseAdapter;
|
|
46
|
+
fortressGetScopedDb?: (model: string) => Promise<DatabaseAdapter>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Minimal Express request shape fortress reads from. Compatible with any modern Express version. */
|
|
50
|
+
export interface ExpressRequest extends FortressExpressFields {
|
|
51
|
+
headers: Record<string, string | string[] | undefined>;
|
|
52
|
+
method: string;
|
|
53
|
+
/** Express's route path without the query string. */
|
|
54
|
+
path: string;
|
|
55
|
+
/** Original path including query string, when supplied by Express. */
|
|
56
|
+
originalUrl?: string;
|
|
57
|
+
/** Node/Express request URL fallback, usually including the query string. */
|
|
58
|
+
url?: string;
|
|
59
|
+
/** Resolved request protocol (`http`/`https`) when supplied by Express. */
|
|
60
|
+
protocol?: string;
|
|
61
|
+
/** Parsed request body, if body middleware ran before this slot. */
|
|
62
|
+
body?: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Minimal Express response shape fortress writes to. */
|
|
66
|
+
export interface ExpressResponse {
|
|
67
|
+
status: (code: number) => ExpressResponse;
|
|
68
|
+
json: (body: unknown) => void;
|
|
69
|
+
setHeader: (name: string, value: string) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The `next(err?)` callback Express middleware signs off with. */
|
|
73
|
+
export type ExpressNextFunction = (err?: unknown) => void;
|
|
74
|
+
/** Express middleware signature fortress's adapter exports use. */
|
|
75
|
+
export type ExpressMiddleware = (req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => void;
|
|
76
|
+
|
|
77
|
+
// --- Route mapping ---
|
|
78
|
+
|
|
79
|
+
/** A `(resource, action)` IAM mapping for an HTTP route. */
|
|
80
|
+
export interface RouteMapping {
|
|
81
|
+
resource: string;
|
|
82
|
+
action: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Options accepted by {@link createRbacMiddleware} (and the express adapter factory). */
|
|
86
|
+
export interface RbacOptions {
|
|
87
|
+
routeMap?: Record<string, RouteMapping>;
|
|
88
|
+
mapRequest?: (method: string, path: string) => RouteMapping | null;
|
|
89
|
+
skipPaths?: string[];
|
|
90
|
+
/**
|
|
91
|
+
* Behavior when a non-skipped route matches no `routeMap`/`mapRequest`
|
|
92
|
+
* entry. `'allow'` (default) treats it as public; `'deny'` fails closed
|
|
93
|
+
* with a 403 so a forgotten mapping can't silently expose a user route.
|
|
94
|
+
* List genuinely public routes in {@link RbacOptions.skipPaths} when using
|
|
95
|
+
* `'deny'`.
|
|
96
|
+
*/
|
|
97
|
+
unmappedRoutes?: 'allow' | 'deny';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Auth middleware ---
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build the Express auth middleware. Resolves the request principal via
|
|
104
|
+
* `fortress.resolvePrincipal`, which tries plugin `resolvePrincipal` hooks
|
|
105
|
+
* (api-key, future OAuth client_credentials, mTLS) first and then falls
|
|
106
|
+
* back to the JWT bearer token (cookie-first, `Authorization: Bearer`
|
|
107
|
+
* second). Populates `fortressSubject`, `fortressUserId` (USER alias),
|
|
108
|
+
* `fortressClaims`, `fortressDb`, and `fortressGetScopedDb` on the request.
|
|
109
|
+
*/
|
|
110
|
+
export function createAuthMiddleware(fortress: Fortress): ExpressMiddleware {
|
|
111
|
+
return async (req, _res, next) => {
|
|
112
|
+
try {
|
|
113
|
+
// Adapt the Express request into a minimal web Request so
|
|
114
|
+
// fortress.resolvePrincipal can read headers / cookies uniformly.
|
|
115
|
+
const headerJar = new Headers();
|
|
116
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
117
|
+
if (Array.isArray(v)) {
|
|
118
|
+
for (const item of v) headerJar.append(k, item);
|
|
119
|
+
}
|
|
120
|
+
else if (typeof v === 'string') {
|
|
121
|
+
headerJar.set(k, v);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const requestPath = expressRequestPath(req);
|
|
125
|
+
const host = expressHeader(req, 'host') ?? 'localhost';
|
|
126
|
+
const protocol = req.protocol ?? expressHeader(req, 'x-forwarded-proto') ?? 'http';
|
|
127
|
+
const probe = new Request(`${protocol}://${host}${requestPath}`, {
|
|
128
|
+
method: req.method,
|
|
129
|
+
headers: headerJar,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const resolved = await fortress.resolvePrincipal(probe);
|
|
133
|
+
if (!resolved) {
|
|
134
|
+
throw new FortressError('UNAUTHORIZED', 'Missing or invalid credentials', 401);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { subject, claims, scopes } = resolved;
|
|
138
|
+
req.fortressSubject = subject;
|
|
139
|
+
if (subject.type === 'USER')
|
|
140
|
+
req.fortressUserId = subject.id;
|
|
141
|
+
if (claims)
|
|
142
|
+
req.fortressClaims = claims;
|
|
143
|
+
if (scopes !== undefined)
|
|
144
|
+
req.fortressScopes = scopes;
|
|
145
|
+
|
|
146
|
+
const plugins = fortress.config.plugins ?? [];
|
|
147
|
+
// Tenant comes from the verified JWT claim, never a client header.
|
|
148
|
+
const requestContext: Record<string, unknown> = {
|
|
149
|
+
tenantId: claims?.customClaims?.tenantId,
|
|
150
|
+
ipAddress: req.headers['x-forwarded-for'] ?? req.headers['x-real-ip'],
|
|
151
|
+
userAgent: req.headers['user-agent'],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const wrappedAdapter = chainAdapterWrappers(plugins, fortress.config.database, requestContext);
|
|
155
|
+
req.fortressDb = wrappedAdapter;
|
|
156
|
+
|
|
157
|
+
const pluginCtx: PluginContext = { db: wrappedAdapter, config: fortress.config };
|
|
158
|
+
req.fortressGetScopedDb = async (model: string): Promise<DatabaseAdapter> => {
|
|
159
|
+
const scopeRule = await collectScopeRules(plugins, subject.id, model, pluginCtx);
|
|
160
|
+
if (!scopeRule)
|
|
161
|
+
return wrappedAdapter;
|
|
162
|
+
return wrapAdapterWithScopeRules(wrappedAdapter, scopeRule);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
next();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
next(err);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- CSRF middleware (user routes only) ---
|
|
174
|
+
|
|
175
|
+
/** Options accepted by {@link createCsrfMiddleware}. */
|
|
176
|
+
export interface CsrfConfig {
|
|
177
|
+
/** Header name required on unsafe methods. Default: `X-Fortress-CSRF`. */
|
|
178
|
+
headerName?: string;
|
|
179
|
+
/** Paths/subtrees exempt from CSRF checking. A trailing `/*` is accepted. */
|
|
180
|
+
skipPaths?: string[];
|
|
181
|
+
/** Methods exempt from CSRF checking. Default: GET, HEAD, OPTIONS. */
|
|
182
|
+
safeMethods?: string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build standalone Express CSRF middleware for host-owned routes. Fortress-
|
|
187
|
+
* managed routes already enforce cookie-aware CSRF inside `handleRequest`.
|
|
188
|
+
* This middleware uses the custom-header strategy and rejects cross-site
|
|
189
|
+
* browser requests on unsafe methods.
|
|
190
|
+
*/
|
|
191
|
+
export function createCsrfMiddleware(config?: CsrfConfig): ExpressMiddleware {
|
|
192
|
+
const headerName = (config?.headerName ?? 'X-Fortress-CSRF').toLowerCase();
|
|
193
|
+
const skipPaths = config?.skipPaths ?? [];
|
|
194
|
+
const safeMethods = new Set((config?.safeMethods ?? ['GET', 'HEAD', 'OPTIONS']).map(method => method.toUpperCase()));
|
|
195
|
+
|
|
196
|
+
return (req, _res, next) => {
|
|
197
|
+
try {
|
|
198
|
+
if (safeMethods.has(req.method.toUpperCase()) || matchesCsrfSkipPath(req.path, skipPaths)) {
|
|
199
|
+
next();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const fetchSite = expressHeader(req, 'sec-fetch-site');
|
|
204
|
+
if (fetchSite === 'cross-site')
|
|
205
|
+
throw new FortressError('FORBIDDEN', 'CSRF: cross-site request rejected', 403);
|
|
206
|
+
if (!expressHeader(req, headerName))
|
|
207
|
+
throw new FortressError('FORBIDDEN', `CSRF: missing ${config?.headerName ?? 'X-Fortress-CSRF'} header`, 403);
|
|
208
|
+
|
|
209
|
+
next();
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
next(error);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- RBAC middleware (user routes only) ---
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Build the Express RBAC middleware for **user-owned routes**. Resolves a
|
|
221
|
+
* `(resource, action)` mapping via `routeMap` / `mapRequest` and enforces
|
|
222
|
+
* the IAM permission check. Fortress-managed routes (`/auth/*`, `/iam/*`,
|
|
223
|
+
* plugin paths) are protected automatically inside `fortress.handleRequest`
|
|
224
|
+
* — this middleware only handles routes the caller registered themselves.
|
|
225
|
+
*/
|
|
226
|
+
export function createRbacMiddleware(fortress: Fortress, options?: RbacOptions): ExpressMiddleware {
|
|
227
|
+
// Express's default router is case-insensitive and ignores one trailing slash.
|
|
228
|
+
// Canonicalize configured paths once so exact and parameterized lookups share
|
|
229
|
+
// those semantics with the request path below.
|
|
230
|
+
const routeMap = normalizeRouteMap(options?.routeMap ?? {});
|
|
231
|
+
const skipPaths = options?.skipPaths ?? [];
|
|
232
|
+
const skipPatterns = skipPaths.map(p => pathToRegex(p));
|
|
233
|
+
const unmappedRoutes = options?.unmappedRoutes ?? 'allow';
|
|
234
|
+
|
|
235
|
+
return async (req, _res, next) => {
|
|
236
|
+
try {
|
|
237
|
+
// Express strips an `app.use('/mount', ...)` prefix from req.path/url.
|
|
238
|
+
// Match against originalUrl so route maps use the same full path as
|
|
239
|
+
// Hono/SvelteKit and as the host wrote in configuration.
|
|
240
|
+
const path = normalizeExpressPath(new URL(expressRequestPath(req), 'http://localhost').pathname);
|
|
241
|
+
const method = req.method;
|
|
242
|
+
|
|
243
|
+
if (skipPatterns.some(pattern => pattern.test(path))) {
|
|
244
|
+
next();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const key = `${method} ${path}`;
|
|
249
|
+
let mapping: RouteMapping | null = routeMap[key] ?? null;
|
|
250
|
+
|
|
251
|
+
if (!mapping) {
|
|
252
|
+
mapping = findRouteMapMatch(method, path, routeMap);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!mapping && options?.mapRequest) {
|
|
256
|
+
mapping = options.mapRequest(method, path);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!mapping) {
|
|
260
|
+
if (unmappedRoutes === 'deny') {
|
|
261
|
+
// Fail closed: an unmapped route under deny mode is refused rather
|
|
262
|
+
// than treated as public. Authenticating won't help — the route
|
|
263
|
+
// needs an explicit mapping or a skipPaths entry.
|
|
264
|
+
throw new FortressError('FORBIDDEN', 'No permission mapping for this route', 403);
|
|
265
|
+
}
|
|
266
|
+
// No declarative mapping — caller treats this as a public route.
|
|
267
|
+
next();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!req.fortressSubject) {
|
|
272
|
+
throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const allowed = await fortress.iam.checkPermission(req.fortressSubject, mapping.resource, mapping.action, {
|
|
276
|
+
credentialScopes: req.fortressScopes,
|
|
277
|
+
});
|
|
278
|
+
if (!allowed) {
|
|
279
|
+
throw new FortressError('FORBIDDEN', 'Insufficient permissions', 403);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
next();
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
next(err);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Error handler ---
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Build the Express error handler. Translates {@link FortressError} into the
|
|
294
|
+
* appropriate HTTP response with `Retry-After` for rate-limited responses,
|
|
295
|
+
* and returns a generic 500 for everything else.
|
|
296
|
+
*
|
|
297
|
+
* Stays synchronous so it composes naturally with Express's `(err, req,
|
|
298
|
+
* res, next)` signature. Shares the response *shape* with core's
|
|
299
|
+
* `errorToResponse` via {@link FortressError.toJSON}. When a `Fortress`
|
|
300
|
+
* instance is provided, unhandled errors are routed to its configured
|
|
301
|
+
* {@link FortressLogger}; otherwise they're silently swallowed (matching
|
|
302
|
+
* the silent-by-default posture of the library).
|
|
303
|
+
*/
|
|
304
|
+
export function createErrorHandler(fortress?: Fortress): (err: unknown, req: ExpressRequest, res: ExpressResponse, next: ExpressNextFunction) => void {
|
|
305
|
+
return (err, _req, res, _next) => {
|
|
306
|
+
if (err instanceof FortressError) {
|
|
307
|
+
if (err.code === 'RATE_LIMITED' && err.retryAfter) {
|
|
308
|
+
res.setHeader('Retry-After', String(err.retryAfter));
|
|
309
|
+
}
|
|
310
|
+
res.status(err.statusCode).json(err.toJSON());
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fortress?.logger.error(
|
|
315
|
+
{ err, message: err instanceof Error ? err.message : 'Unknown error' },
|
|
316
|
+
'unhandled error in express middleware',
|
|
317
|
+
);
|
|
318
|
+
res.status(500).json({ code: 'INTERNAL_ERROR', message: 'Internal server error', statusCode: 500 });
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Plugin middleware ---
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Express middleware that executes plugin-defined middleware for a given
|
|
326
|
+
* position (`before-auth`, `after-auth`, `after-rbac`). Used internally by
|
|
327
|
+
* {@link createExpressMiddleware}; expose if you want to mount the plugin
|
|
328
|
+
* middleware slots manually.
|
|
329
|
+
*/
|
|
330
|
+
export function createExpressPluginMiddleware(
|
|
331
|
+
fortress: Fortress,
|
|
332
|
+
position: MiddlewareDefinition['position'],
|
|
333
|
+
): ExpressMiddleware {
|
|
334
|
+
const plugins = fortress.config.plugins ?? [];
|
|
335
|
+
|
|
336
|
+
return async (req, _res, next) => {
|
|
337
|
+
try {
|
|
338
|
+
const ctx: PluginContext = { db: fortress.config.database, config: fortress.config };
|
|
339
|
+
const headers = new Headers();
|
|
340
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
341
|
+
if (Array.isArray(value)) {
|
|
342
|
+
for (const item of value) headers.append(name, item);
|
|
343
|
+
}
|
|
344
|
+
else if (typeof value === 'string') {
|
|
345
|
+
headers.set(name, value);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const requestPath = expressRequestPath(req);
|
|
349
|
+
const forwardedProtocol = req.headers['x-forwarded-proto'];
|
|
350
|
+
const protocol = req.protocol
|
|
351
|
+
?? (Array.isArray(forwardedProtocol) ? forwardedProtocol[0] : forwardedProtocol)
|
|
352
|
+
?? 'http';
|
|
353
|
+
const hostHeader = req.headers.host;
|
|
354
|
+
const host = (Array.isArray(hostHeader) ? hostHeader[0] : hostHeader) ?? 'localhost';
|
|
355
|
+
const method = req.method.toUpperCase();
|
|
356
|
+
let body: BodyInit | undefined;
|
|
357
|
+
if (method !== 'GET' && method !== 'HEAD' && req.body !== undefined) {
|
|
358
|
+
if (typeof req.body === 'string' || req.body instanceof URLSearchParams)
|
|
359
|
+
body = req.body;
|
|
360
|
+
else if (req.body instanceof ArrayBuffer || ArrayBuffer.isView(req.body))
|
|
361
|
+
body = req.body as BodyInit;
|
|
362
|
+
else if (headers.get('content-type')?.toLowerCase().startsWith('application/x-www-form-urlencoded'))
|
|
363
|
+
body = new URLSearchParams(Object.entries(req.body as Record<string, unknown>).map(([key, value]) => [key, String(value)]));
|
|
364
|
+
else
|
|
365
|
+
body = JSON.stringify(req.body);
|
|
366
|
+
}
|
|
367
|
+
const requestContext: PluginRequestContext = {
|
|
368
|
+
request: new Request(`${protocol}://${host}${requestPath}`, {
|
|
369
|
+
method,
|
|
370
|
+
headers,
|
|
371
|
+
body,
|
|
372
|
+
}),
|
|
373
|
+
fortressSubject: req.fortressSubject,
|
|
374
|
+
fortressUserId: req.fortressUserId,
|
|
375
|
+
fortressClaims: req.fortressClaims,
|
|
376
|
+
fortressScopes: req.fortressScopes,
|
|
377
|
+
};
|
|
378
|
+
await executePluginMiddleware(
|
|
379
|
+
plugins,
|
|
380
|
+
position,
|
|
381
|
+
new URL(requestPath, 'http://localhost').pathname,
|
|
382
|
+
ctx,
|
|
383
|
+
requestContext,
|
|
384
|
+
);
|
|
385
|
+
next();
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
next(err);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- Factory ---
|
|
394
|
+
|
|
395
|
+
/** Options for {@link createExpressMiddleware}. Extends {@link RbacOptions}. */
|
|
396
|
+
export interface ExpressAdapterOptions extends RbacOptions {
|
|
397
|
+
/** Standalone CSRF middleware options for host-owned Express routes. */
|
|
398
|
+
csrf?: CsrfConfig;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Build the full set of fortress Express middleware: auth, CSRF, RBAC, error
|
|
403
|
+
* handler, and the three plugin middleware slots (`beforeAuth`, `afterAuth`,
|
|
404
|
+
* `afterRbac`). Mount each in the corresponding place in your Express app.
|
|
405
|
+
*/
|
|
406
|
+
export function createExpressMiddleware(fortress: Fortress, options?: ExpressAdapterOptions): {
|
|
407
|
+
authMiddleware: ExpressMiddleware;
|
|
408
|
+
csrfMiddleware: ExpressMiddleware;
|
|
409
|
+
rbacMiddleware: ExpressMiddleware;
|
|
410
|
+
errorHandler: ReturnType<typeof createErrorHandler>;
|
|
411
|
+
pluginMiddleware: {
|
|
412
|
+
beforeAuth: ExpressMiddleware;
|
|
413
|
+
afterAuth: ExpressMiddleware;
|
|
414
|
+
afterRbac: ExpressMiddleware;
|
|
415
|
+
};
|
|
416
|
+
} {
|
|
417
|
+
return {
|
|
418
|
+
authMiddleware: createAuthMiddleware(fortress),
|
|
419
|
+
csrfMiddleware: createCsrfMiddleware(options?.csrf),
|
|
420
|
+
rbacMiddleware: createRbacMiddleware(fortress, options),
|
|
421
|
+
errorHandler: createErrorHandler(fortress),
|
|
422
|
+
pluginMiddleware: {
|
|
423
|
+
beforeAuth: createExpressPluginMiddleware(fortress, 'before-auth'),
|
|
424
|
+
afterAuth: createExpressPluginMiddleware(fortress, 'after-auth'),
|
|
425
|
+
afterRbac: createExpressPluginMiddleware(fortress, 'after-rbac'),
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// --- Helpers ---
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Read the resolved request principal. Works for every subject kind
|
|
434
|
+
* (`USER`, `SERVICE_ACCOUNT`, ...). Throws 401 if the auth middleware did
|
|
435
|
+
* not run or no credential was present.
|
|
436
|
+
*/
|
|
437
|
+
export function getSubject(req: ExpressRequest): Subject {
|
|
438
|
+
if (!req.fortressSubject) {
|
|
439
|
+
throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
|
|
440
|
+
}
|
|
441
|
+
return req.fortressSubject;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Read the authenticated user ID. Throws 401 if the request was
|
|
446
|
+
* authenticated by a non-USER subject (e.g. a service account via
|
|
447
|
+
* api-key) — use {@link getSubject} for handlers that accept any
|
|
448
|
+
* principal.
|
|
449
|
+
*/
|
|
450
|
+
export function getUserId(req: ExpressRequest): string {
|
|
451
|
+
if (!req.fortressSubject || req.fortressSubject.type !== 'USER') {
|
|
452
|
+
throw new FortressError('UNAUTHORIZED', 'User not authenticated', 401);
|
|
453
|
+
}
|
|
454
|
+
return req.fortressSubject.id;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Read the verified JWT claims. Only populated when the request was
|
|
459
|
+
* authenticated via a JWT — api-key principals have no JWT claims and
|
|
460
|
+
* this helper throws 401.
|
|
461
|
+
*/
|
|
462
|
+
export function getClaims(req: ExpressRequest): TokenClaims {
|
|
463
|
+
if (!req.fortressClaims) {
|
|
464
|
+
throw new FortressError('UNAUTHORIZED', 'No JWT claims on this request', 401);
|
|
465
|
+
}
|
|
466
|
+
return req.fortressClaims;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Read the per-request fortress database adapter (with plugin wrappers applied). */
|
|
470
|
+
export function getDb(req: ExpressRequest): DatabaseAdapter {
|
|
471
|
+
if (!req.fortressDb) {
|
|
472
|
+
throw new FortressError('UNAUTHORIZED', 'User not authenticated', 401);
|
|
473
|
+
}
|
|
474
|
+
return req.fortressDb;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Read the per-request fortress database adapter scoped to a specific model (applies any active row-level scope rules). */
|
|
478
|
+
export function getScopedDb(req: ExpressRequest, model: string): Promise<DatabaseAdapter> {
|
|
479
|
+
if (!req.fortressGetScopedDb) {
|
|
480
|
+
throw new FortressError('UNAUTHORIZED', 'User not authenticated', 401);
|
|
481
|
+
}
|
|
482
|
+
return req.fortressGetScopedDb(model);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// --- Internal route matching ---
|
|
486
|
+
|
|
487
|
+
function findRouteMapMatch(method: string, path: string, routeMap: Record<string, RouteMapping>): RouteMapping | null {
|
|
488
|
+
for (const [pattern, mapping] of Object.entries(routeMap)) {
|
|
489
|
+
const [patternMethod, patternPath] = pattern.split(' ', 2);
|
|
490
|
+
if (patternMethod !== method)
|
|
491
|
+
continue;
|
|
492
|
+
if (pathToRegex(patternPath).test(path))
|
|
493
|
+
return mapping;
|
|
494
|
+
}
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function expressRequestPath(req: ExpressRequest): string {
|
|
499
|
+
const path = req.originalUrl ?? req.url ?? req.path;
|
|
500
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function expressHeader(req: ExpressRequest, name: string): string | undefined {
|
|
504
|
+
const target = name.toLowerCase();
|
|
505
|
+
for (const [headerName, value] of Object.entries(req.headers)) {
|
|
506
|
+
if (headerName.toLowerCase() !== target)
|
|
507
|
+
continue;
|
|
508
|
+
return Array.isArray(value) ? value[0] : value;
|
|
509
|
+
}
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function matchesCsrfSkipPath(path: string, skipPaths: string[]): boolean {
|
|
514
|
+
return skipPaths.some((configured) => {
|
|
515
|
+
const withoutWildcard = configured.endsWith('/*') ? configured.slice(0, -2) : configured;
|
|
516
|
+
const base = withoutWildcard.length > 1 && withoutWildcard.endsWith('/')
|
|
517
|
+
? withoutWildcard.slice(0, -1)
|
|
518
|
+
: withoutWildcard;
|
|
519
|
+
return path === base || path.startsWith(`${base}/`);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function normalizeRouteMap(routeMap: Record<string, RouteMapping>): Record<string, RouteMapping> {
|
|
524
|
+
return Object.fromEntries(Object.entries(routeMap).map(([pattern, mapping]) => {
|
|
525
|
+
const separator = pattern.indexOf(' ');
|
|
526
|
+
if (separator < 0)
|
|
527
|
+
return [pattern, mapping];
|
|
528
|
+
const method = pattern.slice(0, separator);
|
|
529
|
+
const path = normalizeExpressPath(pattern.slice(separator + 1));
|
|
530
|
+
return [`${method} ${path}`, mapping];
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function normalizeExpressPath(path: string): string {
|
|
535
|
+
const lowerCased = path.toLowerCase();
|
|
536
|
+
return lowerCased.length > 1 && lowerCased.endsWith('/')
|
|
537
|
+
? lowerCased.slice(0, -1)
|
|
538
|
+
: lowerCased;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function pathToRegex(pattern: string): RegExp {
|
|
542
|
+
// Escape literals first, then add back only the route syntax Fortress
|
|
543
|
+
// supports. This prevents dots (and other regex metacharacters) in a
|
|
544
|
+
// configured path from matching unintended requests.
|
|
545
|
+
const regexStr = normalizeExpressPath(pattern)
|
|
546
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
547
|
+
.replace(/:[^/]+/g, '[^/]+')
|
|
548
|
+
.replace(/\\\*/g, '.*')
|
|
549
|
+
.replace(/\//g, '\\/');
|
|
550
|
+
return new RegExp(`^${regexStr}$`, 'i');
|
|
551
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { EndpointDefinition } from '../core/endpoint';
|
|
2
|
+
import type { Fortress } from '../core/fortress';
|
|
3
|
+
import type {
|
|
4
|
+
ProtectedRouteContext,
|
|
5
|
+
ProtectedRouteHandler,
|
|
6
|
+
ProtectedRouteTarget,
|
|
7
|
+
ProtectOptions,
|
|
8
|
+
} from '../core/http/protect';
|
|
9
|
+
import type { ExpressMiddleware, ExpressRequest, ExpressResponse } from './middleware';
|
|
10
|
+
import { protect } from '../core/http/protect';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Express-flavoured host callback. `E` flows in from the endpoint passed to
|
|
14
|
+
* `protectedRoute()`, so `ctx.body` / `ctx.query` / `ctx.params` / `ctx.input`
|
|
15
|
+
* are typed from the endpoint's phantom generics. String targets degrade to
|
|
16
|
+
* the loose default.
|
|
17
|
+
*/
|
|
18
|
+
export type ExpressProtectedRouteHandler<
|
|
19
|
+
|
|
20
|
+
E extends EndpointDefinition<any, any, any, any> = EndpointDefinition,
|
|
21
|
+
TResult = unknown,
|
|
22
|
+
> = (
|
|
23
|
+
req: ExpressRequest,
|
|
24
|
+
res: ExpressResponse,
|
|
25
|
+
ctx: ProtectedRouteContext<E>,
|
|
26
|
+
) => TResult | Response | Promise<TResult | Response>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a host-owned Express route in Fortress's protection pipeline.
|
|
30
|
+
*
|
|
31
|
+
* Two overloads, mirroring `protect()`:
|
|
32
|
+
*
|
|
33
|
+
* - **Typed target** — passing an `EndpointDefinition` flows its phantom
|
|
34
|
+
* generics through `ctx`.
|
|
35
|
+
* - **String target** — passing a unique `handler` name keeps the loose
|
|
36
|
+
* `Record<string, unknown>` / `unknown` typing.
|
|
37
|
+
*/
|
|
38
|
+
export function protectedRoute<
|
|
39
|
+
|
|
40
|
+
E extends EndpointDefinition<any, any, any, any>,
|
|
41
|
+
TResult = unknown,
|
|
42
|
+
>(
|
|
43
|
+
fortress: Fortress,
|
|
44
|
+
target: E,
|
|
45
|
+
handler: ExpressProtectedRouteHandler<E, TResult>,
|
|
46
|
+
options?: ProtectOptions,
|
|
47
|
+
): ExpressMiddleware;
|
|
48
|
+
export function protectedRoute<TResult = unknown>(
|
|
49
|
+
fortress: Fortress,
|
|
50
|
+
target: string,
|
|
51
|
+
handler: ExpressProtectedRouteHandler<EndpointDefinition, TResult>,
|
|
52
|
+
options?: ProtectOptions,
|
|
53
|
+
): ExpressMiddleware;
|
|
54
|
+
export function protectedRoute(
|
|
55
|
+
fortress: Fortress,
|
|
56
|
+
target: ProtectedRouteTarget,
|
|
57
|
+
|
|
58
|
+
handler: ExpressProtectedRouteHandler<any, unknown>,
|
|
59
|
+
options: ProtectOptions = {},
|
|
60
|
+
): ExpressMiddleware {
|
|
61
|
+
return async (req, res, next) => {
|
|
62
|
+
try {
|
|
63
|
+
const request = expressToWebRequest(req);
|
|
64
|
+
// Cast: core `protect` overloads require a concrete branch; impl is loose.
|
|
65
|
+
const protectedHandler = (protect as (
|
|
66
|
+
f: Fortress,
|
|
67
|
+
t: ProtectedRouteTarget,
|
|
68
|
+
h: ProtectedRouteHandler,
|
|
69
|
+
o?: ProtectOptions,
|
|
70
|
+
) => (request: Request) => Promise<Response>)(
|
|
71
|
+
fortress,
|
|
72
|
+
target,
|
|
73
|
+
ctx => handler(req, res, ctx),
|
|
74
|
+
{ ...options, method: options.method ?? req.method },
|
|
75
|
+
);
|
|
76
|
+
const response = await protectedHandler(request);
|
|
77
|
+
await sendWebResponseToExpress(response, res);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
next(err);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function expressToWebRequest(req: ExpressRequest): Request {
|
|
86
|
+
const host = (req.headers.host as string | undefined) ?? 'localhost';
|
|
87
|
+
const protocol = ((req as { protocol?: string }).protocol) ?? 'http';
|
|
88
|
+
const originalUrl = ((req as { originalUrl?: string }).originalUrl) ?? req.path;
|
|
89
|
+
const search = originalUrl.includes('?') ? `?${originalUrl.split('?')[1]}` : '';
|
|
90
|
+
const url = `${protocol}://${host}${req.path}${search}`;
|
|
91
|
+
|
|
92
|
+
const headers = new Headers();
|
|
93
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
94
|
+
if (Array.isArray(v)) {
|
|
95
|
+
for (const item of v) headers.append(k, item);
|
|
96
|
+
}
|
|
97
|
+
else if (typeof v === 'string') {
|
|
98
|
+
headers.set(k, v);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const init: RequestInit = { method: req.method, headers };
|
|
103
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
104
|
+
const body = (req as { body?: unknown }).body;
|
|
105
|
+
if (body !== undefined && body !== null) {
|
|
106
|
+
if (typeof body === 'string') {
|
|
107
|
+
init.body = body;
|
|
108
|
+
}
|
|
109
|
+
else if (body instanceof Uint8Array) {
|
|
110
|
+
init.body = body as BodyInit;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
init.body = JSON.stringify(body);
|
|
114
|
+
if (!headers.has('content-type'))
|
|
115
|
+
headers.set('content-type', 'application/json');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new Request(url, init);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function sendWebResponseToExpress(response: Response, res: ExpressResponse): Promise<void> {
|
|
124
|
+
res.status(response.status);
|
|
125
|
+
for (const [k, v] of response.headers) {
|
|
126
|
+
if (k.toLowerCase() !== 'set-cookie')
|
|
127
|
+
res.setHeader(k, v);
|
|
128
|
+
}
|
|
129
|
+
const setCookies = response.headers.getSetCookie();
|
|
130
|
+
if (setCookies.length > 0) {
|
|
131
|
+
(res as { setHeader: (name: string, value: string | string[]) => void })
|
|
132
|
+
.setHeader('set-cookie', setCookies);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
const isJson = (response.headers.get('content-type') ?? '').includes('json');
|
|
137
|
+
if (isJson) {
|
|
138
|
+
try {
|
|
139
|
+
res.json(JSON.parse(text));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
res.json({ raw: text });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const resWithSend = res as unknown as { send?: (body: string) => void };
|
|
147
|
+
if (resWithSend.send)
|
|
148
|
+
resWithSend.send(text);
|
|
149
|
+
else
|
|
150
|
+
res.json(text);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type { ProtectedRouteContext, ProtectedRouteTarget, ProtectOptions };
|