@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
package/dist/hono.cjs
ADDED
|
@@ -0,0 +1,1530 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/hono/index.ts
|
|
21
|
+
var hono_exports = {};
|
|
22
|
+
__export(hono_exports, {
|
|
23
|
+
buildRouteDefinition: () => buildRouteDefinition,
|
|
24
|
+
convertRoutes: () => convertRoutes,
|
|
25
|
+
createCsrfMiddleware: () => createCsrfMiddleware,
|
|
26
|
+
createHonoMiddleware: () => createHonoMiddleware,
|
|
27
|
+
createPluginMiddleware: () => createPluginMiddleware,
|
|
28
|
+
createSecurityHeadersMiddleware: () => createSecurityHeadersMiddleware,
|
|
29
|
+
fetcherSchemaConverter: () => fetcherSchemaConverter,
|
|
30
|
+
getClaims: () => getClaims,
|
|
31
|
+
getDb: () => getDb,
|
|
32
|
+
getFortressRoutes: () => getFortressRoutes,
|
|
33
|
+
getScopedDb: () => getScopedDb,
|
|
34
|
+
getSubject: () => getSubject,
|
|
35
|
+
getUserId: () => getUserId,
|
|
36
|
+
identitySchemaConverter: () => identitySchemaConverter,
|
|
37
|
+
mountFortress: () => mountFortress,
|
|
38
|
+
mountFortressOpenAPI: () => mountFortressOpenAPI,
|
|
39
|
+
protectedRoute: () => protectedRoute,
|
|
40
|
+
toJSONSchemaConverter: () => toJSONSchemaConverter,
|
|
41
|
+
vBody: () => vBody,
|
|
42
|
+
vParam: () => vParam,
|
|
43
|
+
vQuery: () => vQuery
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(hono_exports);
|
|
46
|
+
|
|
47
|
+
// src/core/errors.ts
|
|
48
|
+
var FortressError = class extends Error {
|
|
49
|
+
code;
|
|
50
|
+
statusCode;
|
|
51
|
+
retryAfter;
|
|
52
|
+
details;
|
|
53
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
54
|
+
oauthError;
|
|
55
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
56
|
+
oauthDescription;
|
|
57
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
58
|
+
oauthErrorUri;
|
|
59
|
+
constructor(code, message, statusCode, options) {
|
|
60
|
+
super(message, { cause: options?.cause });
|
|
61
|
+
this.code = code;
|
|
62
|
+
this.statusCode = statusCode;
|
|
63
|
+
this.retryAfter = options?.retryAfter;
|
|
64
|
+
this.details = options?.details;
|
|
65
|
+
this.oauthError = options?.oauthError;
|
|
66
|
+
this.oauthDescription = options?.oauthDescription;
|
|
67
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
68
|
+
}
|
|
69
|
+
toJSON() {
|
|
70
|
+
return {
|
|
71
|
+
code: this.code,
|
|
72
|
+
message: this.message,
|
|
73
|
+
statusCode: this.statusCode,
|
|
74
|
+
...this.details !== void 0 && { details: this.details }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var Errors = {
|
|
79
|
+
unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
|
|
80
|
+
tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
|
|
81
|
+
sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
|
|
82
|
+
sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
|
|
83
|
+
forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
|
|
84
|
+
badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
|
|
85
|
+
notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
|
|
86
|
+
conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
|
|
87
|
+
unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
|
|
88
|
+
rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
|
|
89
|
+
database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
|
|
90
|
+
serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
|
|
91
|
+
validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
|
|
92
|
+
/**
|
|
93
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
94
|
+
*
|
|
95
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
96
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
97
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
98
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
99
|
+
* on the machine-readable `error` field.
|
|
100
|
+
*
|
|
101
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
102
|
+
* - `invalid_client` → 401
|
|
103
|
+
* - everything else → 400
|
|
104
|
+
*/
|
|
105
|
+
oauth: (error, description, options) => {
|
|
106
|
+
const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
|
|
107
|
+
const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
|
|
108
|
+
return new FortressError(code, description ?? error, status, {
|
|
109
|
+
oauthError: error,
|
|
110
|
+
oauthDescription: description,
|
|
111
|
+
oauthErrorUri: options?.errorUri,
|
|
112
|
+
cause: options?.cause
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
117
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
118
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
119
|
+
* service-method call would produce.
|
|
120
|
+
*
|
|
121
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
122
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
123
|
+
* failures still become structured `FortressError`s.
|
|
124
|
+
*/
|
|
125
|
+
fromHttpResponse: (status, body) => {
|
|
126
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
127
|
+
const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
|
|
128
|
+
const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
|
|
129
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
function isFortressErrorCode(code) {
|
|
133
|
+
return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
|
|
134
|
+
}
|
|
135
|
+
function statusToErrorCode(status) {
|
|
136
|
+
if (status === 401)
|
|
137
|
+
return "UNAUTHORIZED";
|
|
138
|
+
if (status === 403)
|
|
139
|
+
return "FORBIDDEN";
|
|
140
|
+
if (status === 404)
|
|
141
|
+
return "NOT_FOUND";
|
|
142
|
+
if (status === 409)
|
|
143
|
+
return "CONFLICT";
|
|
144
|
+
if (status === 422)
|
|
145
|
+
return "UNPROCESSABLE_ENTITY";
|
|
146
|
+
if (status === 429)
|
|
147
|
+
return "RATE_LIMITED";
|
|
148
|
+
if (status === 503)
|
|
149
|
+
return "SERVICE_UNAVAILABLE";
|
|
150
|
+
if (status >= 500)
|
|
151
|
+
return "DATABASE_ERROR";
|
|
152
|
+
return "BAD_REQUEST";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/core/http/match.ts
|
|
156
|
+
function segmentSpecificity(segment) {
|
|
157
|
+
if (segment === "*")
|
|
158
|
+
return 0;
|
|
159
|
+
if (segment.startsWith(":"))
|
|
160
|
+
return 1;
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
function canonicalizePath(pathname) {
|
|
164
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
165
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
166
|
+
}
|
|
167
|
+
function buildRouteTable(endpoints) {
|
|
168
|
+
const table = endpoints.map((ep) => {
|
|
169
|
+
const segments = canonicalizePath(ep.path).split("/").filter(Boolean);
|
|
170
|
+
const paramNames = [];
|
|
171
|
+
for (const seg of segments) {
|
|
172
|
+
if (seg.startsWith(":"))
|
|
173
|
+
paramNames.push(seg.slice(1));
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
endpoint: ep,
|
|
177
|
+
method: ep.method.toUpperCase(),
|
|
178
|
+
segments,
|
|
179
|
+
paramNames
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
return table.map((route, index) => ({ route, index })).sort((a, b) => {
|
|
183
|
+
const length = Math.max(a.route.segments.length, b.route.segments.length);
|
|
184
|
+
for (let i = 0; i < length; i++) {
|
|
185
|
+
const difference = segmentSpecificity(b.route.segments[i] ?? "") - segmentSpecificity(a.route.segments[i] ?? "");
|
|
186
|
+
if (difference !== 0)
|
|
187
|
+
return difference;
|
|
188
|
+
}
|
|
189
|
+
return a.index - b.index;
|
|
190
|
+
}).map(({ route }) => route);
|
|
191
|
+
}
|
|
192
|
+
function matchRoute(table, method, pathname) {
|
|
193
|
+
const upperMethod = method.toUpperCase();
|
|
194
|
+
const pathSegments = canonicalizePath(pathname).split("/").filter(Boolean);
|
|
195
|
+
for (const route of table) {
|
|
196
|
+
if (route.method !== upperMethod)
|
|
197
|
+
continue;
|
|
198
|
+
if (route.segments.length !== pathSegments.length)
|
|
199
|
+
continue;
|
|
200
|
+
const params = {};
|
|
201
|
+
let matched = true;
|
|
202
|
+
for (let i = 0; i < route.segments.length; i++) {
|
|
203
|
+
const routeSeg = route.segments[i];
|
|
204
|
+
const pathSeg = pathSegments[i];
|
|
205
|
+
if (routeSeg.startsWith(":")) {
|
|
206
|
+
params[routeSeg.slice(1)] = decodeURIComponent(pathSeg);
|
|
207
|
+
} else if (routeSeg !== "*" && routeSeg !== pathSeg) {
|
|
208
|
+
matched = false;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (matched)
|
|
213
|
+
return { endpoint: route.endpoint, params };
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/core/plugin-runner.ts
|
|
219
|
+
function chainAdapterWrappers(plugins, baseAdapter, requestContext) {
|
|
220
|
+
let adapter = baseAdapter;
|
|
221
|
+
for (const plugin of plugins) {
|
|
222
|
+
if (plugin.wrapAdapter) {
|
|
223
|
+
adapter = plugin.wrapAdapter(adapter, requestContext);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return adapter;
|
|
227
|
+
}
|
|
228
|
+
async function collectScopeRules(plugins, userId, model, ctx) {
|
|
229
|
+
const allFilters = [];
|
|
230
|
+
const allDefaults = {};
|
|
231
|
+
for (const plugin of plugins) {
|
|
232
|
+
if (!plugin.scopeRules) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const rule = await plugin.scopeRules(userId, model, ctx);
|
|
236
|
+
if (!rule) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
allFilters.push(...rule.filters);
|
|
240
|
+
Object.assign(allDefaults, rule.defaults);
|
|
241
|
+
}
|
|
242
|
+
if (allFilters.length === 0 && Object.keys(allDefaults).length === 0) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return { filters: allFilters, defaults: allDefaults };
|
|
246
|
+
}
|
|
247
|
+
function wrapAdapterWithScopeRules(adapter, scopeRule) {
|
|
248
|
+
const { filters, defaults } = scopeRule;
|
|
249
|
+
return {
|
|
250
|
+
...adapter,
|
|
251
|
+
create: (params) => adapter.create({
|
|
252
|
+
...params,
|
|
253
|
+
data: { ...params.data, ...defaults }
|
|
254
|
+
}),
|
|
255
|
+
findOne: (params) => adapter.findOne({
|
|
256
|
+
...params,
|
|
257
|
+
where: [...params.where, ...filters]
|
|
258
|
+
}),
|
|
259
|
+
findMany: (params) => adapter.findMany({
|
|
260
|
+
...params,
|
|
261
|
+
where: [...params.where ?? [], ...filters]
|
|
262
|
+
}),
|
|
263
|
+
update: (params) => Promise.resolve().then(() => {
|
|
264
|
+
for (const key of Object.keys(defaults)) {
|
|
265
|
+
if (Object.hasOwn(params.data, key))
|
|
266
|
+
throw Errors.forbidden(`Cannot update scoped field '${key}'`);
|
|
267
|
+
}
|
|
268
|
+
return adapter.update({
|
|
269
|
+
...params,
|
|
270
|
+
where: [...params.where, ...filters]
|
|
271
|
+
});
|
|
272
|
+
}),
|
|
273
|
+
delete: (params) => adapter.delete({
|
|
274
|
+
...params,
|
|
275
|
+
where: [...params.where, ...filters]
|
|
276
|
+
}),
|
|
277
|
+
count: (params) => adapter.count({
|
|
278
|
+
...params,
|
|
279
|
+
where: [...params.where ?? [], ...filters]
|
|
280
|
+
}),
|
|
281
|
+
transaction: (fn) => adapter.transaction((tx) => fn(wrapAdapterWithScopeRules(tx, scopeRule)))
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function middlewarePathToRegex(pattern) {
|
|
285
|
+
const regexStr = canonicalizePath(pattern).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+").replace(/\\\*/g, ".*").replace(/\//g, "\\/");
|
|
286
|
+
return new RegExp(`^${regexStr}$`);
|
|
287
|
+
}
|
|
288
|
+
function collectPluginMiddleware(plugins, position) {
|
|
289
|
+
const result = [];
|
|
290
|
+
for (const plugin of plugins) {
|
|
291
|
+
if (!plugin.middleware)
|
|
292
|
+
continue;
|
|
293
|
+
for (const mw of plugin.middleware) {
|
|
294
|
+
if (mw.position === position) {
|
|
295
|
+
result.push({ plugin, middleware: mw });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
async function executePluginMiddleware(plugins, position, requestPath, ctx, request) {
|
|
302
|
+
const canonicalRequestPath = canonicalizePath(requestPath);
|
|
303
|
+
const matching = collectPluginMiddleware(plugins, position).filter(({ middleware: mw }) => middlewarePathToRegex(mw.path).test(canonicalRequestPath));
|
|
304
|
+
if (matching.length === 0)
|
|
305
|
+
return;
|
|
306
|
+
let index = 0;
|
|
307
|
+
async function runNext() {
|
|
308
|
+
if (index >= matching.length)
|
|
309
|
+
return;
|
|
310
|
+
const current = matching[index++];
|
|
311
|
+
await current.middleware.handler(ctx, request, runNext);
|
|
312
|
+
}
|
|
313
|
+
await runNext();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/hono/middleware/auth.ts
|
|
317
|
+
function createAuthMiddleware(fortress) {
|
|
318
|
+
return async (c, next) => {
|
|
319
|
+
const resolved = await fortress.resolvePrincipal(c.req.raw);
|
|
320
|
+
if (!resolved) {
|
|
321
|
+
throw new FortressError(
|
|
322
|
+
"UNAUTHORIZED",
|
|
323
|
+
"Missing or invalid credentials",
|
|
324
|
+
401
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const { subject, claims, scopes } = resolved;
|
|
328
|
+
c.set("fortressSubject", subject);
|
|
329
|
+
if (subject.type === "USER")
|
|
330
|
+
c.set("fortressUserId", subject.id);
|
|
331
|
+
if (claims)
|
|
332
|
+
c.set("fortressClaims", claims);
|
|
333
|
+
if (scopes !== void 0)
|
|
334
|
+
c.set("fortressScopes", scopes);
|
|
335
|
+
const plugins = fortress.config.plugins ?? [];
|
|
336
|
+
const requestContext = {
|
|
337
|
+
tenantId: claims?.customClaims?.tenantId,
|
|
338
|
+
ipAddress: c.req.header("X-Forwarded-For") ?? c.req.header("X-Real-IP"),
|
|
339
|
+
userAgent: c.req.header("User-Agent")
|
|
340
|
+
};
|
|
341
|
+
const wrappedAdapter = chainAdapterWrappers(
|
|
342
|
+
plugins,
|
|
343
|
+
fortress.config.database,
|
|
344
|
+
requestContext
|
|
345
|
+
);
|
|
346
|
+
c.set("fortressDb", wrappedAdapter);
|
|
347
|
+
const pluginCtx = {
|
|
348
|
+
db: wrappedAdapter,
|
|
349
|
+
config: fortress.config
|
|
350
|
+
};
|
|
351
|
+
c.set(
|
|
352
|
+
"fortressGetScopedDb",
|
|
353
|
+
async (model) => {
|
|
354
|
+
const scopeRule = await collectScopeRules(
|
|
355
|
+
plugins,
|
|
356
|
+
subject.id,
|
|
357
|
+
model,
|
|
358
|
+
pluginCtx
|
|
359
|
+
);
|
|
360
|
+
if (!scopeRule)
|
|
361
|
+
return wrappedAdapter;
|
|
362
|
+
return wrapAdapterWithScopeRules(wrappedAdapter, scopeRule);
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
await next();
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function getSubject(c) {
|
|
369
|
+
const subject = c.get("fortressSubject");
|
|
370
|
+
if (!subject) {
|
|
371
|
+
throw new FortressError("UNAUTHORIZED", "Not authenticated", 401);
|
|
372
|
+
}
|
|
373
|
+
return subject;
|
|
374
|
+
}
|
|
375
|
+
function getUserId(c) {
|
|
376
|
+
const subject = c.get("fortressSubject");
|
|
377
|
+
if (!subject || subject.type !== "USER") {
|
|
378
|
+
throw new FortressError("UNAUTHORIZED", "User not authenticated", 401);
|
|
379
|
+
}
|
|
380
|
+
return subject.id;
|
|
381
|
+
}
|
|
382
|
+
function getClaims(c) {
|
|
383
|
+
const claims = c.get("fortressClaims");
|
|
384
|
+
if (!claims) {
|
|
385
|
+
throw new FortressError("UNAUTHORIZED", "No JWT claims on this request", 401);
|
|
386
|
+
}
|
|
387
|
+
return claims;
|
|
388
|
+
}
|
|
389
|
+
function getDb(c) {
|
|
390
|
+
const db = c.get("fortressDb");
|
|
391
|
+
if (!db) {
|
|
392
|
+
throw new FortressError("UNAUTHORIZED", "Not authenticated", 401);
|
|
393
|
+
}
|
|
394
|
+
return db;
|
|
395
|
+
}
|
|
396
|
+
function getScopedDb(c, model) {
|
|
397
|
+
const fn = c.get("fortressGetScopedDb");
|
|
398
|
+
if (!fn) {
|
|
399
|
+
throw new FortressError("UNAUTHORIZED", "Not authenticated", 401);
|
|
400
|
+
}
|
|
401
|
+
return fn(model);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/core/http/error-response.ts
|
|
405
|
+
function errorToResponse(err, logger) {
|
|
406
|
+
if (err instanceof FortressError) {
|
|
407
|
+
const headers = { "Content-Type": "application/json" };
|
|
408
|
+
if (err.code === "RATE_LIMITED" && err.retryAfter !== void 0) {
|
|
409
|
+
headers["Retry-After"] = String(err.retryAfter);
|
|
410
|
+
}
|
|
411
|
+
if (err.oauthError) {
|
|
412
|
+
const body2 = { error: err.oauthError };
|
|
413
|
+
if (err.oauthDescription)
|
|
414
|
+
body2.error_description = err.oauthDescription;
|
|
415
|
+
if (err.oauthErrorUri)
|
|
416
|
+
body2.error_uri = err.oauthErrorUri;
|
|
417
|
+
return new Response(JSON.stringify(body2), {
|
|
418
|
+
status: err.statusCode,
|
|
419
|
+
headers
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return new Response(JSON.stringify(err.toJSON()), {
|
|
423
|
+
status: err.statusCode,
|
|
424
|
+
headers
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
logger?.error(
|
|
428
|
+
{ err, message: err instanceof Error ? err.message : "Unknown error" },
|
|
429
|
+
"unhandled error in fortress.handleRequest"
|
|
430
|
+
);
|
|
431
|
+
const body = {
|
|
432
|
+
code: "INTERNAL_ERROR",
|
|
433
|
+
message: "Internal server error",
|
|
434
|
+
statusCode: 500
|
|
435
|
+
};
|
|
436
|
+
return new Response(JSON.stringify(body), {
|
|
437
|
+
status: 500,
|
|
438
|
+
headers: { "Content-Type": "application/json" }
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function withCookies(response, setCookies) {
|
|
442
|
+
if (setCookies.length === 0)
|
|
443
|
+
return response;
|
|
444
|
+
const headers = new Headers(response.headers);
|
|
445
|
+
for (const cookie of setCookies) {
|
|
446
|
+
headers.append("set-cookie", cookie);
|
|
447
|
+
}
|
|
448
|
+
return new Response(response.body, {
|
|
449
|
+
status: response.status,
|
|
450
|
+
statusText: response.statusText,
|
|
451
|
+
headers
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/hono/middleware/error-handler.ts
|
|
456
|
+
function createErrorHandler() {
|
|
457
|
+
return (err) => errorToResponse(err);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/hono/middleware/plugin-middleware.ts
|
|
461
|
+
function createPluginMiddleware(fortress, position) {
|
|
462
|
+
const plugins = fortress.config.plugins ?? [];
|
|
463
|
+
return async (c, next) => {
|
|
464
|
+
const path = c.req.path;
|
|
465
|
+
const ctx = { db: fortress.config.database, config: fortress.config };
|
|
466
|
+
const requestContext = {
|
|
467
|
+
request: c.req.raw,
|
|
468
|
+
fortressSubject: c.get("fortressSubject"),
|
|
469
|
+
fortressUserId: c.get("fortressUserId"),
|
|
470
|
+
fortressClaims: c.get("fortressClaims"),
|
|
471
|
+
fortressScopes: c.get("fortressScopes")
|
|
472
|
+
};
|
|
473
|
+
await executePluginMiddleware(plugins, position, path, ctx, requestContext);
|
|
474
|
+
await next();
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/hono/middleware/rbac.ts
|
|
479
|
+
function createRbacMiddleware(fortress, options) {
|
|
480
|
+
const routeMap = options?.routeMap ?? {};
|
|
481
|
+
const skipPaths = options?.skipPaths ?? [];
|
|
482
|
+
const skipPatterns = skipPaths.map((p) => pathToRegex(p));
|
|
483
|
+
const unmappedRoutes = options?.unmappedRoutes ?? (options?.defaultDeny ? "deny" : "allow");
|
|
484
|
+
return async (c, next) => {
|
|
485
|
+
const path = c.req.path;
|
|
486
|
+
const method = c.req.method;
|
|
487
|
+
if (skipPatterns.some((pattern) => pattern.test(path))) {
|
|
488
|
+
await next();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const key = `${method} ${path}`;
|
|
492
|
+
let mapping = routeMap[key] ?? null;
|
|
493
|
+
if (!mapping) {
|
|
494
|
+
mapping = findRouteMapMatch(method, path, routeMap);
|
|
495
|
+
}
|
|
496
|
+
if (!mapping && options?.mapRequest) {
|
|
497
|
+
mapping = options.mapRequest(method, path);
|
|
498
|
+
}
|
|
499
|
+
if (!mapping) {
|
|
500
|
+
if (unmappedRoutes === "deny") {
|
|
501
|
+
throw new FortressError("FORBIDDEN", "No permission mapping for this route", 403);
|
|
502
|
+
}
|
|
503
|
+
await next();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const subject = c.get("fortressSubject");
|
|
507
|
+
if (!subject) {
|
|
508
|
+
throw new FortressError("UNAUTHORIZED", "Not authenticated", 401);
|
|
509
|
+
}
|
|
510
|
+
const allowed = await fortress.iam.checkPermission(subject, mapping.resource, mapping.action, {
|
|
511
|
+
credentialScopes: c.get("fortressScopes")
|
|
512
|
+
});
|
|
513
|
+
if (!allowed) {
|
|
514
|
+
throw new FortressError("FORBIDDEN", "Insufficient permissions", 403);
|
|
515
|
+
}
|
|
516
|
+
await next();
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function findRouteMapMatch(method, path, routeMap) {
|
|
520
|
+
for (const [pattern, mapping] of Object.entries(routeMap)) {
|
|
521
|
+
const [patternMethod, patternPath] = pattern.split(" ", 2);
|
|
522
|
+
if (patternMethod !== method)
|
|
523
|
+
continue;
|
|
524
|
+
const regex = pathToRegex(patternPath);
|
|
525
|
+
if (regex.test(path))
|
|
526
|
+
return mapping;
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
function pathToRegex(pattern) {
|
|
531
|
+
const regexStr = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+").replace(/\\\*/g, ".*").replace(/\//g, "\\/");
|
|
532
|
+
return new RegExp(`^${regexStr}$`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/hono/convert-routes.ts
|
|
536
|
+
var TRAILING_SLASH_REGEX = /\/$/;
|
|
537
|
+
var RE_LEADING_SLASH = /^\//;
|
|
538
|
+
var RE_PARAM_BRACE = /\{(\w+)\}/g;
|
|
539
|
+
var RE_SLASH = /\//g;
|
|
540
|
+
var RE_NON_WORD = /\W/g;
|
|
541
|
+
function fromOpenAPIPath(path) {
|
|
542
|
+
return path.replace(RE_PARAM_BRACE, ":$1");
|
|
543
|
+
}
|
|
544
|
+
function toOperationId(method, path) {
|
|
545
|
+
const normalized = path.replace(RE_LEADING_SLASH, "").replace(RE_PARAM_BRACE, "$1").replace(RE_SLASH, "_").replace(RE_NON_WORD, "_");
|
|
546
|
+
return `${method.toLowerCase()}_${normalized}`;
|
|
547
|
+
}
|
|
548
|
+
var VALID_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
|
|
549
|
+
var SECURITY_REVERSE = {
|
|
550
|
+
bearer: "bearer",
|
|
551
|
+
bearerauth: "bearer",
|
|
552
|
+
basic: "basic",
|
|
553
|
+
basicauth: "basic",
|
|
554
|
+
apikey: "apiKey",
|
|
555
|
+
apikeyauth: "apiKey"
|
|
556
|
+
};
|
|
557
|
+
function mapSecurity(security) {
|
|
558
|
+
const result = [];
|
|
559
|
+
for (const entry of security) {
|
|
560
|
+
const keys = Object.keys(entry);
|
|
561
|
+
if (keys.length === 0) {
|
|
562
|
+
result.push("none");
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
for (const key of keys) {
|
|
566
|
+
const mapped = SECURITY_REVERSE[key.toLowerCase()];
|
|
567
|
+
if (mapped && !result.includes(mapped)) {
|
|
568
|
+
result.push(mapped);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
function convertRoutes(routes, options) {
|
|
575
|
+
const { schemaConverter, prefix } = options;
|
|
576
|
+
const result = [];
|
|
577
|
+
for (const route of routes) {
|
|
578
|
+
const method = route.method.toUpperCase();
|
|
579
|
+
if (!VALID_METHODS.has(method))
|
|
580
|
+
continue;
|
|
581
|
+
const rawPath = prefix ? `${prefix.replace(TRAILING_SLASH_REGEX, "")}${route.path}` : route.path;
|
|
582
|
+
const path = fromOpenAPIPath(rawPath);
|
|
583
|
+
const handler = toOperationId(method, rawPath);
|
|
584
|
+
const meta = {
|
|
585
|
+
summary: route.summary ?? ""
|
|
586
|
+
};
|
|
587
|
+
if (route.tags)
|
|
588
|
+
meta.tags = route.tags;
|
|
589
|
+
if (route.description)
|
|
590
|
+
meta.description = route.description;
|
|
591
|
+
if (route.deprecated)
|
|
592
|
+
meta.deprecated = true;
|
|
593
|
+
if (route.security) {
|
|
594
|
+
const mapped = mapSecurity(route.security);
|
|
595
|
+
if (mapped.length > 0)
|
|
596
|
+
meta.security = mapped;
|
|
597
|
+
}
|
|
598
|
+
const input = {};
|
|
599
|
+
let hasInput = false;
|
|
600
|
+
if (route.request?.body?.content?.["application/json"]?.schema) {
|
|
601
|
+
input.body = schemaConverter(route.request.body.content["application/json"].schema);
|
|
602
|
+
hasInput = true;
|
|
603
|
+
}
|
|
604
|
+
if (route.request?.params) {
|
|
605
|
+
input.params = schemaConverter(route.request.params);
|
|
606
|
+
hasInput = true;
|
|
607
|
+
}
|
|
608
|
+
if (route.request?.query) {
|
|
609
|
+
input.query = schemaConverter(route.request.query);
|
|
610
|
+
hasInput = true;
|
|
611
|
+
}
|
|
612
|
+
const responses = {};
|
|
613
|
+
let hasResponses = false;
|
|
614
|
+
for (const [status, resp] of Object.entries(route.responses)) {
|
|
615
|
+
const entry = { description: resp.description };
|
|
616
|
+
if (resp.content?.["application/json"]?.schema) {
|
|
617
|
+
entry.schema = schemaConverter(resp.content["application/json"].schema);
|
|
618
|
+
}
|
|
619
|
+
responses[Number(status)] = entry;
|
|
620
|
+
hasResponses = true;
|
|
621
|
+
}
|
|
622
|
+
const endpoint = {
|
|
623
|
+
method,
|
|
624
|
+
path,
|
|
625
|
+
handler,
|
|
626
|
+
meta
|
|
627
|
+
};
|
|
628
|
+
if (hasInput)
|
|
629
|
+
endpoint.input = input;
|
|
630
|
+
if (hasResponses)
|
|
631
|
+
endpoint.responses = responses;
|
|
632
|
+
result.push(endpoint);
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/hono/converters.ts
|
|
638
|
+
var import_openapi2 = require("@bajustone/fetcher/openapi");
|
|
639
|
+
|
|
640
|
+
// src/core/schema-builder.ts
|
|
641
|
+
var import_openapi = require("@bajustone/fetcher/openapi");
|
|
642
|
+
var import_schema = require("@bajustone/fetcher/schema");
|
|
643
|
+
function collectRefNames(schema, acc) {
|
|
644
|
+
if (!schema || typeof schema !== "object")
|
|
645
|
+
return acc;
|
|
646
|
+
if (typeof schema.$ref === "string") {
|
|
647
|
+
const i = schema.$ref.lastIndexOf("/");
|
|
648
|
+
acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
|
|
649
|
+
}
|
|
650
|
+
if (schema.properties) {
|
|
651
|
+
for (const key of Object.keys(schema.properties))
|
|
652
|
+
collectRefNames(schema.properties[key], acc);
|
|
653
|
+
}
|
|
654
|
+
collectRefNames(schema.items, acc);
|
|
655
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
656
|
+
const variants = schema[key];
|
|
657
|
+
if (variants) {
|
|
658
|
+
for (const variant of variants)
|
|
659
|
+
collectRefNames(variant, acc);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object")
|
|
663
|
+
collectRefNames(schema.additionalProperties, acc);
|
|
664
|
+
return acc;
|
|
665
|
+
}
|
|
666
|
+
var refDefinitionContexts = /* @__PURE__ */ new WeakMap();
|
|
667
|
+
function refName(refPath) {
|
|
668
|
+
const i = refPath.lastIndexOf("/");
|
|
669
|
+
return i >= 0 ? refPath.slice(i + 1) : refPath;
|
|
670
|
+
}
|
|
671
|
+
function resolveRefDefs(schema) {
|
|
672
|
+
if (collectRefNames(schema, /* @__PURE__ */ new Set()).size === 0)
|
|
673
|
+
return void 0;
|
|
674
|
+
const defs = /* @__PURE__ */ Object.create(null);
|
|
675
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
676
|
+
const hasOwn = (value, key) => Object.hasOwn(value, key);
|
|
677
|
+
const visit = (node, inherited) => {
|
|
678
|
+
if (!node || typeof node !== "object")
|
|
679
|
+
return;
|
|
680
|
+
const context = refDefinitionContexts.get(node) ?? inherited;
|
|
681
|
+
if (typeof node.$ref === "string") {
|
|
682
|
+
const name = refName(node.$ref);
|
|
683
|
+
const component = context && hasOwn(context, name) ? context[name] : void 0;
|
|
684
|
+
if (component) {
|
|
685
|
+
if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
|
|
686
|
+
throw new Error(`Conflicting component definitions for $ref '${name}'`);
|
|
687
|
+
}
|
|
688
|
+
const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
|
|
689
|
+
defs[name] = component;
|
|
690
|
+
if (firstResolution)
|
|
691
|
+
visit(component, context);
|
|
692
|
+
} else if (!hasOwn(defs, name)) {
|
|
693
|
+
defs[name] = {};
|
|
694
|
+
unresolved.add(name);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (node.properties) {
|
|
698
|
+
for (const child of Object.values(node.properties))
|
|
699
|
+
visit(child, context);
|
|
700
|
+
}
|
|
701
|
+
visit(node.items, context);
|
|
702
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
703
|
+
for (const child of node[key] ?? [])
|
|
704
|
+
visit(child, context);
|
|
705
|
+
}
|
|
706
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object")
|
|
707
|
+
visit(node.additionalProperties, context);
|
|
708
|
+
};
|
|
709
|
+
visit(schema);
|
|
710
|
+
return defs;
|
|
711
|
+
}
|
|
712
|
+
function createStandardProps(schema) {
|
|
713
|
+
let validateFn;
|
|
714
|
+
return {
|
|
715
|
+
version: 1,
|
|
716
|
+
vendor: "fortress",
|
|
717
|
+
validate(value) {
|
|
718
|
+
if (!validateFn) {
|
|
719
|
+
const defs = resolveRefDefs(schema);
|
|
720
|
+
validateFn = (0, import_openapi.fromJSONSchema)(schema, defs)["~standard"].validate;
|
|
721
|
+
}
|
|
722
|
+
return validateFn(value);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function toFortressSchema(schema, refDefinitions, standardProps) {
|
|
727
|
+
if (refDefinitions)
|
|
728
|
+
refDefinitionContexts.set(schema, refDefinitions);
|
|
729
|
+
return Object.assign(schema, {
|
|
730
|
+
"~standard": standardProps ?? createStandardProps(schema)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function str(opts) {
|
|
734
|
+
const o = typeof opts === "string" ? { description: opts } : opts ?? {};
|
|
735
|
+
const s = { type: "string" };
|
|
736
|
+
if (o.description)
|
|
737
|
+
s.description = o.description;
|
|
738
|
+
if (o.min !== void 0)
|
|
739
|
+
s.minLength = o.min;
|
|
740
|
+
if (o.max !== void 0)
|
|
741
|
+
s.maxLength = o.max;
|
|
742
|
+
if (o.pattern !== void 0)
|
|
743
|
+
s.pattern = o.pattern;
|
|
744
|
+
if (o.format !== void 0)
|
|
745
|
+
s.format = o.format;
|
|
746
|
+
return toFortressSchema(s);
|
|
747
|
+
}
|
|
748
|
+
function int(opts) {
|
|
749
|
+
const o = typeof opts === "string" ? { description: opts } : opts ?? {};
|
|
750
|
+
const s = { type: "integer" };
|
|
751
|
+
if (o.description)
|
|
752
|
+
s.description = o.description;
|
|
753
|
+
if (o.min !== void 0)
|
|
754
|
+
s.minimum = o.min;
|
|
755
|
+
if (o.max !== void 0)
|
|
756
|
+
s.maximum = o.max;
|
|
757
|
+
return toFortressSchema(s);
|
|
758
|
+
}
|
|
759
|
+
function obj(properties, ...required) {
|
|
760
|
+
const s = { type: "object", properties };
|
|
761
|
+
if (required.length > 0)
|
|
762
|
+
s.required = required;
|
|
763
|
+
return toFortressSchema(s);
|
|
764
|
+
}
|
|
765
|
+
function makeFormatBuilder(factory) {
|
|
766
|
+
const f = factory();
|
|
767
|
+
return (description) => {
|
|
768
|
+
const s = { type: "string" };
|
|
769
|
+
if (f.format)
|
|
770
|
+
s.format = f.format;
|
|
771
|
+
if (f.pattern)
|
|
772
|
+
s.pattern = f.pattern;
|
|
773
|
+
if (description)
|
|
774
|
+
s.description = description;
|
|
775
|
+
return toFortressSchema(s);
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
var email = makeFormatBuilder(import_schema.email);
|
|
779
|
+
var uuid = makeFormatBuilder(import_schema.uuid);
|
|
780
|
+
var url = makeFormatBuilder(import_schema.url);
|
|
781
|
+
var datetime = makeFormatBuilder(import_schema.datetime);
|
|
782
|
+
var date = makeFormatBuilder(import_schema.date);
|
|
783
|
+
var time = makeFormatBuilder(import_schema.time);
|
|
784
|
+
function isStandardSchema(value) {
|
|
785
|
+
return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
|
|
786
|
+
}
|
|
787
|
+
function isFortressSchema(value) {
|
|
788
|
+
return isStandardSchema(value) && value["~standard"].vendor === "fortress" && ("type" in value || "$ref" in value || "oneOf" in value || "anyOf" in value);
|
|
789
|
+
}
|
|
790
|
+
var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
|
|
791
|
+
function hasJsonSchemaShape(value) {
|
|
792
|
+
if (typeof value !== "object" || value === null)
|
|
793
|
+
return false;
|
|
794
|
+
const v = value;
|
|
795
|
+
if ("$ref" in v || "oneOf" in v || "anyOf" in v || "allOf" in v)
|
|
796
|
+
return true;
|
|
797
|
+
if (typeof v.type === "string" && JSON_SCHEMA_TYPES.has(v.type))
|
|
798
|
+
return true;
|
|
799
|
+
if (Array.isArray(v.type) && v.type.every((t) => typeof t === "string" && JSON_SCHEMA_TYPES.has(t)))
|
|
800
|
+
return true;
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
function extractJsonSchema(schema) {
|
|
804
|
+
if (isFortressSchema(schema)) {
|
|
805
|
+
return schema;
|
|
806
|
+
}
|
|
807
|
+
if (isStandardSchema(schema)) {
|
|
808
|
+
const std = schema["~standard"];
|
|
809
|
+
if (std?.jsonSchema?.input) {
|
|
810
|
+
return std.jsonSchema.input({ target: "draft-2020-12" });
|
|
811
|
+
}
|
|
812
|
+
if (hasJsonSchemaShape(schema)) {
|
|
813
|
+
return schema;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return {};
|
|
817
|
+
}
|
|
818
|
+
var ErrorEnvelope = obj({
|
|
819
|
+
code: str("Machine-readable error code"),
|
|
820
|
+
message: str("Human-readable error message"),
|
|
821
|
+
statusCode: int("HTTP status code"),
|
|
822
|
+
details: toFortressSchema({ description: "Optional structured error details" })
|
|
823
|
+
}, "code", "message", "statusCode");
|
|
824
|
+
|
|
825
|
+
// src/hono/converters.ts
|
|
826
|
+
var identitySchemaConverter = (jsonSchema) => jsonSchema;
|
|
827
|
+
var fetcherSchemaConverter = (jsonSchema) => (0, import_openapi2.fromJSONSchema)(jsonSchema);
|
|
828
|
+
var toJSONSchemaConverter = (schema) => extractJsonSchema(schema);
|
|
829
|
+
|
|
830
|
+
// src/hono/handle.ts
|
|
831
|
+
function mountFortress(app, fortress, options = {}) {
|
|
832
|
+
const prefix = options.prefix ?? "";
|
|
833
|
+
const routeTable = buildRouteTable(fortress.manifest.filter((route) => route.mounted));
|
|
834
|
+
app.use("*", async (c, next) => {
|
|
835
|
+
const url2 = new URL(c.req.url);
|
|
836
|
+
let pathname = url2.pathname;
|
|
837
|
+
if (prefix && pathname.startsWith(prefix)) {
|
|
838
|
+
pathname = pathname.slice(prefix.length) || "/";
|
|
839
|
+
} else if (prefix) {
|
|
840
|
+
await next();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (!matchRoute(routeTable, c.req.method, pathname)) {
|
|
844
|
+
await next();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const rewritten = new Request(`${url2.origin}${pathname}${url2.search}`, c.req.raw);
|
|
848
|
+
return fortress.handleRequest(rewritten);
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/hono/middleware/csrf.ts
|
|
853
|
+
function createCsrfMiddleware(config) {
|
|
854
|
+
const headerName = config?.headerName ?? "X-Fortress-CSRF";
|
|
855
|
+
const skipPaths = config?.skipPaths ?? [];
|
|
856
|
+
const safeMethods = config?.safeMethods ?? ["GET", "HEAD", "OPTIONS"];
|
|
857
|
+
return async (c, next) => {
|
|
858
|
+
const method = c.req.method.toUpperCase();
|
|
859
|
+
if (safeMethods.includes(method)) {
|
|
860
|
+
await next();
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const path = new URL(c.req.url).pathname;
|
|
864
|
+
if (matchesSkipPath(path, skipPaths)) {
|
|
865
|
+
await next();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const fetchSite = c.req.header("Sec-Fetch-Site");
|
|
869
|
+
if (fetchSite === "cross-site") {
|
|
870
|
+
return c.json({ error: "CSRF_REJECTED", message: "Cross-site requests are not allowed" }, 403);
|
|
871
|
+
}
|
|
872
|
+
const csrfHeader = c.req.header(headerName);
|
|
873
|
+
if (!csrfHeader) {
|
|
874
|
+
return c.json({ error: "CSRF_MISSING", message: "Missing CSRF header" }, 403);
|
|
875
|
+
}
|
|
876
|
+
await next();
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function matchesSkipPath(path, skipPaths) {
|
|
880
|
+
return skipPaths.some((configured) => {
|
|
881
|
+
const withoutWildcard = configured.endsWith("/*") ? configured.slice(0, -2) : configured;
|
|
882
|
+
const base = withoutWildcard.length > 1 && withoutWildcard.endsWith("/") ? withoutWildcard.slice(0, -1) : withoutWildcard;
|
|
883
|
+
return path === base || path.startsWith(`${base}/`);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/hono/middleware/security-headers.ts
|
|
888
|
+
function createSecurityHeadersMiddleware(config = {}) {
|
|
889
|
+
const hstsMaxAge = config.hstsMaxAge ?? 63072e3;
|
|
890
|
+
const hstsIncludeSubdomains = config.hstsIncludeSubdomains ?? true;
|
|
891
|
+
const hstsPreload = config.hstsPreload ?? false;
|
|
892
|
+
const frameOptions = config.frameOptions ?? "DENY";
|
|
893
|
+
const noSniff = config.noSniff ?? true;
|
|
894
|
+
const csp = config.contentSecurityPolicy ?? "default-src 'self'";
|
|
895
|
+
const referrerPolicy = config.referrerPolicy ?? "strict-origin-when-cross-origin";
|
|
896
|
+
const crossDomain = config.permittedCrossDomainPolicies ?? "none";
|
|
897
|
+
let hstsValue = null;
|
|
898
|
+
if (hstsMaxAge > 0) {
|
|
899
|
+
hstsValue = `max-age=${hstsMaxAge}`;
|
|
900
|
+
if (hstsIncludeSubdomains)
|
|
901
|
+
hstsValue += "; includeSubDomains";
|
|
902
|
+
if (hstsPreload)
|
|
903
|
+
hstsValue += "; preload";
|
|
904
|
+
}
|
|
905
|
+
return async (c, next) => {
|
|
906
|
+
try {
|
|
907
|
+
await next();
|
|
908
|
+
} finally {
|
|
909
|
+
if (hstsValue)
|
|
910
|
+
c.header("Strict-Transport-Security", hstsValue);
|
|
911
|
+
if (frameOptions)
|
|
912
|
+
c.header("X-Frame-Options", frameOptions);
|
|
913
|
+
if (noSniff)
|
|
914
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
915
|
+
if (csp)
|
|
916
|
+
c.header("Content-Security-Policy", csp);
|
|
917
|
+
if (referrerPolicy)
|
|
918
|
+
c.header("Referrer-Policy", referrerPolicy);
|
|
919
|
+
if (crossDomain)
|
|
920
|
+
c.header("X-Permitted-Cross-Domain-Policies", crossDomain);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/hono/openapi.ts
|
|
926
|
+
function toOpenAPIPath(path) {
|
|
927
|
+
return path.replace(/:(\w+)/g, "{$1}");
|
|
928
|
+
}
|
|
929
|
+
function buildRouteDefinition(ep, schemaConverter) {
|
|
930
|
+
const openAPIPath = toOpenAPIPath(ep.path);
|
|
931
|
+
const method = ep.method.toLowerCase();
|
|
932
|
+
const routeDef = {
|
|
933
|
+
method,
|
|
934
|
+
path: openAPIPath,
|
|
935
|
+
operationId: ep.handler,
|
|
936
|
+
responses: {}
|
|
937
|
+
};
|
|
938
|
+
if (ep.meta?.summary)
|
|
939
|
+
routeDef.summary = ep.meta.summary;
|
|
940
|
+
if (ep.meta?.description)
|
|
941
|
+
routeDef.description = ep.meta.description;
|
|
942
|
+
if (ep.meta?.tags)
|
|
943
|
+
routeDef.tags = ep.meta.tags;
|
|
944
|
+
if (ep.meta?.deprecated)
|
|
945
|
+
routeDef.deprecated = true;
|
|
946
|
+
if (ep.meta?.security) {
|
|
947
|
+
const sec = [];
|
|
948
|
+
for (const s of ep.meta.security) {
|
|
949
|
+
if (s === "none")
|
|
950
|
+
sec.push({});
|
|
951
|
+
else if (s === "bearer")
|
|
952
|
+
sec.push({ bearerAuth: [] });
|
|
953
|
+
else if (s === "basic")
|
|
954
|
+
sec.push({ basicAuth: [] });
|
|
955
|
+
else if (s === "apiKey")
|
|
956
|
+
sec.push({ apiKeyAuth: [] });
|
|
957
|
+
}
|
|
958
|
+
routeDef.security = sec;
|
|
959
|
+
}
|
|
960
|
+
const request = {};
|
|
961
|
+
if (ep.input?.body) {
|
|
962
|
+
request.body = {
|
|
963
|
+
content: {
|
|
964
|
+
"application/json": { schema: schemaConverter(ep.input.body) }
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
if (ep.input?.params?.properties) {
|
|
969
|
+
const shape = {};
|
|
970
|
+
const required = new Set(ep.input.params.required ?? []);
|
|
971
|
+
for (const [key, schema] of Object.entries(ep.input.params.properties)) {
|
|
972
|
+
const converted = schemaConverter({ ...schema, type: schema.type ?? "string" });
|
|
973
|
+
shape[key] = required.has(key) ? converted : converted;
|
|
974
|
+
}
|
|
975
|
+
request.params = schemaConverter({
|
|
976
|
+
type: "object",
|
|
977
|
+
properties: ep.input.params.properties,
|
|
978
|
+
required: ep.input.params.required
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
if (ep.input?.query?.properties) {
|
|
982
|
+
request.query = schemaConverter(ep.input.query);
|
|
983
|
+
}
|
|
984
|
+
if (Object.keys(request).length > 0) {
|
|
985
|
+
routeDef.request = request;
|
|
986
|
+
}
|
|
987
|
+
const responses = {};
|
|
988
|
+
if (ep.responses) {
|
|
989
|
+
for (const [status, resp] of Object.entries(ep.responses)) {
|
|
990
|
+
const entry = {
|
|
991
|
+
description: resp.description
|
|
992
|
+
};
|
|
993
|
+
if (resp.schema) {
|
|
994
|
+
entry.content = {
|
|
995
|
+
"application/json": { schema: schemaConverter(resp.schema) }
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
responses[Number(status)] = entry;
|
|
999
|
+
}
|
|
1000
|
+
} else {
|
|
1001
|
+
responses[200] = { description: "Success" };
|
|
1002
|
+
}
|
|
1003
|
+
routeDef.responses = responses;
|
|
1004
|
+
return routeDef;
|
|
1005
|
+
}
|
|
1006
|
+
function mountFortressOpenAPI(app, fortress, options) {
|
|
1007
|
+
const skipPaths = new Set(options.skipPaths ?? []);
|
|
1008
|
+
if (app.openAPIRegistry) {
|
|
1009
|
+
app.openAPIRegistry.registerComponent("securitySchemes", "bearerAuth", {
|
|
1010
|
+
type: "http",
|
|
1011
|
+
scheme: "bearer",
|
|
1012
|
+
bearerFormat: "JWT"
|
|
1013
|
+
});
|
|
1014
|
+
app.openAPIRegistry.registerComponent("securitySchemes", "basicAuth", {
|
|
1015
|
+
type: "http",
|
|
1016
|
+
scheme: "basic"
|
|
1017
|
+
});
|
|
1018
|
+
app.openAPIRegistry.registerComponent("securitySchemes", "apiKeyAuth", {
|
|
1019
|
+
type: "apiKey",
|
|
1020
|
+
name: "X-API-Key",
|
|
1021
|
+
in: "header"
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
options.registerSecuritySchemes?.(app);
|
|
1025
|
+
for (const ep of fortress.endpoints) {
|
|
1026
|
+
if (skipPaths.has(ep.path))
|
|
1027
|
+
continue;
|
|
1028
|
+
const routeDef = buildRouteDefinition(ep, options.schemaConverter);
|
|
1029
|
+
const route = options.createRoute(routeDef);
|
|
1030
|
+
const handler = createAutoHandler(fortress, ep);
|
|
1031
|
+
app.openapi(route, handler);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function getFortressRoutes(fortress, schemaConverter, createRoute) {
|
|
1035
|
+
const result = {};
|
|
1036
|
+
for (const ep of fortress.endpoints) {
|
|
1037
|
+
const routeDef = buildRouteDefinition(ep, schemaConverter);
|
|
1038
|
+
result[ep.handler] = createRoute(routeDef);
|
|
1039
|
+
}
|
|
1040
|
+
return result;
|
|
1041
|
+
}
|
|
1042
|
+
function createAutoHandler(fortress, _ep) {
|
|
1043
|
+
return async (c) => {
|
|
1044
|
+
const raw = c.req?.raw;
|
|
1045
|
+
if (raw)
|
|
1046
|
+
return fortress.handleRequest(raw);
|
|
1047
|
+
const request = new Request(c.req.url, {
|
|
1048
|
+
method: c.req.method,
|
|
1049
|
+
headers: c.req.raw?.headers ?? void 0,
|
|
1050
|
+
body: ["GET", "HEAD"].includes(c.req.method) ? void 0 : await c.req.text()
|
|
1051
|
+
});
|
|
1052
|
+
return fortress.handleRequest(request);
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/core/validation.ts
|
|
1057
|
+
var import_openapi3 = require("@bajustone/fetcher/openapi");
|
|
1058
|
+
var DECIMAL_INTEGER_RE = /^-?\d+$/;
|
|
1059
|
+
var DECIMAL_NUMBER_RE = /^-?\d+(?:\.\d+)?$/;
|
|
1060
|
+
var rawSchemaValidators = /* @__PURE__ */ new WeakMap();
|
|
1061
|
+
function standardSchemaFor(schema) {
|
|
1062
|
+
if ("~standard" in schema)
|
|
1063
|
+
return schema;
|
|
1064
|
+
let compiled = rawSchemaValidators.get(schema);
|
|
1065
|
+
if (!compiled) {
|
|
1066
|
+
compiled = (0, import_openapi3.fromJSONSchema)(schema);
|
|
1067
|
+
rawSchemaValidators.set(schema, compiled);
|
|
1068
|
+
}
|
|
1069
|
+
return compiled;
|
|
1070
|
+
}
|
|
1071
|
+
function coerceBySchema(schema, data) {
|
|
1072
|
+
if (!schema || !data || schema.type !== "object" || !schema.properties)
|
|
1073
|
+
return data;
|
|
1074
|
+
const out = { ...data };
|
|
1075
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
1076
|
+
const value = out[key];
|
|
1077
|
+
if (typeof value !== "string")
|
|
1078
|
+
continue;
|
|
1079
|
+
const coerced = coerceScalar(propSchema, value);
|
|
1080
|
+
if (coerced !== void 0)
|
|
1081
|
+
out[key] = coerced;
|
|
1082
|
+
}
|
|
1083
|
+
return out;
|
|
1084
|
+
}
|
|
1085
|
+
function coerceScalar(schema, value) {
|
|
1086
|
+
const type = schema.type ?? numericConstraintType(schema);
|
|
1087
|
+
switch (type) {
|
|
1088
|
+
case "integer": {
|
|
1089
|
+
if (!DECIMAL_INTEGER_RE.test(value))
|
|
1090
|
+
return void 0;
|
|
1091
|
+
const n = Number(value);
|
|
1092
|
+
if (Number.isFinite(n) && Number.isSafeInteger(n) && String(n) === value)
|
|
1093
|
+
return n;
|
|
1094
|
+
return void 0;
|
|
1095
|
+
}
|
|
1096
|
+
case "number": {
|
|
1097
|
+
if (!DECIMAL_NUMBER_RE.test(value))
|
|
1098
|
+
return void 0;
|
|
1099
|
+
const n = Number(value);
|
|
1100
|
+
if (Number.isFinite(n) && (!Number.isInteger(n) || Number.isSafeInteger(n)) && String(n) === value)
|
|
1101
|
+
return n;
|
|
1102
|
+
return void 0;
|
|
1103
|
+
}
|
|
1104
|
+
case "boolean": {
|
|
1105
|
+
if (value === "true")
|
|
1106
|
+
return true;
|
|
1107
|
+
if (value === "false")
|
|
1108
|
+
return false;
|
|
1109
|
+
return void 0;
|
|
1110
|
+
}
|
|
1111
|
+
default:
|
|
1112
|
+
return void 0;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function numericConstraintType(schema) {
|
|
1116
|
+
if (typeof schema.const === "number")
|
|
1117
|
+
return Number.isInteger(schema.const) ? "integer" : "number";
|
|
1118
|
+
if (schema.enum && schema.enum.length > 0 && schema.enum.every((value) => typeof value === "number"))
|
|
1119
|
+
return schema.enum.every((value) => Number.isInteger(value)) ? "integer" : "number";
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
async function validateRequest(input, data) {
|
|
1123
|
+
if (!input)
|
|
1124
|
+
return;
|
|
1125
|
+
const allIssues = [];
|
|
1126
|
+
if (input.bodySchema || input.body) {
|
|
1127
|
+
const issues = await validateSchema(input.bodySchema ?? input.body, data.body, "body");
|
|
1128
|
+
allIssues.push(...issues);
|
|
1129
|
+
}
|
|
1130
|
+
if (input.querySchema || input.query) {
|
|
1131
|
+
const issues = await validateSchema(input.querySchema ?? input.query, data.query, "query");
|
|
1132
|
+
allIssues.push(...issues);
|
|
1133
|
+
}
|
|
1134
|
+
if (input.paramsSchema || input.params) {
|
|
1135
|
+
const issues = await validateSchema(input.paramsSchema ?? input.params, data.params, "params");
|
|
1136
|
+
allIssues.push(...issues);
|
|
1137
|
+
}
|
|
1138
|
+
if (allIssues.length > 0) {
|
|
1139
|
+
throw Errors.validationError(allIssues);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async function validateSchema(schema, data, location) {
|
|
1143
|
+
const result = await standardSchemaFor(schema)["~standard"].validate(data);
|
|
1144
|
+
if (result.issues) {
|
|
1145
|
+
return result.issues.map((issue) => ({
|
|
1146
|
+
path: issue.path,
|
|
1147
|
+
message: issue.message,
|
|
1148
|
+
location
|
|
1149
|
+
}));
|
|
1150
|
+
}
|
|
1151
|
+
return [];
|
|
1152
|
+
}
|
|
1153
|
+
async function validateValue(schema, data, location) {
|
|
1154
|
+
const result = await schema["~standard"].validate(data);
|
|
1155
|
+
if (result.issues) {
|
|
1156
|
+
throw Errors.validationError(
|
|
1157
|
+
result.issues.map((issue) => ({
|
|
1158
|
+
path: issue.path,
|
|
1159
|
+
message: issue.message,
|
|
1160
|
+
location
|
|
1161
|
+
}))
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
return result.value;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/core/http/cookie-serialize.ts
|
|
1168
|
+
function parseCookieHeader(header) {
|
|
1169
|
+
if (!header)
|
|
1170
|
+
return {};
|
|
1171
|
+
const out = {};
|
|
1172
|
+
for (const part of header.split(";")) {
|
|
1173
|
+
const eq = part.indexOf("=");
|
|
1174
|
+
if (eq === -1)
|
|
1175
|
+
continue;
|
|
1176
|
+
const name = part.slice(0, eq).trim();
|
|
1177
|
+
if (!name)
|
|
1178
|
+
continue;
|
|
1179
|
+
const raw = part.slice(eq + 1).trim();
|
|
1180
|
+
try {
|
|
1181
|
+
out[name] = decodeURIComponent(raw);
|
|
1182
|
+
} catch {
|
|
1183
|
+
out[name] = raw;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return out;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/core/http/csrf.ts
|
|
1190
|
+
function resolveCsrfConfig(config) {
|
|
1191
|
+
return {
|
|
1192
|
+
enabled: config?.enabled ?? true,
|
|
1193
|
+
headerName: config?.headerName ?? "X-Fortress-CSRF",
|
|
1194
|
+
skipPaths: config?.skipPaths ?? [],
|
|
1195
|
+
rejectSameSite: config?.rejectSameSite ?? false
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
1199
|
+
function matchesSkipPath2(path, skipPaths) {
|
|
1200
|
+
for (const skip of skipPaths) {
|
|
1201
|
+
if (path === skip)
|
|
1202
|
+
return true;
|
|
1203
|
+
if (path.startsWith(`${skip}/`))
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
function isCookieAuthenticated(request, cookies) {
|
|
1209
|
+
const jar = parseCookieHeader(request.headers.get("cookie"));
|
|
1210
|
+
const hasCookie = (name) => typeof jar[name] === "string" && jar[name].length > 0;
|
|
1211
|
+
return hasCookie(cookies.accessName) || hasCookie(cookies.refreshName);
|
|
1212
|
+
}
|
|
1213
|
+
function enforceCsrf(request, pathname, csrf, cookies) {
|
|
1214
|
+
if (!csrf.enabled)
|
|
1215
|
+
return;
|
|
1216
|
+
if (SAFE_METHODS.has(request.method.toUpperCase()))
|
|
1217
|
+
return;
|
|
1218
|
+
if (matchesSkipPath2(pathname, csrf.skipPaths))
|
|
1219
|
+
return;
|
|
1220
|
+
const fetchSite = request.headers.get("sec-fetch-site");
|
|
1221
|
+
const cookieAuthenticated = isCookieAuthenticated(request, cookies);
|
|
1222
|
+
if (!cookieAuthenticated) {
|
|
1223
|
+
if (request.headers.has("authorization") || request.headers.has("x-api-key"))
|
|
1224
|
+
return;
|
|
1225
|
+
if (fetchSite === "cross-site")
|
|
1226
|
+
throw Errors.forbidden("CSRF: cross-site request rejected");
|
|
1227
|
+
const origin = request.headers.get("origin");
|
|
1228
|
+
if (origin) {
|
|
1229
|
+
try {
|
|
1230
|
+
if (new URL(origin).origin !== new URL(request.url).origin)
|
|
1231
|
+
throw Errors.forbidden("CSRF: cross-origin request rejected");
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
if (error instanceof Error && "code" in error)
|
|
1234
|
+
throw error;
|
|
1235
|
+
throw Errors.forbidden("CSRF: invalid Origin header");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (fetchSite === "cross-site")
|
|
1241
|
+
throw Errors.forbidden("CSRF: cross-site request rejected");
|
|
1242
|
+
if (csrf.rejectSameSite && fetchSite === "same-site")
|
|
1243
|
+
throw Errors.forbidden("CSRF: same-site request rejected");
|
|
1244
|
+
const tokenHeader = request.headers.get(csrf.headerName);
|
|
1245
|
+
if (!tokenHeader)
|
|
1246
|
+
throw Errors.forbidden(`CSRF: missing ${csrf.headerName} header`);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/core/http/fortress-rbac.ts
|
|
1250
|
+
async function enforceFortressPermission(endpoint, subject, enforcement, scopes) {
|
|
1251
|
+
const security = endpoint.meta?.security;
|
|
1252
|
+
if (endpoint.meta?.permission) {
|
|
1253
|
+
if (!subject)
|
|
1254
|
+
throw Errors.unauthorized("Not authenticated");
|
|
1255
|
+
const allowed = await enforcement.checkPermission(
|
|
1256
|
+
subject,
|
|
1257
|
+
endpoint.meta.permission.resource,
|
|
1258
|
+
endpoint.meta.permission.action,
|
|
1259
|
+
scopes
|
|
1260
|
+
);
|
|
1261
|
+
if (!allowed)
|
|
1262
|
+
throw Errors.forbidden("Insufficient permissions");
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (security?.includes("none") || security?.includes("basic"))
|
|
1266
|
+
return;
|
|
1267
|
+
if (security?.includes("bearer")) {
|
|
1268
|
+
if (!subject)
|
|
1269
|
+
throw Errors.unauthorized("Not authenticated");
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
throw Errors.forbidden("No permission mapping for this route");
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/core/http/plugin-middleware.ts
|
|
1276
|
+
async function runPluginMiddleware(plugins, config, phase, ctx) {
|
|
1277
|
+
if (plugins.length === 0)
|
|
1278
|
+
return;
|
|
1279
|
+
const pluginCtx = { db: config.database, config };
|
|
1280
|
+
const path = new URL(ctx.request.url).pathname;
|
|
1281
|
+
await executePluginMiddleware(plugins, phase, path, pluginCtx, ctx);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/core/http/principal.ts
|
|
1285
|
+
async function tryPluginPrincipal(fortress, request) {
|
|
1286
|
+
const plugins = fortress.config.plugins ?? [];
|
|
1287
|
+
for (const plugin of plugins) {
|
|
1288
|
+
if (!plugin.resolvePrincipal)
|
|
1289
|
+
continue;
|
|
1290
|
+
const resolved = await plugin.resolvePrincipal(request, {
|
|
1291
|
+
db: fortress.config.database,
|
|
1292
|
+
config: fortress.config,
|
|
1293
|
+
auth: fortress.auth,
|
|
1294
|
+
iam: fortress.iam
|
|
1295
|
+
});
|
|
1296
|
+
if (resolved)
|
|
1297
|
+
return { subject: resolved.subject, claims: resolved.claims, scopes: resolved.scopes };
|
|
1298
|
+
}
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/core/http/protect.ts
|
|
1303
|
+
function routeKey(route) {
|
|
1304
|
+
return `${route.method.toUpperCase()} ${route.path}`;
|
|
1305
|
+
}
|
|
1306
|
+
function findEndpoint(fortress, target, method) {
|
|
1307
|
+
if (typeof target !== "string") {
|
|
1308
|
+
return target;
|
|
1309
|
+
}
|
|
1310
|
+
const matches = fortress.endpoints.filter((endpoint) => endpoint.handler === target);
|
|
1311
|
+
if (matches.length === 0)
|
|
1312
|
+
throw Errors.notFound(`No endpoint found for handler '${target}'`);
|
|
1313
|
+
if (method) {
|
|
1314
|
+
const byMethod = matches.find((endpoint) => endpoint.method.toUpperCase() === method.toUpperCase());
|
|
1315
|
+
if (byMethod)
|
|
1316
|
+
return byMethod;
|
|
1317
|
+
}
|
|
1318
|
+
if (matches.length > 1) {
|
|
1319
|
+
throw Errors.badRequest(
|
|
1320
|
+
`Handler '${target}' maps to multiple endpoints; pass the EndpointDefinition or ProtectOptions.method.`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
return matches[0];
|
|
1324
|
+
}
|
|
1325
|
+
function findManifestEntry(fortress, endpoint) {
|
|
1326
|
+
const key = routeKey(endpoint);
|
|
1327
|
+
const entry = fortress.manifest.find((route) => `${route.method.toUpperCase()} ${route.path}` === key);
|
|
1328
|
+
if (!entry)
|
|
1329
|
+
throw Errors.notFound(`No route manifest entry found for ${key}`);
|
|
1330
|
+
return entry;
|
|
1331
|
+
}
|
|
1332
|
+
function rewriteRequestPath(request, pathname) {
|
|
1333
|
+
const current = new URL(request.url);
|
|
1334
|
+
if (current.pathname === pathname)
|
|
1335
|
+
return request;
|
|
1336
|
+
const rewritten = new URL(request.url);
|
|
1337
|
+
rewritten.pathname = pathname;
|
|
1338
|
+
return new Request(rewritten, request);
|
|
1339
|
+
}
|
|
1340
|
+
async function parseJsonBody(request) {
|
|
1341
|
+
if (request.method === "GET" || request.method === "HEAD")
|
|
1342
|
+
return void 0;
|
|
1343
|
+
if (!(request.headers.get("content-type") ?? "").includes("json"))
|
|
1344
|
+
return void 0;
|
|
1345
|
+
return request.clone().json().catch(() => void 0);
|
|
1346
|
+
}
|
|
1347
|
+
function objectOrEmpty(value) {
|
|
1348
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1349
|
+
}
|
|
1350
|
+
function successStatus(endpoint) {
|
|
1351
|
+
const status = Object.keys(endpoint.responses ?? {}).map(Number).find((code) => code >= 200 && code < 300);
|
|
1352
|
+
return status ?? 200;
|
|
1353
|
+
}
|
|
1354
|
+
function jsonResponse(body, status) {
|
|
1355
|
+
return new Response(JSON.stringify(body ?? { ok: true }), {
|
|
1356
|
+
status,
|
|
1357
|
+
headers: { "Content-Type": "application/json" }
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
function maybeAttachAuthCookies(fortress, result, response) {
|
|
1361
|
+
if (result instanceof Response)
|
|
1362
|
+
return response;
|
|
1363
|
+
const obj2 = result;
|
|
1364
|
+
if (typeof obj2?.accessToken !== "string")
|
|
1365
|
+
return response;
|
|
1366
|
+
return withCookies(response, fortress.serializeAuthCookies({
|
|
1367
|
+
accessToken: obj2.accessToken,
|
|
1368
|
+
refreshToken: typeof obj2.refreshToken === "string" ? obj2.refreshToken : null
|
|
1369
|
+
}));
|
|
1370
|
+
}
|
|
1371
|
+
function protect(fortress, target, handler, options = {}) {
|
|
1372
|
+
const endpoint = findEndpoint(fortress, target, options.method);
|
|
1373
|
+
const manifest = findManifestEntry(fortress, endpoint);
|
|
1374
|
+
const endpointRouteTable = buildRouteTable([endpoint]);
|
|
1375
|
+
const csrfConfig = resolveCsrfConfig(fortress.config.csrf);
|
|
1376
|
+
const plugins = fortress.config.plugins ?? [];
|
|
1377
|
+
return async function protectedRequestHandler(request) {
|
|
1378
|
+
let response;
|
|
1379
|
+
try {
|
|
1380
|
+
const url2 = new URL(request.url);
|
|
1381
|
+
const pipelinePath = options.path ?? url2.pathname;
|
|
1382
|
+
const pipelineRequest = rewriteRequestPath(request, pipelinePath);
|
|
1383
|
+
await runPluginMiddleware(plugins, fortress.config, "before-auth", { request: pipelineRequest });
|
|
1384
|
+
enforceCsrf(pipelineRequest, pipelinePath, csrfConfig, fortress.cookies);
|
|
1385
|
+
let subject;
|
|
1386
|
+
let userId;
|
|
1387
|
+
let claims;
|
|
1388
|
+
let scopes;
|
|
1389
|
+
const selfManagedBearer = endpoint.meta?.bearerKind === "oauth";
|
|
1390
|
+
if (!selfManagedBearer) {
|
|
1391
|
+
const pluginResolved = await tryPluginPrincipal(fortress, pipelineRequest);
|
|
1392
|
+
if (pluginResolved) {
|
|
1393
|
+
subject = pluginResolved.subject;
|
|
1394
|
+
claims = pluginResolved.claims;
|
|
1395
|
+
scopes = pluginResolved.scopes;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const requiresBearer = !selfManagedBearer && ((endpoint.meta?.security?.includes("bearer") ?? false) || !!endpoint.meta?.permission);
|
|
1399
|
+
if (!subject && requiresBearer) {
|
|
1400
|
+
const token = fortress.extractAccessToken(pipelineRequest);
|
|
1401
|
+
if (!token)
|
|
1402
|
+
throw Errors.unauthorized("Missing access token");
|
|
1403
|
+
try {
|
|
1404
|
+
claims = await fortress.auth.verifyToken(token);
|
|
1405
|
+
subject = { type: claims.subjectType, id: claims.sub };
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
if (err instanceof FortressError)
|
|
1408
|
+
throw err;
|
|
1409
|
+
throw Errors.unauthorized("Invalid access token");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (subject?.type === "USER")
|
|
1413
|
+
userId = subject.id;
|
|
1414
|
+
await runPluginMiddleware(plugins, fortress.config, "after-auth", {
|
|
1415
|
+
request: pipelineRequest,
|
|
1416
|
+
fortressSubject: subject,
|
|
1417
|
+
fortressUserId: userId,
|
|
1418
|
+
fortressClaims: claims,
|
|
1419
|
+
fortressScopes: scopes
|
|
1420
|
+
});
|
|
1421
|
+
if (!selfManagedBearer) {
|
|
1422
|
+
await enforceFortressPermission(endpoint, subject, {
|
|
1423
|
+
checkPermission: (subj, resource, action, credentialScopes) => fortress.iam.checkPermission(subj, resource, action, { credentialScopes })
|
|
1424
|
+
}, scopes);
|
|
1425
|
+
}
|
|
1426
|
+
await runPluginMiddleware(plugins, fortress.config, "after-rbac", {
|
|
1427
|
+
request: pipelineRequest,
|
|
1428
|
+
fortressSubject: subject,
|
|
1429
|
+
fortressUserId: userId,
|
|
1430
|
+
fortressClaims: claims,
|
|
1431
|
+
fortressScopes: scopes
|
|
1432
|
+
});
|
|
1433
|
+
const body = await parseJsonBody(request);
|
|
1434
|
+
const rawQuery = Object.fromEntries(url2.searchParams);
|
|
1435
|
+
const match = matchRoute(endpointRouteTable, endpoint.method, options.path ?? url2.pathname);
|
|
1436
|
+
const rawParams = options.params ?? match?.params ?? {};
|
|
1437
|
+
const query = coerceBySchema(endpoint.input?.query, rawQuery) ?? rawQuery;
|
|
1438
|
+
const params = coerceBySchema(endpoint.input?.params, rawParams) ?? rawParams;
|
|
1439
|
+
await validateRequest(endpoint.input, { body, query, params });
|
|
1440
|
+
const input = { ...objectOrEmpty(body), ...query, ...params };
|
|
1441
|
+
const result = await handler({
|
|
1442
|
+
request,
|
|
1443
|
+
endpoint,
|
|
1444
|
+
manifest,
|
|
1445
|
+
subject,
|
|
1446
|
+
userId,
|
|
1447
|
+
claims,
|
|
1448
|
+
scopes,
|
|
1449
|
+
params,
|
|
1450
|
+
query,
|
|
1451
|
+
body,
|
|
1452
|
+
input,
|
|
1453
|
+
respond: (status, responseBody) => jsonResponse(responseBody, status)
|
|
1454
|
+
// Cast: the runtime values are untyped records; the generic context
|
|
1455
|
+
// type narrows them for the caller, but the impl signature uses the
|
|
1456
|
+
// loose `ProtectedRouteContext<EndpointDefinition>`.
|
|
1457
|
+
});
|
|
1458
|
+
response = result instanceof Response ? result : jsonResponse(result, successStatus(endpoint));
|
|
1459
|
+
if (options.attachAuthCookies ?? true)
|
|
1460
|
+
response = maybeAttachAuthCookies(fortress, result, response);
|
|
1461
|
+
return response;
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
response = errorToResponse(err, fortress.logger);
|
|
1464
|
+
return response;
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// src/hono/protect.ts
|
|
1470
|
+
function protectedRoute(fortress, target, handler, options = {}) {
|
|
1471
|
+
return async (c) => {
|
|
1472
|
+
const protectedHandler = protect(
|
|
1473
|
+
fortress,
|
|
1474
|
+
target,
|
|
1475
|
+
(ctx) => handler(c, ctx),
|
|
1476
|
+
{ ...options, method: options.method ?? c.req.method }
|
|
1477
|
+
);
|
|
1478
|
+
return protectedHandler(c.req.raw);
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/hono/validated.ts
|
|
1483
|
+
async function vBody(c, schema) {
|
|
1484
|
+
const body = await c.req.json().catch(() => void 0);
|
|
1485
|
+
return validateValue(schema, body, "body");
|
|
1486
|
+
}
|
|
1487
|
+
async function vParam(c, schema) {
|
|
1488
|
+
return validateValue(schema, c.req.param(), "params");
|
|
1489
|
+
}
|
|
1490
|
+
async function vQuery(c, schema) {
|
|
1491
|
+
return validateValue(schema, c.req.query(), "query");
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/hono/index.ts
|
|
1495
|
+
function createHonoMiddleware(fortress, options) {
|
|
1496
|
+
return {
|
|
1497
|
+
authMiddleware: createAuthMiddleware(fortress),
|
|
1498
|
+
rbacMiddleware: createRbacMiddleware(fortress, options),
|
|
1499
|
+
errorHandler: createErrorHandler(),
|
|
1500
|
+
pluginMiddleware: {
|
|
1501
|
+
beforeAuth: createPluginMiddleware(fortress, "before-auth"),
|
|
1502
|
+
afterAuth: createPluginMiddleware(fortress, "after-auth"),
|
|
1503
|
+
afterRbac: createPluginMiddleware(fortress, "after-rbac")
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1508
|
+
0 && (module.exports = {
|
|
1509
|
+
buildRouteDefinition,
|
|
1510
|
+
convertRoutes,
|
|
1511
|
+
createCsrfMiddleware,
|
|
1512
|
+
createHonoMiddleware,
|
|
1513
|
+
createPluginMiddleware,
|
|
1514
|
+
createSecurityHeadersMiddleware,
|
|
1515
|
+
fetcherSchemaConverter,
|
|
1516
|
+
getClaims,
|
|
1517
|
+
getDb,
|
|
1518
|
+
getFortressRoutes,
|
|
1519
|
+
getScopedDb,
|
|
1520
|
+
getSubject,
|
|
1521
|
+
getUserId,
|
|
1522
|
+
identitySchemaConverter,
|
|
1523
|
+
mountFortress,
|
|
1524
|
+
mountFortressOpenAPI,
|
|
1525
|
+
protectedRoute,
|
|
1526
|
+
toJSONSchemaConverter,
|
|
1527
|
+
vBody,
|
|
1528
|
+
vParam,
|
|
1529
|
+
vQuery
|
|
1530
|
+
});
|