@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,131 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { Fortress } from '../../core/fortress';
|
|
3
|
+
import type { FortressEnv } from './auth';
|
|
4
|
+
import { FortressError } from '../../core/errors';
|
|
5
|
+
|
|
6
|
+
/** A `(resource, action)` IAM mapping for an HTTP route. */
|
|
7
|
+
export interface RouteMapping {
|
|
8
|
+
resource: string;
|
|
9
|
+
action: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Options accepted by {@link createRbacMiddleware} (and the Hono adapter factory). */
|
|
13
|
+
export interface RbacOptions {
|
|
14
|
+
/** Declarative route-to-resource mapping: 'METHOD /path' → { resource, action } */
|
|
15
|
+
routeMap?: Record<string, RouteMapping>;
|
|
16
|
+
/** Dynamic mapping function (used if routeMap doesn't match) */
|
|
17
|
+
mapRequest?: (method: string, path: string) => RouteMapping | null;
|
|
18
|
+
/** Paths that skip permission checks entirely (supports * wildcards) */
|
|
19
|
+
skipPaths?: string[];
|
|
20
|
+
/** Behavior for non-skipped routes absent from routeMap/mapRequest. */
|
|
21
|
+
unmappedRoutes?: 'allow' | 'deny';
|
|
22
|
+
/** @deprecated Use `unmappedRoutes: 'deny'`. */
|
|
23
|
+
defaultDeny?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hono middleware that checks IAM permissions for **user-owned routes**
|
|
28
|
+
* via a declarative route map. Fortress-managed routes (`/auth/*`,
|
|
29
|
+
* `/iam/*`, plugin paths) are protected automatically inside
|
|
30
|
+
* `fortress.handleRequest` — this middleware only handles routes the
|
|
31
|
+
* caller registered themselves.
|
|
32
|
+
*
|
|
33
|
+
* Lookup order:
|
|
34
|
+
* 1. {@link RbacOptions.skipPaths} — if matched, allow.
|
|
35
|
+
* 2. {@link RbacOptions.routeMap} — exact `'METHOD /path'` then
|
|
36
|
+
* parameterized match.
|
|
37
|
+
* 3. {@link RbacOptions.mapRequest} — dynamic resolver fallback.
|
|
38
|
+
* 4. No mapping found → allow, unless {@link RbacOptions.defaultDeny} is set,
|
|
39
|
+
* in which case the route is refused with a 403 (fail closed).
|
|
40
|
+
*
|
|
41
|
+
* Throws {@link FortressError} on missing/insufficient permissions.
|
|
42
|
+
*/
|
|
43
|
+
export function createRbacMiddleware(
|
|
44
|
+
fortress: Fortress,
|
|
45
|
+
options?: RbacOptions,
|
|
46
|
+
): MiddlewareHandler<FortressEnv> {
|
|
47
|
+
const routeMap = options?.routeMap ?? {};
|
|
48
|
+
const skipPaths = options?.skipPaths ?? [];
|
|
49
|
+
const skipPatterns = skipPaths.map(p => pathToRegex(p));
|
|
50
|
+
const unmappedRoutes = options?.unmappedRoutes
|
|
51
|
+
?? (options?.defaultDeny ? 'deny' : 'allow');
|
|
52
|
+
|
|
53
|
+
return async (c, next) => {
|
|
54
|
+
const path = c.req.path;
|
|
55
|
+
const method = c.req.method;
|
|
56
|
+
|
|
57
|
+
if (skipPatterns.some(pattern => pattern.test(path))) {
|
|
58
|
+
await next();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const key = `${method} ${path}`;
|
|
63
|
+
let mapping: RouteMapping | null = routeMap[key] ?? null;
|
|
64
|
+
|
|
65
|
+
if (!mapping) {
|
|
66
|
+
mapping = findRouteMapMatch(method, path, routeMap);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!mapping && options?.mapRequest) {
|
|
70
|
+
mapping = options.mapRequest(method, path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!mapping) {
|
|
74
|
+
if (unmappedRoutes === 'deny') {
|
|
75
|
+
// Fail closed: an unmapped route is refused rather than treated as
|
|
76
|
+
// public. Genuinely public routes belong in skipPaths.
|
|
77
|
+
throw new FortressError('FORBIDDEN', 'No permission mapping for this route', 403);
|
|
78
|
+
}
|
|
79
|
+
// No declarative mapping — caller treats this as a public route from
|
|
80
|
+
// the adapter's perspective. (Fortress-managed paths have already
|
|
81
|
+
// been caught and protected inside fortress.handleRequest.)
|
|
82
|
+
await next();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const subject = c.get('fortressSubject');
|
|
87
|
+
if (!subject) {
|
|
88
|
+
throw new FortressError('UNAUTHORIZED', 'Not authenticated', 401);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const allowed = await fortress.iam.checkPermission(subject, mapping.resource, mapping.action, {
|
|
92
|
+
credentialScopes: c.get('fortressScopes'),
|
|
93
|
+
});
|
|
94
|
+
if (!allowed) {
|
|
95
|
+
throw new FortressError('FORBIDDEN', 'Insufficient permissions', 403);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await next();
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Match a request against parameterized route map entries.
|
|
104
|
+
* 'GET /api/users/:id' matches 'GET /api/users/123'
|
|
105
|
+
*/
|
|
106
|
+
function findRouteMapMatch(method: string, path: string, routeMap: Record<string, RouteMapping>): RouteMapping | null {
|
|
107
|
+
for (const [pattern, mapping] of Object.entries(routeMap)) {
|
|
108
|
+
const [patternMethod, patternPath] = pattern.split(' ', 2);
|
|
109
|
+
if (patternMethod !== method)
|
|
110
|
+
continue;
|
|
111
|
+
|
|
112
|
+
const regex = pathToRegex(patternPath);
|
|
113
|
+
if (regex.test(path))
|
|
114
|
+
return mapping;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Convert a route pattern to a regex.
|
|
121
|
+
* :param → [^/]+
|
|
122
|
+
* → .*
|
|
123
|
+
*/
|
|
124
|
+
function pathToRegex(pattern: string): RegExp {
|
|
125
|
+
const regexStr = pattern
|
|
126
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
127
|
+
.replace(/:[^/]+/g, '[^/]+')
|
|
128
|
+
.replace(/\\\*/g, '.*')
|
|
129
|
+
.replace(/\//g, '\\/');
|
|
130
|
+
return new RegExp(`^${regexStr}$`);
|
|
131
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createSecurityHeadersMiddleware } from './security-headers';
|
|
4
|
+
|
|
5
|
+
describe('security headers middleware', () => {
|
|
6
|
+
it('sets all default security headers', async () => {
|
|
7
|
+
const app = new Hono();
|
|
8
|
+
app.use('*', createSecurityHeadersMiddleware());
|
|
9
|
+
app.get('/test', c => c.text('ok'));
|
|
10
|
+
|
|
11
|
+
const res = await app.request('/test');
|
|
12
|
+
expect(res.status).toBe(200);
|
|
13
|
+
expect(res.headers.get('Strict-Transport-Security')).toBe('max-age=63072000; includeSubDomains');
|
|
14
|
+
expect(res.headers.get('X-Frame-Options')).toBe('DENY');
|
|
15
|
+
expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
|
|
16
|
+
expect(res.headers.get('Content-Security-Policy')).toBe('default-src \'self\'');
|
|
17
|
+
expect(res.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
|
18
|
+
expect(res.headers.get('X-Permitted-Cross-Domain-Policies')).toBe('none');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('sets security headers on error responses', async () => {
|
|
22
|
+
const app = new Hono();
|
|
23
|
+
app.use('*', createSecurityHeadersMiddleware());
|
|
24
|
+
app.get('/error', () => {
|
|
25
|
+
throw new Error('boom');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const res = await app.request('/error');
|
|
29
|
+
expect(res.status).toBe(500);
|
|
30
|
+
expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
|
|
31
|
+
expect(res.headers.get('X-Frame-Options')).toBe('DENY');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('allows custom CSP', async () => {
|
|
35
|
+
const app = new Hono();
|
|
36
|
+
app.use('*', createSecurityHeadersMiddleware({
|
|
37
|
+
contentSecurityPolicy: 'default-src \'self\'; script-src \'self\' cdn.example.com',
|
|
38
|
+
}));
|
|
39
|
+
app.get('/test', c => c.text('ok'));
|
|
40
|
+
|
|
41
|
+
const res = await app.request('/test');
|
|
42
|
+
expect(res.headers.get('Content-Security-Policy')).toBe('default-src \'self\'; script-src \'self\' cdn.example.com');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('allows disabling individual headers', async () => {
|
|
46
|
+
const app = new Hono();
|
|
47
|
+
app.use('*', createSecurityHeadersMiddleware({
|
|
48
|
+
frameOptions: false,
|
|
49
|
+
contentSecurityPolicy: false,
|
|
50
|
+
referrerPolicy: false,
|
|
51
|
+
}));
|
|
52
|
+
app.get('/test', c => c.text('ok'));
|
|
53
|
+
|
|
54
|
+
const res = await app.request('/test');
|
|
55
|
+
expect(res.headers.get('X-Frame-Options')).toBeNull();
|
|
56
|
+
expect(res.headers.get('Content-Security-Policy')).toBeNull();
|
|
57
|
+
expect(res.headers.get('Referrer-Policy')).toBeNull();
|
|
58
|
+
// Others still present
|
|
59
|
+
expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('supports HSTS preload', async () => {
|
|
63
|
+
const app = new Hono();
|
|
64
|
+
app.use('*', createSecurityHeadersMiddleware({ hstsPreload: true }));
|
|
65
|
+
app.get('/test', c => c.text('ok'));
|
|
66
|
+
|
|
67
|
+
const res = await app.request('/test');
|
|
68
|
+
expect(res.headers.get('Strict-Transport-Security')).toBe('max-age=63072000; includeSubDomains; preload');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('disables HSTS when maxAge is 0', async () => {
|
|
72
|
+
const app = new Hono();
|
|
73
|
+
app.use('*', createSecurityHeadersMiddleware({ hstsMaxAge: 0 }));
|
|
74
|
+
app.get('/test', c => c.text('ok'));
|
|
75
|
+
|
|
76
|
+
const res = await app.request('/test');
|
|
77
|
+
expect(res.headers.get('Strict-Transport-Security')).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('allows SAMEORIGIN for X-Frame-Options', async () => {
|
|
81
|
+
const app = new Hono();
|
|
82
|
+
app.use('*', createSecurityHeadersMiddleware({ frameOptions: 'SAMEORIGIN' }));
|
|
83
|
+
app.get('/test', c => c.text('ok'));
|
|
84
|
+
|
|
85
|
+
const res = await app.request('/test');
|
|
86
|
+
expect(res.headers.get('X-Frame-Options')).toBe('SAMEORIGIN');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
|
|
3
|
+
/** Options accepted by {@link createSecurityHeadersMiddleware}. */
|
|
4
|
+
export interface SecurityHeadersConfig {
|
|
5
|
+
/** Strict-Transport-Security max-age in seconds. Default: 63072000 (2 years). Set to 0 to disable. */
|
|
6
|
+
hstsMaxAge?: number;
|
|
7
|
+
/** Include subdomains in HSTS. Default: true. */
|
|
8
|
+
hstsIncludeSubdomains?: boolean;
|
|
9
|
+
/** HSTS preload flag. Default: false. */
|
|
10
|
+
hstsPreload?: boolean;
|
|
11
|
+
/** X-Frame-Options value. Default: 'DENY'. Set to false to disable. */
|
|
12
|
+
frameOptions?: 'DENY' | 'SAMEORIGIN' | false;
|
|
13
|
+
/** X-Content-Type-Options nosniff. Default: true. */
|
|
14
|
+
noSniff?: boolean;
|
|
15
|
+
/** Content-Security-Policy value. Default: "default-src 'self'". Set to false to disable. */
|
|
16
|
+
contentSecurityPolicy?: string | false;
|
|
17
|
+
/** Referrer-Policy value. Default: 'strict-origin-when-cross-origin'. Set to false to disable. */
|
|
18
|
+
referrerPolicy?: string | false;
|
|
19
|
+
/** X-Permitted-Cross-Domain-Policies value. Default: 'none'. Set to false to disable. */
|
|
20
|
+
permittedCrossDomainPolicies?: string | false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hono middleware that sets security-related HTTP headers.
|
|
25
|
+
* All headers are enabled by default with secure values.
|
|
26
|
+
*/
|
|
27
|
+
export function createSecurityHeadersMiddleware(config: SecurityHeadersConfig = {}): MiddlewareHandler {
|
|
28
|
+
const hstsMaxAge = config.hstsMaxAge ?? 63072000;
|
|
29
|
+
const hstsIncludeSubdomains = config.hstsIncludeSubdomains ?? true;
|
|
30
|
+
const hstsPreload = config.hstsPreload ?? false;
|
|
31
|
+
const frameOptions = config.frameOptions ?? 'DENY';
|
|
32
|
+
const noSniff = config.noSniff ?? true;
|
|
33
|
+
const csp = config.contentSecurityPolicy ?? 'default-src \'self\'';
|
|
34
|
+
const referrerPolicy = config.referrerPolicy ?? 'strict-origin-when-cross-origin';
|
|
35
|
+
const crossDomain = config.permittedCrossDomainPolicies ?? 'none';
|
|
36
|
+
|
|
37
|
+
// Pre-build HSTS header value
|
|
38
|
+
let hstsValue: string | null = null;
|
|
39
|
+
if (hstsMaxAge > 0) {
|
|
40
|
+
hstsValue = `max-age=${hstsMaxAge}`;
|
|
41
|
+
if (hstsIncludeSubdomains)
|
|
42
|
+
hstsValue += '; includeSubDomains';
|
|
43
|
+
if (hstsPreload)
|
|
44
|
+
hstsValue += '; preload';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return async (c, next) => {
|
|
48
|
+
try {
|
|
49
|
+
await next();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
// Apply to success and error responses alike. Security headers should
|
|
53
|
+
// not disappear on the responses most likely to contain diagnostics.
|
|
54
|
+
if (hstsValue)
|
|
55
|
+
c.header('Strict-Transport-Security', hstsValue);
|
|
56
|
+
if (frameOptions)
|
|
57
|
+
c.header('X-Frame-Options', frameOptions);
|
|
58
|
+
if (noSniff)
|
|
59
|
+
c.header('X-Content-Type-Options', 'nosniff');
|
|
60
|
+
if (csp)
|
|
61
|
+
c.header('Content-Security-Policy', csp);
|
|
62
|
+
if (referrerPolicy)
|
|
63
|
+
c.header('Referrer-Policy', referrerPolicy);
|
|
64
|
+
if (crossDomain)
|
|
65
|
+
c.header('X-Permitted-Cross-Domain-Policies', crossDomain);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono OpenAPI adapter for Fortress.
|
|
3
|
+
*
|
|
4
|
+
* Schema-library agnostic — accepts a converter function from the user.
|
|
5
|
+
* Users pass their own JSON Schema → schema-library converter (Zod, Valibot, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Users pass their schema library's JSON Schema converter:
|
|
8
|
+
* - Zod v4: `z.fromJSONSchema`
|
|
9
|
+
* - TypeBox: schemas are JSON Schema natively
|
|
10
|
+
* - ArkType: `@ark/jsonschema`
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { EndpointDefinition } from '../core/endpoint';
|
|
14
|
+
import type { Fortress } from '../core/fortress';
|
|
15
|
+
import type { JSONSchema } from '../core/json-schema';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A function that converts a JSON Schema to whatever schema type your
|
|
19
|
+
* framework adapter expects (e.g. Zod schema, Valibot schema). Used by
|
|
20
|
+
* {@link buildRouteDefinition} so fortress's OpenAPI generator stays
|
|
21
|
+
* schema-library agnostic.
|
|
22
|
+
*/
|
|
23
|
+
export type SchemaConverter<T = unknown> = (jsonSchema: JSONSchema) => T;
|
|
24
|
+
|
|
25
|
+
interface MountOptions<T = unknown> {
|
|
26
|
+
/** Convert JSON Schema → your schema library (e.g., jsonSchemaToZod from '@bajustone/fortress/zod') */
|
|
27
|
+
schemaConverter: SchemaConverter<T>;
|
|
28
|
+
/** Paths to skip (won't be mounted) */
|
|
29
|
+
skipPaths?: string[];
|
|
30
|
+
/** Function to create a route definition from an endpoint (framework-specific) */
|
|
31
|
+
createRoute: (def: Record<string, unknown>) => unknown;
|
|
32
|
+
/** Function to register security schemes on the app */
|
|
33
|
+
registerSecuritySchemes?: (app: unknown) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface GenericOpenAPIApp {
|
|
37
|
+
openapi: (route: unknown, handler: (...args: any[]) => any) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toOpenAPIPath(path: string): string {
|
|
41
|
+
return path.replace(/:(\w+)/g, '{$1}');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a framework-agnostic route definition object from an EndpointDefinition.
|
|
46
|
+
* The schemaConverter transforms JSON Schema into whatever the target framework expects.
|
|
47
|
+
*/
|
|
48
|
+
export function buildRouteDefinition<T>(
|
|
49
|
+
ep: EndpointDefinition,
|
|
50
|
+
schemaConverter: SchemaConverter<T>,
|
|
51
|
+
): Record<string, unknown> {
|
|
52
|
+
const openAPIPath = toOpenAPIPath(ep.path);
|
|
53
|
+
const method = ep.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch';
|
|
54
|
+
|
|
55
|
+
const routeDef: Record<string, unknown> = {
|
|
56
|
+
method,
|
|
57
|
+
path: openAPIPath,
|
|
58
|
+
operationId: ep.handler,
|
|
59
|
+
responses: {} as Record<number, { description: string; content?: Record<string, unknown> }>,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (ep.meta?.summary)
|
|
63
|
+
routeDef.summary = ep.meta.summary;
|
|
64
|
+
if (ep.meta?.description)
|
|
65
|
+
routeDef.description = ep.meta.description;
|
|
66
|
+
if (ep.meta?.tags)
|
|
67
|
+
routeDef.tags = ep.meta.tags;
|
|
68
|
+
if (ep.meta?.deprecated)
|
|
69
|
+
routeDef.deprecated = true;
|
|
70
|
+
|
|
71
|
+
// Security
|
|
72
|
+
if (ep.meta?.security) {
|
|
73
|
+
const sec: Array<Record<string, string[]>> = [];
|
|
74
|
+
for (const s of ep.meta.security) {
|
|
75
|
+
if (s === 'none')
|
|
76
|
+
sec.push({});
|
|
77
|
+
else if (s === 'bearer')
|
|
78
|
+
sec.push({ bearerAuth: [] });
|
|
79
|
+
else if (s === 'basic')
|
|
80
|
+
sec.push({ basicAuth: [] });
|
|
81
|
+
else if (s === 'apiKey')
|
|
82
|
+
sec.push({ apiKeyAuth: [] });
|
|
83
|
+
}
|
|
84
|
+
routeDef.security = sec;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Request
|
|
88
|
+
const request: Record<string, unknown> = {};
|
|
89
|
+
|
|
90
|
+
if (ep.input?.body) {
|
|
91
|
+
request.body = {
|
|
92
|
+
content: {
|
|
93
|
+
'application/json': { schema: schemaConverter(ep.input.body) },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (ep.input?.params?.properties) {
|
|
99
|
+
const shape: Record<string, unknown> = {};
|
|
100
|
+
const required = new Set(ep.input.params.required ?? []);
|
|
101
|
+
|
|
102
|
+
for (const [key, schema] of Object.entries(ep.input.params.properties)) {
|
|
103
|
+
const converted = schemaConverter({ ...schema, type: schema.type ?? 'string' });
|
|
104
|
+
shape[key] = required.has(key) ? converted : converted;
|
|
105
|
+
}
|
|
106
|
+
// Wrap params in an object schema
|
|
107
|
+
request.params = schemaConverter({
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: ep.input.params.properties,
|
|
110
|
+
required: ep.input.params.required,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (ep.input?.query?.properties) {
|
|
115
|
+
request.query = schemaConverter(ep.input.query);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Object.keys(request).length > 0) {
|
|
119
|
+
routeDef.request = request;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Responses
|
|
123
|
+
const responses: Record<number, { description: string; content?: Record<string, unknown> }> = {};
|
|
124
|
+
if (ep.responses) {
|
|
125
|
+
for (const [status, resp] of Object.entries(ep.responses)) {
|
|
126
|
+
const entry: { description: string; content?: Record<string, unknown> } = {
|
|
127
|
+
description: resp.description,
|
|
128
|
+
};
|
|
129
|
+
if (resp.schema) {
|
|
130
|
+
entry.content = {
|
|
131
|
+
'application/json': { schema: schemaConverter(resp.schema) },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
responses[Number(status)] = entry;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
responses[200] = { description: 'Success' };
|
|
139
|
+
}
|
|
140
|
+
routeDef.responses = responses;
|
|
141
|
+
|
|
142
|
+
return routeDef;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Auto-mount all Fortress endpoints on an OpenAPI-compatible Hono app.
|
|
147
|
+
*
|
|
148
|
+
* Schema-library agnostic: you provide the converter.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```ts
|
|
152
|
+
* import { OpenAPIHono, createRoute } from '@hono/zod-openapi';
|
|
153
|
+
* import { mountFortressOpenAPI } from '@bajustone/fortress/hono';
|
|
154
|
+
* import { jsonSchemaToZod } from '@bajustone/fortress/zod';
|
|
155
|
+
*
|
|
156
|
+
* const app = new OpenAPIHono();
|
|
157
|
+
* mountFortressOpenAPI(app, fortress, {
|
|
158
|
+
* schemaConverter: jsonSchemaToZod,
|
|
159
|
+
* createRoute,
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export function mountFortressOpenAPI<T>(
|
|
164
|
+
app: GenericOpenAPIApp & { openAPIRegistry?: { registerComponent: (type: string, name: string, schema: Record<string, unknown>) => void } },
|
|
165
|
+
fortress: Fortress,
|
|
166
|
+
options: MountOptions<T>,
|
|
167
|
+
): void {
|
|
168
|
+
const skipPaths = new Set(options.skipPaths ?? []);
|
|
169
|
+
|
|
170
|
+
// Register security schemes if the app supports it
|
|
171
|
+
if (app.openAPIRegistry) {
|
|
172
|
+
app.openAPIRegistry.registerComponent('securitySchemes', 'bearerAuth', {
|
|
173
|
+
type: 'http',
|
|
174
|
+
scheme: 'bearer',
|
|
175
|
+
bearerFormat: 'JWT',
|
|
176
|
+
});
|
|
177
|
+
app.openAPIRegistry.registerComponent('securitySchemes', 'basicAuth', {
|
|
178
|
+
type: 'http',
|
|
179
|
+
scheme: 'basic',
|
|
180
|
+
});
|
|
181
|
+
app.openAPIRegistry.registerComponent('securitySchemes', 'apiKeyAuth', {
|
|
182
|
+
type: 'apiKey',
|
|
183
|
+
name: 'X-API-Key',
|
|
184
|
+
in: 'header',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Custom security scheme registration
|
|
189
|
+
options.registerSecuritySchemes?.(app);
|
|
190
|
+
|
|
191
|
+
for (const ep of fortress.endpoints) {
|
|
192
|
+
if (skipPaths.has(ep.path))
|
|
193
|
+
continue;
|
|
194
|
+
|
|
195
|
+
const routeDef = buildRouteDefinition(ep, options.schemaConverter);
|
|
196
|
+
const route = options.createRoute(routeDef);
|
|
197
|
+
const handler = createAutoHandler(fortress, ep);
|
|
198
|
+
|
|
199
|
+
app.openapi(route, handler);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get all Fortress endpoint definitions as framework route definitions.
|
|
205
|
+
* Returns a map of `{ [handlerName]: routeDef }`.
|
|
206
|
+
*/
|
|
207
|
+
export function getFortressRoutes<T>(
|
|
208
|
+
fortress: Fortress,
|
|
209
|
+
schemaConverter: SchemaConverter<T>,
|
|
210
|
+
createRoute: (def: Record<string, unknown>) => unknown,
|
|
211
|
+
): Record<string, unknown> {
|
|
212
|
+
const result: Record<string, unknown> = {};
|
|
213
|
+
|
|
214
|
+
for (const ep of fortress.endpoints) {
|
|
215
|
+
const routeDef = buildRouteDefinition(ep, schemaConverter);
|
|
216
|
+
result[ep.handler] = createRoute(routeDef);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Auto-handler wiring ──────���──────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function createAutoHandler(fortress: Fortress, _ep: EndpointDefinition): (c: any) => Promise<Response> {
|
|
225
|
+
return async (c: any) => {
|
|
226
|
+
const raw = c.req?.raw as Request | undefined;
|
|
227
|
+
if (raw)
|
|
228
|
+
return fortress.handleRequest(raw);
|
|
229
|
+
|
|
230
|
+
const request = new Request(c.req.url, {
|
|
231
|
+
method: c.req.method,
|
|
232
|
+
headers: c.req.raw?.headers ?? undefined,
|
|
233
|
+
body: ['GET', 'HEAD'].includes(c.req.method) ? undefined : await c.req.text(),
|
|
234
|
+
});
|
|
235
|
+
return fortress.handleRequest(request);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Context, Env, MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { EndpointDefinition } from '../core/endpoint';
|
|
3
|
+
import type { Fortress } from '../core/fortress';
|
|
4
|
+
import type {
|
|
5
|
+
ProtectedRouteContext,
|
|
6
|
+
ProtectedRouteHandler,
|
|
7
|
+
ProtectedRouteTarget,
|
|
8
|
+
ProtectOptions,
|
|
9
|
+
} from '../core/http/protect';
|
|
10
|
+
import { protect } from '../core/http/protect';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hono-flavoured host callback. `E` flows in from the endpoint passed to
|
|
14
|
+
* `protectedRoute()`, so `ctx.body` / `ctx.query` / `ctx.params` / `ctx.input`
|
|
15
|
+
* are typed from the endpoint's phantom generics.
|
|
16
|
+
*
|
|
17
|
+
* `HEnv` is Hono's environment generic (renamed from `E` so it no longer
|
|
18
|
+
* collides with the endpoint generic). String targets degrade to the
|
|
19
|
+
* loose default just like the core helper.
|
|
20
|
+
*/
|
|
21
|
+
export type HonoProtectedRouteHandler<
|
|
22
|
+
|
|
23
|
+
E extends EndpointDefinition<any, any, any, any> = EndpointDefinition,
|
|
24
|
+
HEnv extends Env = Env,
|
|
25
|
+
TResult = unknown,
|
|
26
|
+
> = (
|
|
27
|
+
c: Context<HEnv>,
|
|
28
|
+
ctx: ProtectedRouteContext<E>,
|
|
29
|
+
) => TResult | Response | Promise<TResult | Response>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wrap a host-owned Hono route in Fortress's route protection pipeline.
|
|
33
|
+
*
|
|
34
|
+
* Two overloads, mirroring `protect()`:
|
|
35
|
+
*
|
|
36
|
+
* - **Typed target** — passing an `EndpointDefinition` flows its phantom
|
|
37
|
+
* generics through `ctx.body` / `ctx.query` / `ctx.params` / `ctx.input`.
|
|
38
|
+
* - **String target** — passing a unique `handler` name keeps the loose
|
|
39
|
+
* `Record<string, unknown>` / `unknown` typing.
|
|
40
|
+
*/
|
|
41
|
+
export function protectedRoute<
|
|
42
|
+
|
|
43
|
+
E extends EndpointDefinition<any, any, any, any>,
|
|
44
|
+
HEnv extends Env = Env,
|
|
45
|
+
TResult = unknown,
|
|
46
|
+
>(
|
|
47
|
+
fortress: Fortress,
|
|
48
|
+
target: E,
|
|
49
|
+
handler: HonoProtectedRouteHandler<E, HEnv, TResult>,
|
|
50
|
+
options?: ProtectOptions,
|
|
51
|
+
): MiddlewareHandler<HEnv>;
|
|
52
|
+
export function protectedRoute<HEnv extends Env = Env, TResult = unknown>(
|
|
53
|
+
fortress: Fortress,
|
|
54
|
+
target: string,
|
|
55
|
+
handler: HonoProtectedRouteHandler<EndpointDefinition, HEnv, TResult>,
|
|
56
|
+
options?: ProtectOptions,
|
|
57
|
+
): MiddlewareHandler<HEnv>;
|
|
58
|
+
export function protectedRoute(
|
|
59
|
+
fortress: Fortress,
|
|
60
|
+
target: ProtectedRouteTarget,
|
|
61
|
+
|
|
62
|
+
handler: HonoProtectedRouteHandler<any, any, unknown>,
|
|
63
|
+
options: ProtectOptions = {},
|
|
64
|
+
): MiddlewareHandler {
|
|
65
|
+
return async (c) => {
|
|
66
|
+
// Cast: core `protect` overloads require a concrete branch; impl is loose.
|
|
67
|
+
const protectedHandler = (protect as (
|
|
68
|
+
f: Fortress,
|
|
69
|
+
t: ProtectedRouteTarget,
|
|
70
|
+
h: ProtectedRouteHandler,
|
|
71
|
+
o?: ProtectOptions,
|
|
72
|
+
) => (request: Request) => Promise<Response>)(
|
|
73
|
+
fortress,
|
|
74
|
+
target,
|
|
75
|
+
ctx => handler(c, ctx),
|
|
76
|
+
{ ...options, method: options.method ?? c.req.method },
|
|
77
|
+
);
|
|
78
|
+
return protectedHandler(c.req.raw);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type {
|
|
83
|
+
ProtectedRouteContext,
|
|
84
|
+
ProtectedRouteHandler,
|
|
85
|
+
ProtectedRouteTarget,
|
|
86
|
+
ProtectOptions,
|
|
87
|
+
};
|