@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,287 @@
|
|
|
1
|
+
import type { ComponentSchemas, EndpointDefinition, SecurityRequirement } from '../../core/endpoint';
|
|
2
|
+
import type { JSONSchema } from '../../core/json-schema';
|
|
3
|
+
|
|
4
|
+
/** Minimal OpenAPI 3.1 spec shape produced by {@link buildOpenAPISpec}. */
|
|
5
|
+
export interface OpenAPISpec {
|
|
6
|
+
openapi: string;
|
|
7
|
+
info: { title: string; version: string; description?: string };
|
|
8
|
+
servers?: Array<{ url: string; description?: string }>;
|
|
9
|
+
tags?: Array<{ name: string; description?: string }>;
|
|
10
|
+
paths: Record<string, Record<string, OpenAPIOperation>>;
|
|
11
|
+
components: {
|
|
12
|
+
schemas: Record<string, JSONSchema>;
|
|
13
|
+
securitySchemes: Record<string, OpenAPISecurityScheme>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface OpenAPIOperation {
|
|
18
|
+
operationId?: string;
|
|
19
|
+
summary?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
deprecated?: boolean;
|
|
23
|
+
security?: Array<Record<string, string[]>>;
|
|
24
|
+
parameters?: OpenAPIParameter[];
|
|
25
|
+
requestBody?: {
|
|
26
|
+
required?: boolean;
|
|
27
|
+
content: { 'application/json': { schema: JSONSchema } };
|
|
28
|
+
};
|
|
29
|
+
responses: Record<string, {
|
|
30
|
+
description: string;
|
|
31
|
+
content?: { 'application/json': { schema: JSONSchema } };
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface OpenAPIParameter {
|
|
36
|
+
name: string;
|
|
37
|
+
in: 'query' | 'path';
|
|
38
|
+
required?: boolean;
|
|
39
|
+
description?: string;
|
|
40
|
+
schema: JSONSchema;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface OpenAPISecurityScheme {
|
|
44
|
+
type: string;
|
|
45
|
+
scheme?: string;
|
|
46
|
+
bearerFormat?: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
in?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Options accepted by {@link buildOpenAPISpec}. */
|
|
52
|
+
export interface SpecBuilderOptions {
|
|
53
|
+
title: string;
|
|
54
|
+
version: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
servers?: Array<{ url: string; description?: string }>;
|
|
57
|
+
/** Optional top-level OpenAPI tags. */
|
|
58
|
+
tags?: Array<{ name: string; description?: string }>;
|
|
59
|
+
/**
|
|
60
|
+
* Operation ID strategy. Defaults to the historical Fortress method+path
|
|
61
|
+
* generator. `fortress.toOpenAPI()` defaults this to `'handler'` so host
|
|
62
|
+
* specs match the endpoint contract names consumers already chose.
|
|
63
|
+
*/
|
|
64
|
+
operationId?: 'methodPath' | 'handler' | ((endpoint: EndpointDefinition) => string | undefined);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SECURITY_SCHEMES: Record<string, OpenAPISecurityScheme> = {
|
|
68
|
+
bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
|
|
69
|
+
basic: { type: 'http', scheme: 'basic' },
|
|
70
|
+
apiKey: { type: 'apiKey', name: 'X-API-Key', in: 'header' },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const SECURITY_MAP: Record<SecurityRequirement, Record<string, string[]>> = {
|
|
74
|
+
bearer: { bearerAuth: [] },
|
|
75
|
+
basic: { basicAuth: [] },
|
|
76
|
+
apiKey: { apiKeyAuth: [] },
|
|
77
|
+
none: {},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const RE_LEADING_SLASH = /^\//;
|
|
81
|
+
const RE_PARAM_COLON = /\/:(\w+)/g;
|
|
82
|
+
const RE_SLASH = /\//g;
|
|
83
|
+
const RE_NON_WORD = /\W/g;
|
|
84
|
+
|
|
85
|
+
function toOperationId(method: string, path: string): string {
|
|
86
|
+
// /auth/sessions/:id → auth_sessions_id, POST → post_auth_sessions_id
|
|
87
|
+
const normalized = path
|
|
88
|
+
.replace(RE_LEADING_SLASH, '')
|
|
89
|
+
.replace(RE_PARAM_COLON, '/$1')
|
|
90
|
+
.replace(RE_SLASH, '_')
|
|
91
|
+
.replace(RE_NON_WORD, '_');
|
|
92
|
+
return `${method.toLowerCase()}_${normalized}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractPathParams(path: string, paramSchemas?: JSONSchema): OpenAPIParameter[] {
|
|
96
|
+
const params: OpenAPIParameter[] = [];
|
|
97
|
+
const matches = path.matchAll(/:(\w+)/g);
|
|
98
|
+
|
|
99
|
+
for (const match of matches) {
|
|
100
|
+
const name = match[1];
|
|
101
|
+
const schema = paramSchemas?.properties?.[name] ?? { type: 'string' as const };
|
|
102
|
+
params.push({
|
|
103
|
+
name,
|
|
104
|
+
in: 'path',
|
|
105
|
+
required: true,
|
|
106
|
+
description: schema.description,
|
|
107
|
+
schema: { type: schema.type ?? 'string' },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return params;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function extractQueryParams(querySchema?: JSONSchema): OpenAPIParameter[] {
|
|
115
|
+
if (!querySchema?.properties)
|
|
116
|
+
return [];
|
|
117
|
+
|
|
118
|
+
const params: OpenAPIParameter[] = [];
|
|
119
|
+
const required = new Set(querySchema.required ?? []);
|
|
120
|
+
|
|
121
|
+
for (const [name, schema] of Object.entries(querySchema.properties)) {
|
|
122
|
+
params.push({
|
|
123
|
+
name,
|
|
124
|
+
in: 'query',
|
|
125
|
+
required: required.has(name),
|
|
126
|
+
description: schema.description,
|
|
127
|
+
schema,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return params;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toOpenAPIPath(path: string): string {
|
|
135
|
+
return path.replace(/:(\w+)/g, '{$1}');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Deep-clean Standard Schema / runtime-only fields from schema objects before
|
|
140
|
+
* embedding them in OpenAPI. Fortress schemas are JSON Schema objects with a
|
|
141
|
+
* `~standard` validator attached; external schema adapters can also hang
|
|
142
|
+
* functions or undefined values off the object. OpenAPI must receive pure
|
|
143
|
+
* JSON-serializable JSON Schema, so strip any key beginning with `~`, any
|
|
144
|
+
* function value, and any `undefined` value recursively.
|
|
145
|
+
*/
|
|
146
|
+
function cleanSchema<T>(value: T): T {
|
|
147
|
+
if (Array.isArray(value))
|
|
148
|
+
return value.map(cleanSchema) as T;
|
|
149
|
+
|
|
150
|
+
if (typeof value === 'object' && value !== null) {
|
|
151
|
+
return Object.fromEntries(
|
|
152
|
+
Object.entries(value as Record<string, unknown>)
|
|
153
|
+
.filter(([key]) => !key.startsWith('~'))
|
|
154
|
+
.filter(([, entry]) => typeof entry !== 'function' && entry !== undefined)
|
|
155
|
+
.map(([key, entry]) => [key, cleanSchema(entry)]),
|
|
156
|
+
) as T;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveOperationId(endpoint: EndpointDefinition, options: SpecBuilderOptions): string | undefined {
|
|
163
|
+
if (typeof options.operationId === 'function')
|
|
164
|
+
return options.operationId(endpoint);
|
|
165
|
+
if (options.operationId === 'handler')
|
|
166
|
+
return endpoint.handler;
|
|
167
|
+
return toOperationId(endpoint.method, endpoint.path);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Walk every endpoint definition and component-schemas record and emit a complete OpenAPI 3.1 {@link OpenAPISpec}. */
|
|
171
|
+
export function buildOpenAPISpec(
|
|
172
|
+
endpoints: EndpointDefinition[],
|
|
173
|
+
componentSchemas: ComponentSchemas,
|
|
174
|
+
options: SpecBuilderOptions,
|
|
175
|
+
): OpenAPISpec {
|
|
176
|
+
const paths: Record<string, Record<string, OpenAPIOperation>> = {};
|
|
177
|
+
const usedSecuritySchemes = new Set<string>();
|
|
178
|
+
|
|
179
|
+
for (const ep of endpoints) {
|
|
180
|
+
const openAPIPath = toOpenAPIPath(ep.path);
|
|
181
|
+
const method = ep.method.toLowerCase();
|
|
182
|
+
|
|
183
|
+
if (!paths[openAPIPath]) {
|
|
184
|
+
paths[openAPIPath] = {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const operation: OpenAPIOperation = {
|
|
188
|
+
operationId: resolveOperationId(ep, options),
|
|
189
|
+
responses: {},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Meta
|
|
193
|
+
if (ep.meta) {
|
|
194
|
+
if (ep.meta.summary)
|
|
195
|
+
operation.summary = ep.meta.summary;
|
|
196
|
+
if (ep.meta.description)
|
|
197
|
+
operation.description = ep.meta.description;
|
|
198
|
+
if (ep.meta.tags)
|
|
199
|
+
operation.tags = ep.meta.tags;
|
|
200
|
+
if (ep.meta.deprecated)
|
|
201
|
+
operation.deprecated = true;
|
|
202
|
+
|
|
203
|
+
// Security
|
|
204
|
+
if (ep.meta.security) {
|
|
205
|
+
const secArr: Array<Record<string, string[]>> = [];
|
|
206
|
+
for (const s of ep.meta.security) {
|
|
207
|
+
if (s === 'none') {
|
|
208
|
+
secArr.push({});
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
secArr.push(SECURITY_MAP[s]);
|
|
212
|
+
usedSecuritySchemes.add(s);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
operation.security = secArr;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Parameters
|
|
220
|
+
const params: OpenAPIParameter[] = [
|
|
221
|
+
...extractPathParams(ep.path, cleanSchema(ep.input?.params)),
|
|
222
|
+
...extractQueryParams(cleanSchema(ep.input?.query)),
|
|
223
|
+
];
|
|
224
|
+
if (params.length > 0) {
|
|
225
|
+
operation.parameters = params;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Request body
|
|
229
|
+
if (ep.input?.body) {
|
|
230
|
+
operation.requestBody = {
|
|
231
|
+
required: true,
|
|
232
|
+
content: { 'application/json': { schema: cleanSchema(ep.input.body) } },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Responses
|
|
237
|
+
if (ep.responses) {
|
|
238
|
+
for (const [status, resp] of Object.entries(ep.responses)) {
|
|
239
|
+
const entry: { description: string; content?: { 'application/json': { schema: JSONSchema } } } = {
|
|
240
|
+
description: resp.description,
|
|
241
|
+
};
|
|
242
|
+
if (resp.schema) {
|
|
243
|
+
entry.content = { 'application/json': { schema: cleanSchema(resp.schema) } };
|
|
244
|
+
}
|
|
245
|
+
operation.responses[status] = entry;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
operation.responses['200'] = { description: 'Success' };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
paths[openAPIPath][method] = operation;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Build security schemes (only include those actually used)
|
|
256
|
+
const securitySchemes: Record<string, OpenAPISecurityScheme> = {};
|
|
257
|
+
const schemeNameMap: Record<string, string> = { bearer: 'bearerAuth', basic: 'basicAuth', apiKey: 'apiKeyAuth' };
|
|
258
|
+
for (const scheme of usedSecuritySchemes) {
|
|
259
|
+
const name = schemeNameMap[scheme];
|
|
260
|
+
if (name && SECURITY_SCHEMES[scheme]) {
|
|
261
|
+
securitySchemes[name] = SECURITY_SCHEMES[scheme];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const spec: OpenAPISpec = {
|
|
266
|
+
openapi: '3.1.0',
|
|
267
|
+
info: {
|
|
268
|
+
title: options.title,
|
|
269
|
+
version: options.version,
|
|
270
|
+
...(options.description && { description: options.description }),
|
|
271
|
+
},
|
|
272
|
+
paths,
|
|
273
|
+
components: {
|
|
274
|
+
schemas: cleanSchema(componentSchemas),
|
|
275
|
+
securitySchemes,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (options.servers && options.servers.length > 0) {
|
|
280
|
+
spec.servers = options.servers;
|
|
281
|
+
}
|
|
282
|
+
if (options.tags && options.tags.length > 0) {
|
|
283
|
+
spec.tags = options.tags;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return spec;
|
|
287
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives the expressRateLimit middleware directly (no real Express runtime)
|
|
3
|
+
* to confirm it calls the plugin's check() and surfaces errors via next(err).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MinimalExpressRequest } from './express';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { FortressError } from '../../core/errors';
|
|
9
|
+
import { createFortress } from '../../core/fortress';
|
|
10
|
+
import { createTestAdapter } from '../../testing';
|
|
11
|
+
import { expressRateLimit } from './express';
|
|
12
|
+
import { rateLimit } from './index';
|
|
13
|
+
|
|
14
|
+
const SECRET = 'express-rate-limit-wrapper-test-secret32!';
|
|
15
|
+
|
|
16
|
+
function setup(maxPerIp: number) {
|
|
17
|
+
const fortress = createFortress({
|
|
18
|
+
jwt: { key: SECRET },
|
|
19
|
+
database: createTestAdapter(),
|
|
20
|
+
plugins: [rateLimit({ rules: { api: { maxPerIp, windowSeconds: 60 } } })],
|
|
21
|
+
});
|
|
22
|
+
return fortress;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mockReq(overrides: Partial<MinimalExpressRequest> = {}): MinimalExpressRequest {
|
|
26
|
+
return { headers: {}, ...overrides };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function invoke(
|
|
30
|
+
mw: (req: MinimalExpressRequest, res: unknown, next: (err?: unknown) => void) => void,
|
|
31
|
+
req: MinimalExpressRequest,
|
|
32
|
+
): Promise<{ err?: unknown; called: boolean }> {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
let called = false;
|
|
35
|
+
mw(req, {}, (err?: unknown) => {
|
|
36
|
+
called = true;
|
|
37
|
+
resolve({ err, called });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('expressRateLimit (framework wrapper)', () => {
|
|
43
|
+
it('calls next() under the limit and next(FortressError) when exceeded', async () => {
|
|
44
|
+
const fortress = setup(2);
|
|
45
|
+
const mw = expressRateLimit(fortress, 'api');
|
|
46
|
+
|
|
47
|
+
const r1 = await invoke(mw, mockReq({ ip: '10.0.0.1' }));
|
|
48
|
+
const r2 = await invoke(mw, mockReq({ ip: '10.0.0.1' }));
|
|
49
|
+
expect(r1.err).toBeUndefined();
|
|
50
|
+
expect(r2.err).toBeUndefined();
|
|
51
|
+
|
|
52
|
+
const r3 = await invoke(mw, mockReq({ ip: '10.0.0.1' }));
|
|
53
|
+
expect(r3.err).toBeInstanceOf(FortressError);
|
|
54
|
+
expect((r3.err as FortressError).code).toBe('RATE_LIMITED');
|
|
55
|
+
expect((r3.err as FortressError).retryAfter).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('falls back to x-forwarded-for when req.ip is missing', async () => {
|
|
59
|
+
const fortress = setup(1);
|
|
60
|
+
const mw = expressRateLimit(fortress, 'api');
|
|
61
|
+
|
|
62
|
+
// First request with XFF only — should pass.
|
|
63
|
+
const r1 = await invoke(mw, mockReq({ headers: { 'x-forwarded-for': '10.0.0.9' } }));
|
|
64
|
+
expect(r1.err).toBeUndefined();
|
|
65
|
+
|
|
66
|
+
// Same XFF, second hit — should be limited.
|
|
67
|
+
const r2 = await invoke(mw, mockReq({ headers: { 'x-forwarded-for': '10.0.0.9' } }));
|
|
68
|
+
expect(r2.err).toBeInstanceOf(FortressError);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('keys by user when fortressUserId is present and keyByUser is on (default)', async () => {
|
|
72
|
+
const fortress = setup(1);
|
|
73
|
+
const mw = expressRateLimit(fortress, 'api');
|
|
74
|
+
|
|
75
|
+
// User 1 gets one hit, then blocked on same IP+user combo.
|
|
76
|
+
const r1 = await invoke(mw, mockReq({ ip: '10.0.0.5', fortressUserId: '1' }));
|
|
77
|
+
const r2 = await invoke(mw, mockReq({ ip: '10.0.0.5', fortressUserId: '1' }));
|
|
78
|
+
expect(r1.err).toBeUndefined();
|
|
79
|
+
expect(r2.err).toBeInstanceOf(FortressError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('throws synchronously during setup when the plugin is not registered', () => {
|
|
83
|
+
const fortress = createFortress({
|
|
84
|
+
jwt: { key: SECRET },
|
|
85
|
+
database: createTestAdapter(),
|
|
86
|
+
});
|
|
87
|
+
expect(() => expressRateLimit(fortress, 'api')).toThrow(/rate-limit plugin is not registered/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware wrapper for the rate-limit plugin.
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter that extracts the client IP (and optional authenticated
|
|
5
|
+
* userId) from an Express request and delegates to
|
|
6
|
+
* `fortress.plugins['rate-limit'].check(rule, keys)`. A `FortressError` with
|
|
7
|
+
* code `RATE_LIMITED` is passed to `next(err)` on exceed — the Express error
|
|
8
|
+
* middleware (`createErrorMiddleware` from `@bajustone/fortress/express`)
|
|
9
|
+
* renders a proper 429 with `Retry-After`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import express from 'express';
|
|
14
|
+
* import { expressRateLimit } from '@bajustone/fortress/plugins/rate-limit/express';
|
|
15
|
+
*
|
|
16
|
+
* const app = express();
|
|
17
|
+
* app.use('/api', expressRateLimit(fortress, 'api'));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Fortress } from '../../core/fortress';
|
|
24
|
+
|
|
25
|
+
// Minimal Express-compatible types so users bring their own express version.
|
|
26
|
+
/** Shape fortress reads from; compatible with any modern Express request. */
|
|
27
|
+
export interface MinimalExpressRequest {
|
|
28
|
+
ip?: string;
|
|
29
|
+
headers: Record<string, string | string[] | undefined>;
|
|
30
|
+
fortressUserId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ExpressRateLimitOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Include the authenticated user in the key (requires fortress auth
|
|
36
|
+
* middleware to have run first, populating `req.fortressUserId`). Defaults
|
|
37
|
+
* to `true` — falls back to IP-only when no user is present.
|
|
38
|
+
*/
|
|
39
|
+
keyByUser?: boolean;
|
|
40
|
+
/** Override IP extraction. Default uses `req.ip` then forwarding headers. */
|
|
41
|
+
extractIp?: (req: MinimalExpressRequest) => string | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RateLimitCheck {
|
|
45
|
+
check: (rule: string, keys: { ip?: string; userId?: string }) => Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultExtractIp(req: MinimalExpressRequest): string | undefined {
|
|
49
|
+
if (req.ip)
|
|
50
|
+
return req.ip;
|
|
51
|
+
const xff = req.headers['x-forwarded-for'];
|
|
52
|
+
if (typeof xff === 'string')
|
|
53
|
+
return xff.split(',')[0].trim();
|
|
54
|
+
if (Array.isArray(xff) && xff.length > 0)
|
|
55
|
+
return xff[0].split(',')[0].trim();
|
|
56
|
+
const xri = req.headers['x-real-ip'];
|
|
57
|
+
return typeof xri === 'string' ? xri : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns an Express middleware that rate-limits requests against the named
|
|
62
|
+
* rule defined in your `rateLimit({ rules: { ... } })` config.
|
|
63
|
+
*/
|
|
64
|
+
export function expressRateLimit(
|
|
65
|
+
fortress: Fortress,
|
|
66
|
+
ruleName: string,
|
|
67
|
+
options: ExpressRateLimitOptions = {},
|
|
68
|
+
): (req: MinimalExpressRequest, _res: unknown, next: (err?: unknown) => void) => void {
|
|
69
|
+
const methods = fortress.plugins['rate-limit'] as unknown as RateLimitCheck | undefined;
|
|
70
|
+
if (!methods?.check) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'rate-limit plugin is not registered — add rateLimit({...}) to your FortressConfig.plugins',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const extractIp = options.extractIp ?? defaultExtractIp;
|
|
76
|
+
const keyByUser = options.keyByUser ?? true;
|
|
77
|
+
|
|
78
|
+
return (req, _res, next) => {
|
|
79
|
+
const ip = extractIp(req);
|
|
80
|
+
const userId = keyByUser ? req.fortressUserId : undefined;
|
|
81
|
+
methods
|
|
82
|
+
.check(ruleName, { ip, userId })
|
|
83
|
+
.then(() => next())
|
|
84
|
+
.catch(next);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createFortress } from '../../core/fortress';
|
|
4
|
+
import { createHonoMiddleware } from '../../hono';
|
|
5
|
+
import { createTestAdapter } from '../../testing';
|
|
6
|
+
import { honoRateLimit } from './hono';
|
|
7
|
+
import { rateLimit } from './index';
|
|
8
|
+
|
|
9
|
+
const SECRET = 'hono-rate-limit-wrapper-test-secret-32!';
|
|
10
|
+
|
|
11
|
+
describe('honoRateLimit (framework wrapper)', () => {
|
|
12
|
+
function setup(ruleMaxPerIp: number) {
|
|
13
|
+
const fortress = createFortress({
|
|
14
|
+
jwt: { key: SECRET },
|
|
15
|
+
database: createTestAdapter(),
|
|
16
|
+
plugins: [
|
|
17
|
+
rateLimit({
|
|
18
|
+
rules: { api: { maxPerIp: ruleMaxPerIp, windowSeconds: 60 } },
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
const { errorHandler } = createHonoMiddleware(fortress);
|
|
23
|
+
const app = new Hono();
|
|
24
|
+
app.onError(errorHandler);
|
|
25
|
+
app.use('/api/*', honoRateLimit(fortress, 'api'));
|
|
26
|
+
app.get('/api/things', c => c.json({ ok: true }));
|
|
27
|
+
return { fortress, app };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it('allows requests under the limit and blocks the next with 429 + Retry-After', async () => {
|
|
31
|
+
const { app } = setup(2);
|
|
32
|
+
const headers = { 'x-forwarded-for': '10.0.0.1' };
|
|
33
|
+
|
|
34
|
+
const r1 = await app.request('/api/things', { headers });
|
|
35
|
+
const r2 = await app.request('/api/things', { headers });
|
|
36
|
+
expect(r1.status).toBe(200);
|
|
37
|
+
expect(r2.status).toBe(200);
|
|
38
|
+
|
|
39
|
+
const r3 = await app.request('/api/things', { headers });
|
|
40
|
+
expect(r3.status).toBe(429);
|
|
41
|
+
expect(r3.headers.get('Retry-After')).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keys by IP — different forwarded-for values have separate buckets', async () => {
|
|
45
|
+
const { app } = setup(1);
|
|
46
|
+
|
|
47
|
+
const a = await app.request('/api/things', { headers: { 'x-forwarded-for': '10.0.0.2' } });
|
|
48
|
+
const b = await app.request('/api/things', { headers: { 'x-forwarded-for': '10.0.0.3' } });
|
|
49
|
+
expect(a.status).toBe(200);
|
|
50
|
+
expect(b.status).toBe(200); // different IP, still in budget
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws synchronously during setup when the plugin is not registered', () => {
|
|
54
|
+
const fortress = createFortress({
|
|
55
|
+
jwt: { key: SECRET },
|
|
56
|
+
database: createTestAdapter(),
|
|
57
|
+
});
|
|
58
|
+
expect(() => honoRateLimit(fortress, 'api')).toThrow(/rate-limit plugin is not registered/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono middleware wrapper for the rate-limit plugin.
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter that extracts the client IP (and optional authenticated
|
|
5
|
+
* userId) from a Hono context and delegates to
|
|
6
|
+
* `fortress.plugins['rate-limit'].check(rule, keys)`. A `FortressError` with
|
|
7
|
+
* code `RATE_LIMITED` is thrown when the limit is exceeded — pair this with
|
|
8
|
+
* `createHonoMiddleware(fortress).errorHandler` (or any handler that calls
|
|
9
|
+
* `errorToResponse`) to render a proper 429 with `Retry-After`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Hono } from 'hono';
|
|
14
|
+
* import { honoRateLimit } from '@bajustone/fortress/plugins/rate-limit/hono';
|
|
15
|
+
*
|
|
16
|
+
* const app = new Hono();
|
|
17
|
+
* app.use('/api/*', honoRateLimit(fortress, 'api'));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Context, MiddlewareHandler } from 'hono';
|
|
24
|
+
import type { Fortress } from '../../core/fortress';
|
|
25
|
+
|
|
26
|
+
export interface HonoRateLimitOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Include the authenticated user in the key (requires fortress auth
|
|
29
|
+
* middleware to have run first, populating `fortressUserId`). Defaults to
|
|
30
|
+
* `true` — falls back to IP-only when no user is present.
|
|
31
|
+
*/
|
|
32
|
+
keyByUser?: boolean;
|
|
33
|
+
/** Override IP extraction. Default reads `X-Forwarded-For` then `X-Real-IP`. */
|
|
34
|
+
extractIp?: (c: Context) => string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RateLimitCheck {
|
|
38
|
+
check: (rule: string, keys: { ip?: string; userId?: string }) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultExtractIp(c: Context): string | undefined {
|
|
42
|
+
const xff = c.req.header('x-forwarded-for');
|
|
43
|
+
if (xff)
|
|
44
|
+
return xff.split(',')[0].trim();
|
|
45
|
+
return c.req.header('x-real-ip');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns a Hono middleware that rate-limits requests against the named rule
|
|
50
|
+
* defined in your `rateLimit({ rules: { ... } })` config.
|
|
51
|
+
*/
|
|
52
|
+
export function honoRateLimit(
|
|
53
|
+
fortress: Fortress,
|
|
54
|
+
ruleName: string,
|
|
55
|
+
options: HonoRateLimitOptions = {},
|
|
56
|
+
): MiddlewareHandler {
|
|
57
|
+
const methods = fortress.plugins['rate-limit'] as unknown as RateLimitCheck | undefined;
|
|
58
|
+
if (!methods?.check) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'rate-limit plugin is not registered — add rateLimit({...}) to your FortressConfig.plugins',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const extractIp = options.extractIp ?? defaultExtractIp;
|
|
64
|
+
const keyByUser = options.keyByUser ?? true;
|
|
65
|
+
|
|
66
|
+
return async (c, next) => {
|
|
67
|
+
const ip = extractIp(c);
|
|
68
|
+
const userId = keyByUser
|
|
69
|
+
? (c.get('fortressUserId' as never) as string | undefined)
|
|
70
|
+
: undefined;
|
|
71
|
+
await methods.check(ruleName, { ip, userId });
|
|
72
|
+
await next();
|
|
73
|
+
};
|
|
74
|
+
}
|