@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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createSvelteKitHandle(fortress, options)` — primary entrypoint for the
|
|
3
|
+
* SvelteKit adapter.
|
|
4
|
+
*
|
|
5
|
+
* Returns a SvelteKit `Handle` hook that:
|
|
6
|
+
*
|
|
7
|
+
* 1. Short-circuits during `vite build` prerender (caller passes
|
|
8
|
+
* `building` to opt out).
|
|
9
|
+
* 2. Intercepts Fortress-managed paths (`/auth/*`, `/iam/*`, plugin paths)
|
|
10
|
+
* by calling `fortress.handleRequest(event.request)` and returning the
|
|
11
|
+
* `Response` directly. This bypasses SvelteKit's built-in CSRF check
|
|
12
|
+
* (which only runs inside `resolve(event)`), but `handleRequest` runs
|
|
13
|
+
* Fortress's own pipeline CSRF check (H5) in its place.
|
|
14
|
+
* 3. For user routes, runs plugin `before-auth` middleware, extracts the
|
|
15
|
+
* access token (cookie-first, Bearer fallback), verifies it,
|
|
16
|
+
* auto-refreshes when expired, populates `event.locals.fortress`, runs
|
|
17
|
+
* plugin `after-auth` and (when a route map matches) RBAC, then
|
|
18
|
+
* delegates to `resolve(event)`.
|
|
19
|
+
*
|
|
20
|
+
* Compose with `sequence()` from `@sveltejs/kit/hooks` if you have other
|
|
21
|
+
* handles.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* // src/hooks.server.ts
|
|
26
|
+
* import { sequence } from '@sveltejs/kit/hooks';
|
|
27
|
+
* import { createSvelteKitHandle } from '@bajustone/fortress/sveltekit';
|
|
28
|
+
* import { fortress } from '$lib/server/fortress';
|
|
29
|
+
*
|
|
30
|
+
* export const handle = sequence(
|
|
31
|
+
* createSvelteKitHandle(fortress, { basePath: '/api' }),
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { DatabaseAdapter } from '../adapters/database';
|
|
37
|
+
import type { Fortress } from '../core/fortress';
|
|
38
|
+
import type { PluginContext } from '../core/plugin';
|
|
39
|
+
import type { AuthTokenPair, RequestMeta, Subject, TokenClaims } from '../core/types';
|
|
40
|
+
import type {
|
|
41
|
+
FortressLocals,
|
|
42
|
+
SvelteKitAdapterOptions,
|
|
43
|
+
SvelteKitHandle,
|
|
44
|
+
SvelteKitRequestEvent,
|
|
45
|
+
} from './types';
|
|
46
|
+
import { Errors, FortressError } from '../core/errors';
|
|
47
|
+
import { errorToResponse } from '../core/http/error-response';
|
|
48
|
+
import { buildRouteTable, matchRoute } from '../core/http/match';
|
|
49
|
+
import { tryPluginPrincipal } from '../core/http/principal';
|
|
50
|
+
import {
|
|
51
|
+
chainAdapterWrappers,
|
|
52
|
+
collectScopeRules,
|
|
53
|
+
wrapAdapterWithScopeRules,
|
|
54
|
+
} from '../core/plugin-runner';
|
|
55
|
+
import { replayCookies, setAuthCookies } from './cookies';
|
|
56
|
+
|
|
57
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
58
|
+
|
|
59
|
+
/** Build the SvelteKit `handle` hook for a Fortress instance. */
|
|
60
|
+
export function createSvelteKitHandle(
|
|
61
|
+
fortress: Fortress<any, any>,
|
|
62
|
+
options: SvelteKitAdapterOptions = {},
|
|
63
|
+
): SvelteKitHandle {
|
|
64
|
+
const basePath = options.basePath ?? '';
|
|
65
|
+
// Pre-build the route table at startup so the handle hook can quickly
|
|
66
|
+
// detect whether a path is a Fortress endpoint without re-parsing.
|
|
67
|
+
const routeTable = buildRouteTable(fortress.manifest.filter(route => route.mounted));
|
|
68
|
+
const skipPatterns = (options.skipPaths ?? []).map(p => pathToRegex(p));
|
|
69
|
+
const routeMap = options.routeMap ?? {};
|
|
70
|
+
const unmappedRoutes = options.unmappedRoutes ?? 'allow';
|
|
71
|
+
// Coalesce overlapping SSR requests that arrive with the same expired
|
|
72
|
+
// cookie pair. Entries remain until every request that joined has finished
|
|
73
|
+
// its full handle/resolve lifecycle—not merely until refresh() settles—so a
|
|
74
|
+
// stale-cookie request still in the same SSR wave receives the successor
|
|
75
|
+
// instead of replaying the predecessor. User agent is part of the key so a
|
|
76
|
+
// mismatched fingerprint can never piggyback on another request's check.
|
|
77
|
+
interface RefreshFlight {
|
|
78
|
+
consumers: number;
|
|
79
|
+
promise: Promise<AuthTokenPair>;
|
|
80
|
+
}
|
|
81
|
+
const refreshFlights = new Map<string, RefreshFlight>();
|
|
82
|
+
const refreshFlightKey = (refreshToken: string, meta: RequestMeta): string =>
|
|
83
|
+
`${refreshToken}\u0000${meta.userAgent ?? ''}`;
|
|
84
|
+
const acquireRefreshFlight = (
|
|
85
|
+
refreshToken: string,
|
|
86
|
+
meta: RequestMeta,
|
|
87
|
+
): { key: string; promise: Promise<AuthTokenPair> } => {
|
|
88
|
+
const key = refreshFlightKey(refreshToken, meta);
|
|
89
|
+
const existing = refreshFlights.get(key);
|
|
90
|
+
if (existing) {
|
|
91
|
+
existing.consumers++;
|
|
92
|
+
return { key, promise: existing.promise };
|
|
93
|
+
}
|
|
94
|
+
const promise = fortress.auth.refresh(refreshToken, meta);
|
|
95
|
+
refreshFlights.set(key, { consumers: 1, promise });
|
|
96
|
+
return { key, promise };
|
|
97
|
+
};
|
|
98
|
+
const releaseRefreshFlight = (key: string): void => {
|
|
99
|
+
const flight = refreshFlights.get(key);
|
|
100
|
+
if (!flight)
|
|
101
|
+
return;
|
|
102
|
+
flight.consumers--;
|
|
103
|
+
if (flight.consumers === 0)
|
|
104
|
+
refreshFlights.delete(key);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return async ({ event, resolve }) => {
|
|
108
|
+
let acquiredRefreshKey: string | undefined;
|
|
109
|
+
try {
|
|
110
|
+
// 1. Strip basePath if provided. Paths outside the prefix are user-owned.
|
|
111
|
+
const fullPath = event.url.pathname;
|
|
112
|
+
const innerPath = stripPrefix(fullPath, basePath);
|
|
113
|
+
|
|
114
|
+
// 2. Fortress-managed path → delegate to core, return Response directly.
|
|
115
|
+
// The handle hook returns BEFORE resolve(), so SvelteKit's own CSRF
|
|
116
|
+
// check is skipped — but handleRequest runs Fortress's pipeline CSRF
|
|
117
|
+
// check (enforceCsrf) for these routes (login, refresh, etc.). We
|
|
118
|
+
// detect fortress paths by matching the canonical endpoint table —
|
|
119
|
+
// this catches /auth/login, /iam/*, plugin routes, OAuth, OpenAPI.
|
|
120
|
+
if (innerPath !== null && matchRoute(routeTable, event.request.method, innerPath)) {
|
|
121
|
+
const rewritten = rewriteRequest(event.request, event.url, innerPath);
|
|
122
|
+
const response = await fortress.handleRequest(rewritten);
|
|
123
|
+
// Mirror Set-Cookie headers into event.cookies so anything else in this
|
|
124
|
+
// request cycle (e.g. a sequence() handle running after this one) sees
|
|
125
|
+
// them via event.cookies.get(). The browser still sees the originals
|
|
126
|
+
// on the returned Response.
|
|
127
|
+
replayCookies(response, event);
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. User route — run plugin `before-auth` middleware.
|
|
132
|
+
await fortress.runPluginMiddleware('before-auth', { request: event.request });
|
|
133
|
+
|
|
134
|
+
// 4. Resolve the request principal. Plugin `resolvePrincipal` hooks
|
|
135
|
+
// (api-key, future OAuth client_credentials, mTLS) run first so
|
|
136
|
+
// non-JWT credentials authenticate uniformly on user routes. If
|
|
137
|
+
// no plugin claims the request, fall back to the JWT bearer token
|
|
138
|
+
// (cookie-first, Authorization: Bearer second) — and if the JWT
|
|
139
|
+
// is expired, try a silent refresh using the refresh cookie so
|
|
140
|
+
// SSR loads stay logged in across token lifetimes.
|
|
141
|
+
let subject: Subject | undefined;
|
|
142
|
+
let userId: string | undefined;
|
|
143
|
+
let claims: TokenClaims | undefined;
|
|
144
|
+
let scopes: string[] | null | undefined;
|
|
145
|
+
|
|
146
|
+
const pluginResolved = await tryPluginPrincipal(fortress, event.request);
|
|
147
|
+
if (pluginResolved) {
|
|
148
|
+
subject = pluginResolved.subject;
|
|
149
|
+
claims = pluginResolved.claims;
|
|
150
|
+
scopes = pluginResolved.scopes;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const token = fortress.extractAccessToken(event.request);
|
|
154
|
+
if (token) {
|
|
155
|
+
try {
|
|
156
|
+
claims = await fortress.auth.verifyToken(token);
|
|
157
|
+
subject = { type: claims.subjectType, id: claims.sub };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Token invalid or expired — try silent refresh. Restricted to
|
|
161
|
+
// safe methods (SSR page loads are GETs): a silent refresh rotates
|
|
162
|
+
// the refresh token, so allowing it on unsafe cross-site requests
|
|
163
|
+
// would be a CSRF-triggered state change. Unsafe requests fall
|
|
164
|
+
// through with no subject; the client re-auths via the
|
|
165
|
+
// CSRF-protected POST /auth/refresh endpoint.
|
|
166
|
+
const refreshToken = SAFE_METHODS.has(event.request.method.toUpperCase())
|
|
167
|
+
? event.cookies.get(fortress.cookies.refreshName)
|
|
168
|
+
: undefined;
|
|
169
|
+
if (refreshToken) {
|
|
170
|
+
try {
|
|
171
|
+
const flight = acquireRefreshFlight(refreshToken, requestMeta(event.request));
|
|
172
|
+
acquiredRefreshKey = flight.key;
|
|
173
|
+
const refreshed = await flight.promise;
|
|
174
|
+
setAuthCookies(event, fortress, refreshed);
|
|
175
|
+
claims = await fortress.auth.verifyToken(refreshed.accessToken);
|
|
176
|
+
subject = { type: claims.subjectType, id: claims.sub };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Refresh failed too — leave locals empty; loaders decide.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (subject?.type === 'USER')
|
|
187
|
+
userId = subject.id;
|
|
188
|
+
|
|
189
|
+
if (subject) {
|
|
190
|
+
populateLocals(
|
|
191
|
+
event as unknown as SvelteKitRequestEvent<FortressLocals>,
|
|
192
|
+
fortress,
|
|
193
|
+
subject,
|
|
194
|
+
claims,
|
|
195
|
+
scopes,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Always set the namespace so consumer code can call `event.locals.fortress?.userId`
|
|
200
|
+
// without optional chaining gymnastics on the namespace itself.
|
|
201
|
+
const locals = event.locals as unknown as FortressLocals;
|
|
202
|
+
locals.fortress ??= {};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 5. Plugin `after-auth` middleware on user routes.
|
|
206
|
+
await fortress.runPluginMiddleware('after-auth', {
|
|
207
|
+
request: event.request,
|
|
208
|
+
fortressSubject: subject,
|
|
209
|
+
fortressUserId: userId,
|
|
210
|
+
fortressClaims: claims,
|
|
211
|
+
fortressScopes: scopes,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// 6. User-route RBAC via routeMap (skip-patterns filtered out first).
|
|
215
|
+
if (!skipPatterns.some(p => p.test(fullPath))) {
|
|
216
|
+
const mapping = matchRouteMap(routeMap, event.request.method, fullPath);
|
|
217
|
+
if (!mapping && unmappedRoutes === 'deny')
|
|
218
|
+
throw Errors.forbidden('No permission mapping for this route');
|
|
219
|
+
if (mapping) {
|
|
220
|
+
if (!subject)
|
|
221
|
+
throw Errors.unauthorized('Not authenticated');
|
|
222
|
+
const allowed = await fortress.iam.checkPermission(subject, mapping.resource, mapping.action, {
|
|
223
|
+
credentialScopes: scopes,
|
|
224
|
+
});
|
|
225
|
+
if (!allowed)
|
|
226
|
+
throw Errors.forbidden('Insufficient permissions');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 7. Plugin `after-rbac` middleware.
|
|
231
|
+
await fortress.runPluginMiddleware('after-rbac', {
|
|
232
|
+
request: event.request,
|
|
233
|
+
fortressSubject: subject,
|
|
234
|
+
fortressUserId: userId,
|
|
235
|
+
fortressClaims: claims,
|
|
236
|
+
fortressScopes: scopes,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// 8. Hand off to SvelteKit's normal route resolution.
|
|
240
|
+
return await resolve(event);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return errorToResponse(err);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
if (acquiredRefreshKey)
|
|
247
|
+
releaseRefreshFlight(acquiredRefreshKey);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── helpers ─────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
function requestMeta(request: Request): RequestMeta {
|
|
255
|
+
return {
|
|
256
|
+
ipAddress:
|
|
257
|
+
request.headers.get('x-forwarded-for')
|
|
258
|
+
?? request.headers.get('x-real-ip')
|
|
259
|
+
?? undefined,
|
|
260
|
+
userAgent: request.headers.get('user-agent') ?? undefined,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Populate `event.locals.fortress` with the request-scoped DB adapter and
|
|
266
|
+
* scope-aware accessor. Builds the same chain that core does for
|
|
267
|
+
* fortress-managed routes (`chainAdapterWrappers` + `collectScopeRules`).
|
|
268
|
+
*/
|
|
269
|
+
function populateLocals(
|
|
270
|
+
event: SvelteKitRequestEvent<FortressLocals>,
|
|
271
|
+
fortress: Fortress<any, any>,
|
|
272
|
+
subject: Subject,
|
|
273
|
+
claims: TokenClaims | undefined,
|
|
274
|
+
scopes: string[] | null | undefined,
|
|
275
|
+
): void {
|
|
276
|
+
const plugins = fortress.config.plugins ?? [];
|
|
277
|
+
// Tenant comes from the verified JWT claim, never a client header.
|
|
278
|
+
const requestContext: Record<string, unknown> = {
|
|
279
|
+
tenantId: claims?.customClaims?.tenantId,
|
|
280
|
+
ipAddress:
|
|
281
|
+
event.request.headers.get('x-forwarded-for')
|
|
282
|
+
?? event.request.headers.get('x-real-ip')
|
|
283
|
+
?? undefined,
|
|
284
|
+
userAgent: event.request.headers.get('user-agent') ?? undefined,
|
|
285
|
+
};
|
|
286
|
+
const wrapped = chainAdapterWrappers(plugins, fortress.config.database, requestContext);
|
|
287
|
+
const pluginCtx: PluginContext = { db: wrapped, config: fortress.config };
|
|
288
|
+
|
|
289
|
+
event.locals.fortress = {
|
|
290
|
+
subject,
|
|
291
|
+
userId: subject.type === 'USER' ? subject.id : undefined,
|
|
292
|
+
claims,
|
|
293
|
+
scopes,
|
|
294
|
+
db: wrapped,
|
|
295
|
+
getScopedDb: async (model: string): Promise<DatabaseAdapter> => {
|
|
296
|
+
const rule = await collectScopeRules(plugins, subject.id, model, pluginCtx);
|
|
297
|
+
if (!rule)
|
|
298
|
+
return wrapped;
|
|
299
|
+
return wrapAdapterWithScopeRules(wrapped, rule);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Strip an optional path prefix. Returns `null` if a prefix was configured
|
|
306
|
+
* but the path doesn't start with it (signal to fall through to user routes).
|
|
307
|
+
*/
|
|
308
|
+
function stripPrefix(pathname: string, prefix: string): string | null {
|
|
309
|
+
if (!prefix)
|
|
310
|
+
return pathname;
|
|
311
|
+
if (!pathname.startsWith(prefix))
|
|
312
|
+
return null;
|
|
313
|
+
return pathname.slice(prefix.length) || '/';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Build a new `Request` with the same method/headers/body as the original
|
|
318
|
+
* but with the path rewritten to drop the basePath. Used so core's route
|
|
319
|
+
* matcher sees the canonical `/auth/*` paths regardless of mount prefix.
|
|
320
|
+
*/
|
|
321
|
+
function rewriteRequest(original: Request, url: URL, innerPath: string): Request {
|
|
322
|
+
const newUrl = `${url.origin}${innerPath}${url.search}`;
|
|
323
|
+
return new Request(newUrl, original);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Convert a simple path pattern (`:param` and `*`) to a regex. */
|
|
327
|
+
function pathToRegex(pattern: string): RegExp {
|
|
328
|
+
const regexStr = pattern
|
|
329
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
330
|
+
.replace(/:[^/]+/g, '[^/]+')
|
|
331
|
+
.replace(/\\\*/g, '.*')
|
|
332
|
+
.replace(/\//g, '\\/');
|
|
333
|
+
return new RegExp(`^${regexStr}$`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Look up a `(resource, action)` mapping for `METHOD path`. */
|
|
337
|
+
function matchRouteMap(
|
|
338
|
+
map: Record<string, { resource: string; action: string }>,
|
|
339
|
+
method: string,
|
|
340
|
+
path: string,
|
|
341
|
+
): { resource: string; action: string } | null {
|
|
342
|
+
const exact = map[`${method} ${path}`];
|
|
343
|
+
if (exact)
|
|
344
|
+
return exact;
|
|
345
|
+
for (const [pattern, mapping] of Object.entries(map)) {
|
|
346
|
+
const [m, p] = pattern.split(' ', 2);
|
|
347
|
+
if (m !== method)
|
|
348
|
+
continue;
|
|
349
|
+
if (pathToRegex(p).test(path))
|
|
350
|
+
return mapping;
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Re-export FortressError so the consumer of this module catches it correctly.
|
|
356
|
+
export { FortressError };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read fortress request context from a SvelteKit `RequestEvent`.
|
|
3
|
+
*
|
|
4
|
+
* Each helper throws `FortressError('UNAUTHORIZED')` when the field is
|
|
5
|
+
* missing — typically because the auth handle hook didn't populate
|
|
6
|
+
* `event.locals.fortress.userId` (i.e. the request was anonymous).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DatabaseAdapter } from '../adapters/database';
|
|
10
|
+
import type { Subject, TokenClaims } from '../core/types';
|
|
11
|
+
import type { FortressLocals, SvelteKitRequestEvent } from './types';
|
|
12
|
+
import { Errors } from '../core/errors';
|
|
13
|
+
|
|
14
|
+
type EventWithFortress = SvelteKitRequestEvent<FortressLocals>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read the resolved principal from the event, throwing 401 if missing.
|
|
18
|
+
* Works for every subject kind (`USER`, `SERVICE_ACCOUNT`, ...).
|
|
19
|
+
*/
|
|
20
|
+
export function getSubject(event: EventWithFortress): Subject {
|
|
21
|
+
const subject = event.locals.fortress?.subject;
|
|
22
|
+
if (!subject)
|
|
23
|
+
throw Errors.unauthorized('Not authenticated');
|
|
24
|
+
return subject;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read the authenticated user ID. Throws 401 when the request was
|
|
29
|
+
* authenticated by a non-USER subject (e.g. a service account via
|
|
30
|
+
* api-key) — use {@link getSubject} for handlers that accept any
|
|
31
|
+
* principal.
|
|
32
|
+
*/
|
|
33
|
+
export function getUserId(event: EventWithFortress): string {
|
|
34
|
+
const subject = event.locals.fortress?.subject;
|
|
35
|
+
if (!subject || subject.type !== 'USER')
|
|
36
|
+
throw Errors.unauthorized('User not authenticated');
|
|
37
|
+
return subject.id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read the verified JWT claims. Only populated when the request was
|
|
42
|
+
* authenticated via a JWT — api-key principals have no JWT claims and
|
|
43
|
+
* this helper throws 401.
|
|
44
|
+
*/
|
|
45
|
+
export function getClaims(event: EventWithFortress): TokenClaims {
|
|
46
|
+
const claims = event.locals.fortress?.claims;
|
|
47
|
+
if (!claims)
|
|
48
|
+
throw Errors.unauthorized('No JWT claims on this request');
|
|
49
|
+
return claims;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read the per-request DB adapter (with plugin wrappers applied). */
|
|
53
|
+
export function getDb(event: EventWithFortress): DatabaseAdapter {
|
|
54
|
+
const db = event.locals.fortress?.db;
|
|
55
|
+
if (!db)
|
|
56
|
+
throw Errors.unauthorized('Not authenticated');
|
|
57
|
+
return db;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read the per-request DB adapter scoped to a specific model. Applies any
|
|
62
|
+
* active row-level scope rules from data-isolation plugins.
|
|
63
|
+
*/
|
|
64
|
+
export function getScopedDb(event: EventWithFortress, model: string): Promise<DatabaseAdapter> {
|
|
65
|
+
const fn = event.locals.fortress?.getScopedDb;
|
|
66
|
+
if (!fn)
|
|
67
|
+
throw Errors.unauthorized('Not authenticated');
|
|
68
|
+
return fn(model);
|
|
69
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvelteKit adapter for fortress.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
*
|
|
6
|
+
* - {@link createSvelteKitHandle} — primary `handle` hook for
|
|
7
|
+
* `hooks.server.ts`. Intercepts Fortress-managed paths and delegates to
|
|
8
|
+
* `fortress.handleRequest`. Handles auto-refresh, locals population, and
|
|
9
|
+
* user-route plugin middleware.
|
|
10
|
+
* - {@link toSvelteKitHandler} — escape hatch for catch-all `+server.ts`
|
|
11
|
+
* files (`src/routes/api/fortress/[...path]/+server.ts`).
|
|
12
|
+
* - {@link fortressActions} — form-action helpers (`login`, `logout`,
|
|
13
|
+
* `register`, `refresh`) for use in `+page.server.ts`.
|
|
14
|
+
* - {@link getUserId} / {@link getClaims} / {@link getDb} /
|
|
15
|
+
* {@link getScopedDb} — read the per-request fortress context inside
|
|
16
|
+
* server load functions and `+server.ts` handlers.
|
|
17
|
+
* - {@link FortressLocals} — type for augmenting SvelteKit's `App.Locals`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* // src/lib/server/fortress.ts
|
|
22
|
+
* import { createFortress } from '@bajustone/fortress';
|
|
23
|
+
* import { createDrizzleAdapter } from '@bajustone/fortress/drizzle';
|
|
24
|
+
*
|
|
25
|
+
* export const fortress = createFortress({
|
|
26
|
+
* database: createDrizzleAdapter(...),
|
|
27
|
+
* jwt: { key: process.env.JWT_SECRET! },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // src/hooks.server.ts
|
|
31
|
+
* import { sequence } from '@sveltejs/kit/hooks';
|
|
32
|
+
* import { createSvelteKitHandle } from '@bajustone/fortress/sveltekit';
|
|
33
|
+
* import { fortress } from '$lib/server/fortress';
|
|
34
|
+
*
|
|
35
|
+
* export const handle = sequence(createSvelteKitHandle(fortress, { basePath: '/api' }));
|
|
36
|
+
*
|
|
37
|
+
* // src/app.d.ts
|
|
38
|
+
* import type { FortressLocals } from '@bajustone/fortress/sveltekit';
|
|
39
|
+
*
|
|
40
|
+
* declare global {
|
|
41
|
+
* namespace App {
|
|
42
|
+
* interface Locals extends FortressLocals {}
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @module
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
export { fortressActions } from './actions';
|
|
51
|
+
export type {
|
|
52
|
+
FortressActionFailure,
|
|
53
|
+
FortressActionOptions,
|
|
54
|
+
FortressActions,
|
|
55
|
+
FortressActionSuccess,
|
|
56
|
+
} from './actions';
|
|
57
|
+
export { toSvelteKitHandler } from './catch-all';
|
|
58
|
+
export type { SvelteKitRouteHandler } from './catch-all';
|
|
59
|
+
export { clearAuthCookies, replayCookies, setAuthCookies } from './cookies';
|
|
60
|
+
export { createSvelteKitHandle } from './handle';
|
|
61
|
+
export { getClaims, getDb, getScopedDb, getSubject, getUserId } from './helpers';
|
|
62
|
+
export { protectedRoute } from './protect';
|
|
63
|
+
export type { ProtectedRouteContext, ProtectedRouteTarget, ProtectOptions, SvelteKitProtectedRouteHandler } from './protect';
|
|
64
|
+
export type {
|
|
65
|
+
FortressLocals,
|
|
66
|
+
SvelteKitAction,
|
|
67
|
+
SvelteKitAdapterOptions,
|
|
68
|
+
SvelteKitCookieOptions,
|
|
69
|
+
SvelteKitCookies,
|
|
70
|
+
SvelteKitHandle,
|
|
71
|
+
SvelteKitRequestEvent,
|
|
72
|
+
} from './types';
|
|
73
|
+
export { vBody, vParam, vQuery } from './validated';
|
|
74
|
+
export type { InferOutput } from './validated';
|
|
@@ -0,0 +1,80 @@
|
|
|
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 { SvelteKitRequestEvent } from './types';
|
|
10
|
+
import { protect } from '../core/http/protect';
|
|
11
|
+
import { replayCookies } from './cookies';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SvelteKit-flavoured host callback. `E` flows in from the endpoint passed
|
|
15
|
+
* to `protectedRoute()`, so `ctx.body` / `ctx.query` / `ctx.params` /
|
|
16
|
+
* `ctx.input` are typed from the endpoint's phantom generics. String
|
|
17
|
+
* targets degrade to the loose default.
|
|
18
|
+
*/
|
|
19
|
+
export type SvelteKitProtectedRouteHandler<
|
|
20
|
+
|
|
21
|
+
E extends EndpointDefinition<any, any, any, any> = EndpointDefinition,
|
|
22
|
+
TResult = unknown,
|
|
23
|
+
> = (
|
|
24
|
+
event: SvelteKitRequestEvent,
|
|
25
|
+
ctx: ProtectedRouteContext<E>,
|
|
26
|
+
) => TResult | Response | Promise<TResult | Response>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a host-owned SvelteKit route/action 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<any, any>,
|
|
44
|
+
target: E,
|
|
45
|
+
handler: SvelteKitProtectedRouteHandler<E, TResult>,
|
|
46
|
+
options?: ProtectOptions,
|
|
47
|
+
): (event: SvelteKitRequestEvent) => Promise<Response>;
|
|
48
|
+
export function protectedRoute<TResult = unknown>(
|
|
49
|
+
fortress: Fortress<any, any>,
|
|
50
|
+
target: string,
|
|
51
|
+
handler: SvelteKitProtectedRouteHandler<EndpointDefinition, TResult>,
|
|
52
|
+
options?: ProtectOptions,
|
|
53
|
+
): (event: SvelteKitRequestEvent) => Promise<Response>;
|
|
54
|
+
export function protectedRoute(
|
|
55
|
+
fortress: Fortress<any, any>,
|
|
56
|
+
target: ProtectedRouteTarget,
|
|
57
|
+
|
|
58
|
+
handler: SvelteKitProtectedRouteHandler<any, unknown>,
|
|
59
|
+
options: ProtectOptions = {},
|
|
60
|
+
): (event: SvelteKitRequestEvent) => Promise<Response> {
|
|
61
|
+
return async (event) => {
|
|
62
|
+
// Cast: core `protect` overloads require a concrete branch; impl is loose.
|
|
63
|
+
const protectedHandler = (protect as (
|
|
64
|
+
f: Fortress,
|
|
65
|
+
t: ProtectedRouteTarget,
|
|
66
|
+
h: ProtectedRouteHandler,
|
|
67
|
+
o?: ProtectOptions,
|
|
68
|
+
) => (request: Request) => Promise<Response>)(
|
|
69
|
+
fortress,
|
|
70
|
+
target,
|
|
71
|
+
ctx => handler(event, ctx),
|
|
72
|
+
{ ...options, method: options.method ?? event.request.method },
|
|
73
|
+
);
|
|
74
|
+
const response = await protectedHandler(event.request);
|
|
75
|
+
replayCookies(response, event);
|
|
76
|
+
return response;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type { ProtectedRouteContext, ProtectedRouteTarget, ProtectOptions };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Action, Handle, RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
import type { Fortress } from '../core/fortress';
|
|
3
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
4
|
+
import { fortressActions } from './actions';
|
|
5
|
+
import { toSvelteKitHandler } from './catch-all';
|
|
6
|
+
import { createSvelteKitHandle } from './handle';
|
|
7
|
+
import { protectedRoute } from './protect';
|
|
8
|
+
|
|
9
|
+
describe('sveltekit public type compatibility', () => {
|
|
10
|
+
it('is assignable to the real strict @sveltejs/kit peer types', () => {
|
|
11
|
+
// Compile-time only. Keeping these assignments in the normal tsc/vitest
|
|
12
|
+
// suite prevents the public adapter surface drifting from the peer types.
|
|
13
|
+
if (false) {
|
|
14
|
+
const fortress = null as unknown as Fortress;
|
|
15
|
+
const handle: Handle = createSvelteKitHandle(fortress);
|
|
16
|
+
const loginAction: Action = fortressActions.login(fortress);
|
|
17
|
+
const registerAction: Action = fortressActions.register(fortress);
|
|
18
|
+
const handlers = toSvelteKitHandler(fortress);
|
|
19
|
+
const get: RequestHandler = handlers.GET;
|
|
20
|
+
const post: RequestHandler = handlers.POST;
|
|
21
|
+
const protectedHandler: RequestHandler = protectedRoute(
|
|
22
|
+
fortress,
|
|
23
|
+
'hostRoute',
|
|
24
|
+
async () => ({ ok: true }),
|
|
25
|
+
);
|
|
26
|
+
void [handle, loginAction, registerAction, get, post, protectedHandler];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expectTypeOf(createSvelteKitHandle).toBeFunction();
|
|
30
|
+
});
|
|
31
|
+
});
|