@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,236 @@
|
|
|
1
|
+
import type { Context, Env, MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { DatabaseAdapter } from '../../adapters/database';
|
|
3
|
+
import type { Fortress } from '../../core/fortress';
|
|
4
|
+
import type { PluginContext } from '../../core/plugin';
|
|
5
|
+
import type { Subject, TokenClaims } from '../../core/types';
|
|
6
|
+
import { FortressError } from '../../core/errors';
|
|
7
|
+
import {
|
|
8
|
+
chainAdapterWrappers,
|
|
9
|
+
collectScopeRules,
|
|
10
|
+
wrapAdapterWithScopeRules,
|
|
11
|
+
} from '../../core/plugin-runner';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hono `Variables` slot populated by the fortress auth middleware.
|
|
15
|
+
*
|
|
16
|
+
* `fortressSubject` is the authoritative principal — set for every
|
|
17
|
+
* authenticated request regardless of credential type (JWT, api-key, future
|
|
18
|
+
* OAuth client_credentials, mTLS, ...). `fortressUserId` is a convenience
|
|
19
|
+
* alias populated **only** when the subject is a `USER`; non-USER subjects
|
|
20
|
+
* (e.g. `SERVICE_ACCOUNT` via api-key) leave it `undefined`.
|
|
21
|
+
*
|
|
22
|
+
* Use {@link FortressEnv} to compose this with your own Hono env types
|
|
23
|
+
* without casts.
|
|
24
|
+
*/
|
|
25
|
+
export interface FortressVariables {
|
|
26
|
+
fortressSubject: Subject;
|
|
27
|
+
fortressUserId?: string;
|
|
28
|
+
fortressClaims?: TokenClaims;
|
|
29
|
+
fortressScopes?: string[] | null;
|
|
30
|
+
fortressDb: DatabaseAdapter;
|
|
31
|
+
fortressGetScopedDb: (model: string) => Promise<DatabaseAdapter>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ExtractVariables<E> = E extends { Variables: infer V } ? V : Record<string, never>;
|
|
35
|
+
type ExtractBindings<E> = E extends { Bindings: infer B } ? B : Record<string, never>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generic Hono env that composes {@link FortressVariables} with the host
|
|
39
|
+
* app's own env types. Use it directly (`Hono<FortressEnv>`) for a
|
|
40
|
+
* Fortress-only app, or parameterize with your existing env
|
|
41
|
+
* (`Hono<FortressEnv<MyEnv>>`) to add Fortress's variables on top of
|
|
42
|
+
* yours — no `Context<AppEnv>` ↔ `Context<FortressEnv>` casts.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* interface MyEnv {
|
|
47
|
+
* Variables: { requestId: string };
|
|
48
|
+
* Bindings: { DB: D1Database };
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* const app = new Hono<FortressEnv<MyEnv>>();
|
|
52
|
+
* app.use(createAuthMiddleware(fortress));
|
|
53
|
+
* app.get('/me', (c) => {
|
|
54
|
+
* const requestId = c.get('requestId'); // host-defined, typed
|
|
55
|
+
* const subject = getSubject(c); // fortress-defined, typed
|
|
56
|
+
* return c.json({ requestId, subject });
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export interface FortressEnv<TAppEnv extends Env = Env> {
|
|
61
|
+
Variables: FortressVariables & ExtractVariables<TAppEnv>;
|
|
62
|
+
Bindings: ExtractBindings<TAppEnv>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Minimum-viable context shape the typed helpers need — works with any `Context<E>` whose `Variables` include {@link FortressVariables}. */
|
|
66
|
+
export type FortressContext<E extends Env = FortressEnv> = Context<E>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hono middleware that resolves the request principal and populates the
|
|
70
|
+
* Hono context with `fortressSubject`, `fortressUserId` (USER alias),
|
|
71
|
+
* `fortressClaims`, `fortressDb`, and `fortressGetScopedDb`.
|
|
72
|
+
*
|
|
73
|
+
* Principal resolution goes through `fortress.resolvePrincipal`, which
|
|
74
|
+
* tries plugin `resolvePrincipal` hooks (api-key, future OAuth
|
|
75
|
+
* client_credentials, mTLS) first and then falls back to the JWT bearer
|
|
76
|
+
* token (cookie-first, `Authorization: Bearer` second). This means the
|
|
77
|
+
* same Hono app can authenticate browsers (cookies), SPA clients (Bearer),
|
|
78
|
+
* and service accounts (`Authorization: ApiKey ...` or `X-API-Key`)
|
|
79
|
+
* without extra wiring on any route.
|
|
80
|
+
*
|
|
81
|
+
* `fortressDb` has plugin `wrapAdapter` applied (e.g. tenancy schema
|
|
82
|
+
* switching). `fortressGetScopedDb(model)` additionally applies
|
|
83
|
+
* `scopeRules` for the requested model.
|
|
84
|
+
*/
|
|
85
|
+
export function createAuthMiddleware(
|
|
86
|
+
fortress: Fortress,
|
|
87
|
+
): MiddlewareHandler<FortressEnv> {
|
|
88
|
+
// Internally typed as the base FortressEnv. The middleware is variance-safe
|
|
89
|
+
// when mounted on a `Hono<FortressEnv<MyApp>>()` because the augmented env's
|
|
90
|
+
// Variables are a superset of FortressVariables — Hono accepts the wider
|
|
91
|
+
// shape during use().
|
|
92
|
+
return async (c, next) => {
|
|
93
|
+
const resolved = await fortress.resolvePrincipal(c.req.raw);
|
|
94
|
+
if (!resolved) {
|
|
95
|
+
throw new FortressError(
|
|
96
|
+
'UNAUTHORIZED',
|
|
97
|
+
'Missing or invalid credentials',
|
|
98
|
+
401,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { subject, claims, scopes } = resolved;
|
|
103
|
+
c.set('fortressSubject', subject);
|
|
104
|
+
if (subject.type === 'USER')
|
|
105
|
+
c.set('fortressUserId', subject.id);
|
|
106
|
+
if (claims)
|
|
107
|
+
c.set('fortressClaims', claims);
|
|
108
|
+
if (scopes !== undefined)
|
|
109
|
+
c.set('fortressScopes', scopes);
|
|
110
|
+
|
|
111
|
+
// Build request context for plugin adapter wrappers. The tenant is taken
|
|
112
|
+
// from the verified JWT claim (set by tenancy's enrichTokenClaims), never
|
|
113
|
+
// a client header — so a caller can only reach a tenant they belong to.
|
|
114
|
+
const plugins = fortress.config.plugins ?? [];
|
|
115
|
+
const requestContext: Record<string, unknown> = {
|
|
116
|
+
tenantId: claims?.customClaims?.tenantId,
|
|
117
|
+
ipAddress:
|
|
118
|
+
c.req.header('X-Forwarded-For') ?? c.req.header('X-Real-IP'),
|
|
119
|
+
userAgent: c.req.header('User-Agent'),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Chain adapter wrappers (e.g., tenancy schema switching)
|
|
123
|
+
const wrappedAdapter = chainAdapterWrappers(
|
|
124
|
+
plugins,
|
|
125
|
+
fortress.config.database,
|
|
126
|
+
requestContext,
|
|
127
|
+
);
|
|
128
|
+
c.set('fortressDb', wrappedAdapter);
|
|
129
|
+
|
|
130
|
+
// Lazy scope rule applicator — routes pick the model they need. Scope
|
|
131
|
+
// rules key off the subject id; for non-USER subjects the plugins that
|
|
132
|
+
// care (data-isolation) typically noop, but the call still works.
|
|
133
|
+
const pluginCtx: PluginContext = {
|
|
134
|
+
db: wrappedAdapter,
|
|
135
|
+
config: fortress.config,
|
|
136
|
+
};
|
|
137
|
+
c.set(
|
|
138
|
+
'fortressGetScopedDb',
|
|
139
|
+
async (model: string): Promise<DatabaseAdapter> => {
|
|
140
|
+
const scopeRule = await collectScopeRules(
|
|
141
|
+
plugins,
|
|
142
|
+
subject.id,
|
|
143
|
+
model,
|
|
144
|
+
pluginCtx,
|
|
145
|
+
);
|
|
146
|
+
if (!scopeRule)
|
|
147
|
+
return wrappedAdapter;
|
|
148
|
+
return wrapAdapterWithScopeRules(wrappedAdapter, scopeRule);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await next();
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the authenticated principal from the Hono context. Works for every
|
|
158
|
+
* subject kind (`USER`, `SERVICE_ACCOUNT`, ...). Generic in the env type
|
|
159
|
+
* so host apps can pass their own `Context<FortressEnv<MyEnv>>` without
|
|
160
|
+
* casts.
|
|
161
|
+
*/
|
|
162
|
+
export function getSubject<E extends Env = FortressEnv>(c: Context<E>): Subject {
|
|
163
|
+
const subject = c.get('fortressSubject' as never) as Subject | undefined;
|
|
164
|
+
if (!subject) {
|
|
165
|
+
throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
|
|
166
|
+
}
|
|
167
|
+
return subject;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get the authenticated user ID from Hono context. Throws 401 if the
|
|
172
|
+
* request was authenticated by a non-USER subject (e.g. a service account
|
|
173
|
+
* via api-key) — use {@link getSubject} for handlers that need to accept
|
|
174
|
+
* any principal.
|
|
175
|
+
*/
|
|
176
|
+
export function getUserId<E extends Env = FortressEnv>(c: Context<E>): string {
|
|
177
|
+
const subject = c.get('fortressSubject' as never) as Subject | undefined;
|
|
178
|
+
if (!subject || subject.type !== 'USER') {
|
|
179
|
+
throw new FortressError('UNAUTHORIZED', 'User not authenticated', 401);
|
|
180
|
+
}
|
|
181
|
+
return subject.id;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the authenticated user's JWT claims from Hono context. Claims are
|
|
186
|
+
* only populated when the request was authenticated via a JWT — api-key
|
|
187
|
+
* principals have no JWT claims. Throws 401 if unavailable.
|
|
188
|
+
*
|
|
189
|
+
* Pass a `TCustomClaims` type parameter to narrow `customClaims` to your
|
|
190
|
+
* deployment's plugin-augmented claim shape (e.g. tenancy adds
|
|
191
|
+
* `tenantId` / `tenantCode`).
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* interface MyClaims { tenantId: string; tenantCode: string }
|
|
196
|
+
* const claims = getClaims<MyClaims>(c);
|
|
197
|
+
* claims.customClaims?.tenantCode; // string | undefined
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export function getClaims<
|
|
201
|
+
TCustomClaims extends object = Record<string, unknown>,
|
|
202
|
+
E extends Env = FortressEnv,
|
|
203
|
+
>(c: Context<E>): TokenClaims & { customClaims?: TCustomClaims } {
|
|
204
|
+
const claims = c.get('fortressClaims' as never) as TokenClaims | undefined;
|
|
205
|
+
if (!claims) {
|
|
206
|
+
throw new FortressError('UNAUTHORIZED', 'No JWT claims on this request', 401);
|
|
207
|
+
}
|
|
208
|
+
return claims as TokenClaims & { customClaims?: TCustomClaims };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get the request-scoped database adapter with plugin wrapAdapter applied.
|
|
213
|
+
*/
|
|
214
|
+
export function getDb<E extends Env = FortressEnv>(c: Context<E>): DatabaseAdapter {
|
|
215
|
+
const db = c.get('fortressDb' as never) as DatabaseAdapter | undefined;
|
|
216
|
+
if (!db) {
|
|
217
|
+
throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
|
|
218
|
+
}
|
|
219
|
+
return db;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get a model-scoped database adapter with both wrapAdapter and scopeRules applied.
|
|
224
|
+
*/
|
|
225
|
+
export function getScopedDb<E extends Env = FortressEnv>(
|
|
226
|
+
c: Context<E>,
|
|
227
|
+
model: string,
|
|
228
|
+
): Promise<DatabaseAdapter> {
|
|
229
|
+
const fn = c.get('fortressGetScopedDb' as never) as
|
|
230
|
+
| ((model: string) => Promise<DatabaseAdapter>)
|
|
231
|
+
| undefined;
|
|
232
|
+
if (!fn) {
|
|
233
|
+
throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
|
|
234
|
+
}
|
|
235
|
+
return fn(model);
|
|
236
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level tests for the Hono adapter typed helpers (P1-6).
|
|
3
|
+
*
|
|
4
|
+
* Uses `expectTypeOf` for compile-time assertions. The body of every `it`
|
|
5
|
+
* is intentionally type-only and contains no runtime assertions; vitest
|
|
6
|
+
* runs them once just to confirm the file imports cleanly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Subject, TokenClaims } from '../../core/types';
|
|
10
|
+
import type { FortressEnv } from './auth';
|
|
11
|
+
import { Hono } from 'hono';
|
|
12
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
13
|
+
import { getClaims, getSubject, getUserId } from './auth';
|
|
14
|
+
|
|
15
|
+
interface MyEnv {
|
|
16
|
+
Variables: { requestId: string };
|
|
17
|
+
Bindings: { DB: 'pretend-binding' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MyCustomClaims {
|
|
21
|
+
tenantId: string;
|
|
22
|
+
tenantCode: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('fortressEnv<TAppEnv> composition', () => {
|
|
26
|
+
it('exposes both host and fortress variables on a composed env', () => {
|
|
27
|
+
// Build a context typed as FortressEnv<MyEnv> and verify the types.
|
|
28
|
+
type Composed = FortressEnv<MyEnv>;
|
|
29
|
+
type Variables = Composed['Variables'];
|
|
30
|
+
|
|
31
|
+
expectTypeOf<Variables['fortressSubject']>().toEqualTypeOf<Subject>();
|
|
32
|
+
expectTypeOf<Variables['requestId']>().toEqualTypeOf<string>();
|
|
33
|
+
expectTypeOf<Composed['Bindings']['DB']>().toEqualTypeOf<'pretend-binding'>();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('app = new Hono<FortressEnv<MyEnv>>() retains host variables', () => {
|
|
37
|
+
const app = new Hono<FortressEnv<MyEnv>>();
|
|
38
|
+
app.get('/x', (c) => {
|
|
39
|
+
const requestId = c.get('requestId');
|
|
40
|
+
expectTypeOf(requestId).toEqualTypeOf<string>();
|
|
41
|
+
return c.json({ requestId });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('typed helpers accept any FortressEnv-compatible context', () => {
|
|
47
|
+
it('getSubject works on the default FortressEnv', () => {
|
|
48
|
+
const app = new Hono<FortressEnv>();
|
|
49
|
+
app.get('/x', (c) => {
|
|
50
|
+
const subject = getSubject(c);
|
|
51
|
+
expectTypeOf(subject).toEqualTypeOf<Subject>();
|
|
52
|
+
return c.json({ subject });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('getSubject / getUserId work on FortressEnv<MyEnv> without casts', () => {
|
|
57
|
+
const app = new Hono<FortressEnv<MyEnv>>();
|
|
58
|
+
app.get('/y', (c) => {
|
|
59
|
+
const subject = getSubject(c);
|
|
60
|
+
const userId = getUserId(c);
|
|
61
|
+
expectTypeOf(subject).toEqualTypeOf<Subject>();
|
|
62
|
+
expectTypeOf(userId).toEqualTypeOf<string>();
|
|
63
|
+
return c.json({ subject, userId });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('getClaims narrows customClaims via the type parameter', () => {
|
|
68
|
+
const app = new Hono<FortressEnv>();
|
|
69
|
+
app.get('/z', (c) => {
|
|
70
|
+
const claims = getClaims<MyCustomClaims>(c);
|
|
71
|
+
expectTypeOf(claims).toEqualTypeOf<TokenClaims & { customClaims?: MyCustomClaims }>();
|
|
72
|
+
if (claims.customClaims) {
|
|
73
|
+
expectTypeOf(claims.customClaims.tenantId).toEqualTypeOf<string>();
|
|
74
|
+
expectTypeOf(claims.customClaims.tenantCode).toEqualTypeOf<string>();
|
|
75
|
+
}
|
|
76
|
+
return c.json({ ok: true });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('fortressEnv default keeps existing callers working', () => {
|
|
82
|
+
it('hono<FortressEnv> still resolves to a usable env', () => {
|
|
83
|
+
type Variables = FortressEnv['Variables'];
|
|
84
|
+
expectTypeOf<Variables['fortressSubject']>().toEqualTypeOf<Subject>();
|
|
85
|
+
// The unconstrained generic falls back to an empty bindings/extra-vars
|
|
86
|
+
// shape; Hono accepts it as a valid Env and the typed helpers work.
|
|
87
|
+
const app = new Hono<FortressEnv>();
|
|
88
|
+
app.get('/x', (c) => {
|
|
89
|
+
const subject = getSubject(c);
|
|
90
|
+
expectTypeOf(subject).toEqualTypeOf<Subject>();
|
|
91
|
+
return c.json({ subject });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createCsrfMiddleware } from './csrf';
|
|
4
|
+
|
|
5
|
+
describe('csrf middleware', () => {
|
|
6
|
+
it('allows GET requests without CSRF header', async () => {
|
|
7
|
+
const app = new Hono();
|
|
8
|
+
app.use('/*', createCsrfMiddleware());
|
|
9
|
+
app.get('/api/data', c => c.json({ ok: true }));
|
|
10
|
+
|
|
11
|
+
const res = await app.request('/api/data', { method: 'GET' });
|
|
12
|
+
|
|
13
|
+
expect(res.status).toBe(200);
|
|
14
|
+
const body = await res.json();
|
|
15
|
+
expect(body).toEqual({ ok: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('allows POST requests with valid CSRF header', async () => {
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.use('/*', createCsrfMiddleware());
|
|
21
|
+
app.post('/api/data', c => c.json({ ok: true }));
|
|
22
|
+
|
|
23
|
+
const res = await app.request('/api/data', {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'X-Fortress-CSRF': '1' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(res.status).toBe(200);
|
|
29
|
+
const body = await res.json();
|
|
30
|
+
expect(body).toEqual({ ok: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('blocks POST requests without CSRF header (403)', async () => {
|
|
34
|
+
const app = new Hono();
|
|
35
|
+
app.use('/*', createCsrfMiddleware());
|
|
36
|
+
app.post('/api/data', c => c.json({ ok: true }));
|
|
37
|
+
|
|
38
|
+
const res = await app.request('/api/data', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(res.status).toBe(403);
|
|
43
|
+
const body = await res.json();
|
|
44
|
+
expect(body.error).toBe('CSRF_MISSING');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('allows requests to skip paths', async () => {
|
|
48
|
+
const app = new Hono();
|
|
49
|
+
app.use('/*', createCsrfMiddleware({ skipPaths: ['/webhook'] }));
|
|
50
|
+
app.post('/webhook', c => c.json({ ok: true }));
|
|
51
|
+
|
|
52
|
+
const res = await app.request('/webhook', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(res.status).toBe(200);
|
|
57
|
+
const body = await res.json();
|
|
58
|
+
expect(body).toEqual({ ok: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('matches skip paths at segment boundaries', async () => {
|
|
62
|
+
const app = new Hono();
|
|
63
|
+
app.use('/*', createCsrfMiddleware({ skipPaths: ['/webhook'] }));
|
|
64
|
+
app.post('/webhook/delivery', c => c.json({ ok: true }));
|
|
65
|
+
app.post('/webhook-evil', c => c.json({ ok: true }));
|
|
66
|
+
|
|
67
|
+
expect((await app.request('/webhook/delivery', { method: 'POST' })).status).toBe(200);
|
|
68
|
+
expect((await app.request('/webhook-evil', { method: 'POST' })).status).toBe(403);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('blocks cross-site requests (Sec-Fetch-Site: cross-site)', async () => {
|
|
72
|
+
const app = new Hono();
|
|
73
|
+
app.use('/*', createCsrfMiddleware());
|
|
74
|
+
app.post('/api/data', c => c.json({ ok: true }));
|
|
75
|
+
|
|
76
|
+
const res = await app.request('/api/data', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'X-Fortress-CSRF': '1',
|
|
80
|
+
'Sec-Fetch-Site': 'cross-site',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(res.status).toBe(403);
|
|
85
|
+
const body = await res.json();
|
|
86
|
+
expect(body.error).toBe('CSRF_REJECTED');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('custom header name works', async () => {
|
|
90
|
+
const app = new Hono();
|
|
91
|
+
app.use('/*', createCsrfMiddleware({ headerName: 'X-Custom-CSRF' }));
|
|
92
|
+
app.post('/api/data', c => c.json({ ok: true }));
|
|
93
|
+
|
|
94
|
+
// Should fail with default header name
|
|
95
|
+
const res1 = await app.request('/api/data', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'X-Fortress-CSRF': '1' },
|
|
98
|
+
});
|
|
99
|
+
expect(res1.status).toBe(403);
|
|
100
|
+
|
|
101
|
+
// Should succeed with custom header name
|
|
102
|
+
const res2 = await app.request('/api/data', {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'X-Custom-CSRF': '1' },
|
|
105
|
+
});
|
|
106
|
+
expect(res2.status).toBe(200);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('custom safe methods work', async () => {
|
|
110
|
+
const app = new Hono();
|
|
111
|
+
app.use('/*', createCsrfMiddleware({ safeMethods: ['GET', 'HEAD', 'OPTIONS', 'POST'] }));
|
|
112
|
+
app.post('/api/data', c => c.json({ ok: true }));
|
|
113
|
+
app.put('/api/data', c => c.json({ ok: true }));
|
|
114
|
+
|
|
115
|
+
// POST is now safe, should pass without CSRF header
|
|
116
|
+
const res1 = await app.request('/api/data', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
});
|
|
119
|
+
expect(res1.status).toBe(200);
|
|
120
|
+
|
|
121
|
+
// PUT is not safe, should fail without CSRF header
|
|
122
|
+
const res2 = await app.request('/api/data', {
|
|
123
|
+
method: 'PUT',
|
|
124
|
+
});
|
|
125
|
+
expect(res2.status).toBe(403);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
|
|
3
|
+
/** Options accepted by {@link createCsrfMiddleware}. */
|
|
4
|
+
export interface CsrfConfig {
|
|
5
|
+
/** Header name to check. Default: 'X-Fortress-CSRF'. */
|
|
6
|
+
headerName?: string;
|
|
7
|
+
/** Paths to skip CSRF checking. */
|
|
8
|
+
skipPaths?: string[];
|
|
9
|
+
/** HTTP methods that don't need CSRF checking. Default: ['GET', 'HEAD', 'OPTIONS']. */
|
|
10
|
+
safeMethods?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CSRF middleware using the custom-header strategy.
|
|
15
|
+
*
|
|
16
|
+
* Browsers enforce CORS preflight on custom headers, so cross-origin requests
|
|
17
|
+
* cannot set them without explicit server permission. This middleware requires
|
|
18
|
+
* a specific header to be present on non-safe HTTP methods.
|
|
19
|
+
*
|
|
20
|
+
* Additionally checks `Sec-Fetch-Site` to reject cross-site requests.
|
|
21
|
+
*/
|
|
22
|
+
export function createCsrfMiddleware(config?: CsrfConfig): MiddlewareHandler {
|
|
23
|
+
const headerName = config?.headerName ?? 'X-Fortress-CSRF';
|
|
24
|
+
const skipPaths = config?.skipPaths ?? [];
|
|
25
|
+
const safeMethods = config?.safeMethods ?? ['GET', 'HEAD', 'OPTIONS'];
|
|
26
|
+
|
|
27
|
+
return async (c, next): Promise<Response | void> => {
|
|
28
|
+
const method = c.req.method.toUpperCase();
|
|
29
|
+
|
|
30
|
+
// Safe methods don't need CSRF protection
|
|
31
|
+
if (safeMethods.includes(method)) {
|
|
32
|
+
await next();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check skip paths at segment boundaries: `/foo` covers `/foo/bar` but
|
|
37
|
+
// never `/foobar`. A trailing `/*` is accepted as subtree notation.
|
|
38
|
+
const path = new URL(c.req.url).pathname;
|
|
39
|
+
if (matchesSkipPath(path, skipPaths)) {
|
|
40
|
+
await next();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check Sec-Fetch-Site header - reject cross-site requests
|
|
45
|
+
const fetchSite = c.req.header('Sec-Fetch-Site');
|
|
46
|
+
if (fetchSite === 'cross-site') {
|
|
47
|
+
return c.json({ error: 'CSRF_REJECTED', message: 'Cross-site requests are not allowed' }, 403);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Require the custom CSRF header
|
|
51
|
+
const csrfHeader = c.req.header(headerName);
|
|
52
|
+
if (!csrfHeader) {
|
|
53
|
+
return c.json({ error: 'CSRF_MISSING', message: 'Missing CSRF header' }, 403);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await next();
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchesSkipPath(path: string, skipPaths: string[]): boolean {
|
|
61
|
+
return skipPaths.some((configured) => {
|
|
62
|
+
const withoutWildcard = configured.endsWith('/*') ? configured.slice(0, -2) : configured;
|
|
63
|
+
const base = withoutWildcard.length > 1 && withoutWildcard.endsWith('/')
|
|
64
|
+
? withoutWildcard.slice(0, -1)
|
|
65
|
+
: withoutWildcard;
|
|
66
|
+
return path === base || path.startsWith(`${base}/`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ErrorHandler } from 'hono';
|
|
2
|
+
import { errorToResponse } from '../../core/http/error-response';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hono error handler that maps FortressError to HTTP responses.
|
|
6
|
+
* Delegates to the framework-agnostic `errorToResponse` in core so the
|
|
7
|
+
* mapping (status codes, `Retry-After`, sanitized 500s) stays consistent
|
|
8
|
+
* across Hono / Express / SvelteKit / `fortress.handleRequest`.
|
|
9
|
+
*/
|
|
10
|
+
export function createErrorHandler(): ErrorHandler {
|
|
11
|
+
return err => errorToResponse(err);
|
|
12
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { Fortress } from '../../core/fortress';
|
|
3
|
+
import type { PluginRequestContext } from '../../core/http/plugin-middleware';
|
|
4
|
+
import type { MiddlewareDefinition } from '../../core/plugin';
|
|
5
|
+
import type { FortressEnv } from './auth';
|
|
6
|
+
import { executePluginMiddleware } from '../../core/plugin-runner';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hono middleware that executes plugin-defined middleware for a given position.
|
|
10
|
+
*
|
|
11
|
+
* Normalizes Hono's context to the shared {@link PluginRequestContext}, so
|
|
12
|
+
* plugin middleware receives the same web-standard Request and resolved auth
|
|
13
|
+
* fields under core, Hono, and Express.
|
|
14
|
+
*/
|
|
15
|
+
export function createPluginMiddleware(
|
|
16
|
+
fortress: Fortress,
|
|
17
|
+
position: MiddlewareDefinition['position'],
|
|
18
|
+
): MiddlewareHandler<FortressEnv> {
|
|
19
|
+
const plugins = fortress.config.plugins ?? [];
|
|
20
|
+
|
|
21
|
+
return async (c, next) => {
|
|
22
|
+
const path = c.req.path;
|
|
23
|
+
const ctx = { db: fortress.config.database, config: fortress.config };
|
|
24
|
+
const requestContext: PluginRequestContext = {
|
|
25
|
+
request: c.req.raw,
|
|
26
|
+
fortressSubject: c.get('fortressSubject'),
|
|
27
|
+
fortressUserId: c.get('fortressUserId'),
|
|
28
|
+
fortressClaims: c.get('fortressClaims'),
|
|
29
|
+
fortressScopes: c.get('fortressScopes'),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
await executePluginMiddleware(plugins, position, path, ctx, requestContext);
|
|
33
|
+
await next();
|
|
34
|
+
};
|
|
35
|
+
}
|