@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,799 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the SvelteKit adapter. We mock just enough of
|
|
3
|
+
* SvelteKit's `RequestEvent` shape to exercise the real fortress instance
|
|
4
|
+
* end-to-end (with the in-memory test DB).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
8
|
+
import type { Fortress } from '../core/fortress';
|
|
9
|
+
import type { Subject } from '../core/types';
|
|
10
|
+
import type { FortressLocals, SvelteKitCookieOptions, SvelteKitCookies } from './types';
|
|
11
|
+
import { isActionFailure, isRedirect } from '@sveltejs/kit';
|
|
12
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
13
|
+
import { signAccessToken } from '../core/auth/jwt';
|
|
14
|
+
import { createFortress } from '../core/fortress';
|
|
15
|
+
import { apiKey } from '../plugins/api-key';
|
|
16
|
+
import { createTestAdapter } from '../testing';
|
|
17
|
+
import { fortressActions } from './actions';
|
|
18
|
+
import { toSvelteKitHandler } from './catch-all';
|
|
19
|
+
import { replayCookies, setAuthCookies } from './cookies';
|
|
20
|
+
import { createSvelteKitHandle } from './handle';
|
|
21
|
+
import { getSubject, getUserId } from './helpers';
|
|
22
|
+
|
|
23
|
+
const SECRET = 'sveltekit-test-secret-32-chars-long!';
|
|
24
|
+
|
|
25
|
+
function makeFortress() {
|
|
26
|
+
return createFortress({
|
|
27
|
+
jwt: { key: SECRET },
|
|
28
|
+
database: createTestAdapter(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build a SvelteKit-shaped Cookies object backed by a Map. */
|
|
33
|
+
function fakeCookies(initial: Record<string, string> = {}): SvelteKitCookies & { _store: Map<string, string> } {
|
|
34
|
+
const store = new Map(Object.entries(initial));
|
|
35
|
+
return {
|
|
36
|
+
_store: store,
|
|
37
|
+
get: name => store.get(name),
|
|
38
|
+
set: (name, value, _opts?: SvelteKitCookieOptions) => {
|
|
39
|
+
store.set(name, value);
|
|
40
|
+
},
|
|
41
|
+
delete: (name) => {
|
|
42
|
+
store.delete(name);
|
|
43
|
+
},
|
|
44
|
+
getAll: () => Array.from(store.entries()).map(([name, value]) => ({ name, value })),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface FakeEventOpts {
|
|
49
|
+
method?: string;
|
|
50
|
+
url?: string;
|
|
51
|
+
headers?: Record<string, string>;
|
|
52
|
+
cookies?: Record<string, string>;
|
|
53
|
+
body?: string | FormData;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build a fake SvelteKit `RequestEvent` for tests. */
|
|
57
|
+
function fakeEvent(opts: FakeEventOpts = {}): RequestEvent & { cookies: ReturnType<typeof fakeCookies> } {
|
|
58
|
+
const url = new URL(opts.url ?? 'http://localhost/');
|
|
59
|
+
const headers = new Headers(opts.headers);
|
|
60
|
+
// Merge cookies into the request header so fortress.extractAccessToken sees them too.
|
|
61
|
+
if (opts.cookies && Object.keys(opts.cookies).length > 0) {
|
|
62
|
+
headers.set('cookie', Object.entries(opts.cookies).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('; '));
|
|
63
|
+
}
|
|
64
|
+
const init: RequestInit = { method: opts.method ?? 'GET', headers };
|
|
65
|
+
if (opts.body !== undefined && (opts.method === 'POST' || opts.method === 'PUT' || opts.method === 'PATCH')) {
|
|
66
|
+
init.body = opts.body;
|
|
67
|
+
}
|
|
68
|
+
const request = new Request(url.toString(), init);
|
|
69
|
+
const cookies = fakeCookies(opts.cookies);
|
|
70
|
+
const event = {
|
|
71
|
+
request,
|
|
72
|
+
url,
|
|
73
|
+
cookies,
|
|
74
|
+
locals: {} as Record<string, unknown>,
|
|
75
|
+
params: {},
|
|
76
|
+
};
|
|
77
|
+
return event as unknown as RequestEvent & { cookies: ReturnType<typeof fakeCookies> };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── createSvelteKitHandle: Fortress-managed paths ───────────────────
|
|
81
|
+
|
|
82
|
+
describe('createSvelteKitHandle: fortress paths', () => {
|
|
83
|
+
let fortress: ReturnType<typeof makeFortress>;
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
fortress = makeFortress();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('intercepts /auth/login and returns Response without calling resolve', async () => {
|
|
90
|
+
await fortress.auth.createUser({ email: 'a@b.co', name: 'Alice', password: 'password1234567' });
|
|
91
|
+
const handle = createSvelteKitHandle(fortress);
|
|
92
|
+
const event = fakeEvent({
|
|
93
|
+
method: 'POST',
|
|
94
|
+
url: 'http://localhost/auth/login',
|
|
95
|
+
headers: { 'content-type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({ identifier: 'a@b.co', password: 'password1234567' }),
|
|
97
|
+
});
|
|
98
|
+
let resolveCalled = false;
|
|
99
|
+
const response = await handle({
|
|
100
|
+
event,
|
|
101
|
+
resolve: async () => {
|
|
102
|
+
resolveCalled = true;
|
|
103
|
+
return new Response('should not run');
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(resolveCalled).toBe(false);
|
|
107
|
+
expect(response.status).toBe(200);
|
|
108
|
+
|
|
109
|
+
// Cookies were set on the response AND replayed into event.cookies
|
|
110
|
+
expect(response.headers.getSetCookie().length).toBe(2);
|
|
111
|
+
expect(event.cookies._store.get(fortress.cookies.accessName)).toBeTruthy();
|
|
112
|
+
expect(event.cookies._store.get(fortress.cookies.refreshName)).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('strips basePath before delegating to core', async () => {
|
|
116
|
+
await fortress.auth.createUser({ email: 'a@b.co', name: 'Alice', password: 'password1234567' });
|
|
117
|
+
const handle = createSvelteKitHandle(fortress, { basePath: '/api' });
|
|
118
|
+
const event = fakeEvent({
|
|
119
|
+
method: 'POST',
|
|
120
|
+
url: 'http://localhost/api/auth/login',
|
|
121
|
+
headers: { 'content-type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({ identifier: 'a@b.co', password: 'password1234567' }),
|
|
123
|
+
});
|
|
124
|
+
const response = await handle({ event, resolve: async () => new Response() });
|
|
125
|
+
expect(response.status).toBe(200);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('falls through to resolve for paths outside basePath', async () => {
|
|
129
|
+
const handle = createSvelteKitHandle(fortress, { basePath: '/api' });
|
|
130
|
+
const event = fakeEvent({ url: 'http://localhost/about' });
|
|
131
|
+
const result = await handle({
|
|
132
|
+
event,
|
|
133
|
+
resolve: async () => new Response('user page'),
|
|
134
|
+
});
|
|
135
|
+
expect(await result.text()).toBe('user page');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── createSvelteKitHandle: user-route auth + auto-refresh ───────────
|
|
140
|
+
|
|
141
|
+
describe('createSvelteKitHandle: user routes', () => {
|
|
142
|
+
let fortress: ReturnType<typeof makeFortress>;
|
|
143
|
+
let accessToken: string;
|
|
144
|
+
|
|
145
|
+
beforeEach(async () => {
|
|
146
|
+
fortress = makeFortress();
|
|
147
|
+
await fortress.auth.createUser({ email: 'a@b.co', name: 'Alice', password: 'password1234567' });
|
|
148
|
+
const result = await fortress.auth.login('a@b.co', 'password1234567');
|
|
149
|
+
if (result.status !== 'success')
|
|
150
|
+
throw new Error('expected success');
|
|
151
|
+
accessToken = result.accessToken;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('populates event.locals.fortress when a valid cookie is present', async () => {
|
|
155
|
+
const handle = createSvelteKitHandle(fortress);
|
|
156
|
+
const event = fakeEvent({
|
|
157
|
+
url: 'http://localhost/dashboard',
|
|
158
|
+
cookies: { [fortress.cookies.accessName]: accessToken },
|
|
159
|
+
});
|
|
160
|
+
let observed: unknown;
|
|
161
|
+
await handle({
|
|
162
|
+
event,
|
|
163
|
+
resolve: async () => {
|
|
164
|
+
observed = (event.locals as { fortress?: { userId?: string } }).fortress?.userId;
|
|
165
|
+
return new Response('ok');
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
expect(observed).toBeTruthy();
|
|
169
|
+
expect(getUserId(event as never)).toBe(observed);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('falls back to Authorization: Bearer when no cookie', async () => {
|
|
173
|
+
const handle = createSvelteKitHandle(fortress);
|
|
174
|
+
const event = fakeEvent({
|
|
175
|
+
url: 'http://localhost/dashboard',
|
|
176
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
177
|
+
});
|
|
178
|
+
await handle({ event, resolve: async () => new Response() });
|
|
179
|
+
expect((event.locals as { fortress?: { userId?: string } }).fortress?.userId).toBeTruthy();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('leaves locals empty when no token at all', async () => {
|
|
183
|
+
const handle = createSvelteKitHandle(fortress);
|
|
184
|
+
const event = fakeEvent({ url: 'http://localhost/dashboard' });
|
|
185
|
+
await handle({ event, resolve: async () => new Response() });
|
|
186
|
+
const locals = (event.locals as { fortress?: { userId?: string } }).fortress;
|
|
187
|
+
expect(locals).toBeDefined();
|
|
188
|
+
expect(locals?.userId).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('auto-refreshes an expired access token using the refresh cookie', async () => {
|
|
192
|
+
// Issue a normal login to get a valid refresh token, then pair it with a
|
|
193
|
+
// deliberately expired access token. This avoids the previous 1-second
|
|
194
|
+
// expiry + sleep race: on slow CI, the freshly refreshed 1-second access
|
|
195
|
+
// token could expire before the adapter verified it and populated locals.
|
|
196
|
+
const freshLogin = await fortress.auth.login('a@b.co', 'password1234567');
|
|
197
|
+
if (freshLogin.status !== 'success')
|
|
198
|
+
throw new Error('expected success');
|
|
199
|
+
const validRefresh = freshLogin.refreshToken;
|
|
200
|
+
const expiredAccess = await signAccessToken({
|
|
201
|
+
sub: freshLogin.user.id,
|
|
202
|
+
subjectType: 'USER',
|
|
203
|
+
name: freshLogin.user.name,
|
|
204
|
+
groups: [],
|
|
205
|
+
iss: 'fortress',
|
|
206
|
+
}, SECRET, -1);
|
|
207
|
+
|
|
208
|
+
const handle = createSvelteKitHandle(fortress);
|
|
209
|
+
const event = fakeEvent({
|
|
210
|
+
url: 'http://localhost/dashboard',
|
|
211
|
+
cookies: {
|
|
212
|
+
[fortress.cookies.accessName]: expiredAccess,
|
|
213
|
+
[fortress.cookies.refreshName]: validRefresh,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
await handle({ event, resolve: async () => new Response() });
|
|
217
|
+
// After auto-refresh, the cookie store should have a NEW access token
|
|
218
|
+
// (different from the expired one) and locals.fortress.userId set.
|
|
219
|
+
const newAccess = event.cookies._store.get(fortress.cookies.accessName);
|
|
220
|
+
expect(newAccess).toBeTruthy();
|
|
221
|
+
expect(newAccess).not.toBe(expiredAccess);
|
|
222
|
+
expect((event.locals as { fortress?: { userId?: string } }).fortress?.userId).toBeTruthy();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('single-flights concurrent silent refreshes and forwards RequestMeta', async () => {
|
|
226
|
+
const local = createFortress({
|
|
227
|
+
jwt: { key: SECRET, validateRefreshFingerprint: true },
|
|
228
|
+
database: createTestAdapter(),
|
|
229
|
+
});
|
|
230
|
+
const user = await local.auth.createUser({
|
|
231
|
+
email: 'single-flight@example.com',
|
|
232
|
+
name: 'Single Flight',
|
|
233
|
+
password: 'password-123456',
|
|
234
|
+
});
|
|
235
|
+
const userAgent = 'SSR Browser/1.0';
|
|
236
|
+
const login = await local.auth.login('single-flight@example.com', 'password-123456', {
|
|
237
|
+
userAgent,
|
|
238
|
+
ipAddress: '192.0.2.42',
|
|
239
|
+
});
|
|
240
|
+
if (login.status !== 'success')
|
|
241
|
+
throw new Error('expected success');
|
|
242
|
+
const expiredAccess = await signAccessToken({
|
|
243
|
+
sub: user.id,
|
|
244
|
+
subjectType: 'USER',
|
|
245
|
+
name: user.name,
|
|
246
|
+
groups: [],
|
|
247
|
+
iss: 'fortress',
|
|
248
|
+
}, SECRET, -1);
|
|
249
|
+
|
|
250
|
+
const originalRefresh = local.auth.refresh;
|
|
251
|
+
let refreshCalls = 0;
|
|
252
|
+
let observedMeta: { ipAddress?: string; userAgent?: string } | undefined;
|
|
253
|
+
local.auth.refresh = async (token, meta) => {
|
|
254
|
+
refreshCalls++;
|
|
255
|
+
observedMeta = meta;
|
|
256
|
+
return originalRefresh(token, meta);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const handle = createSvelteKitHandle(local);
|
|
260
|
+
const makeEvent = () => fakeEvent({
|
|
261
|
+
url: 'http://localhost/dashboard',
|
|
262
|
+
headers: {
|
|
263
|
+
'user-agent': userAgent,
|
|
264
|
+
'x-forwarded-for': '192.0.2.42',
|
|
265
|
+
},
|
|
266
|
+
cookies: {
|
|
267
|
+
[local.cookies.accessName]: expiredAccess,
|
|
268
|
+
[local.cookies.refreshName]: login.refreshToken,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const first = makeEvent();
|
|
272
|
+
const second = makeEvent();
|
|
273
|
+
let releaseFirstResolve!: () => void;
|
|
274
|
+
const firstResolveGate = new Promise<void>((resolve) => {
|
|
275
|
+
releaseFirstResolve = resolve;
|
|
276
|
+
});
|
|
277
|
+
let markFirstResolveEntered!: () => void;
|
|
278
|
+
const firstResolveEntered = new Promise<void>((resolve) => {
|
|
279
|
+
markFirstResolveEntered = resolve;
|
|
280
|
+
});
|
|
281
|
+
const firstRequest = handle({
|
|
282
|
+
event: first,
|
|
283
|
+
resolve: async () => {
|
|
284
|
+
markFirstResolveEntered();
|
|
285
|
+
await firstResolveGate;
|
|
286
|
+
return new Response();
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Wait until refresh() has already settled and the first request is still
|
|
291
|
+
// inside resolve(). A correct flight must remain joinable for this whole
|
|
292
|
+
// overlapping request lifetime, not only while refresh() is pending.
|
|
293
|
+
await firstResolveEntered;
|
|
294
|
+
await handle({ event: second, resolve: async () => new Response() });
|
|
295
|
+
releaseFirstResolve();
|
|
296
|
+
await firstRequest;
|
|
297
|
+
|
|
298
|
+
expect(refreshCalls).toBe(1);
|
|
299
|
+
expect(observedMeta).toEqual({ ipAddress: '192.0.2.42', userAgent });
|
|
300
|
+
const firstSuccessor = first.cookies._store.get(local.cookies.refreshName);
|
|
301
|
+
const secondSuccessor = second.cookies._store.get(local.cookies.refreshName);
|
|
302
|
+
expect(firstSuccessor).toBeTruthy();
|
|
303
|
+
if (!firstSuccessor)
|
|
304
|
+
throw new Error('Expected refresh successor');
|
|
305
|
+
expect(secondSuccessor).toBe(firstSuccessor);
|
|
306
|
+
expect((first.locals as FortressLocals).fortress.userId).toBe(user.id);
|
|
307
|
+
expect((second.locals as FortressLocals).fortress.userId).toBe(user.id);
|
|
308
|
+
|
|
309
|
+
// The shared successor remains usable; no losing refresh revoked it.
|
|
310
|
+
await expect(originalRefresh(firstSuccessor, {
|
|
311
|
+
userAgent,
|
|
312
|
+
ipAddress: '192.0.2.42',
|
|
313
|
+
})).resolves.toBeDefined();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('does not let mismatched fingerprint metadata join a refresh flight', async () => {
|
|
317
|
+
const local = createFortress({
|
|
318
|
+
jwt: {
|
|
319
|
+
key: SECRET,
|
|
320
|
+
validateRefreshFingerprint: true,
|
|
321
|
+
session: { refreshGraceSeconds: 30 },
|
|
322
|
+
},
|
|
323
|
+
database: createTestAdapter(),
|
|
324
|
+
});
|
|
325
|
+
const user = await local.auth.createUser({
|
|
326
|
+
email: 'fingerprint-flight@example.com',
|
|
327
|
+
name: 'Fingerprint Flight',
|
|
328
|
+
password: 'password-123456',
|
|
329
|
+
});
|
|
330
|
+
const legitimateAgent = 'Legitimate Browser/1.0';
|
|
331
|
+
const login = await local.auth.login(
|
|
332
|
+
'fingerprint-flight@example.com',
|
|
333
|
+
'password-123456',
|
|
334
|
+
{ userAgent: legitimateAgent },
|
|
335
|
+
);
|
|
336
|
+
if (login.status !== 'success')
|
|
337
|
+
throw new Error('expected success');
|
|
338
|
+
const expiredAccess = await signAccessToken({
|
|
339
|
+
sub: user.id,
|
|
340
|
+
subjectType: 'USER',
|
|
341
|
+
name: user.name,
|
|
342
|
+
groups: [],
|
|
343
|
+
iss: 'fortress',
|
|
344
|
+
}, SECRET, -1);
|
|
345
|
+
const originalRefresh = local.auth.refresh;
|
|
346
|
+
let refreshCalls = 0;
|
|
347
|
+
local.auth.refresh = async (token, meta) => {
|
|
348
|
+
refreshCalls++;
|
|
349
|
+
return originalRefresh(token, meta);
|
|
350
|
+
};
|
|
351
|
+
const handle = createSvelteKitHandle(local);
|
|
352
|
+
const eventFor = (userAgent: string) => fakeEvent({
|
|
353
|
+
url: 'http://localhost/dashboard',
|
|
354
|
+
headers: { 'user-agent': userAgent },
|
|
355
|
+
cookies: {
|
|
356
|
+
[local.cookies.accessName]: expiredAccess,
|
|
357
|
+
[local.cookies.refreshName]: login.refreshToken,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
let releaseLegitimate!: () => void;
|
|
362
|
+
const legitimateGate = new Promise<void>((resolve) => {
|
|
363
|
+
releaseLegitimate = resolve;
|
|
364
|
+
});
|
|
365
|
+
let markLegitimateEntered!: () => void;
|
|
366
|
+
const legitimateEntered = new Promise<void>((resolve) => {
|
|
367
|
+
markLegitimateEntered = resolve;
|
|
368
|
+
});
|
|
369
|
+
const legitimate = eventFor(legitimateAgent);
|
|
370
|
+
const legitimateRequest = handle({
|
|
371
|
+
event: legitimate,
|
|
372
|
+
resolve: async () => {
|
|
373
|
+
markLegitimateEntered();
|
|
374
|
+
await legitimateGate;
|
|
375
|
+
return new Response();
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
await legitimateEntered;
|
|
379
|
+
|
|
380
|
+
const mismatched = eventFor('Different Browser/9.9');
|
|
381
|
+
await handle({ event: mismatched, resolve: async () => new Response() });
|
|
382
|
+
expect(refreshCalls).toBe(2);
|
|
383
|
+
expect((mismatched.locals as FortressLocals).fortress.userId).toBeUndefined();
|
|
384
|
+
expect(mismatched.cookies._store.get(local.cookies.refreshName)).toBe(login.refreshToken);
|
|
385
|
+
|
|
386
|
+
releaseLegitimate();
|
|
387
|
+
await legitimateRequest;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('does NOT silently refresh on unsafe methods (CSRF — P1.5/H5)', async () => {
|
|
391
|
+
// A silent refresh rotates the refresh token, so it must not be triggered
|
|
392
|
+
// by an unsafe (cross-site-reachable) request. SSR loads are GETs; an
|
|
393
|
+
// expired-access POST should fall through with no subject and no rotation.
|
|
394
|
+
const freshLogin = await fortress.auth.login('a@b.co', 'password1234567');
|
|
395
|
+
if (freshLogin.status !== 'success')
|
|
396
|
+
throw new Error('expected success');
|
|
397
|
+
const validRefresh = freshLogin.refreshToken;
|
|
398
|
+
const expiredAccess = await signAccessToken({
|
|
399
|
+
sub: freshLogin.user.id,
|
|
400
|
+
subjectType: 'USER',
|
|
401
|
+
name: freshLogin.user.name,
|
|
402
|
+
groups: [],
|
|
403
|
+
iss: 'fortress',
|
|
404
|
+
}, SECRET, -1);
|
|
405
|
+
|
|
406
|
+
const handle = createSvelteKitHandle(fortress);
|
|
407
|
+
const event = fakeEvent({
|
|
408
|
+
method: 'POST',
|
|
409
|
+
url: 'http://localhost/dashboard',
|
|
410
|
+
cookies: {
|
|
411
|
+
[fortress.cookies.accessName]: expiredAccess,
|
|
412
|
+
[fortress.cookies.refreshName]: validRefresh,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
await handle({ event, resolve: async () => new Response() });
|
|
416
|
+
// No rotation: access cookie unchanged, no authenticated subject.
|
|
417
|
+
expect(event.cookies._store.get(fortress.cookies.accessName)).toBe(expiredAccess);
|
|
418
|
+
expect((event.locals as { fortress?: { userId?: string } }).fortress?.userId).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ── toSvelteKitHandler ──────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
describe('toSvelteKitHandler', () => {
|
|
425
|
+
it('exports {GET, POST, PUT, DELETE, PATCH} that all delegate to handleRequest', async () => {
|
|
426
|
+
const fortress = makeFortress();
|
|
427
|
+
await fortress.auth.createUser({ email: 'a@b.co', name: 'Alice', password: 'password1234567' });
|
|
428
|
+
const handlers = toSvelteKitHandler(fortress);
|
|
429
|
+
expect(typeof handlers.GET).toBe('function');
|
|
430
|
+
expect(typeof handlers.POST).toBe('function');
|
|
431
|
+
expect(typeof handlers.PUT).toBe('function');
|
|
432
|
+
expect(typeof handlers.DELETE).toBe('function');
|
|
433
|
+
expect(typeof handlers.PATCH).toBe('function');
|
|
434
|
+
|
|
435
|
+
const event = fakeEvent({
|
|
436
|
+
method: 'POST',
|
|
437
|
+
url: 'http://localhost/auth/login',
|
|
438
|
+
headers: { 'content-type': 'application/json' },
|
|
439
|
+
body: JSON.stringify({ identifier: 'a@b.co', password: 'password1234567' }),
|
|
440
|
+
});
|
|
441
|
+
const response = await handlers.POST(event);
|
|
442
|
+
expect(response.status).toBe(200);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ── fortressActions ─────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
describe('fortressActions.login', () => {
|
|
449
|
+
it('sets cookies on success and returns { success: true }', async () => {
|
|
450
|
+
const fortress = makeFortress();
|
|
451
|
+
await fortress.auth.createUser({ email: 'a@b.co', name: 'Alice', password: 'password1234567' });
|
|
452
|
+
const action = fortressActions.login(fortress);
|
|
453
|
+
const form = new FormData();
|
|
454
|
+
form.set('identifier', 'a@b.co');
|
|
455
|
+
form.set('password', 'password1234567');
|
|
456
|
+
const event = fakeEvent({ method: 'POST', body: form });
|
|
457
|
+
const result = await action(event);
|
|
458
|
+
expect(result).toEqual({ success: true, pending: false });
|
|
459
|
+
expect(event.cookies._store.get(fortress.cookies.accessName)).toBeTruthy();
|
|
460
|
+
expect(event.cookies._store.get(fortress.cookies.refreshName)).toBeTruthy();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('returns the typed pending challenge without setting cookies', async () => {
|
|
464
|
+
const fortress = createFortress({
|
|
465
|
+
jwt: { key: SECRET },
|
|
466
|
+
database: createTestAdapter(),
|
|
467
|
+
plugins: [{
|
|
468
|
+
name: 'factor',
|
|
469
|
+
hooks: {
|
|
470
|
+
postAuthGate: {
|
|
471
|
+
reason: 'two-factor',
|
|
472
|
+
evaluate: async () => ({}),
|
|
473
|
+
verify: async () => {},
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
}],
|
|
477
|
+
});
|
|
478
|
+
await fortress.auth.createUser({ email: 'pending@b.co', name: 'Pending', password: 'password1234567' });
|
|
479
|
+
const action = fortressActions.login(fortress);
|
|
480
|
+
const form = new FormData();
|
|
481
|
+
form.set('identifier', 'pending@b.co');
|
|
482
|
+
form.set('password', 'password1234567');
|
|
483
|
+
const event = fakeEvent({ method: 'POST', body: form });
|
|
484
|
+
|
|
485
|
+
const result = await action(event);
|
|
486
|
+
expect(result).toMatchObject({
|
|
487
|
+
success: true,
|
|
488
|
+
pending: true,
|
|
489
|
+
challenge: { reason: 'two-factor', continuationToken: expect.any(String) },
|
|
490
|
+
});
|
|
491
|
+
expect(event.cookies._store.size).toBe(0);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('returns fail() shape on bad credentials', async () => {
|
|
495
|
+
const fortress = makeFortress();
|
|
496
|
+
const action = fortressActions.login(fortress);
|
|
497
|
+
const form = new FormData();
|
|
498
|
+
form.set('identifier', 'nope@b.co');
|
|
499
|
+
form.set('password', 'wrong');
|
|
500
|
+
const event = fakeEvent({ method: 'POST', body: form });
|
|
501
|
+
const result = await action(event);
|
|
502
|
+
expect(isActionFailure(result)).toBe(true);
|
|
503
|
+
if (!isActionFailure(result))
|
|
504
|
+
throw new Error('Expected SvelteKit ActionFailure');
|
|
505
|
+
expect(result.status).toBe(401);
|
|
506
|
+
expect((result.data as unknown as { code: string }).code).toBe('UNAUTHORIZED');
|
|
507
|
+
expect(event.cookies._store.size).toBe(0);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('throws a 303 Response when redirectTo is configured', async () => {
|
|
511
|
+
const fortress = makeFortress();
|
|
512
|
+
await fortress.auth.createUser({ email: 'a@b.co', name: 'Alice', password: 'password1234567' });
|
|
513
|
+
const action = fortressActions.login(fortress, { redirectTo: '/dashboard' });
|
|
514
|
+
const form = new FormData();
|
|
515
|
+
form.set('identifier', 'a@b.co');
|
|
516
|
+
form.set('password', 'password1234567');
|
|
517
|
+
const event = fakeEvent({ method: 'POST', body: form });
|
|
518
|
+
|
|
519
|
+
let thrown: unknown;
|
|
520
|
+
try {
|
|
521
|
+
await action(event);
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
thrown = error;
|
|
525
|
+
}
|
|
526
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
527
|
+
if (!isRedirect(thrown))
|
|
528
|
+
throw new Error('Expected SvelteKit Redirect');
|
|
529
|
+
expect(thrown.status).toBe(303);
|
|
530
|
+
expect(thrown.location).toBe('/dashboard');
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe('fortressActions.logout', () => {
|
|
535
|
+
it('clears auth cookies', async () => {
|
|
536
|
+
const fortress = makeFortress();
|
|
537
|
+
await fortress.auth.createUser({ email: 'a@b.co', name: 'Alice', password: 'password1234567' });
|
|
538
|
+
const login = await fortress.auth.login('a@b.co', 'password1234567');
|
|
539
|
+
if (login.status !== 'success')
|
|
540
|
+
throw new Error('expected success');
|
|
541
|
+
|
|
542
|
+
const action = fortressActions.logout(fortress);
|
|
543
|
+
const event = fakeEvent({
|
|
544
|
+
method: 'POST',
|
|
545
|
+
cookies: {
|
|
546
|
+
[fortress.cookies.accessName]: login.accessToken,
|
|
547
|
+
[fortress.cookies.refreshName]: login.refreshToken,
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
await action(event);
|
|
551
|
+
expect(event.cookies._store.has(fortress.cookies.accessName)).toBe(false);
|
|
552
|
+
expect(event.cookies._store.has(fortress.cookies.refreshName)).toBe(false);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// ── cookies utilities ───────────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
describe('replayCookies', () => {
|
|
559
|
+
it('mirrors Set-Cookie headers from a Response into event.cookies', async () => {
|
|
560
|
+
const event = fakeEvent();
|
|
561
|
+
const response = new Response('{}', {
|
|
562
|
+
status: 200,
|
|
563
|
+
headers: {
|
|
564
|
+
'Content-Type': 'application/json',
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
response.headers.append('set-cookie', 'foo=bar; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=60');
|
|
568
|
+
response.headers.append('set-cookie', 'baz=qux; Path=/');
|
|
569
|
+
|
|
570
|
+
replayCookies(response, event);
|
|
571
|
+
expect(event.cookies._store.get('foo')).toBe('bar');
|
|
572
|
+
expect(event.cookies._store.get('baz')).toBe('qux');
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('setAuthCookies', () => {
|
|
577
|
+
it('sets both access and refresh cookies with Fortress config names', () => {
|
|
578
|
+
const fortress = makeFortress();
|
|
579
|
+
const event = fakeEvent();
|
|
580
|
+
setAuthCookies(event, fortress, { accessToken: 'a', refreshToken: 'r' });
|
|
581
|
+
expect(event.cookies._store.get(fortress.cookies.accessName)).toBe('a');
|
|
582
|
+
expect(event.cookies._store.get(fortress.cookies.refreshName)).toBe('r');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('omits the refresh cookie when refreshToken is null', () => {
|
|
586
|
+
const fortress = makeFortress();
|
|
587
|
+
const event = fakeEvent();
|
|
588
|
+
setAuthCookies(event, fortress, { accessToken: 'a', refreshToken: null });
|
|
589
|
+
expect(event.cookies._store.get(fortress.cookies.accessName)).toBe('a');
|
|
590
|
+
expect(event.cookies._store.has(fortress.cookies.refreshName)).toBe(false);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// ── createSvelteKitHandle: api-key on user routes ───────────────────
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* These tests cover the gap this branch closed: plugin `resolvePrincipal`
|
|
598
|
+
* hooks (api-key) must fire on user-owned routes, not just Fortress-managed
|
|
599
|
+
* ones. Every request goes through `tryPluginPrincipal` before the JWT
|
|
600
|
+
* fallback, so both USER and SERVICE_ACCOUNT api-keys authenticate SvelteKit
|
|
601
|
+
* user routes uniformly and `event.locals.fortress.subject` is the
|
|
602
|
+
* authoritative principal.
|
|
603
|
+
*/
|
|
604
|
+
describe('createSvelteKitHandle: api-key on user routes', () => {
|
|
605
|
+
async function setupWithApiKey() {
|
|
606
|
+
const fortress: Fortress<any> = createFortress({
|
|
607
|
+
jwt: { key: SECRET },
|
|
608
|
+
database: createTestAdapter(),
|
|
609
|
+
plugins: [apiKey({ prefix: 'test' })],
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const user = await fortress.auth.createUser({
|
|
613
|
+
email: 'human@example.com',
|
|
614
|
+
name: 'Human',
|
|
615
|
+
password: 'password-123456',
|
|
616
|
+
});
|
|
617
|
+
const deployRole = await fortress.iam.createRole('deployer', [
|
|
618
|
+
{ resource: 'deploy', action: 'run' },
|
|
619
|
+
]);
|
|
620
|
+
await fortress.iam.bindRoleToUser(user.id, deployRole.id);
|
|
621
|
+
|
|
622
|
+
const sa = await fortress.iam.createServiceAccount({ name: 'ci-deploy-bot' });
|
|
623
|
+
await fortress.iam.bindRoleToServiceAccount(sa.id, deployRole.id);
|
|
624
|
+
const saNoPerm = await fortress.iam.createServiceAccount({ name: 'observer-bot' });
|
|
625
|
+
|
|
626
|
+
const { key: userKey } = await fortress.plugins['api-key'].createKey({
|
|
627
|
+
subject: { type: 'USER', id: user.id },
|
|
628
|
+
name: 'user-key',
|
|
629
|
+
});
|
|
630
|
+
const { key: saKey } = await fortress.plugins['api-key'].createKey({
|
|
631
|
+
subject: { type: 'SERVICE_ACCOUNT', id: sa.id },
|
|
632
|
+
name: 'sa-key',
|
|
633
|
+
});
|
|
634
|
+
const { key: saKeyNoPerm } = await fortress.plugins['api-key'].createKey({
|
|
635
|
+
subject: { type: 'SERVICE_ACCOUNT', id: saNoPerm.id },
|
|
636
|
+
name: 'observer-key',
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
return { fortress, user, sa, userKey, saKey, saKeyNoPerm };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
it('populates locals.fortress.subject for a USER api-key', async () => {
|
|
643
|
+
const { fortress, user, userKey } = await setupWithApiKey();
|
|
644
|
+
const handle = createSvelteKitHandle(fortress);
|
|
645
|
+
const event = fakeEvent({
|
|
646
|
+
url: 'http://localhost/dashboard',
|
|
647
|
+
headers: { authorization: `ApiKey ${userKey}` },
|
|
648
|
+
});
|
|
649
|
+
await handle({ event, resolve: async () => new Response() });
|
|
650
|
+
const locals = event.locals as unknown as FortressLocals;
|
|
651
|
+
expect(locals.fortress?.subject).toEqual({ type: 'USER', id: user.id });
|
|
652
|
+
expect(locals.fortress?.userId).toBe(user.id);
|
|
653
|
+
expect(getSubject(event as never)).toEqual({ type: 'USER', id: user.id });
|
|
654
|
+
expect(getUserId(event as never)).toBe(user.id);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('populates locals.fortress.subject for a SERVICE_ACCOUNT api-key; userId stays undefined', async () => {
|
|
658
|
+
const { fortress, sa, saKey } = await setupWithApiKey();
|
|
659
|
+
const handle = createSvelteKitHandle(fortress);
|
|
660
|
+
const event = fakeEvent({
|
|
661
|
+
url: 'http://localhost/dashboard',
|
|
662
|
+
headers: { 'x-api-key': saKey },
|
|
663
|
+
});
|
|
664
|
+
await handle({ event, resolve: async () => new Response() });
|
|
665
|
+
const locals = event.locals as unknown as FortressLocals;
|
|
666
|
+
expect(locals.fortress?.subject).toEqual({ type: 'SERVICE_ACCOUNT', id: sa.id });
|
|
667
|
+
// userId is USER-only
|
|
668
|
+
expect(locals.fortress?.userId).toBeUndefined();
|
|
669
|
+
expect(getSubject(event as never).type).toBe('SERVICE_ACCOUNT');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('getUserId throws for a SERVICE_ACCOUNT principal', async () => {
|
|
673
|
+
const { fortress, saKey } = await setupWithApiKey();
|
|
674
|
+
const handle = createSvelteKitHandle(fortress);
|
|
675
|
+
const event = fakeEvent({
|
|
676
|
+
url: 'http://localhost/dashboard',
|
|
677
|
+
headers: { 'x-api-key': saKey },
|
|
678
|
+
});
|
|
679
|
+
await handle({ event, resolve: async () => new Response() });
|
|
680
|
+
expect(() => getUserId(event as never)).toThrow(/User not authenticated/);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('still falls back to JWT bearer when no api-key header is present', async () => {
|
|
684
|
+
const { fortress, user } = await setupWithApiKey();
|
|
685
|
+
const login = await fortress.auth.login('human@example.com', 'password-123456');
|
|
686
|
+
if (login.status !== 'success')
|
|
687
|
+
throw new Error('login should succeed');
|
|
688
|
+
const handle = createSvelteKitHandle(fortress);
|
|
689
|
+
const event = fakeEvent({
|
|
690
|
+
url: 'http://localhost/dashboard',
|
|
691
|
+
headers: { authorization: `Bearer ${login.accessToken}` },
|
|
692
|
+
});
|
|
693
|
+
await handle({ event, resolve: async () => new Response() });
|
|
694
|
+
const locals = event.locals as unknown as FortressLocals;
|
|
695
|
+
expect(locals.fortress?.subject).toEqual({ type: 'USER', id: user.id });
|
|
696
|
+
expect(locals.fortress?.claims).toBeDefined();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('plugin resolvers win over a present JWT (api-key takes priority)', async () => {
|
|
700
|
+
const { fortress, sa, saKey } = await setupWithApiKey();
|
|
701
|
+
const login = await fortress.auth.login('human@example.com', 'password-123456');
|
|
702
|
+
if (login.status !== 'success')
|
|
703
|
+
throw new Error('login should succeed');
|
|
704
|
+
const handle = createSvelteKitHandle(fortress);
|
|
705
|
+
const event = fakeEvent({
|
|
706
|
+
url: 'http://localhost/dashboard',
|
|
707
|
+
headers: {
|
|
708
|
+
'authorization': `Bearer ${login.accessToken}`,
|
|
709
|
+
'x-api-key': saKey,
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
await handle({ event, resolve: async () => new Response() });
|
|
713
|
+
const locals = event.locals as unknown as FortressLocals;
|
|
714
|
+
expect(locals.fortress?.subject).toEqual({ type: 'SERVICE_ACCOUNT', id: sa.id });
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('leaves locals empty for an unknown api-key (no fallthrough to JWT)', async () => {
|
|
718
|
+
const { fortress } = await setupWithApiKey();
|
|
719
|
+
const handle = createSvelteKitHandle(fortress);
|
|
720
|
+
const event = fakeEvent({
|
|
721
|
+
url: 'http://localhost/dashboard',
|
|
722
|
+
headers: { 'x-api-key': 'test_sk_not-a-real-key' },
|
|
723
|
+
});
|
|
724
|
+
await handle({ event, resolve: async () => new Response() });
|
|
725
|
+
const locals = event.locals as unknown as FortressLocals;
|
|
726
|
+
expect(locals.fortress?.subject).toBeUndefined();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('route-map RBAC allows a SERVICE_ACCOUNT with the required permission', async () => {
|
|
730
|
+
const { fortress, saKey } = await setupWithApiKey();
|
|
731
|
+
const handle = createSvelteKitHandle(fortress, {
|
|
732
|
+
routeMap: { 'POST /deploy/run': { resource: 'deploy', action: 'run' } },
|
|
733
|
+
});
|
|
734
|
+
const event = fakeEvent({
|
|
735
|
+
method: 'POST',
|
|
736
|
+
url: 'http://localhost/deploy/run',
|
|
737
|
+
headers: { authorization: `ApiKey ${saKey}` },
|
|
738
|
+
});
|
|
739
|
+
let resolved = false;
|
|
740
|
+
let observed: Subject | undefined;
|
|
741
|
+
const response = await handle({
|
|
742
|
+
event,
|
|
743
|
+
resolve: async () => {
|
|
744
|
+
resolved = true;
|
|
745
|
+
observed = (event.locals as unknown as FortressLocals).fortress?.subject;
|
|
746
|
+
return new Response('ok');
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
expect(response.status).toBe(200);
|
|
750
|
+
expect(resolved).toBe(true);
|
|
751
|
+
expect(observed?.type).toBe('SERVICE_ACCOUNT');
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('route-map RBAC denies a SERVICE_ACCOUNT without the permission', async () => {
|
|
755
|
+
const { fortress, saKeyNoPerm } = await setupWithApiKey();
|
|
756
|
+
const handle = createSvelteKitHandle(fortress, {
|
|
757
|
+
routeMap: { 'POST /deploy/run': { resource: 'deploy', action: 'run' } },
|
|
758
|
+
});
|
|
759
|
+
const event = fakeEvent({
|
|
760
|
+
method: 'POST',
|
|
761
|
+
url: 'http://localhost/deploy/run',
|
|
762
|
+
headers: { 'x-api-key': saKeyNoPerm },
|
|
763
|
+
});
|
|
764
|
+
let resolved = false;
|
|
765
|
+
const response = await handle({
|
|
766
|
+
event,
|
|
767
|
+
resolve: async () => {
|
|
768
|
+
resolved = true;
|
|
769
|
+
return new Response('should not reach');
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
expect(resolved).toBe(false);
|
|
773
|
+
expect(response.status).toBe(403);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('route-map RBAC can fail closed for unmapped routes', async () => {
|
|
777
|
+
const { fortress } = await setupWithApiKey();
|
|
778
|
+
const handle = createSvelteKitHandle(fortress, {
|
|
779
|
+
routeMap: { 'POST /deploy/run': { resource: 'deploy', action: 'run' } },
|
|
780
|
+
unmappedRoutes: 'deny',
|
|
781
|
+
});
|
|
782
|
+
const event = fakeEvent({ method: 'GET', url: 'http://localhost/reports' });
|
|
783
|
+
const response = await handle({ event, resolve: async () => new Response('should not reach') });
|
|
784
|
+
expect(response.status).toBe(403);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('route-map RBAC returns 401 when there is no credential at all', async () => {
|
|
788
|
+
const { fortress } = await setupWithApiKey();
|
|
789
|
+
const handle = createSvelteKitHandle(fortress, {
|
|
790
|
+
routeMap: { 'POST /deploy/run': { resource: 'deploy', action: 'run' } },
|
|
791
|
+
});
|
|
792
|
+
const event = fakeEvent({
|
|
793
|
+
method: 'POST',
|
|
794
|
+
url: 'http://localhost/deploy/run',
|
|
795
|
+
});
|
|
796
|
+
const response = await handle({ event, resolve: async () => new Response('x') });
|
|
797
|
+
expect(response.status).toBe(401);
|
|
798
|
+
});
|
|
799
|
+
});
|