@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,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding-window rate limiting plugin for fortress.
|
|
3
|
+
*
|
|
4
|
+
* Protects built-in Fortress auth endpoints (`login`, `register`, `refresh`,
|
|
5
|
+
* plus OAuth token issuance and API-key creation when those plugins are
|
|
6
|
+
* mounted) and exposes a `check()` method + per-framework middleware
|
|
7
|
+
* wrappers so consumers can rate-limit any of their own routes.
|
|
8
|
+
*
|
|
9
|
+
* Ships with an in-memory store; bring-your-own store via the
|
|
10
|
+
* {@link RateLimitStore} interface for distributed deployments (Redis, etc).
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PluginRequestContext } from '../../core/http/plugin-middleware';
|
|
16
|
+
import type { FortressPlugin, MiddlewareDefinition, PluginContext } from '../../core/plugin';
|
|
17
|
+
import type { RateLimitStore } from './memory-store';
|
|
18
|
+
import { Errors } from '../../core/errors';
|
|
19
|
+
import { createMemoryStore } from './memory-store';
|
|
20
|
+
|
|
21
|
+
export type { RateLimitStore } from './memory-store';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A single rate-limit rule. At least one of `maxPerIp` / `maxPerUser` must be
|
|
25
|
+
* set; if neither is set the rule is a no-op. Keys are checked independently
|
|
26
|
+
* and the first to exceed short-circuits with a `rateLimited` error.
|
|
27
|
+
*/
|
|
28
|
+
export interface RateLimitRule {
|
|
29
|
+
/** Max requests per client IP in the window. */
|
|
30
|
+
maxPerIp?: number;
|
|
31
|
+
/** Max requests per authenticated user in the window. Ignored if the check has no userId. */
|
|
32
|
+
maxPerUser?: number;
|
|
33
|
+
/** Sliding window length in seconds. */
|
|
34
|
+
windowSeconds: number;
|
|
35
|
+
/** Optional key-namespace override. Defaults to the rule name. */
|
|
36
|
+
keyPrefix?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Rate-limit config block for login (per-account adds an email-scoped key). */
|
|
40
|
+
export interface LoginRateLimit {
|
|
41
|
+
maxPerIp?: number;
|
|
42
|
+
maxPerAccount?: number;
|
|
43
|
+
windowSeconds?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SimpleRateLimit {
|
|
47
|
+
maxPerIp?: number;
|
|
48
|
+
maxPerUser?: number;
|
|
49
|
+
windowSeconds?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Sentinel passed to the `login` / `register` blocks to opt out of the always-on defaults. */
|
|
53
|
+
export interface DisabledBlock {
|
|
54
|
+
disabled: true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RateLimitConfig {
|
|
58
|
+
/**
|
|
59
|
+
* Limit for `POST /auth/login`. **Always on with defaults** when the plugin
|
|
60
|
+
* is registered — pass `{ disabled: true }` to turn it off. This is a gate
|
|
61
|
+
* plugin; leaving the default protects against silent loss of auth DoS
|
|
62
|
+
* protection on upgrade.
|
|
63
|
+
*/
|
|
64
|
+
login?: LoginRateLimit | DisabledBlock;
|
|
65
|
+
/**
|
|
66
|
+
* Limit for `POST /auth/register`. Same default-on / explicit-disable
|
|
67
|
+
* behavior as `login` — both are security-critical gates.
|
|
68
|
+
*/
|
|
69
|
+
register?: { maxPerIp?: number; windowSeconds?: number } | DisabledBlock;
|
|
70
|
+
/** Limit for `POST /auth/refresh` (runs in `beforeTokenRefresh` hook). */
|
|
71
|
+
refresh?: SimpleRateLimit;
|
|
72
|
+
/** Limit for OAuth `POST /oauth/token` (IP-scoped; client_id lives in the form body). */
|
|
73
|
+
oauthToken?: { maxPerIp?: number; windowSeconds?: number };
|
|
74
|
+
/** Limit for API-key issuance `POST /api-key/keys` (requires authenticated user). */
|
|
75
|
+
apiKeyIssue?: SimpleRateLimit;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Named rules referenced by `methods.check(name, keys)` and the
|
|
79
|
+
* per-framework middleware wrappers. Use these for your own app routes
|
|
80
|
+
* or for Fortress endpoints that don't have a dedicated config block
|
|
81
|
+
* (magic-link, 2FA verify, email verification — these are methods-only,
|
|
82
|
+
* so wire the limiter in your own handler).
|
|
83
|
+
*/
|
|
84
|
+
rules?: Record<string, RateLimitRule>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extra path-based bindings for any route served by `fortress.handleRequest`.
|
|
88
|
+
* Each entry binds a named rule (or inline rule) to a path glob.
|
|
89
|
+
*/
|
|
90
|
+
paths?: PathBinding[];
|
|
91
|
+
|
|
92
|
+
/** Custom store for rate-limit counters. Default: in-memory sliding window. */
|
|
93
|
+
store?: RateLimitStore;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Declarative rate-limit binding for a Fortress-dispatched path. Only fires
|
|
98
|
+
* for routes that flow through `fortress.handleRequest`.
|
|
99
|
+
*
|
|
100
|
+
* **When to use `paths` vs the framework wrappers**:
|
|
101
|
+
* - Framework wrappers (`honoRateLimit` / `expressRateLimit` /
|
|
102
|
+
* `svelteKitRateLimit`) — use when you have a middleware layer around your
|
|
103
|
+
* routes. Mount on any path, Fortress-owned or user-owned.
|
|
104
|
+
* - `paths` — use in serverless / framework-less deployments that call
|
|
105
|
+
* `fortress.handleRequest` directly, or when you prefer declarative config
|
|
106
|
+
* co-located with the rest of the rate-limit setup.
|
|
107
|
+
*
|
|
108
|
+
* Both target the same store. **Don't stack both on the same path** — each
|
|
109
|
+
* match increments the counter, so double-wrapping halves the effective
|
|
110
|
+
* limit.
|
|
111
|
+
*/
|
|
112
|
+
export interface PathBinding {
|
|
113
|
+
/** Path glob (supports `*` and `:param`). */
|
|
114
|
+
match: string;
|
|
115
|
+
/** HTTP methods to restrict to (uppercase). Omit to match all methods. */
|
|
116
|
+
methods?: string[];
|
|
117
|
+
/** Pipeline phase. `after-auth` required when keying per user. Default: `before-auth`. */
|
|
118
|
+
position?: 'before-auth' | 'after-auth';
|
|
119
|
+
/** Named rule key in `config.rules`, or an inline rule. */
|
|
120
|
+
rule: string | RateLimitRule;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const DEFAULT_LOGIN = { maxPerIp: 10, maxPerAccount: 5, windowSeconds: 900 };
|
|
124
|
+
const DEFAULT_REGISTER = { maxPerIp: 3, windowSeconds: 3600 };
|
|
125
|
+
const DEFAULT_REFRESH = { maxPerIp: 60, maxPerUser: 60, windowSeconds: 60 };
|
|
126
|
+
const DEFAULT_OAUTH_TOKEN = { maxPerIp: 60, windowSeconds: 60 };
|
|
127
|
+
const DEFAULT_API_KEY_ISSUE = { maxPerIp: 10, maxPerUser: 10, windowSeconds: 3600 };
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalize an IPv6 address to its /64 prefix to prevent bypass via address rotation.
|
|
131
|
+
* IPv4 addresses are returned as-is.
|
|
132
|
+
*/
|
|
133
|
+
export function normalizeIp(ip: string | undefined | null): string {
|
|
134
|
+
if (!ip)
|
|
135
|
+
return 'unknown';
|
|
136
|
+
if (ip.startsWith('::ffff:'))
|
|
137
|
+
return ip.slice(7);
|
|
138
|
+
if (ip.includes(':'))
|
|
139
|
+
return ip.split(':').slice(0, 4).join(':');
|
|
140
|
+
return ip;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Canonical key for account-scoped login limits. */
|
|
144
|
+
export function normalizeAccountIdentifier(identifier: string): string {
|
|
145
|
+
return identifier.trim().normalize('NFC').toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Read an IP from a standard web Request's forwarding headers. */
|
|
149
|
+
function ipFromRequest(request: Request): string {
|
|
150
|
+
const xff = request.headers.get('x-forwarded-for');
|
|
151
|
+
if (xff)
|
|
152
|
+
return xff.split(',')[0].trim();
|
|
153
|
+
return request.headers.get('x-real-ip') ?? '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface CheckKeys {
|
|
157
|
+
ip?: string | null;
|
|
158
|
+
userId?: string | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function runRule(
|
|
162
|
+
store: RateLimitStore,
|
|
163
|
+
ruleName: string,
|
|
164
|
+
rule: RateLimitRule,
|
|
165
|
+
keys: CheckKeys,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
const windowMs = rule.windowSeconds * 1000;
|
|
168
|
+
const prefix = rule.keyPrefix ?? ruleName;
|
|
169
|
+
|
|
170
|
+
// IP-keyed check: normalize undefined → 'unknown' so missing client IP
|
|
171
|
+
// still increments (matches legacy behavior and guards against request
|
|
172
|
+
// smuggling via omitted headers). Skip only when `maxPerIp` is unset.
|
|
173
|
+
if (rule.maxPerIp != null) {
|
|
174
|
+
const ip = normalizeIp(keys.ip);
|
|
175
|
+
const res = await store.increment(`${prefix}:ip:${ip}`, windowMs);
|
|
176
|
+
if (res.count > rule.maxPerIp) {
|
|
177
|
+
const retryAfter = Math.max(Math.ceil((res.resetAt - Date.now()) / 1000), 1);
|
|
178
|
+
throw Errors.rateLimited(retryAfter);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// User-keyed check: only runs when a userId is actually present — no
|
|
183
|
+
// fallback identifier, as unauthenticated requests can't be rate-limited
|
|
184
|
+
// per account here.
|
|
185
|
+
if (rule.maxPerUser != null && keys.userId != null) {
|
|
186
|
+
const res = await store.increment(`${prefix}:user:${keys.userId}`, windowMs);
|
|
187
|
+
if (res.count > rule.maxPerUser) {
|
|
188
|
+
const retryAfter = Math.max(Math.ceil((res.resetAt - Date.now()) / 1000), 1);
|
|
189
|
+
throw Errors.rateLimited(retryAfter);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Rate limit plugin factory. Returns a {@link FortressPlugin} that enforces
|
|
196
|
+
* sliding-window limits on Fortress's sensitive auth endpoints and exposes a
|
|
197
|
+
* programmatic `check()` method for rate-limiting arbitrary user routes.
|
|
198
|
+
*
|
|
199
|
+
* @example Basic usage
|
|
200
|
+
* ```ts
|
|
201
|
+
* rateLimit({
|
|
202
|
+
* login: { maxPerIp: 10, maxPerAccount: 5, windowSeconds: 900 },
|
|
203
|
+
* refresh: { maxPerIp: 60, windowSeconds: 60 },
|
|
204
|
+
* rules: {
|
|
205
|
+
* api: { maxPerIp: 100, windowSeconds: 60 },
|
|
206
|
+
* },
|
|
207
|
+
* })
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @example Rate-limiting your own routes (Hono)
|
|
211
|
+
* ```ts
|
|
212
|
+
* import { honoRateLimit } from '@bajustone/fortress/plugins/rate-limit/hono';
|
|
213
|
+
* app.use('/api/*', honoRateLimit(fortress, 'api'));
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
/** True when the user passed `{ disabled: true }` to opt out of a gate block. */
|
|
217
|
+
function isDisabled(
|
|
218
|
+
block: LoginRateLimit | { maxPerIp?: number; windowSeconds?: number } | DisabledBlock | undefined,
|
|
219
|
+
): block is DisabledBlock {
|
|
220
|
+
return !!block && 'disabled' in block && block.disabled === true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function rateLimit(config: RateLimitConfig = {}): FortressPlugin {
|
|
224
|
+
const store = config.store ?? createMemoryStore();
|
|
225
|
+
|
|
226
|
+
// Gate blocks — always on with defaults; `{ disabled: true }` opts out.
|
|
227
|
+
const loginCfg = isDisabled(config.login)
|
|
228
|
+
? undefined
|
|
229
|
+
: { ...DEFAULT_LOGIN, ...(config.login as LoginRateLimit | undefined) };
|
|
230
|
+
const registerCfg = isDisabled(config.register)
|
|
231
|
+
? undefined
|
|
232
|
+
: { ...DEFAULT_REGISTER, ...(config.register as { maxPerIp?: number; windowSeconds?: number } | undefined) };
|
|
233
|
+
// Opt-in blocks — stay off unless the block is present.
|
|
234
|
+
const refreshCfg = config.refresh ? { ...DEFAULT_REFRESH, ...config.refresh } : undefined;
|
|
235
|
+
const oauthTokenCfg = config.oauthToken ? { ...DEFAULT_OAUTH_TOKEN, ...config.oauthToken } : undefined;
|
|
236
|
+
const apiKeyIssueCfg = config.apiKeyIssue ? { ...DEFAULT_API_KEY_ISSUE, ...config.apiKeyIssue } : undefined;
|
|
237
|
+
|
|
238
|
+
// Build the rule registry. User rules first, then built-in names (built-ins
|
|
239
|
+
// win on collision — prevents accidental override of the auth-endpoint limits).
|
|
240
|
+
const rules: Record<string, RateLimitRule> = { ...config.rules };
|
|
241
|
+
|
|
242
|
+
if (loginCfg) {
|
|
243
|
+
rules.login = { maxPerIp: loginCfg.maxPerIp, windowSeconds: loginCfg.windowSeconds };
|
|
244
|
+
// Per-account limit uses the login rule's window but a distinct key prefix.
|
|
245
|
+
if (loginCfg.maxPerAccount != null) {
|
|
246
|
+
rules['login:account'] = {
|
|
247
|
+
maxPerUser: loginCfg.maxPerAccount,
|
|
248
|
+
windowSeconds: loginCfg.windowSeconds,
|
|
249
|
+
keyPrefix: 'login:account',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (registerCfg)
|
|
254
|
+
rules.register = { maxPerIp: registerCfg.maxPerIp, windowSeconds: registerCfg.windowSeconds };
|
|
255
|
+
if (refreshCfg) {
|
|
256
|
+
rules.refresh = {
|
|
257
|
+
maxPerIp: refreshCfg.maxPerIp,
|
|
258
|
+
maxPerUser: refreshCfg.maxPerUser,
|
|
259
|
+
windowSeconds: refreshCfg.windowSeconds,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (oauthTokenCfg)
|
|
263
|
+
rules.oauthToken = { maxPerIp: oauthTokenCfg.maxPerIp, windowSeconds: oauthTokenCfg.windowSeconds };
|
|
264
|
+
if (apiKeyIssueCfg) {
|
|
265
|
+
rules.apiKeyIssue = {
|
|
266
|
+
maxPerIp: apiKeyIssueCfg.maxPerIp,
|
|
267
|
+
maxPerUser: apiKeyIssueCfg.maxPerUser,
|
|
268
|
+
windowSeconds: apiKeyIssueCfg.windowSeconds,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function check(ruleName: string, keys: CheckKeys): Promise<void> {
|
|
273
|
+
const rule = rules[ruleName];
|
|
274
|
+
if (!rule)
|
|
275
|
+
throw new Error(`rate-limit: unknown rule '${ruleName}' — declare it in config.rules or add the matching endpoint config block`);
|
|
276
|
+
await runRule(store, ruleName, rule, keys);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Build MiddlewareDefinition entries for each bound endpoint config block +
|
|
280
|
+
// user-provided `paths`. Each handler rate-checks then calls next().
|
|
281
|
+
const middleware: MiddlewareDefinition[] = [];
|
|
282
|
+
|
|
283
|
+
const bindBuiltin = (
|
|
284
|
+
ruleName: string,
|
|
285
|
+
match: string,
|
|
286
|
+
methodFilter: string[] | undefined,
|
|
287
|
+
position: 'before-auth' | 'after-auth',
|
|
288
|
+
): void => {
|
|
289
|
+
middleware.push({
|
|
290
|
+
path: match,
|
|
291
|
+
position,
|
|
292
|
+
handler: async (_ctx: PluginContext, request: PluginRequestContext, next: () => Promise<void>) => {
|
|
293
|
+
const req = request.request;
|
|
294
|
+
if (!(req instanceof Request))
|
|
295
|
+
throw Errors.badRequest('PluginRequestContext.request is required');
|
|
296
|
+
if (methodFilter && !methodFilter.includes(req.method)) {
|
|
297
|
+
await next();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const { fortressUserId: userId } = request;
|
|
301
|
+
await check(ruleName, { ip: ipFromRequest(req), userId });
|
|
302
|
+
await next();
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (oauthTokenCfg)
|
|
308
|
+
bindBuiltin('oauthToken', '/oauth/token', ['POST'], 'before-auth');
|
|
309
|
+
if (apiKeyIssueCfg)
|
|
310
|
+
bindBuiltin('apiKeyIssue', '/api-key/keys', ['POST'], 'after-auth');
|
|
311
|
+
|
|
312
|
+
for (const binding of config.paths ?? []) {
|
|
313
|
+
const inlineRule = typeof binding.rule === 'object' ? binding.rule : null;
|
|
314
|
+
const ruleName = typeof binding.rule === 'string' ? binding.rule : `path:${binding.match}`;
|
|
315
|
+
if (inlineRule)
|
|
316
|
+
rules[ruleName] = inlineRule;
|
|
317
|
+
else if (!rules[ruleName])
|
|
318
|
+
throw new Error(`rate-limit: unknown rule reference '${binding.rule}' in paths config`);
|
|
319
|
+
|
|
320
|
+
const position = binding.position ?? 'before-auth';
|
|
321
|
+
const methodFilter = binding.methods?.map(m => m.toUpperCase());
|
|
322
|
+
middleware.push({
|
|
323
|
+
path: binding.match,
|
|
324
|
+
position,
|
|
325
|
+
handler: async (_ctx: PluginContext, request: PluginRequestContext, next: () => Promise<void>) => {
|
|
326
|
+
const req = request.request;
|
|
327
|
+
if (!(req instanceof Request))
|
|
328
|
+
throw Errors.badRequest('PluginRequestContext.request is required');
|
|
329
|
+
if (methodFilter && !methodFilter.includes(req.method)) {
|
|
330
|
+
await next();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const { fortressUserId: userId } = request;
|
|
334
|
+
await check(ruleName, { ip: ipFromRequest(req), userId });
|
|
335
|
+
await next();
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
name: 'rate-limit',
|
|
342
|
+
|
|
343
|
+
...(middleware.length > 0 ? { middleware } : {}),
|
|
344
|
+
|
|
345
|
+
hooks: {
|
|
346
|
+
async beforeLogin(ctx) {
|
|
347
|
+
if (!loginCfg)
|
|
348
|
+
return;
|
|
349
|
+
const ip = ctx.meta?.ipAddress;
|
|
350
|
+
const email = ctx.email;
|
|
351
|
+
// Per-IP
|
|
352
|
+
if (loginCfg.maxPerIp != null)
|
|
353
|
+
await check('login', { ip });
|
|
354
|
+
// Per-account (identifier keyed as userId slot on the synthetic rule)
|
|
355
|
+
if (loginCfg.maxPerAccount != null)
|
|
356
|
+
await check('login:account', { userId: normalizeAccountIdentifier(email) });
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
async beforeRegister(ctx) {
|
|
360
|
+
if (!registerCfg)
|
|
361
|
+
return;
|
|
362
|
+
await check('register', { ip: ctx.meta?.ipAddress });
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
async beforeTokenRefresh(ctx) {
|
|
366
|
+
if (!refreshCfg)
|
|
367
|
+
return;
|
|
368
|
+
const ip = ctx.meta?.ipAddress;
|
|
369
|
+
// Best-effort per-user limit: the hook runs before the refresh token
|
|
370
|
+
// is verified, so userId isn't known yet. IP-only here; per-user is
|
|
371
|
+
// enforced via the `/auth/refresh`-aware middleware when you mount
|
|
372
|
+
// refresh under a user-scoped wrapper in your own code.
|
|
373
|
+
await check('refresh', { ip });
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
methods: (_ctx: PluginContext) => ({
|
|
378
|
+
check,
|
|
379
|
+
/** Read-only view of the resolved rule registry (debug / introspection). */
|
|
380
|
+
listRules: (): string[] => Object.keys(rules),
|
|
381
|
+
}),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface RateLimitStore {
|
|
2
|
+
/** Increment the counter for a key within a sliding window. Returns current count and reset time. */
|
|
3
|
+
increment: (key: string, windowMs: number) => Promise<{ count: number; resetAt: number }>;
|
|
4
|
+
/** Get the current counter for the most recently used window. */
|
|
5
|
+
get: (key: string) => Promise<{ count: number; resetAt: number } | null>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface WindowRecord {
|
|
9
|
+
/** Retained for the largest window ever used by this key. */
|
|
10
|
+
timestamps: number[];
|
|
11
|
+
maxWindowMs: number;
|
|
12
|
+
lastWindowMs: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** In-memory sliding-window store with per-key retention. */
|
|
16
|
+
export function createMemoryStore(): RateLimitStore {
|
|
17
|
+
const store = new Map<string, WindowRecord>();
|
|
18
|
+
|
|
19
|
+
const cleanupInterval = setInterval(() => {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
for (const [key, record] of store) {
|
|
22
|
+
const timestamps = record.timestamps.filter(timestamp => now - timestamp < record.maxWindowMs);
|
|
23
|
+
if (timestamps.length === 0)
|
|
24
|
+
store.delete(key);
|
|
25
|
+
else
|
|
26
|
+
store.set(key, { ...record, timestamps });
|
|
27
|
+
}
|
|
28
|
+
}, 60_000);
|
|
29
|
+
|
|
30
|
+
if (typeof cleanupInterval === 'object' && 'unref' in cleanupInterval)
|
|
31
|
+
(cleanupInterval as { unref: () => void }).unref();
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
async increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }> {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const existing = store.get(key);
|
|
37
|
+
const maxWindowMs = Math.max(existing?.maxWindowMs ?? 0, windowMs);
|
|
38
|
+
const retained = (existing?.timestamps ?? [])
|
|
39
|
+
.filter(timestamp => timestamp > now - maxWindowMs);
|
|
40
|
+
retained.push(now);
|
|
41
|
+
store.set(key, { timestamps: retained, maxWindowMs, lastWindowMs: windowMs });
|
|
42
|
+
|
|
43
|
+
const active = retained.filter(timestamp => timestamp > now - windowMs);
|
|
44
|
+
return { count: active.length, resetAt: active[0] + windowMs };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async get(key: string): Promise<{ count: number; resetAt: number } | null> {
|
|
48
|
+
const record = store.get(key);
|
|
49
|
+
if (!record)
|
|
50
|
+
return null;
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const active = record.timestamps.filter(timestamp => timestamp > now - record.lastWindowMs);
|
|
53
|
+
if (active.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
count: active.length,
|
|
57
|
+
resetAt: active[0] + record.lastWindowMs,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|