@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,861 @@
|
|
|
1
|
+
import type { Fortress } from '../core/fortress';
|
|
2
|
+
import type { FortressEnv } from './middleware/auth';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { FortressError } from '../core/errors';
|
|
6
|
+
import { createFortress } from '../core/fortress';
|
|
7
|
+
import { oauth } from '../plugins/oauth';
|
|
8
|
+
import { openapi } from '../plugins/openapi';
|
|
9
|
+
import { createTestAdapter } from '../testing';
|
|
10
|
+
import { mountFortress } from './handle';
|
|
11
|
+
import { getClaims, getDb, getUserId } from './helpers';
|
|
12
|
+
import { createHonoMiddleware } from './index';
|
|
13
|
+
import { createCsrfMiddleware } from './middleware/csrf';
|
|
14
|
+
|
|
15
|
+
const SECRET = 'hono-handler-test-secret-at-least-32!!';
|
|
16
|
+
|
|
17
|
+
// --- Helper ---
|
|
18
|
+
|
|
19
|
+
function createApp(fortress: Fortress, options?: Parameters<typeof createHonoMiddleware>[1]): Hono<FortressEnv> {
|
|
20
|
+
const { authMiddleware, rbacMiddleware, errorHandler } = createHonoMiddleware(fortress, options);
|
|
21
|
+
const app = new Hono<FortressEnv>();
|
|
22
|
+
app.onError(errorHandler);
|
|
23
|
+
|
|
24
|
+
app.post('/auth/login', async (c) => {
|
|
25
|
+
const { identifier, password } = await c.req.json();
|
|
26
|
+
const result = await fortress.auth.login(identifier, password);
|
|
27
|
+
return c.json(result);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.post('/auth/register', async (c) => {
|
|
31
|
+
const data = await c.req.json();
|
|
32
|
+
const user = await fortress.auth.createUser(data);
|
|
33
|
+
return c.json(user);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.use('/api/*', authMiddleware);
|
|
37
|
+
if (options?.routeMap) {
|
|
38
|
+
app.use('/api/*', rbacMiddleware);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return app;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function seedAndLogin(fortress: Fortress, app: Hono<FortressEnv>): Promise<{ token: string; userId: string }> {
|
|
45
|
+
const user = await fortress.auth.createUser({
|
|
46
|
+
email: 'test@example.com',
|
|
47
|
+
name: 'Test User',
|
|
48
|
+
password: 'password-123456',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const res = await app.request('/auth/login', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ identifier: 'test@example.com', password: 'password-123456' }),
|
|
55
|
+
});
|
|
56
|
+
const data = await res.json() as any;
|
|
57
|
+
return { token: data.accessToken, userId: user.id };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =====================
|
|
61
|
+
// Error Handler
|
|
62
|
+
// =====================
|
|
63
|
+
|
|
64
|
+
describe('error handler', () => {
|
|
65
|
+
let app: Hono<FortressEnv>;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
const fortress = createFortress({ jwt: { key: SECRET }, database: createTestAdapter() });
|
|
69
|
+
app = createApp(fortress);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns structured JSON for FortressError', async () => {
|
|
73
|
+
const res = await app.request('/api/anything');
|
|
74
|
+
expect(res.status).toBe(401);
|
|
75
|
+
const body = await res.json() as any;
|
|
76
|
+
expect(body.code).toBe('UNAUTHORIZED');
|
|
77
|
+
expect(body.message).toBeDefined();
|
|
78
|
+
expect(body.statusCode).toBe(401);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns 500 for unknown errors without stack trace', async () => {
|
|
82
|
+
const fortress = createFortress({ jwt: { key: SECRET }, database: createTestAdapter() });
|
|
83
|
+
const { errorHandler } = createHonoMiddleware(fortress);
|
|
84
|
+
const testApp = new Hono();
|
|
85
|
+
testApp.onError(errorHandler);
|
|
86
|
+
testApp.get('/blow-up', () => {
|
|
87
|
+
throw new Error('internal failure');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const res = await testApp.request('/blow-up');
|
|
91
|
+
expect(res.status).toBe(500);
|
|
92
|
+
const body = await res.json() as any;
|
|
93
|
+
expect(body.code).toBe('INTERNAL_ERROR');
|
|
94
|
+
expect(body.message).toBe('Internal server error');
|
|
95
|
+
expect(body).not.toHaveProperty('stack');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('adds Retry-After header for RATE_LIMITED errors', async () => {
|
|
99
|
+
const fortress = createFortress({ jwt: { key: SECRET }, database: createTestAdapter() });
|
|
100
|
+
const { errorHandler } = createHonoMiddleware(fortress);
|
|
101
|
+
const testApp = new Hono();
|
|
102
|
+
testApp.onError(errorHandler);
|
|
103
|
+
testApp.get('/rate-limited', () => {
|
|
104
|
+
throw new FortressError('RATE_LIMITED', 'Too many requests', 429, { retryAfter: 60 });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const res = await testApp.request('/rate-limited');
|
|
108
|
+
expect(res.status).toBe(429);
|
|
109
|
+
expect(res.headers.get('Retry-After')).toBe('60');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('maps various error codes to correct status codes', async () => {
|
|
113
|
+
const fortress = createFortress({ jwt: { key: SECRET }, database: createTestAdapter() });
|
|
114
|
+
const { errorHandler } = createHonoMiddleware(fortress);
|
|
115
|
+
const testApp = new Hono();
|
|
116
|
+
testApp.onError(errorHandler);
|
|
117
|
+
|
|
118
|
+
testApp.get('/forbidden', () => {
|
|
119
|
+
throw new FortressError('FORBIDDEN', 'Nope', 403);
|
|
120
|
+
});
|
|
121
|
+
testApp.get('/not-found', () => {
|
|
122
|
+
throw new FortressError('NOT_FOUND', 'Gone', 404);
|
|
123
|
+
});
|
|
124
|
+
testApp.get('/conflict', () => {
|
|
125
|
+
throw new FortressError('CONFLICT', 'Dupe', 409);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect((await testApp.request('/forbidden')).status).toBe(403);
|
|
129
|
+
expect((await testApp.request('/not-found')).status).toBe(404);
|
|
130
|
+
expect((await testApp.request('/conflict')).status).toBe(409);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// =====================
|
|
135
|
+
// Auth Middleware
|
|
136
|
+
// =====================
|
|
137
|
+
|
|
138
|
+
describe('auth middleware', () => {
|
|
139
|
+
let fortress: Fortress;
|
|
140
|
+
let app: Hono<FortressEnv>;
|
|
141
|
+
|
|
142
|
+
beforeEach(async () => {
|
|
143
|
+
fortress = createFortress({ jwt: { key: SECRET }, database: createTestAdapter() });
|
|
144
|
+
app = createApp(fortress);
|
|
145
|
+
|
|
146
|
+
app.get('/api/me', (c) => {
|
|
147
|
+
const userId = getUserId(c);
|
|
148
|
+
const claims = getClaims(c);
|
|
149
|
+
return c.json({ userId, name: claims.name, groups: claims.groups });
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('extracts user ID and claims from valid token', async () => {
|
|
154
|
+
const { token, userId } = await seedAndLogin(fortress, app);
|
|
155
|
+
|
|
156
|
+
const res = await app.request('/api/me', {
|
|
157
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
158
|
+
});
|
|
159
|
+
expect(res.status).toBe(200);
|
|
160
|
+
const body = await res.json() as any;
|
|
161
|
+
expect(body.userId).toBe(userId);
|
|
162
|
+
expect(body.name).toBe('Test User');
|
|
163
|
+
expect(body.groups).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects requests without Authorization header', async () => {
|
|
167
|
+
const res = await app.request('/api/me');
|
|
168
|
+
expect(res.status).toBe(401);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('rejects malformed Authorization header', async () => {
|
|
172
|
+
const res = await app.request('/api/me', {
|
|
173
|
+
headers: { Authorization: 'NotBearer token' },
|
|
174
|
+
});
|
|
175
|
+
expect(res.status).toBe(401);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('rejects expired/invalid tokens', async () => {
|
|
179
|
+
const res = await app.request('/api/me', {
|
|
180
|
+
headers: { Authorization: 'Bearer totally.invalid.token' },
|
|
181
|
+
});
|
|
182
|
+
expect(res.status).toBe(401);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('provides database adapter via getDb()', async () => {
|
|
186
|
+
app.get('/api/db-test', (c) => {
|
|
187
|
+
const db = getDb(c);
|
|
188
|
+
return c.json({ hasCreate: typeof db.create === 'function' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const { token } = await seedAndLogin(fortress, app);
|
|
192
|
+
const res = await app.request('/api/db-test', {
|
|
193
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
194
|
+
});
|
|
195
|
+
expect(res.status).toBe(200);
|
|
196
|
+
const body = await res.json() as any;
|
|
197
|
+
expect(body.hasCreate).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// =====================
|
|
202
|
+
// RBAC Middleware
|
|
203
|
+
// =====================
|
|
204
|
+
|
|
205
|
+
describe('rbac middleware', () => {
|
|
206
|
+
let fortress: Fortress;
|
|
207
|
+
let app: Hono<FortressEnv>;
|
|
208
|
+
|
|
209
|
+
beforeEach(async () => {
|
|
210
|
+
fortress = createFortress({ jwt: { key: SECRET }, database: createTestAdapter() });
|
|
211
|
+
app = createApp(fortress, {
|
|
212
|
+
routeMap: {
|
|
213
|
+
'GET /api/posts': { resource: 'post', action: 'list' },
|
|
214
|
+
'POST /api/posts': { resource: 'post', action: 'create' },
|
|
215
|
+
'GET /api/posts/:id': { resource: 'post', action: 'read' },
|
|
216
|
+
'PUT /api/posts/:id': { resource: 'post', action: 'update' },
|
|
217
|
+
'DELETE /api/posts/:id': { resource: 'post', action: 'delete' },
|
|
218
|
+
},
|
|
219
|
+
skipPaths: ['/health', '/public/*'],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
app.get('/api/posts', c => c.json({ posts: [] }));
|
|
223
|
+
app.post('/api/posts', c => c.json({ created: true }));
|
|
224
|
+
app.get('/api/posts/:id', c => c.json({ id: c.req.param('id') }));
|
|
225
|
+
app.put('/api/posts/:id', c => c.json({ updated: true }));
|
|
226
|
+
app.delete('/api/posts/:id', c => c.json({ deleted: true }));
|
|
227
|
+
app.get('/api/unmapped', c => c.json({ ok: true }));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('denies when user has no permissions', async () => {
|
|
231
|
+
const { token } = await seedAndLogin(fortress, app);
|
|
232
|
+
|
|
233
|
+
const res = await app.request('/api/posts', {
|
|
234
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
235
|
+
});
|
|
236
|
+
expect(res.status).toBe(403);
|
|
237
|
+
const body = await res.json() as any;
|
|
238
|
+
expect(body.code).toBe('FORBIDDEN');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('allows when user has matching permission', async () => {
|
|
242
|
+
const { token, userId } = await seedAndLogin(fortress, app);
|
|
243
|
+
const role = await fortress.iam.createRole('reader', [{ resource: 'post', action: 'list' }]);
|
|
244
|
+
await fortress.iam.bindRoleToUser(userId, role.id);
|
|
245
|
+
|
|
246
|
+
const res = await app.request('/api/posts', {
|
|
247
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
248
|
+
});
|
|
249
|
+
expect(res.status).toBe(200);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('enforces per-action permission: allow read, deny create', async () => {
|
|
253
|
+
const { token, userId } = await seedAndLogin(fortress, app);
|
|
254
|
+
const role = await fortress.iam.createRole('reader', [{ resource: 'post', action: 'list' }]);
|
|
255
|
+
await fortress.iam.bindRoleToUser(userId, role.id);
|
|
256
|
+
|
|
257
|
+
const getRes = await app.request('/api/posts', {
|
|
258
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
259
|
+
});
|
|
260
|
+
expect(getRes.status).toBe(200);
|
|
261
|
+
|
|
262
|
+
const postRes = await app.request('/api/posts', {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({}),
|
|
266
|
+
});
|
|
267
|
+
expect(postRes.status).toBe(403);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('matches PUT and DELETE on parameterized routes', async () => {
|
|
271
|
+
const { token, userId } = await seedAndLogin(fortress, app);
|
|
272
|
+
const role = await fortress.iam.createRole('editor', [
|
|
273
|
+
{ resource: 'post', action: 'update' },
|
|
274
|
+
{ resource: 'post', action: 'delete' },
|
|
275
|
+
]);
|
|
276
|
+
await fortress.iam.bindRoleToUser(userId, role.id);
|
|
277
|
+
|
|
278
|
+
const putRes = await app.request('/api/posts/42', {
|
|
279
|
+
method: 'PUT',
|
|
280
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
281
|
+
body: JSON.stringify({}),
|
|
282
|
+
});
|
|
283
|
+
expect(putRes.status).toBe(200);
|
|
284
|
+
|
|
285
|
+
const deleteRes = await app.request('/api/posts/42', {
|
|
286
|
+
method: 'DELETE',
|
|
287
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
288
|
+
});
|
|
289
|
+
expect(deleteRes.status).toBe(200);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('passes through unmapped routes without permission check', async () => {
|
|
293
|
+
const { token } = await seedAndLogin(fortress, app);
|
|
294
|
+
|
|
295
|
+
const res = await app.request('/api/unmapped', {
|
|
296
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
297
|
+
});
|
|
298
|
+
expect(res.status).toBe(200);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('supports dynamic mapRequest fallback', async () => {
|
|
302
|
+
const mapFortress = createFortress({ jwt: { key: SECRET }, database: createTestAdapter() });
|
|
303
|
+
// Pass a routeMap (even empty) so rbacMiddleware is applied by createApp
|
|
304
|
+
const mapApp = createApp(mapFortress, {
|
|
305
|
+
routeMap: {},
|
|
306
|
+
mapRequest: (method, path) => {
|
|
307
|
+
if (method === 'GET' && path.startsWith('/api/dynamic/'))
|
|
308
|
+
return { resource: 'dynamic', action: 'read' };
|
|
309
|
+
return null;
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
mapApp.get('/api/dynamic/:id', c => c.json({ id: c.req.param('id') }));
|
|
314
|
+
|
|
315
|
+
const { token, userId } = await seedAndLogin(mapFortress, mapApp);
|
|
316
|
+
|
|
317
|
+
// No permission → denied
|
|
318
|
+
const denied = await mapApp.request('/api/dynamic/1', {
|
|
319
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
320
|
+
});
|
|
321
|
+
expect(denied.status).toBe(403);
|
|
322
|
+
|
|
323
|
+
// Grant permission → allowed
|
|
324
|
+
const role = await mapFortress.iam.createRole('dyn-reader', [{ resource: 'dynamic', action: 'read' }]);
|
|
325
|
+
await mapFortress.iam.bindRoleToUser(userId, role.id);
|
|
326
|
+
|
|
327
|
+
// Need a fresh token since claims are cached
|
|
328
|
+
const loginRes = await mapApp.request('/auth/login', {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
headers: { 'Content-Type': 'application/json' },
|
|
331
|
+
body: JSON.stringify({ identifier: 'test@example.com', password: 'password-123456' }),
|
|
332
|
+
});
|
|
333
|
+
const newToken = ((await loginRes.json()) as any).accessToken;
|
|
334
|
+
|
|
335
|
+
const allowed = await mapApp.request('/api/dynamic/1', {
|
|
336
|
+
headers: { Authorization: `Bearer ${newToken}` },
|
|
337
|
+
});
|
|
338
|
+
expect(allowed.status).toBe(200);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// =====================
|
|
343
|
+
// CSRF Middleware
|
|
344
|
+
// =====================
|
|
345
|
+
|
|
346
|
+
describe('csrf middleware', () => {
|
|
347
|
+
let app: Hono;
|
|
348
|
+
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
app = new Hono();
|
|
351
|
+
app.use('*', createCsrfMiddleware());
|
|
352
|
+
app.get('/data', c => c.json({ ok: true }));
|
|
353
|
+
app.post('/data', c => c.json({ ok: true }));
|
|
354
|
+
app.put('/data', c => c.json({ ok: true }));
|
|
355
|
+
app.patch('/data', c => c.json({ ok: true }));
|
|
356
|
+
app.delete('/data', c => c.json({ ok: true }));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('allows GET without CSRF header', async () => {
|
|
360
|
+
const res = await app.request('/data');
|
|
361
|
+
expect(res.status).toBe(200);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('allows POST with CSRF header', async () => {
|
|
365
|
+
const res = await app.request('/data', {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: { 'X-Fortress-CSRF': '1', 'Content-Type': 'application/json' },
|
|
368
|
+
body: '{}',
|
|
369
|
+
});
|
|
370
|
+
expect(res.status).toBe(200);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('rejects POST without CSRF header', async () => {
|
|
374
|
+
const res = await app.request('/data', {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
377
|
+
body: '{}',
|
|
378
|
+
});
|
|
379
|
+
expect(res.status).toBe(403);
|
|
380
|
+
const body = await res.json() as any;
|
|
381
|
+
expect(body.error).toBe('CSRF_MISSING');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('rejects PUT without CSRF header', async () => {
|
|
385
|
+
const res = await app.request('/data', {
|
|
386
|
+
method: 'PUT',
|
|
387
|
+
headers: { 'Content-Type': 'application/json' },
|
|
388
|
+
body: '{}',
|
|
389
|
+
});
|
|
390
|
+
expect(res.status).toBe(403);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('rejects PATCH without CSRF header', async () => {
|
|
394
|
+
const res = await app.request('/data', {
|
|
395
|
+
method: 'PATCH',
|
|
396
|
+
headers: { 'Content-Type': 'application/json' },
|
|
397
|
+
body: '{}',
|
|
398
|
+
});
|
|
399
|
+
expect(res.status).toBe(403);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('rejects DELETE without CSRF header', async () => {
|
|
403
|
+
const res = await app.request('/data', { method: 'DELETE' });
|
|
404
|
+
expect(res.status).toBe(403);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('allows PUT/PATCH/DELETE with CSRF header', async () => {
|
|
408
|
+
for (const method of ['PUT', 'PATCH', 'DELETE']) {
|
|
409
|
+
const res = await app.request('/data', {
|
|
410
|
+
method,
|
|
411
|
+
headers: { 'X-Fortress-CSRF': '1', 'Content-Type': 'application/json' },
|
|
412
|
+
body: method !== 'DELETE' ? '{}' : undefined,
|
|
413
|
+
});
|
|
414
|
+
expect(res.status).toBe(200);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('rejects cross-site requests via Sec-Fetch-Site', async () => {
|
|
419
|
+
const res = await app.request('/data', {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: {
|
|
422
|
+
'X-Fortress-CSRF': '1',
|
|
423
|
+
'Sec-Fetch-Site': 'cross-site',
|
|
424
|
+
'Content-Type': 'application/json',
|
|
425
|
+
},
|
|
426
|
+
body: '{}',
|
|
427
|
+
});
|
|
428
|
+
expect(res.status).toBe(403);
|
|
429
|
+
const body = await res.json() as any;
|
|
430
|
+
expect(body.error).toBe('CSRF_REJECTED');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('allows same-origin requests via Sec-Fetch-Site', async () => {
|
|
434
|
+
const res = await app.request('/data', {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: {
|
|
437
|
+
'X-Fortress-CSRF': '1',
|
|
438
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
439
|
+
'Content-Type': 'application/json',
|
|
440
|
+
},
|
|
441
|
+
body: '{}',
|
|
442
|
+
});
|
|
443
|
+
expect(res.status).toBe(200);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('supports custom header name', async () => {
|
|
447
|
+
const customApp = new Hono();
|
|
448
|
+
customApp.use('*', createCsrfMiddleware({ headerName: 'X-Custom-CSRF' }));
|
|
449
|
+
customApp.post('/data', c => c.json({ ok: true }));
|
|
450
|
+
|
|
451
|
+
// Wrong header name
|
|
452
|
+
const bad = await customApp.request('/data', {
|
|
453
|
+
method: 'POST',
|
|
454
|
+
headers: { 'X-Fortress-CSRF': '1', 'Content-Type': 'application/json' },
|
|
455
|
+
body: '{}',
|
|
456
|
+
});
|
|
457
|
+
expect(bad.status).toBe(403);
|
|
458
|
+
|
|
459
|
+
// Correct header name
|
|
460
|
+
const good = await customApp.request('/data', {
|
|
461
|
+
method: 'POST',
|
|
462
|
+
headers: { 'X-Custom-CSRF': '1', 'Content-Type': 'application/json' },
|
|
463
|
+
body: '{}',
|
|
464
|
+
});
|
|
465
|
+
expect(good.status).toBe(200);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('supports skip paths', async () => {
|
|
469
|
+
const skipApp = new Hono();
|
|
470
|
+
skipApp.use('*', createCsrfMiddleware({ skipPaths: ['/webhook/'] }));
|
|
471
|
+
skipApp.post('/webhook/github', c => c.json({ ok: true }));
|
|
472
|
+
skipApp.post('/api/data', c => c.json({ ok: true }));
|
|
473
|
+
|
|
474
|
+
const skipped = await skipApp.request('/webhook/github', {
|
|
475
|
+
method: 'POST',
|
|
476
|
+
headers: { 'Content-Type': 'application/json' },
|
|
477
|
+
body: '{}',
|
|
478
|
+
});
|
|
479
|
+
expect(skipped.status).toBe(200);
|
|
480
|
+
|
|
481
|
+
const enforced = await skipApp.request('/api/data', {
|
|
482
|
+
method: 'POST',
|
|
483
|
+
headers: { 'Content-Type': 'application/json' },
|
|
484
|
+
body: '{}',
|
|
485
|
+
});
|
|
486
|
+
expect(enforced.status).toBe(403);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// =====================
|
|
491
|
+
// Plugin Route Mounting
|
|
492
|
+
// =====================
|
|
493
|
+
|
|
494
|
+
describe('plugin route mounting: OAuth', () => {
|
|
495
|
+
let fortress: Fortress<any>;
|
|
496
|
+
let app: Hono<FortressEnv>;
|
|
497
|
+
let oauthClient: { clientId: string; clientSecret: string };
|
|
498
|
+
|
|
499
|
+
beforeEach(async () => {
|
|
500
|
+
fortress = createFortress({
|
|
501
|
+
jwt: { key: SECRET },
|
|
502
|
+
database: createTestAdapter(),
|
|
503
|
+
plugins: [oauth({ issuerUrl: 'https://auth.example.com' })],
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
app = new Hono<FortressEnv>();
|
|
507
|
+
const { errorHandler } = createHonoMiddleware(fortress);
|
|
508
|
+
app.onError(errorHandler);
|
|
509
|
+
|
|
510
|
+
mountFortress(app, fortress);
|
|
511
|
+
|
|
512
|
+
// Create an OAuth client
|
|
513
|
+
const methods = fortress.plugins.oauth as any;
|
|
514
|
+
oauthClient = await methods.createClient({
|
|
515
|
+
name: 'Test App',
|
|
516
|
+
redirectUris: ['https://app.example.com/callback'],
|
|
517
|
+
grantTypes: ['authorization_code', 'client_credentials'],
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('gET /.well-known/openid-configuration returns discovery document', async () => {
|
|
522
|
+
const res = await app.request('/oauth/.well-known/openid-configuration');
|
|
523
|
+
expect(res.status).toBe(200);
|
|
524
|
+
const body = await res.json() as any;
|
|
525
|
+
expect(body.issuer).toBe('https://auth.example.com');
|
|
526
|
+
expect(body.token_endpoint).toContain('/oauth/token');
|
|
527
|
+
expect(body.grant_types_supported).toContain('authorization_code');
|
|
528
|
+
expect(body.grant_types_supported).toContain('client_credentials');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('pOST /oauth/token with client_credentials grant', async () => {
|
|
532
|
+
const basicAuth = btoa(`${oauthClient.clientId}:${oauthClient.clientSecret}`);
|
|
533
|
+
|
|
534
|
+
const res = await app.request('/oauth/token', {
|
|
535
|
+
method: 'POST',
|
|
536
|
+
headers: {
|
|
537
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
538
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
539
|
+
},
|
|
540
|
+
body: 'grant_type=client_credentials&scope=read',
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
expect(res.status).toBe(200);
|
|
544
|
+
const body = await res.json() as any;
|
|
545
|
+
expect(body.access_token).toBeTruthy();
|
|
546
|
+
expect(body.token_type).toBe('Bearer');
|
|
547
|
+
expect(body.expires_in).toBeDefined();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('pOST /oauth/token rejects invalid client credentials', async () => {
|
|
551
|
+
const basicAuth = btoa(`${oauthClient.clientId}:wrong-secret`);
|
|
552
|
+
|
|
553
|
+
const res = await app.request('/oauth/token', {
|
|
554
|
+
method: 'POST',
|
|
555
|
+
headers: {
|
|
556
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
557
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
558
|
+
},
|
|
559
|
+
body: 'grant_type=client_credentials',
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
expect(res.status).toBe(401);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('pOST /oauth/token rejects unsupported grant_type', async () => {
|
|
566
|
+
const basicAuth = btoa(`${oauthClient.clientId}:${oauthClient.clientSecret}`);
|
|
567
|
+
|
|
568
|
+
const res = await app.request('/oauth/token', {
|
|
569
|
+
method: 'POST',
|
|
570
|
+
headers: {
|
|
571
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
572
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
573
|
+
},
|
|
574
|
+
body: 'grant_type=password',
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
expect(res.status).toBe(400);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('pOST /oauth/introspect validates token', async () => {
|
|
581
|
+
const basicAuth = btoa(`${oauthClient.clientId}:${oauthClient.clientSecret}`);
|
|
582
|
+
|
|
583
|
+
// First get a token
|
|
584
|
+
const tokenRes = await app.request('/oauth/token', {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers: {
|
|
587
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
588
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
589
|
+
},
|
|
590
|
+
body: 'grant_type=client_credentials',
|
|
591
|
+
});
|
|
592
|
+
const tokenBody = await tokenRes.json() as any;
|
|
593
|
+
|
|
594
|
+
// Introspect it
|
|
595
|
+
const introspectRes = await app.request('/oauth/introspect', {
|
|
596
|
+
method: 'POST',
|
|
597
|
+
headers: {
|
|
598
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
599
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
600
|
+
},
|
|
601
|
+
body: `token=${tokenBody.access_token}`,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
expect(introspectRes.status).toBe(200);
|
|
605
|
+
const body = await introspectRes.json() as any;
|
|
606
|
+
expect(body.active).toBe(true);
|
|
607
|
+
expect(body.client_id).toBe(oauthClient.clientId);
|
|
608
|
+
expect(body.token_type).toBe('Bearer');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('pOST /oauth/introspect rejects without client auth', async () => {
|
|
612
|
+
const res = await app.request('/oauth/introspect', {
|
|
613
|
+
method: 'POST',
|
|
614
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
615
|
+
body: 'token=some-token',
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
expect(res.status).toBe(401);
|
|
619
|
+
const body = await res.json() as any;
|
|
620
|
+
expect(body.error).toBe('invalid_client');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('pOST /oauth/introspect returns inactive for invalid token', async () => {
|
|
624
|
+
const basicAuth = btoa(`${oauthClient.clientId}:${oauthClient.clientSecret}`);
|
|
625
|
+
|
|
626
|
+
const res = await app.request('/oauth/introspect', {
|
|
627
|
+
method: 'POST',
|
|
628
|
+
headers: {
|
|
629
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
630
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
631
|
+
},
|
|
632
|
+
body: 'token=nonexistent-token',
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
expect(res.status).toBe(200);
|
|
636
|
+
const body = await res.json() as any;
|
|
637
|
+
expect(body.active).toBe(false);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('pOST /oauth/revoke succeeds for valid token', async () => {
|
|
641
|
+
const basicAuth = btoa(`${oauthClient.clientId}:${oauthClient.clientSecret}`);
|
|
642
|
+
|
|
643
|
+
// Get token
|
|
644
|
+
const tokenRes = await app.request('/oauth/token', {
|
|
645
|
+
method: 'POST',
|
|
646
|
+
headers: {
|
|
647
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
648
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
649
|
+
},
|
|
650
|
+
body: 'grant_type=client_credentials',
|
|
651
|
+
});
|
|
652
|
+
const { access_token } = await tokenRes.json() as any;
|
|
653
|
+
|
|
654
|
+
// Revoke
|
|
655
|
+
const revokeRes = await app.request('/oauth/revoke', {
|
|
656
|
+
method: 'POST',
|
|
657
|
+
headers: {
|
|
658
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
659
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
660
|
+
},
|
|
661
|
+
body: `token=${access_token}`,
|
|
662
|
+
});
|
|
663
|
+
expect(revokeRes.status).toBe(200);
|
|
664
|
+
|
|
665
|
+
// Introspect should show inactive
|
|
666
|
+
const introspectRes = await app.request('/oauth/introspect', {
|
|
667
|
+
method: 'POST',
|
|
668
|
+
headers: {
|
|
669
|
+
'Authorization': `Basic ${basicAuth}`,
|
|
670
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
671
|
+
},
|
|
672
|
+
body: `token=${access_token}`,
|
|
673
|
+
});
|
|
674
|
+
const body = await introspectRes.json() as any;
|
|
675
|
+
expect(body.active).toBe(false);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('gET /oauth/userinfo requires Bearer token', async () => {
|
|
679
|
+
const res = await app.request('/oauth/userinfo');
|
|
680
|
+
expect(res.status).toBe(401);
|
|
681
|
+
const body = await res.json() as any;
|
|
682
|
+
expect(body.error).toBe('invalid_token');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('gET /oauth/userinfo scope-gates identity claims for a valid token', async () => {
|
|
686
|
+
// Create user and auth code
|
|
687
|
+
const user = await fortress.auth.createUser({
|
|
688
|
+
email: 'oauth-user@test.com',
|
|
689
|
+
name: 'OAuth User',
|
|
690
|
+
password: 'password-123456',
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const methods = fortress.plugins.oauth as any;
|
|
694
|
+
const { code } = await methods.createAuthorizationCode({
|
|
695
|
+
clientId: oauthClient.clientId,
|
|
696
|
+
userId: user.id,
|
|
697
|
+
redirectUri: 'https://app.example.com/callback',
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const tokenResult = await methods.exchangeCode({
|
|
701
|
+
code,
|
|
702
|
+
clientId: oauthClient.clientId,
|
|
703
|
+
clientSecret: oauthClient.clientSecret,
|
|
704
|
+
redirectUri: 'https://app.example.com/callback',
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const res = await app.request('/oauth/userinfo', {
|
|
708
|
+
headers: { Authorization: `Bearer ${tokenResult.accessToken}` },
|
|
709
|
+
});
|
|
710
|
+
expect(res.status).toBe(200);
|
|
711
|
+
const body = await res.json() as any;
|
|
712
|
+
expect(body.sub).toBe(String(user.id));
|
|
713
|
+
expect(body.email).toBeUndefined();
|
|
714
|
+
expect(body.name).toBeUndefined();
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// =====================
|
|
719
|
+
// Plugin Route Mounting: OpenAPI with prefix
|
|
720
|
+
// =====================
|
|
721
|
+
|
|
722
|
+
describe('plugin route mounting: OpenAPI with prefix', () => {
|
|
723
|
+
it('scalar UI data-url resolves to prefixed spec path', async () => {
|
|
724
|
+
const fortress = createFortress({
|
|
725
|
+
jwt: { key: SECRET },
|
|
726
|
+
database: createTestAdapter(),
|
|
727
|
+
plugins: [openapi()],
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const app = new Hono<FortressEnv>();
|
|
731
|
+
const { errorHandler } = createHonoMiddleware(fortress);
|
|
732
|
+
app.onError(errorHandler);
|
|
733
|
+
|
|
734
|
+
mountFortress(app, fortress, { prefix: '/api/v1' });
|
|
735
|
+
|
|
736
|
+
// Spec should be served at the prefixed path
|
|
737
|
+
const specRes = await app.request('/api/v1/openapi.json');
|
|
738
|
+
expect(specRes.status).toBe(200);
|
|
739
|
+
const spec = await specRes.json() as any;
|
|
740
|
+
expect(spec.openapi).toBe('3.1.0');
|
|
741
|
+
|
|
742
|
+
// UI should be served at the prefixed path
|
|
743
|
+
const uiRes = await app.request('/api/v1/openapi');
|
|
744
|
+
expect(uiRes.status).toBe(200);
|
|
745
|
+
const html = await uiRes.text();
|
|
746
|
+
expect(html).toContain('data-url="./openapi.json"');
|
|
747
|
+
// Must NOT contain the absolute unprefixed path
|
|
748
|
+
expect(html).not.toContain('data-url="/openapi.json"');
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('unprefixed spec path returns 404', async () => {
|
|
752
|
+
const fortress = createFortress({
|
|
753
|
+
jwt: { key: SECRET },
|
|
754
|
+
database: createTestAdapter(),
|
|
755
|
+
plugins: [openapi()],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const app = new Hono<FortressEnv>();
|
|
759
|
+
mountFortress(app, fortress, { prefix: '/api/v1' });
|
|
760
|
+
|
|
761
|
+
const res = await app.request('/openapi.json');
|
|
762
|
+
expect(res.status).toBe(404);
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// =====================
|
|
767
|
+
// Full auth flow via Hono
|
|
768
|
+
// =====================
|
|
769
|
+
|
|
770
|
+
describe('full auth flow via Hono', () => {
|
|
771
|
+
let fortress: Fortress;
|
|
772
|
+
let app: Hono<FortressEnv>;
|
|
773
|
+
|
|
774
|
+
beforeEach(async () => {
|
|
775
|
+
fortress = createFortress({
|
|
776
|
+
jwt: { key: SECRET },
|
|
777
|
+
database: createTestAdapter(),
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
app = createApp(fortress);
|
|
781
|
+
|
|
782
|
+
app.post('/auth/refresh', async (c) => {
|
|
783
|
+
const { refreshToken } = await c.req.json();
|
|
784
|
+
const result = await fortress.auth.refresh(refreshToken);
|
|
785
|
+
return c.json(result);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
app.post('/auth/logout', async (c) => {
|
|
789
|
+
const { refreshToken } = await c.req.json();
|
|
790
|
+
await fortress.auth.logout(refreshToken);
|
|
791
|
+
return c.json({ ok: true });
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
app.get('/api/me', (c) => {
|
|
795
|
+
const userId = getUserId(c);
|
|
796
|
+
return c.json({ userId });
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('register → login → access protected → refresh → access again → logout', async () => {
|
|
801
|
+
// Register
|
|
802
|
+
const regRes = await app.request('/auth/register', {
|
|
803
|
+
method: 'POST',
|
|
804
|
+
headers: { 'Content-Type': 'application/json' },
|
|
805
|
+
body: JSON.stringify({ email: 'flow@test.com', name: 'Flow', password: 'password-123456' }),
|
|
806
|
+
});
|
|
807
|
+
expect(regRes.status).toBe(200);
|
|
808
|
+
|
|
809
|
+
// Login
|
|
810
|
+
const loginRes = await app.request('/auth/login', {
|
|
811
|
+
method: 'POST',
|
|
812
|
+
headers: { 'Content-Type': 'application/json' },
|
|
813
|
+
body: JSON.stringify({ identifier: 'flow@test.com', password: 'password-123456' }),
|
|
814
|
+
});
|
|
815
|
+
expect(loginRes.status).toBe(200);
|
|
816
|
+
const loginData = await loginRes.json() as any;
|
|
817
|
+
expect(loginData.accessToken).toBeTruthy();
|
|
818
|
+
expect(loginData.refreshToken).toBeTruthy();
|
|
819
|
+
|
|
820
|
+
// Access protected route
|
|
821
|
+
const meRes = await app.request('/api/me', {
|
|
822
|
+
headers: { Authorization: `Bearer ${loginData.accessToken}` },
|
|
823
|
+
});
|
|
824
|
+
expect(meRes.status).toBe(200);
|
|
825
|
+
const meData = await meRes.json() as any;
|
|
826
|
+
expect(meData.userId).toBe(loginData.user.id);
|
|
827
|
+
|
|
828
|
+
// Refresh
|
|
829
|
+
const refreshRes = await app.request('/auth/refresh', {
|
|
830
|
+
method: 'POST',
|
|
831
|
+
headers: { 'Content-Type': 'application/json' },
|
|
832
|
+
body: JSON.stringify({ refreshToken: loginData.refreshToken }),
|
|
833
|
+
});
|
|
834
|
+
expect(refreshRes.status).toBe(200);
|
|
835
|
+
const refreshData = await refreshRes.json() as any;
|
|
836
|
+
expect(refreshData.accessToken).toBeTruthy();
|
|
837
|
+
expect(refreshData.refreshToken).toBeTruthy();
|
|
838
|
+
|
|
839
|
+
// Access with new token
|
|
840
|
+
const meRes2 = await app.request('/api/me', {
|
|
841
|
+
headers: { Authorization: `Bearer ${refreshData.accessToken}` },
|
|
842
|
+
});
|
|
843
|
+
expect(meRes2.status).toBe(200);
|
|
844
|
+
|
|
845
|
+
// Logout
|
|
846
|
+
const logoutRes = await app.request('/auth/logout', {
|
|
847
|
+
method: 'POST',
|
|
848
|
+
headers: { 'Content-Type': 'application/json' },
|
|
849
|
+
body: JSON.stringify({ refreshToken: refreshData.refreshToken }),
|
|
850
|
+
});
|
|
851
|
+
expect(logoutRes.status).toBe(200);
|
|
852
|
+
|
|
853
|
+
// Refresh after logout should fail
|
|
854
|
+
const failedRefresh = await app.request('/auth/refresh', {
|
|
855
|
+
method: 'POST',
|
|
856
|
+
headers: { 'Content-Type': 'application/json' },
|
|
857
|
+
body: JSON.stringify({ refreshToken: refreshData.refreshToken }),
|
|
858
|
+
});
|
|
859
|
+
expect(failedRefresh.status).toBe(401);
|
|
860
|
+
});
|
|
861
|
+
});
|