@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,1207 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var FortressError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
statusCode;
|
|
5
|
+
retryAfter;
|
|
6
|
+
details;
|
|
7
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
8
|
+
oauthError;
|
|
9
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
10
|
+
oauthDescription;
|
|
11
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
12
|
+
oauthErrorUri;
|
|
13
|
+
constructor(code, message, statusCode, options) {
|
|
14
|
+
super(message, { cause: options?.cause });
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.retryAfter = options?.retryAfter;
|
|
18
|
+
this.details = options?.details;
|
|
19
|
+
this.oauthError = options?.oauthError;
|
|
20
|
+
this.oauthDescription = options?.oauthDescription;
|
|
21
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
22
|
+
}
|
|
23
|
+
toJSON() {
|
|
24
|
+
return {
|
|
25
|
+
code: this.code,
|
|
26
|
+
message: this.message,
|
|
27
|
+
statusCode: this.statusCode,
|
|
28
|
+
...this.details !== void 0 && { details: this.details }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var Errors = {
|
|
33
|
+
unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
|
|
34
|
+
tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
|
|
35
|
+
sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
|
|
36
|
+
sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
|
|
37
|
+
forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
|
|
38
|
+
badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
|
|
39
|
+
notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
|
|
40
|
+
conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
|
|
41
|
+
unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
|
|
42
|
+
rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
|
|
43
|
+
database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
|
|
44
|
+
serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
|
|
45
|
+
validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
|
|
46
|
+
/**
|
|
47
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
48
|
+
*
|
|
49
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
50
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
51
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
52
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
53
|
+
* on the machine-readable `error` field.
|
|
54
|
+
*
|
|
55
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
56
|
+
* - `invalid_client` → 401
|
|
57
|
+
* - everything else → 400
|
|
58
|
+
*/
|
|
59
|
+
oauth: (error, description, options) => {
|
|
60
|
+
const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
|
|
61
|
+
const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
|
|
62
|
+
return new FortressError(code, description ?? error, status, {
|
|
63
|
+
oauthError: error,
|
|
64
|
+
oauthDescription: description,
|
|
65
|
+
oauthErrorUri: options?.errorUri,
|
|
66
|
+
cause: options?.cause
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
71
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
72
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
73
|
+
* service-method call would produce.
|
|
74
|
+
*
|
|
75
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
76
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
77
|
+
* failures still become structured `FortressError`s.
|
|
78
|
+
*/
|
|
79
|
+
fromHttpResponse: (status, body) => {
|
|
80
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
81
|
+
const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
|
|
82
|
+
const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
|
|
83
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function isFortressErrorCode(code) {
|
|
87
|
+
return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
|
|
88
|
+
}
|
|
89
|
+
function statusToErrorCode(status) {
|
|
90
|
+
if (status === 401)
|
|
91
|
+
return "UNAUTHORIZED";
|
|
92
|
+
if (status === 403)
|
|
93
|
+
return "FORBIDDEN";
|
|
94
|
+
if (status === 404)
|
|
95
|
+
return "NOT_FOUND";
|
|
96
|
+
if (status === 409)
|
|
97
|
+
return "CONFLICT";
|
|
98
|
+
if (status === 422)
|
|
99
|
+
return "UNPROCESSABLE_ENTITY";
|
|
100
|
+
if (status === 429)
|
|
101
|
+
return "RATE_LIMITED";
|
|
102
|
+
if (status === 503)
|
|
103
|
+
return "SERVICE_UNAVAILABLE";
|
|
104
|
+
if (status >= 500)
|
|
105
|
+
return "DATABASE_ERROR";
|
|
106
|
+
return "BAD_REQUEST";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/core/http/cookie-serialize.ts
|
|
110
|
+
function parseCookieHeader(header) {
|
|
111
|
+
if (!header)
|
|
112
|
+
return {};
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const part of header.split(";")) {
|
|
115
|
+
const eq = part.indexOf("=");
|
|
116
|
+
if (eq === -1)
|
|
117
|
+
continue;
|
|
118
|
+
const name = part.slice(0, eq).trim();
|
|
119
|
+
if (!name)
|
|
120
|
+
continue;
|
|
121
|
+
const raw = part.slice(eq + 1).trim();
|
|
122
|
+
try {
|
|
123
|
+
out[name] = decodeURIComponent(raw);
|
|
124
|
+
} catch {
|
|
125
|
+
out[name] = raw;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/sveltekit/cookies.ts
|
|
132
|
+
function setAuthCookies(event, fortress, payload) {
|
|
133
|
+
const cookies = fortress.cookies;
|
|
134
|
+
const accessExpiry = fortress.config.jwt.accessTokenExpirySeconds ?? 900;
|
|
135
|
+
const refreshExpiry = fortress.config.jwt.refreshTokenExpirySeconds ?? 604800;
|
|
136
|
+
event.cookies.set(cookies.accessName, payload.accessToken, optsFor(cookies, accessExpiry));
|
|
137
|
+
if (payload.refreshToken) {
|
|
138
|
+
event.cookies.set(cookies.refreshName, payload.refreshToken, optsFor(cookies, refreshExpiry));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function clearAuthCookies(event, fortress) {
|
|
142
|
+
const cookies = fortress.cookies;
|
|
143
|
+
event.cookies.delete(cookies.accessName, optsFor(cookies));
|
|
144
|
+
event.cookies.delete(cookies.refreshName, optsFor(cookies));
|
|
145
|
+
}
|
|
146
|
+
function replayCookies(response, event) {
|
|
147
|
+
const setCookies = response.headers.getSetCookie();
|
|
148
|
+
for (const raw of setCookies) {
|
|
149
|
+
const parsed = parseSetCookie(raw);
|
|
150
|
+
if (!parsed)
|
|
151
|
+
continue;
|
|
152
|
+
event.cookies.set(parsed.name, parsed.value, parsed.opts);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function optsFor(cookies, maxAgeSeconds) {
|
|
156
|
+
const opts = {
|
|
157
|
+
path: cookies.path,
|
|
158
|
+
httpOnly: true,
|
|
159
|
+
secure: cookies.secure,
|
|
160
|
+
sameSite: cookies.sameSite
|
|
161
|
+
};
|
|
162
|
+
if (cookies.domain)
|
|
163
|
+
opts.domain = cookies.domain;
|
|
164
|
+
if (maxAgeSeconds !== void 0)
|
|
165
|
+
opts.maxAge = maxAgeSeconds;
|
|
166
|
+
return opts;
|
|
167
|
+
}
|
|
168
|
+
function parseSetCookie(raw) {
|
|
169
|
+
const parts = raw.split(";").map((p) => p.trim());
|
|
170
|
+
if (parts.length === 0)
|
|
171
|
+
return null;
|
|
172
|
+
const first = parts.shift();
|
|
173
|
+
if (!first)
|
|
174
|
+
return null;
|
|
175
|
+
const eq = first.indexOf("=");
|
|
176
|
+
if (eq === -1)
|
|
177
|
+
return null;
|
|
178
|
+
const name = first.slice(0, eq).trim();
|
|
179
|
+
const rawValue = first.slice(eq + 1).trim();
|
|
180
|
+
let value;
|
|
181
|
+
try {
|
|
182
|
+
value = decodeURIComponent(rawValue);
|
|
183
|
+
} catch {
|
|
184
|
+
value = rawValue;
|
|
185
|
+
}
|
|
186
|
+
const opts = { path: "/" };
|
|
187
|
+
for (const part of parts) {
|
|
188
|
+
const [k, v = ""] = part.split("=", 2);
|
|
189
|
+
const key = k.toLowerCase();
|
|
190
|
+
if (key === "path")
|
|
191
|
+
opts.path = v;
|
|
192
|
+
else if (key === "domain")
|
|
193
|
+
opts.domain = v;
|
|
194
|
+
else if (key === "httponly")
|
|
195
|
+
opts.httpOnly = true;
|
|
196
|
+
else if (key === "secure")
|
|
197
|
+
opts.secure = true;
|
|
198
|
+
else if (key === "samesite")
|
|
199
|
+
opts.sameSite = v.toLowerCase();
|
|
200
|
+
else if (key === "max-age")
|
|
201
|
+
opts.maxAge = Number(v);
|
|
202
|
+
else if (key === "expires")
|
|
203
|
+
opts.expires = new Date(v);
|
|
204
|
+
}
|
|
205
|
+
return { name, value, opts };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/sveltekit/actions.ts
|
|
209
|
+
async function actionFailure(status, data) {
|
|
210
|
+
const { fail } = await import("@sveltejs/kit");
|
|
211
|
+
return fail(status, data);
|
|
212
|
+
}
|
|
213
|
+
async function actionRedirect(location) {
|
|
214
|
+
const { redirect } = await import("@sveltejs/kit");
|
|
215
|
+
return redirect(303, location);
|
|
216
|
+
}
|
|
217
|
+
function field(form, name) {
|
|
218
|
+
const v = form.get(name);
|
|
219
|
+
return typeof v === "string" ? v : "";
|
|
220
|
+
}
|
|
221
|
+
function buildMeta(event) {
|
|
222
|
+
return {
|
|
223
|
+
ipAddress: event.request.headers.get("x-forwarded-for") ?? event.request.headers.get("x-real-ip") ?? void 0,
|
|
224
|
+
userAgent: event.request.headers.get("user-agent") ?? void 0
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
var fortressActions = {
|
|
228
|
+
/**
|
|
229
|
+
* Sign in with `identifier` + `password` form fields. Sets the access +
|
|
230
|
+
* refresh cookies on success. Optionally redirects.
|
|
231
|
+
*/
|
|
232
|
+
login(fortress, opts = {}) {
|
|
233
|
+
return async (event) => {
|
|
234
|
+
const form = await event.request.formData();
|
|
235
|
+
const identifier = field(form, "identifier");
|
|
236
|
+
const password = field(form, "password");
|
|
237
|
+
try {
|
|
238
|
+
const result = await fortress.auth.login(identifier, password, buildMeta(event));
|
|
239
|
+
if (result.status === "pending")
|
|
240
|
+
return { success: true, pending: true, challenge: result.pending };
|
|
241
|
+
if (result.status !== "success")
|
|
242
|
+
return { success: true, pending: false };
|
|
243
|
+
setAuthCookies(event, fortress, {
|
|
244
|
+
accessToken: result.accessToken,
|
|
245
|
+
refreshToken: result.refreshToken
|
|
246
|
+
});
|
|
247
|
+
if (opts.redirectTo)
|
|
248
|
+
await actionRedirect(opts.redirectTo);
|
|
249
|
+
return { success: true, pending: false };
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err instanceof FortressError) {
|
|
252
|
+
return actionFailure(err.statusCode, { error: err.message, code: err.code });
|
|
253
|
+
}
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
/**
|
|
259
|
+
* Register a new user from `email`, `name`, `password` form fields. On
|
|
260
|
+
* success, immediately logs them in (sets cookies) and optionally
|
|
261
|
+
* redirects. Mirrors the typical sign-up UX.
|
|
262
|
+
*/
|
|
263
|
+
register(fortress, opts = {}) {
|
|
264
|
+
return async (event) => {
|
|
265
|
+
const form = await event.request.formData();
|
|
266
|
+
const data = {
|
|
267
|
+
email: field(form, "email"),
|
|
268
|
+
name: field(form, "name"),
|
|
269
|
+
password: field(form, "password")
|
|
270
|
+
};
|
|
271
|
+
try {
|
|
272
|
+
await fortress.auth.createUser(data);
|
|
273
|
+
const result = await fortress.auth.login(data.email, data.password ?? "", buildMeta(event));
|
|
274
|
+
if (result.status === "pending")
|
|
275
|
+
return { success: true, pending: true, challenge: result.pending };
|
|
276
|
+
if (result.status !== "success")
|
|
277
|
+
return { success: true, pending: false };
|
|
278
|
+
setAuthCookies(event, fortress, {
|
|
279
|
+
accessToken: result.accessToken,
|
|
280
|
+
refreshToken: result.refreshToken
|
|
281
|
+
});
|
|
282
|
+
if (opts.redirectTo)
|
|
283
|
+
await actionRedirect(opts.redirectTo);
|
|
284
|
+
return { success: true, pending: false };
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err instanceof FortressError) {
|
|
287
|
+
return actionFailure(err.statusCode, { error: err.message, code: err.code });
|
|
288
|
+
}
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
/**
|
|
294
|
+
* Revoke the current refresh token (if present) and clear both auth
|
|
295
|
+
* cookies. Optionally redirects.
|
|
296
|
+
*/
|
|
297
|
+
logout(fortress, opts = {}) {
|
|
298
|
+
return async (event) => {
|
|
299
|
+
const refreshToken = event.cookies.get(fortress.cookies.refreshName);
|
|
300
|
+
if (refreshToken) {
|
|
301
|
+
try {
|
|
302
|
+
await fortress.auth.logout(refreshToken);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
clearAuthCookies(event, fortress);
|
|
307
|
+
if (opts.redirectTo)
|
|
308
|
+
await actionRedirect(opts.redirectTo);
|
|
309
|
+
return { success: true, pending: false };
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
/**
|
|
313
|
+
* Manually rotate the access + refresh tokens using the cookie. Useful
|
|
314
|
+
* if you want a `?/refresh` form action for SPAs that need to extend a
|
|
315
|
+
* session without a full reload.
|
|
316
|
+
*/
|
|
317
|
+
refresh(fortress) {
|
|
318
|
+
return async (event) => {
|
|
319
|
+
const refreshToken = event.cookies.get(fortress.cookies.refreshName);
|
|
320
|
+
if (!refreshToken) {
|
|
321
|
+
return actionFailure(401, { error: "No refresh token", code: "UNAUTHORIZED" });
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const result = await fortress.auth.refresh(refreshToken, buildMeta(event));
|
|
325
|
+
setAuthCookies(event, fortress, result);
|
|
326
|
+
return { success: true, pending: false };
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (err instanceof FortressError) {
|
|
329
|
+
return actionFailure(err.statusCode, { error: err.message, code: err.code });
|
|
330
|
+
}
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/sveltekit/catch-all.ts
|
|
338
|
+
function toSvelteKitHandler(fortress) {
|
|
339
|
+
const handler = (event) => fortress.handleRequest(event.request);
|
|
340
|
+
return {
|
|
341
|
+
GET: handler,
|
|
342
|
+
POST: handler,
|
|
343
|
+
PUT: handler,
|
|
344
|
+
DELETE: handler,
|
|
345
|
+
PATCH: handler
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/core/http/error-response.ts
|
|
350
|
+
function errorToResponse(err, logger) {
|
|
351
|
+
if (err instanceof FortressError) {
|
|
352
|
+
const headers = { "Content-Type": "application/json" };
|
|
353
|
+
if (err.code === "RATE_LIMITED" && err.retryAfter !== void 0) {
|
|
354
|
+
headers["Retry-After"] = String(err.retryAfter);
|
|
355
|
+
}
|
|
356
|
+
if (err.oauthError) {
|
|
357
|
+
const body2 = { error: err.oauthError };
|
|
358
|
+
if (err.oauthDescription)
|
|
359
|
+
body2.error_description = err.oauthDescription;
|
|
360
|
+
if (err.oauthErrorUri)
|
|
361
|
+
body2.error_uri = err.oauthErrorUri;
|
|
362
|
+
return new Response(JSON.stringify(body2), {
|
|
363
|
+
status: err.statusCode,
|
|
364
|
+
headers
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return new Response(JSON.stringify(err.toJSON()), {
|
|
368
|
+
status: err.statusCode,
|
|
369
|
+
headers
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
logger?.error(
|
|
373
|
+
{ err, message: err instanceof Error ? err.message : "Unknown error" },
|
|
374
|
+
"unhandled error in fortress.handleRequest"
|
|
375
|
+
);
|
|
376
|
+
const body = {
|
|
377
|
+
code: "INTERNAL_ERROR",
|
|
378
|
+
message: "Internal server error",
|
|
379
|
+
statusCode: 500
|
|
380
|
+
};
|
|
381
|
+
return new Response(JSON.stringify(body), {
|
|
382
|
+
status: 500,
|
|
383
|
+
headers: { "Content-Type": "application/json" }
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
function withCookies(response, setCookies) {
|
|
387
|
+
if (setCookies.length === 0)
|
|
388
|
+
return response;
|
|
389
|
+
const headers = new Headers(response.headers);
|
|
390
|
+
for (const cookie of setCookies) {
|
|
391
|
+
headers.append("set-cookie", cookie);
|
|
392
|
+
}
|
|
393
|
+
return new Response(response.body, {
|
|
394
|
+
status: response.status,
|
|
395
|
+
statusText: response.statusText,
|
|
396
|
+
headers
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/core/http/match.ts
|
|
401
|
+
function segmentSpecificity(segment) {
|
|
402
|
+
if (segment === "*")
|
|
403
|
+
return 0;
|
|
404
|
+
if (segment.startsWith(":"))
|
|
405
|
+
return 1;
|
|
406
|
+
return 2;
|
|
407
|
+
}
|
|
408
|
+
function canonicalizePath(pathname) {
|
|
409
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
410
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
411
|
+
}
|
|
412
|
+
function buildRouteTable(endpoints) {
|
|
413
|
+
const table = endpoints.map((ep) => {
|
|
414
|
+
const segments = canonicalizePath(ep.path).split("/").filter(Boolean);
|
|
415
|
+
const paramNames = [];
|
|
416
|
+
for (const seg of segments) {
|
|
417
|
+
if (seg.startsWith(":"))
|
|
418
|
+
paramNames.push(seg.slice(1));
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
endpoint: ep,
|
|
422
|
+
method: ep.method.toUpperCase(),
|
|
423
|
+
segments,
|
|
424
|
+
paramNames
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
return table.map((route, index) => ({ route, index })).sort((a, b) => {
|
|
428
|
+
const length = Math.max(a.route.segments.length, b.route.segments.length);
|
|
429
|
+
for (let i = 0; i < length; i++) {
|
|
430
|
+
const difference = segmentSpecificity(b.route.segments[i] ?? "") - segmentSpecificity(a.route.segments[i] ?? "");
|
|
431
|
+
if (difference !== 0)
|
|
432
|
+
return difference;
|
|
433
|
+
}
|
|
434
|
+
return a.index - b.index;
|
|
435
|
+
}).map(({ route }) => route);
|
|
436
|
+
}
|
|
437
|
+
function matchRoute(table, method, pathname) {
|
|
438
|
+
const upperMethod = method.toUpperCase();
|
|
439
|
+
const pathSegments = canonicalizePath(pathname).split("/").filter(Boolean);
|
|
440
|
+
for (const route of table) {
|
|
441
|
+
if (route.method !== upperMethod)
|
|
442
|
+
continue;
|
|
443
|
+
if (route.segments.length !== pathSegments.length)
|
|
444
|
+
continue;
|
|
445
|
+
const params = {};
|
|
446
|
+
let matched = true;
|
|
447
|
+
for (let i = 0; i < route.segments.length; i++) {
|
|
448
|
+
const routeSeg = route.segments[i];
|
|
449
|
+
const pathSeg = pathSegments[i];
|
|
450
|
+
if (routeSeg.startsWith(":")) {
|
|
451
|
+
params[routeSeg.slice(1)] = decodeURIComponent(pathSeg);
|
|
452
|
+
} else if (routeSeg !== "*" && routeSeg !== pathSeg) {
|
|
453
|
+
matched = false;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (matched)
|
|
458
|
+
return { endpoint: route.endpoint, params };
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/core/http/principal.ts
|
|
464
|
+
async function tryPluginPrincipal(fortress, request) {
|
|
465
|
+
const plugins = fortress.config.plugins ?? [];
|
|
466
|
+
for (const plugin of plugins) {
|
|
467
|
+
if (!plugin.resolvePrincipal)
|
|
468
|
+
continue;
|
|
469
|
+
const resolved = await plugin.resolvePrincipal(request, {
|
|
470
|
+
db: fortress.config.database,
|
|
471
|
+
config: fortress.config,
|
|
472
|
+
auth: fortress.auth,
|
|
473
|
+
iam: fortress.iam
|
|
474
|
+
});
|
|
475
|
+
if (resolved)
|
|
476
|
+
return { subject: resolved.subject, claims: resolved.claims, scopes: resolved.scopes };
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/core/plugin-runner.ts
|
|
482
|
+
function chainAdapterWrappers(plugins, baseAdapter, requestContext) {
|
|
483
|
+
let adapter = baseAdapter;
|
|
484
|
+
for (const plugin of plugins) {
|
|
485
|
+
if (plugin.wrapAdapter) {
|
|
486
|
+
adapter = plugin.wrapAdapter(adapter, requestContext);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return adapter;
|
|
490
|
+
}
|
|
491
|
+
async function collectScopeRules(plugins, userId, model, ctx) {
|
|
492
|
+
const allFilters = [];
|
|
493
|
+
const allDefaults = {};
|
|
494
|
+
for (const plugin of plugins) {
|
|
495
|
+
if (!plugin.scopeRules) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const rule = await plugin.scopeRules(userId, model, ctx);
|
|
499
|
+
if (!rule) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
allFilters.push(...rule.filters);
|
|
503
|
+
Object.assign(allDefaults, rule.defaults);
|
|
504
|
+
}
|
|
505
|
+
if (allFilters.length === 0 && Object.keys(allDefaults).length === 0) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return { filters: allFilters, defaults: allDefaults };
|
|
509
|
+
}
|
|
510
|
+
function wrapAdapterWithScopeRules(adapter, scopeRule) {
|
|
511
|
+
const { filters, defaults } = scopeRule;
|
|
512
|
+
return {
|
|
513
|
+
...adapter,
|
|
514
|
+
create: (params) => adapter.create({
|
|
515
|
+
...params,
|
|
516
|
+
data: { ...params.data, ...defaults }
|
|
517
|
+
}),
|
|
518
|
+
findOne: (params) => adapter.findOne({
|
|
519
|
+
...params,
|
|
520
|
+
where: [...params.where, ...filters]
|
|
521
|
+
}),
|
|
522
|
+
findMany: (params) => adapter.findMany({
|
|
523
|
+
...params,
|
|
524
|
+
where: [...params.where ?? [], ...filters]
|
|
525
|
+
}),
|
|
526
|
+
update: (params) => Promise.resolve().then(() => {
|
|
527
|
+
for (const key of Object.keys(defaults)) {
|
|
528
|
+
if (Object.hasOwn(params.data, key))
|
|
529
|
+
throw Errors.forbidden(`Cannot update scoped field '${key}'`);
|
|
530
|
+
}
|
|
531
|
+
return adapter.update({
|
|
532
|
+
...params,
|
|
533
|
+
where: [...params.where, ...filters]
|
|
534
|
+
});
|
|
535
|
+
}),
|
|
536
|
+
delete: (params) => adapter.delete({
|
|
537
|
+
...params,
|
|
538
|
+
where: [...params.where, ...filters]
|
|
539
|
+
}),
|
|
540
|
+
count: (params) => adapter.count({
|
|
541
|
+
...params,
|
|
542
|
+
where: [...params.where ?? [], ...filters]
|
|
543
|
+
}),
|
|
544
|
+
transaction: (fn) => adapter.transaction((tx) => fn(wrapAdapterWithScopeRules(tx, scopeRule)))
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function middlewarePathToRegex(pattern) {
|
|
548
|
+
const regexStr = canonicalizePath(pattern).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+").replace(/\\\*/g, ".*").replace(/\//g, "\\/");
|
|
549
|
+
return new RegExp(`^${regexStr}$`);
|
|
550
|
+
}
|
|
551
|
+
function collectPluginMiddleware(plugins, position) {
|
|
552
|
+
const result = [];
|
|
553
|
+
for (const plugin of plugins) {
|
|
554
|
+
if (!plugin.middleware)
|
|
555
|
+
continue;
|
|
556
|
+
for (const mw of plugin.middleware) {
|
|
557
|
+
if (mw.position === position) {
|
|
558
|
+
result.push({ plugin, middleware: mw });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
564
|
+
async function executePluginMiddleware(plugins, position, requestPath, ctx, request) {
|
|
565
|
+
const canonicalRequestPath = canonicalizePath(requestPath);
|
|
566
|
+
const matching = collectPluginMiddleware(plugins, position).filter(({ middleware: mw }) => middlewarePathToRegex(mw.path).test(canonicalRequestPath));
|
|
567
|
+
if (matching.length === 0)
|
|
568
|
+
return;
|
|
569
|
+
let index = 0;
|
|
570
|
+
async function runNext() {
|
|
571
|
+
if (index >= matching.length)
|
|
572
|
+
return;
|
|
573
|
+
const current = matching[index++];
|
|
574
|
+
await current.middleware.handler(ctx, request, runNext);
|
|
575
|
+
}
|
|
576
|
+
await runNext();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/sveltekit/handle.ts
|
|
580
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
581
|
+
function createSvelteKitHandle(fortress, options = {}) {
|
|
582
|
+
const basePath = options.basePath ?? "";
|
|
583
|
+
const routeTable = buildRouteTable(fortress.manifest.filter((route) => route.mounted));
|
|
584
|
+
const skipPatterns = (options.skipPaths ?? []).map((p) => pathToRegex(p));
|
|
585
|
+
const routeMap = options.routeMap ?? {};
|
|
586
|
+
const unmappedRoutes = options.unmappedRoutes ?? "allow";
|
|
587
|
+
const refreshFlights = /* @__PURE__ */ new Map();
|
|
588
|
+
const refreshFlightKey = (refreshToken, meta) => `${refreshToken}\0${meta.userAgent ?? ""}`;
|
|
589
|
+
const acquireRefreshFlight = (refreshToken, meta) => {
|
|
590
|
+
const key = refreshFlightKey(refreshToken, meta);
|
|
591
|
+
const existing = refreshFlights.get(key);
|
|
592
|
+
if (existing) {
|
|
593
|
+
existing.consumers++;
|
|
594
|
+
return { key, promise: existing.promise };
|
|
595
|
+
}
|
|
596
|
+
const promise = fortress.auth.refresh(refreshToken, meta);
|
|
597
|
+
refreshFlights.set(key, { consumers: 1, promise });
|
|
598
|
+
return { key, promise };
|
|
599
|
+
};
|
|
600
|
+
const releaseRefreshFlight = (key) => {
|
|
601
|
+
const flight = refreshFlights.get(key);
|
|
602
|
+
if (!flight)
|
|
603
|
+
return;
|
|
604
|
+
flight.consumers--;
|
|
605
|
+
if (flight.consumers === 0)
|
|
606
|
+
refreshFlights.delete(key);
|
|
607
|
+
};
|
|
608
|
+
return async ({ event, resolve }) => {
|
|
609
|
+
let acquiredRefreshKey;
|
|
610
|
+
try {
|
|
611
|
+
const fullPath = event.url.pathname;
|
|
612
|
+
const innerPath = stripPrefix(fullPath, basePath);
|
|
613
|
+
if (innerPath !== null && matchRoute(routeTable, event.request.method, innerPath)) {
|
|
614
|
+
const rewritten = rewriteRequest(event.request, event.url, innerPath);
|
|
615
|
+
const response = await fortress.handleRequest(rewritten);
|
|
616
|
+
replayCookies(response, event);
|
|
617
|
+
return response;
|
|
618
|
+
}
|
|
619
|
+
await fortress.runPluginMiddleware("before-auth", { request: event.request });
|
|
620
|
+
let subject;
|
|
621
|
+
let userId;
|
|
622
|
+
let claims;
|
|
623
|
+
let scopes;
|
|
624
|
+
const pluginResolved = await tryPluginPrincipal(fortress, event.request);
|
|
625
|
+
if (pluginResolved) {
|
|
626
|
+
subject = pluginResolved.subject;
|
|
627
|
+
claims = pluginResolved.claims;
|
|
628
|
+
scopes = pluginResolved.scopes;
|
|
629
|
+
} else {
|
|
630
|
+
const token = fortress.extractAccessToken(event.request);
|
|
631
|
+
if (token) {
|
|
632
|
+
try {
|
|
633
|
+
claims = await fortress.auth.verifyToken(token);
|
|
634
|
+
subject = { type: claims.subjectType, id: claims.sub };
|
|
635
|
+
} catch {
|
|
636
|
+
const refreshToken = SAFE_METHODS.has(event.request.method.toUpperCase()) ? event.cookies.get(fortress.cookies.refreshName) : void 0;
|
|
637
|
+
if (refreshToken) {
|
|
638
|
+
try {
|
|
639
|
+
const flight = acquireRefreshFlight(refreshToken, requestMeta(event.request));
|
|
640
|
+
acquiredRefreshKey = flight.key;
|
|
641
|
+
const refreshed = await flight.promise;
|
|
642
|
+
setAuthCookies(event, fortress, refreshed);
|
|
643
|
+
claims = await fortress.auth.verifyToken(refreshed.accessToken);
|
|
644
|
+
subject = { type: claims.subjectType, id: claims.sub };
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (subject?.type === "USER")
|
|
652
|
+
userId = subject.id;
|
|
653
|
+
if (subject) {
|
|
654
|
+
populateLocals(
|
|
655
|
+
event,
|
|
656
|
+
fortress,
|
|
657
|
+
subject,
|
|
658
|
+
claims,
|
|
659
|
+
scopes
|
|
660
|
+
);
|
|
661
|
+
} else {
|
|
662
|
+
const locals = event.locals;
|
|
663
|
+
locals.fortress ??= {};
|
|
664
|
+
}
|
|
665
|
+
await fortress.runPluginMiddleware("after-auth", {
|
|
666
|
+
request: event.request,
|
|
667
|
+
fortressSubject: subject,
|
|
668
|
+
fortressUserId: userId,
|
|
669
|
+
fortressClaims: claims,
|
|
670
|
+
fortressScopes: scopes
|
|
671
|
+
});
|
|
672
|
+
if (!skipPatterns.some((p) => p.test(fullPath))) {
|
|
673
|
+
const mapping = matchRouteMap(routeMap, event.request.method, fullPath);
|
|
674
|
+
if (!mapping && unmappedRoutes === "deny")
|
|
675
|
+
throw Errors.forbidden("No permission mapping for this route");
|
|
676
|
+
if (mapping) {
|
|
677
|
+
if (!subject)
|
|
678
|
+
throw Errors.unauthorized("Not authenticated");
|
|
679
|
+
const allowed = await fortress.iam.checkPermission(subject, mapping.resource, mapping.action, {
|
|
680
|
+
credentialScopes: scopes
|
|
681
|
+
});
|
|
682
|
+
if (!allowed)
|
|
683
|
+
throw Errors.forbidden("Insufficient permissions");
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
await fortress.runPluginMiddleware("after-rbac", {
|
|
687
|
+
request: event.request,
|
|
688
|
+
fortressSubject: subject,
|
|
689
|
+
fortressUserId: userId,
|
|
690
|
+
fortressClaims: claims,
|
|
691
|
+
fortressScopes: scopes
|
|
692
|
+
});
|
|
693
|
+
return await resolve(event);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
return errorToResponse(err);
|
|
696
|
+
} finally {
|
|
697
|
+
if (acquiredRefreshKey)
|
|
698
|
+
releaseRefreshFlight(acquiredRefreshKey);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
function requestMeta(request) {
|
|
703
|
+
return {
|
|
704
|
+
ipAddress: request.headers.get("x-forwarded-for") ?? request.headers.get("x-real-ip") ?? void 0,
|
|
705
|
+
userAgent: request.headers.get("user-agent") ?? void 0
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function populateLocals(event, fortress, subject, claims, scopes) {
|
|
709
|
+
const plugins = fortress.config.plugins ?? [];
|
|
710
|
+
const requestContext = {
|
|
711
|
+
tenantId: claims?.customClaims?.tenantId,
|
|
712
|
+
ipAddress: event.request.headers.get("x-forwarded-for") ?? event.request.headers.get("x-real-ip") ?? void 0,
|
|
713
|
+
userAgent: event.request.headers.get("user-agent") ?? void 0
|
|
714
|
+
};
|
|
715
|
+
const wrapped = chainAdapterWrappers(plugins, fortress.config.database, requestContext);
|
|
716
|
+
const pluginCtx = { db: wrapped, config: fortress.config };
|
|
717
|
+
event.locals.fortress = {
|
|
718
|
+
subject,
|
|
719
|
+
userId: subject.type === "USER" ? subject.id : void 0,
|
|
720
|
+
claims,
|
|
721
|
+
scopes,
|
|
722
|
+
db: wrapped,
|
|
723
|
+
getScopedDb: async (model) => {
|
|
724
|
+
const rule = await collectScopeRules(plugins, subject.id, model, pluginCtx);
|
|
725
|
+
if (!rule)
|
|
726
|
+
return wrapped;
|
|
727
|
+
return wrapAdapterWithScopeRules(wrapped, rule);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function stripPrefix(pathname, prefix) {
|
|
732
|
+
if (!prefix)
|
|
733
|
+
return pathname;
|
|
734
|
+
if (!pathname.startsWith(prefix))
|
|
735
|
+
return null;
|
|
736
|
+
return pathname.slice(prefix.length) || "/";
|
|
737
|
+
}
|
|
738
|
+
function rewriteRequest(original, url, innerPath) {
|
|
739
|
+
const newUrl = `${url.origin}${innerPath}${url.search}`;
|
|
740
|
+
return new Request(newUrl, original);
|
|
741
|
+
}
|
|
742
|
+
function pathToRegex(pattern) {
|
|
743
|
+
const regexStr = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+").replace(/\\\*/g, ".*").replace(/\//g, "\\/");
|
|
744
|
+
return new RegExp(`^${regexStr}$`);
|
|
745
|
+
}
|
|
746
|
+
function matchRouteMap(map, method, path) {
|
|
747
|
+
const exact = map[`${method} ${path}`];
|
|
748
|
+
if (exact)
|
|
749
|
+
return exact;
|
|
750
|
+
for (const [pattern, mapping] of Object.entries(map)) {
|
|
751
|
+
const [m, p] = pattern.split(" ", 2);
|
|
752
|
+
if (m !== method)
|
|
753
|
+
continue;
|
|
754
|
+
if (pathToRegex(p).test(path))
|
|
755
|
+
return mapping;
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/sveltekit/helpers.ts
|
|
761
|
+
function getSubject(event) {
|
|
762
|
+
const subject = event.locals.fortress?.subject;
|
|
763
|
+
if (!subject)
|
|
764
|
+
throw Errors.unauthorized("Not authenticated");
|
|
765
|
+
return subject;
|
|
766
|
+
}
|
|
767
|
+
function getUserId(event) {
|
|
768
|
+
const subject = event.locals.fortress?.subject;
|
|
769
|
+
if (!subject || subject.type !== "USER")
|
|
770
|
+
throw Errors.unauthorized("User not authenticated");
|
|
771
|
+
return subject.id;
|
|
772
|
+
}
|
|
773
|
+
function getClaims(event) {
|
|
774
|
+
const claims = event.locals.fortress?.claims;
|
|
775
|
+
if (!claims)
|
|
776
|
+
throw Errors.unauthorized("No JWT claims on this request");
|
|
777
|
+
return claims;
|
|
778
|
+
}
|
|
779
|
+
function getDb(event) {
|
|
780
|
+
const db = event.locals.fortress?.db;
|
|
781
|
+
if (!db)
|
|
782
|
+
throw Errors.unauthorized("Not authenticated");
|
|
783
|
+
return db;
|
|
784
|
+
}
|
|
785
|
+
function getScopedDb(event, model) {
|
|
786
|
+
const fn = event.locals.fortress?.getScopedDb;
|
|
787
|
+
if (!fn)
|
|
788
|
+
throw Errors.unauthorized("Not authenticated");
|
|
789
|
+
return fn(model);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/core/validation.ts
|
|
793
|
+
import { fromJSONSchema } from "@bajustone/fetcher/openapi";
|
|
794
|
+
var DECIMAL_INTEGER_RE = /^-?\d+$/;
|
|
795
|
+
var DECIMAL_NUMBER_RE = /^-?\d+(?:\.\d+)?$/;
|
|
796
|
+
var rawSchemaValidators = /* @__PURE__ */ new WeakMap();
|
|
797
|
+
function standardSchemaFor(schema) {
|
|
798
|
+
if ("~standard" in schema)
|
|
799
|
+
return schema;
|
|
800
|
+
let compiled = rawSchemaValidators.get(schema);
|
|
801
|
+
if (!compiled) {
|
|
802
|
+
compiled = fromJSONSchema(schema);
|
|
803
|
+
rawSchemaValidators.set(schema, compiled);
|
|
804
|
+
}
|
|
805
|
+
return compiled;
|
|
806
|
+
}
|
|
807
|
+
function coerceBySchema(schema, data) {
|
|
808
|
+
if (!schema || !data || schema.type !== "object" || !schema.properties)
|
|
809
|
+
return data;
|
|
810
|
+
const out = { ...data };
|
|
811
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
812
|
+
const value = out[key];
|
|
813
|
+
if (typeof value !== "string")
|
|
814
|
+
continue;
|
|
815
|
+
const coerced = coerceScalar(propSchema, value);
|
|
816
|
+
if (coerced !== void 0)
|
|
817
|
+
out[key] = coerced;
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
function coerceScalar(schema, value) {
|
|
822
|
+
const type = schema.type ?? numericConstraintType(schema);
|
|
823
|
+
switch (type) {
|
|
824
|
+
case "integer": {
|
|
825
|
+
if (!DECIMAL_INTEGER_RE.test(value))
|
|
826
|
+
return void 0;
|
|
827
|
+
const n = Number(value);
|
|
828
|
+
if (Number.isFinite(n) && Number.isSafeInteger(n) && String(n) === value)
|
|
829
|
+
return n;
|
|
830
|
+
return void 0;
|
|
831
|
+
}
|
|
832
|
+
case "number": {
|
|
833
|
+
if (!DECIMAL_NUMBER_RE.test(value))
|
|
834
|
+
return void 0;
|
|
835
|
+
const n = Number(value);
|
|
836
|
+
if (Number.isFinite(n) && (!Number.isInteger(n) || Number.isSafeInteger(n)) && String(n) === value)
|
|
837
|
+
return n;
|
|
838
|
+
return void 0;
|
|
839
|
+
}
|
|
840
|
+
case "boolean": {
|
|
841
|
+
if (value === "true")
|
|
842
|
+
return true;
|
|
843
|
+
if (value === "false")
|
|
844
|
+
return false;
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
847
|
+
default:
|
|
848
|
+
return void 0;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function numericConstraintType(schema) {
|
|
852
|
+
if (typeof schema.const === "number")
|
|
853
|
+
return Number.isInteger(schema.const) ? "integer" : "number";
|
|
854
|
+
if (schema.enum && schema.enum.length > 0 && schema.enum.every((value) => typeof value === "number"))
|
|
855
|
+
return schema.enum.every((value) => Number.isInteger(value)) ? "integer" : "number";
|
|
856
|
+
return void 0;
|
|
857
|
+
}
|
|
858
|
+
async function validateRequest(input, data) {
|
|
859
|
+
if (!input)
|
|
860
|
+
return;
|
|
861
|
+
const allIssues = [];
|
|
862
|
+
if (input.bodySchema || input.body) {
|
|
863
|
+
const issues = await validateSchema(input.bodySchema ?? input.body, data.body, "body");
|
|
864
|
+
allIssues.push(...issues);
|
|
865
|
+
}
|
|
866
|
+
if (input.querySchema || input.query) {
|
|
867
|
+
const issues = await validateSchema(input.querySchema ?? input.query, data.query, "query");
|
|
868
|
+
allIssues.push(...issues);
|
|
869
|
+
}
|
|
870
|
+
if (input.paramsSchema || input.params) {
|
|
871
|
+
const issues = await validateSchema(input.paramsSchema ?? input.params, data.params, "params");
|
|
872
|
+
allIssues.push(...issues);
|
|
873
|
+
}
|
|
874
|
+
if (allIssues.length > 0) {
|
|
875
|
+
throw Errors.validationError(allIssues);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async function validateSchema(schema, data, location) {
|
|
879
|
+
const result = await standardSchemaFor(schema)["~standard"].validate(data);
|
|
880
|
+
if (result.issues) {
|
|
881
|
+
return result.issues.map((issue) => ({
|
|
882
|
+
path: issue.path,
|
|
883
|
+
message: issue.message,
|
|
884
|
+
location
|
|
885
|
+
}));
|
|
886
|
+
}
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
async function validateValue(schema, data, location) {
|
|
890
|
+
const result = await schema["~standard"].validate(data);
|
|
891
|
+
if (result.issues) {
|
|
892
|
+
throw Errors.validationError(
|
|
893
|
+
result.issues.map((issue) => ({
|
|
894
|
+
path: issue.path,
|
|
895
|
+
message: issue.message,
|
|
896
|
+
location
|
|
897
|
+
}))
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
return result.value;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/core/http/csrf.ts
|
|
904
|
+
function resolveCsrfConfig(config) {
|
|
905
|
+
return {
|
|
906
|
+
enabled: config?.enabled ?? true,
|
|
907
|
+
headerName: config?.headerName ?? "X-Fortress-CSRF",
|
|
908
|
+
skipPaths: config?.skipPaths ?? [],
|
|
909
|
+
rejectSameSite: config?.rejectSameSite ?? false
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
var SAFE_METHODS2 = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
913
|
+
function matchesSkipPath(path, skipPaths) {
|
|
914
|
+
for (const skip of skipPaths) {
|
|
915
|
+
if (path === skip)
|
|
916
|
+
return true;
|
|
917
|
+
if (path.startsWith(`${skip}/`))
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
function isCookieAuthenticated(request, cookies) {
|
|
923
|
+
const jar = parseCookieHeader(request.headers.get("cookie"));
|
|
924
|
+
const hasCookie = (name) => typeof jar[name] === "string" && jar[name].length > 0;
|
|
925
|
+
return hasCookie(cookies.accessName) || hasCookie(cookies.refreshName);
|
|
926
|
+
}
|
|
927
|
+
function enforceCsrf(request, pathname, csrf, cookies) {
|
|
928
|
+
if (!csrf.enabled)
|
|
929
|
+
return;
|
|
930
|
+
if (SAFE_METHODS2.has(request.method.toUpperCase()))
|
|
931
|
+
return;
|
|
932
|
+
if (matchesSkipPath(pathname, csrf.skipPaths))
|
|
933
|
+
return;
|
|
934
|
+
const fetchSite = request.headers.get("sec-fetch-site");
|
|
935
|
+
const cookieAuthenticated = isCookieAuthenticated(request, cookies);
|
|
936
|
+
if (!cookieAuthenticated) {
|
|
937
|
+
if (request.headers.has("authorization") || request.headers.has("x-api-key"))
|
|
938
|
+
return;
|
|
939
|
+
if (fetchSite === "cross-site")
|
|
940
|
+
throw Errors.forbidden("CSRF: cross-site request rejected");
|
|
941
|
+
const origin = request.headers.get("origin");
|
|
942
|
+
if (origin) {
|
|
943
|
+
try {
|
|
944
|
+
if (new URL(origin).origin !== new URL(request.url).origin)
|
|
945
|
+
throw Errors.forbidden("CSRF: cross-origin request rejected");
|
|
946
|
+
} catch (error) {
|
|
947
|
+
if (error instanceof Error && "code" in error)
|
|
948
|
+
throw error;
|
|
949
|
+
throw Errors.forbidden("CSRF: invalid Origin header");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (fetchSite === "cross-site")
|
|
955
|
+
throw Errors.forbidden("CSRF: cross-site request rejected");
|
|
956
|
+
if (csrf.rejectSameSite && fetchSite === "same-site")
|
|
957
|
+
throw Errors.forbidden("CSRF: same-site request rejected");
|
|
958
|
+
const tokenHeader = request.headers.get(csrf.headerName);
|
|
959
|
+
if (!tokenHeader)
|
|
960
|
+
throw Errors.forbidden(`CSRF: missing ${csrf.headerName} header`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/core/http/fortress-rbac.ts
|
|
964
|
+
async function enforceFortressPermission(endpoint, subject, enforcement, scopes) {
|
|
965
|
+
const security = endpoint.meta?.security;
|
|
966
|
+
if (endpoint.meta?.permission) {
|
|
967
|
+
if (!subject)
|
|
968
|
+
throw Errors.unauthorized("Not authenticated");
|
|
969
|
+
const allowed = await enforcement.checkPermission(
|
|
970
|
+
subject,
|
|
971
|
+
endpoint.meta.permission.resource,
|
|
972
|
+
endpoint.meta.permission.action,
|
|
973
|
+
scopes
|
|
974
|
+
);
|
|
975
|
+
if (!allowed)
|
|
976
|
+
throw Errors.forbidden("Insufficient permissions");
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (security?.includes("none") || security?.includes("basic"))
|
|
980
|
+
return;
|
|
981
|
+
if (security?.includes("bearer")) {
|
|
982
|
+
if (!subject)
|
|
983
|
+
throw Errors.unauthorized("Not authenticated");
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
throw Errors.forbidden("No permission mapping for this route");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/core/http/plugin-middleware.ts
|
|
990
|
+
async function runPluginMiddleware(plugins, config, phase, ctx) {
|
|
991
|
+
if (plugins.length === 0)
|
|
992
|
+
return;
|
|
993
|
+
const pluginCtx = { db: config.database, config };
|
|
994
|
+
const path = new URL(ctx.request.url).pathname;
|
|
995
|
+
await executePluginMiddleware(plugins, phase, path, pluginCtx, ctx);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/core/http/protect.ts
|
|
999
|
+
function routeKey(route) {
|
|
1000
|
+
return `${route.method.toUpperCase()} ${route.path}`;
|
|
1001
|
+
}
|
|
1002
|
+
function findEndpoint(fortress, target, method) {
|
|
1003
|
+
if (typeof target !== "string") {
|
|
1004
|
+
return target;
|
|
1005
|
+
}
|
|
1006
|
+
const matches = fortress.endpoints.filter((endpoint) => endpoint.handler === target);
|
|
1007
|
+
if (matches.length === 0)
|
|
1008
|
+
throw Errors.notFound(`No endpoint found for handler '${target}'`);
|
|
1009
|
+
if (method) {
|
|
1010
|
+
const byMethod = matches.find((endpoint) => endpoint.method.toUpperCase() === method.toUpperCase());
|
|
1011
|
+
if (byMethod)
|
|
1012
|
+
return byMethod;
|
|
1013
|
+
}
|
|
1014
|
+
if (matches.length > 1) {
|
|
1015
|
+
throw Errors.badRequest(
|
|
1016
|
+
`Handler '${target}' maps to multiple endpoints; pass the EndpointDefinition or ProtectOptions.method.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return matches[0];
|
|
1020
|
+
}
|
|
1021
|
+
function findManifestEntry(fortress, endpoint) {
|
|
1022
|
+
const key = routeKey(endpoint);
|
|
1023
|
+
const entry = fortress.manifest.find((route) => `${route.method.toUpperCase()} ${route.path}` === key);
|
|
1024
|
+
if (!entry)
|
|
1025
|
+
throw Errors.notFound(`No route manifest entry found for ${key}`);
|
|
1026
|
+
return entry;
|
|
1027
|
+
}
|
|
1028
|
+
function rewriteRequestPath(request, pathname) {
|
|
1029
|
+
const current = new URL(request.url);
|
|
1030
|
+
if (current.pathname === pathname)
|
|
1031
|
+
return request;
|
|
1032
|
+
const rewritten = new URL(request.url);
|
|
1033
|
+
rewritten.pathname = pathname;
|
|
1034
|
+
return new Request(rewritten, request);
|
|
1035
|
+
}
|
|
1036
|
+
async function parseJsonBody(request) {
|
|
1037
|
+
if (request.method === "GET" || request.method === "HEAD")
|
|
1038
|
+
return void 0;
|
|
1039
|
+
if (!(request.headers.get("content-type") ?? "").includes("json"))
|
|
1040
|
+
return void 0;
|
|
1041
|
+
return request.clone().json().catch(() => void 0);
|
|
1042
|
+
}
|
|
1043
|
+
function objectOrEmpty(value) {
|
|
1044
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1045
|
+
}
|
|
1046
|
+
function successStatus(endpoint) {
|
|
1047
|
+
const status = Object.keys(endpoint.responses ?? {}).map(Number).find((code) => code >= 200 && code < 300);
|
|
1048
|
+
return status ?? 200;
|
|
1049
|
+
}
|
|
1050
|
+
function jsonResponse(body, status) {
|
|
1051
|
+
return new Response(JSON.stringify(body ?? { ok: true }), {
|
|
1052
|
+
status,
|
|
1053
|
+
headers: { "Content-Type": "application/json" }
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
function maybeAttachAuthCookies(fortress, result, response) {
|
|
1057
|
+
if (result instanceof Response)
|
|
1058
|
+
return response;
|
|
1059
|
+
const obj = result;
|
|
1060
|
+
if (typeof obj?.accessToken !== "string")
|
|
1061
|
+
return response;
|
|
1062
|
+
return withCookies(response, fortress.serializeAuthCookies({
|
|
1063
|
+
accessToken: obj.accessToken,
|
|
1064
|
+
refreshToken: typeof obj.refreshToken === "string" ? obj.refreshToken : null
|
|
1065
|
+
}));
|
|
1066
|
+
}
|
|
1067
|
+
function protect(fortress, target, handler, options = {}) {
|
|
1068
|
+
const endpoint = findEndpoint(fortress, target, options.method);
|
|
1069
|
+
const manifest = findManifestEntry(fortress, endpoint);
|
|
1070
|
+
const endpointRouteTable = buildRouteTable([endpoint]);
|
|
1071
|
+
const csrfConfig = resolveCsrfConfig(fortress.config.csrf);
|
|
1072
|
+
const plugins = fortress.config.plugins ?? [];
|
|
1073
|
+
return async function protectedRequestHandler(request) {
|
|
1074
|
+
let response;
|
|
1075
|
+
try {
|
|
1076
|
+
const url = new URL(request.url);
|
|
1077
|
+
const pipelinePath = options.path ?? url.pathname;
|
|
1078
|
+
const pipelineRequest = rewriteRequestPath(request, pipelinePath);
|
|
1079
|
+
await runPluginMiddleware(plugins, fortress.config, "before-auth", { request: pipelineRequest });
|
|
1080
|
+
enforceCsrf(pipelineRequest, pipelinePath, csrfConfig, fortress.cookies);
|
|
1081
|
+
let subject;
|
|
1082
|
+
let userId;
|
|
1083
|
+
let claims;
|
|
1084
|
+
let scopes;
|
|
1085
|
+
const selfManagedBearer = endpoint.meta?.bearerKind === "oauth";
|
|
1086
|
+
if (!selfManagedBearer) {
|
|
1087
|
+
const pluginResolved = await tryPluginPrincipal(fortress, pipelineRequest);
|
|
1088
|
+
if (pluginResolved) {
|
|
1089
|
+
subject = pluginResolved.subject;
|
|
1090
|
+
claims = pluginResolved.claims;
|
|
1091
|
+
scopes = pluginResolved.scopes;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const requiresBearer = !selfManagedBearer && ((endpoint.meta?.security?.includes("bearer") ?? false) || !!endpoint.meta?.permission);
|
|
1095
|
+
if (!subject && requiresBearer) {
|
|
1096
|
+
const token = fortress.extractAccessToken(pipelineRequest);
|
|
1097
|
+
if (!token)
|
|
1098
|
+
throw Errors.unauthorized("Missing access token");
|
|
1099
|
+
try {
|
|
1100
|
+
claims = await fortress.auth.verifyToken(token);
|
|
1101
|
+
subject = { type: claims.subjectType, id: claims.sub };
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
if (err instanceof FortressError)
|
|
1104
|
+
throw err;
|
|
1105
|
+
throw Errors.unauthorized("Invalid access token");
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (subject?.type === "USER")
|
|
1109
|
+
userId = subject.id;
|
|
1110
|
+
await runPluginMiddleware(plugins, fortress.config, "after-auth", {
|
|
1111
|
+
request: pipelineRequest,
|
|
1112
|
+
fortressSubject: subject,
|
|
1113
|
+
fortressUserId: userId,
|
|
1114
|
+
fortressClaims: claims,
|
|
1115
|
+
fortressScopes: scopes
|
|
1116
|
+
});
|
|
1117
|
+
if (!selfManagedBearer) {
|
|
1118
|
+
await enforceFortressPermission(endpoint, subject, {
|
|
1119
|
+
checkPermission: (subj, resource, action, credentialScopes) => fortress.iam.checkPermission(subj, resource, action, { credentialScopes })
|
|
1120
|
+
}, scopes);
|
|
1121
|
+
}
|
|
1122
|
+
await runPluginMiddleware(plugins, fortress.config, "after-rbac", {
|
|
1123
|
+
request: pipelineRequest,
|
|
1124
|
+
fortressSubject: subject,
|
|
1125
|
+
fortressUserId: userId,
|
|
1126
|
+
fortressClaims: claims,
|
|
1127
|
+
fortressScopes: scopes
|
|
1128
|
+
});
|
|
1129
|
+
const body = await parseJsonBody(request);
|
|
1130
|
+
const rawQuery = Object.fromEntries(url.searchParams);
|
|
1131
|
+
const match = matchRoute(endpointRouteTable, endpoint.method, options.path ?? url.pathname);
|
|
1132
|
+
const rawParams = options.params ?? match?.params ?? {};
|
|
1133
|
+
const query = coerceBySchema(endpoint.input?.query, rawQuery) ?? rawQuery;
|
|
1134
|
+
const params = coerceBySchema(endpoint.input?.params, rawParams) ?? rawParams;
|
|
1135
|
+
await validateRequest(endpoint.input, { body, query, params });
|
|
1136
|
+
const input = { ...objectOrEmpty(body), ...query, ...params };
|
|
1137
|
+
const result = await handler({
|
|
1138
|
+
request,
|
|
1139
|
+
endpoint,
|
|
1140
|
+
manifest,
|
|
1141
|
+
subject,
|
|
1142
|
+
userId,
|
|
1143
|
+
claims,
|
|
1144
|
+
scopes,
|
|
1145
|
+
params,
|
|
1146
|
+
query,
|
|
1147
|
+
body,
|
|
1148
|
+
input,
|
|
1149
|
+
respond: (status, responseBody) => jsonResponse(responseBody, status)
|
|
1150
|
+
// Cast: the runtime values are untyped records; the generic context
|
|
1151
|
+
// type narrows them for the caller, but the impl signature uses the
|
|
1152
|
+
// loose `ProtectedRouteContext<EndpointDefinition>`.
|
|
1153
|
+
});
|
|
1154
|
+
response = result instanceof Response ? result : jsonResponse(result, successStatus(endpoint));
|
|
1155
|
+
if (options.attachAuthCookies ?? true)
|
|
1156
|
+
response = maybeAttachAuthCookies(fortress, result, response);
|
|
1157
|
+
return response;
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
response = errorToResponse(err, fortress.logger);
|
|
1160
|
+
return response;
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/sveltekit/protect.ts
|
|
1166
|
+
function protectedRoute(fortress, target, handler, options = {}) {
|
|
1167
|
+
return async (event) => {
|
|
1168
|
+
const protectedHandler = protect(
|
|
1169
|
+
fortress,
|
|
1170
|
+
target,
|
|
1171
|
+
(ctx) => handler(event, ctx),
|
|
1172
|
+
{ ...options, method: options.method ?? event.request.method }
|
|
1173
|
+
);
|
|
1174
|
+
const response = await protectedHandler(event.request);
|
|
1175
|
+
replayCookies(response, event);
|
|
1176
|
+
return response;
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/sveltekit/validated.ts
|
|
1181
|
+
async function vBody(event, schema) {
|
|
1182
|
+
const body = await event.request.json().catch(() => void 0);
|
|
1183
|
+
return validateValue(schema, body, "body");
|
|
1184
|
+
}
|
|
1185
|
+
async function vParam(event, schema) {
|
|
1186
|
+
return validateValue(schema, event.params, "params");
|
|
1187
|
+
}
|
|
1188
|
+
async function vQuery(event, schema) {
|
|
1189
|
+
return validateValue(schema, Object.fromEntries(event.url.searchParams), "query");
|
|
1190
|
+
}
|
|
1191
|
+
export {
|
|
1192
|
+
clearAuthCookies,
|
|
1193
|
+
createSvelteKitHandle,
|
|
1194
|
+
fortressActions,
|
|
1195
|
+
getClaims,
|
|
1196
|
+
getDb,
|
|
1197
|
+
getScopedDb,
|
|
1198
|
+
getSubject,
|
|
1199
|
+
getUserId,
|
|
1200
|
+
protectedRoute,
|
|
1201
|
+
replayCookies,
|
|
1202
|
+
setAuthCookies,
|
|
1203
|
+
toSvelteKitHandler,
|
|
1204
|
+
vBody,
|
|
1205
|
+
vParam,
|
|
1206
|
+
vQuery
|
|
1207
|
+
};
|