@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,394 @@
|
|
|
1
|
+
# Observability
|
|
2
|
+
|
|
3
|
+
Fortress has three layered observability surfaces. All three are zero-default — Fortress never writes to stderr, never imports `@opentelemetry/api`, and never allocates metric objects unless you explicitly opt in.
|
|
4
|
+
|
|
5
|
+
## 1. Pluggable Logger (`config.logger`)
|
|
6
|
+
|
|
7
|
+
The `FortressLogger` interface is structurally compatible with `pino`'s `BaseLogger` and Fastify's `FastifyBaseLogger`, so a `pino()` instance or `fastify.log` drops in directly with zero adapter code.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
export interface FortressLogger {
|
|
11
|
+
level: LogLevel | 'silent' | string;
|
|
12
|
+
fatal: LogFn;
|
|
13
|
+
error: LogFn;
|
|
14
|
+
warn: LogFn;
|
|
15
|
+
info: LogFn;
|
|
16
|
+
debug: LogFn;
|
|
17
|
+
trace: LogFn;
|
|
18
|
+
silent: LogFn;
|
|
19
|
+
child?: (bindings: Record<string, unknown>) => FortressLogger;
|
|
20
|
+
isLevelEnabled?: (level: LogLevel) => boolean;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Where `LogFn` accepts any of three shapes:
|
|
25
|
+
```typescript
|
|
26
|
+
logger.info('a string');
|
|
27
|
+
logger.info({ key: 'value' }, 'string with meta');
|
|
28
|
+
logger.info('format %s', 'arg');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Default: `SILENT_LOGGER`
|
|
32
|
+
|
|
33
|
+
If you don't pass `config.logger`, Fortress uses a no-op logger where every method is a shared arrow function. Zero allocation per call.
|
|
34
|
+
|
|
35
|
+
### Pino example
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import pino from 'pino';
|
|
39
|
+
import { createFortress } from '@bajustone/fortress';
|
|
40
|
+
|
|
41
|
+
const fortress = createFortress({
|
|
42
|
+
jwt: { key: process.env.JWT_SECRET! },
|
|
43
|
+
database: db,
|
|
44
|
+
logger: pino({ level: 'info' }),
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Fastify example
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import Fastify from 'fastify';
|
|
52
|
+
import { createFortress } from '@bajustone/fortress';
|
|
53
|
+
|
|
54
|
+
const app = Fastify({ logger: { level: 'info' } });
|
|
55
|
+
const fortress = createFortress({
|
|
56
|
+
jwt: { key: process.env.JWT_SECRET! },
|
|
57
|
+
database: db,
|
|
58
|
+
logger: app.log,
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Console wrapper
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const consoleLogger = {
|
|
66
|
+
level: 'info',
|
|
67
|
+
fatal: (...a) => console.error('[fatal]', ...a),
|
|
68
|
+
error: (...a) => console.error('[error]', ...a),
|
|
69
|
+
warn: (...a) => console.warn('[warn]', ...a),
|
|
70
|
+
info: (...a) => console.info('[info]', ...a),
|
|
71
|
+
debug: (...a) => console.debug('[debug]', ...a),
|
|
72
|
+
trace: (...a) => console.debug('[trace]', ...a),
|
|
73
|
+
silent: () => {},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const fortress = createFortress({
|
|
77
|
+
jwt: { key: process.env.JWT_SECRET! },
|
|
78
|
+
database: db,
|
|
79
|
+
logger: consoleLogger,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### What Fortress logs
|
|
84
|
+
|
|
85
|
+
- **Plugin token-claim overwrite** (dev only, `warn` level) — when one plugin's `enrichTokenClaims` overwrites a claim set by an earlier plugin.
|
|
86
|
+
- **Refresh token fingerprint mismatch** (`warn` level) — when `config.jwt.validateRefreshFingerprint === 'warn'` detects a UA change on refresh without rejecting.
|
|
87
|
+
- **Unhandled errors in `fortress.handleRequest`** (`error` level) — anything that reaches the outer catch that isn't a `FortressError`.
|
|
88
|
+
- **Unhandled errors in the Express error handler** (`error` level) — same, for the Express adapter.
|
|
89
|
+
- **Observer failures** (`error` level) — when an auth/IAM/permission-check listener throws or rejects.
|
|
90
|
+
|
|
91
|
+
Fortress does **not** call `logger.debug` or `logger.info` on hot paths like `checkPermission`. Hot-path observability goes through metrics and the sync `PermissionCheckEvent` observer, both of which are bounded and allocation-free when nobody subscribes. If you want per-check logs, attach a permission-check observer and call your logger explicitly — you'll pay the cost you opt into.
|
|
92
|
+
|
|
93
|
+
## 2. Observer Lists
|
|
94
|
+
|
|
95
|
+
Three independent listener lists on the `Fortress` instance. Each `add…Observer` call returns an `() => void` unsubscribe function. Listener exceptions are routed to `logger.error` and the remaining listeners continue.
|
|
96
|
+
|
|
97
|
+
### `auth.addAuthObserver`
|
|
98
|
+
|
|
99
|
+
Async, cold-path. Fires on every auth lifecycle event.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import type { AuthEvent } from '@bajustone/fortress';
|
|
103
|
+
|
|
104
|
+
const off = fortress.auth.addAuthObserver(async (event: AuthEvent) => {
|
|
105
|
+
switch (event.eventType) {
|
|
106
|
+
case 'LOGIN_SUCCESS':
|
|
107
|
+
// actor, identifier, ipAddress, userAgent available
|
|
108
|
+
break;
|
|
109
|
+
case 'LOGIN_FAILURE':
|
|
110
|
+
// identifier + error.code + error.message available
|
|
111
|
+
break;
|
|
112
|
+
case 'TOKEN_REUSE_DETECTED':
|
|
113
|
+
// metadata.tokenFamily available; send security alert
|
|
114
|
+
break;
|
|
115
|
+
// LOGOUT | REGISTER | TOKEN_REFRESH | TOKEN_FINGERPRINT_MISMATCH
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Later...
|
|
120
|
+
off(); // unsubscribe
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `iam.addIamObserver`
|
|
124
|
+
|
|
125
|
+
Async, cold-path. Fires on IAM mutation events (`ROLE_CREATED`, `ROLE_BOUND`, `PERMISSION_CHANGED`, `GROUP_MEMBER_ADDED`, etc.). Used by the audit-log plugin and the api-key plugin for cascade deletes.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
fortress.iam.addIamObserver(async (event) => {
|
|
129
|
+
if (event.eventType === 'ROLE_BOUND') {
|
|
130
|
+
await notifySlack(`Role ${event.targetId} bound to subject ${event.metadata?.subjectId}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `iam.addPermissionCheckObserver`
|
|
136
|
+
|
|
137
|
+
**Synchronous, hot-path.** Fires on every `checkPermission` call with latency and cache-hit information.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
fortress.iam.addPermissionCheckObserver((event) => {
|
|
141
|
+
// This runs on the hot path — keep it bounded.
|
|
142
|
+
if (!event.allowed) {
|
|
143
|
+
metrics.increment('auth.denies', { resource: event.resource });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Why synchronous?** The listener signature returns `void`, not `Promise<void>`, to make it impossible to accidentally await expensive work inline. If you need async work, fire-and-forget: `void asyncWork()`.
|
|
149
|
+
|
|
150
|
+
**Why separate from `addIamObserver`?** Permission checks fire thousands of times per minute in a busy app. Mixing them with mutation events would spam the audit-log plugin.
|
|
151
|
+
|
|
152
|
+
### Error-routing gotcha (async observers only)
|
|
153
|
+
|
|
154
|
+
The auth and IAM observer lists route listener failures to `logger.error` so one bad listener can't break the caller. But the safety net only engages when the listener **returns** the promise. Consider the two shapes:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// ✅ Routed — the listener returns the promise, the list attaches .catch
|
|
158
|
+
fortress.auth.addAuthObserver(async (event) => {
|
|
159
|
+
await siem.log(event); // a rejection here ends up at logger.error
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ✅ Routed — equivalent; the async function returns an implicit promise
|
|
163
|
+
fortress.auth.addAuthObserver(event => siem.log(event));
|
|
164
|
+
|
|
165
|
+
// ❌ Not routed — the listener returns `undefined`, the list has no handle
|
|
166
|
+
// on the inflight promise, and a rejection escapes to the runtime's
|
|
167
|
+
// unhandled-rejection handler.
|
|
168
|
+
fortress.auth.addAuthObserver((event) => {
|
|
169
|
+
void siem.log(event);
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`void asyncWork()` is an explicit opt-out of the safety net. It's sometimes what you want — you accept the risk because you don't want the listener to wait on a cross-network call — but if you want rejections surfaced in your logger, return the promise. Fortress does not install a process-level `unhandledRejection` handler on your behalf.
|
|
174
|
+
|
|
175
|
+
The permission-check observer is synchronous (returns `void`, not `Promise<void>`), so this gotcha doesn't apply to it — there's no async work to lose track of.
|
|
176
|
+
|
|
177
|
+
## 3. OpenTelemetry Adapter (opt-in)
|
|
178
|
+
|
|
179
|
+
The `@bajustone/fortress/otel` sub-path ships a single factory: `createOtelTelemetry()`. It's the **only file in the repository that imports `@opentelemetry/api`**, and the import is dynamic (`await import('@opentelemetry/api')`). Runtimes that don't import the `/otel` sub-path never resolve the peer dep.
|
|
180
|
+
|
|
181
|
+
### Install
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
bun add @opentelemetry/api
|
|
185
|
+
# Plus the SDK you want (this example uses Node SDK with OTLP):
|
|
186
|
+
bun add @opentelemetry/sdk-node \
|
|
187
|
+
@opentelemetry/exporter-trace-otlp-http \
|
|
188
|
+
@opentelemetry/exporter-metrics-otlp-http
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`@opentelemetry/api` is declared as an **optional peer dependency** on the Fortress `package.json`, so it's not installed automatically — you explicitly add it when you opt in.
|
|
192
|
+
|
|
193
|
+
### Wire it up
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { createFortress } from '@bajustone/fortress';
|
|
197
|
+
import { createOtelTelemetry } from '@bajustone/fortress/otel';
|
|
198
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
199
|
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
200
|
+
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
201
|
+
|
|
202
|
+
// 1. Start an OTel SDK (registers global Tracer/Meter providers).
|
|
203
|
+
const sdk = new NodeSDK({
|
|
204
|
+
metricReader: new PeriodicExportingMetricReader({
|
|
205
|
+
exporter: new OTLPMetricExporter({ url: process.env.OTEL_ENDPOINT! }),
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
sdk.start();
|
|
209
|
+
|
|
210
|
+
// 2. Wire the Fortress adapter.
|
|
211
|
+
const observability = await createOtelTelemetry({ name: 'my-app-auth' });
|
|
212
|
+
const fortress = createFortress({
|
|
213
|
+
jwt: { key: process.env.JWT_SECRET! },
|
|
214
|
+
database: db,
|
|
215
|
+
observability,
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Metric catalog
|
|
220
|
+
|
|
221
|
+
| Metric | Type | Unit | Attributes |
|
|
222
|
+
|---|---|---|---|
|
|
223
|
+
| `fortress.auth.events.total` | counter | — | `event`, `outcome`, `method` |
|
|
224
|
+
| `fortress.iam.events.total` | counter | — | `event` |
|
|
225
|
+
| `fortress.iam.permission_check.duration` | histogram | s | `subject_type`, `result`, `cached` |
|
|
226
|
+
| `fortress.iam.permission_check.cache.hits` | counter | — | — |
|
|
227
|
+
| `fortress.iam.permission_check.cache.misses` | counter | — | — |
|
|
228
|
+
| `db.client.operation.duration` | histogram | s | `db.system.name`, `db.operation.name`, `db.collection.name` |
|
|
229
|
+
|
|
230
|
+
**Histogram bucket boundaries:**
|
|
231
|
+
|
|
232
|
+
- `fortress.iam.permission_check.duration`: `[0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1]`
|
|
233
|
+
- `db.client.operation.duration`: `[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10]` (OTel advisory default for DB clients)
|
|
234
|
+
|
|
235
|
+
### Span catalog
|
|
236
|
+
|
|
237
|
+
- `fortress.iam.permission_check.deny` — fired only on denied permission checks. Attributes: `subject.type`, `subject.id`, `resource`, `action`, `cached`. Allowed checks are metric-only — no span — to keep the hot path cheap.
|
|
238
|
+
|
|
239
|
+
### Cardinality rules (important)
|
|
240
|
+
|
|
241
|
+
Fortress **never** places user IDs, emails, tenant IDs, session IDs, or raw resource IDs on metric attributes. Those would cause cardinality explosions — every monitoring vendor charges you for high-cardinality metrics, and Prometheus will eat itself.
|
|
242
|
+
|
|
243
|
+
Allowed metric attributes: `result`, `method`, `outcome`, `event`, `subject_type`, `cached`, and the three standard `db.*` semconv attributes. Everything else goes on spans (pivotable per trace) or logs (grep-able).
|
|
244
|
+
|
|
245
|
+
### What Fortress does **not** emit
|
|
246
|
+
|
|
247
|
+
- **`http.server.request.duration`** — that's the host framework's job via `@opentelemetry/instrumentation-http`, `@opentelemetry/instrumentation-hono`, `@opentelemetry/instrumentation-express`, etc. Emitting it here would double-count.
|
|
248
|
+
- **Structured logs via `@opentelemetry/api-logs`** — that package is explicitly not for library authors and is alpha. Fortress uses its own `FortressLogger` contract, which is pino-compatible.
|
|
249
|
+
|
|
250
|
+
## Provider contract (advanced)
|
|
251
|
+
|
|
252
|
+
If you want to plug a non-OTel metric backend (custom Prometheus client, DataDog StatsD client, etc.), you can implement the `TelemetryProvider` interface directly instead of using the `/otel` adapter:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import type { TelemetryProvider } from '@bajustone/fortress';
|
|
256
|
+
|
|
257
|
+
const customTelemetry: TelemetryProvider = {
|
|
258
|
+
tracer: {
|
|
259
|
+
startSpan: (name, attrs) => makeSpan(name, attrs),
|
|
260
|
+
startActiveSpan: async (name, attrs, callback) => {
|
|
261
|
+
// Your backend must keep this span active across the async callback so
|
|
262
|
+
// nested database/IAM spans inherit it.
|
|
263
|
+
return myBackend.withActiveSpan(makeSpan(name, attrs), callback);
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
meter: {
|
|
267
|
+
createCounter: (name, opts) => ({ add: (v, a) => myBackend.counter(name).add(v, a) }),
|
|
268
|
+
createHistogram: (name, opts) => ({ record: (v, a) => myBackend.histogram(name).observe(v, a) }),
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
createFortress({ /* ... */ observability: customTelemetry });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Optional `startActiveSpan` is part of the frozen provider contract and is used at the request boundary for async parent propagation. Existing providers that implement only `startSpan` remain valid (Fortress falls back to it, without parent propagation). This is the escape hatch—most users should stick with `createOtelTelemetry()` because the OpenTelemetry SDK ecosystem already has exporters for every major backend.
|
|
276
|
+
|
|
277
|
+
## 4. Event catalog
|
|
278
|
+
|
|
279
|
+
Fortress emits structured events through three observer surfaces:
|
|
280
|
+
|
|
281
|
+
- `fortress.auth.addAuthObserver(listener)` — user-lifecycle and token events.
|
|
282
|
+
- `fortress.iam.addIamObserver(listener)` — IAM mutation events.
|
|
283
|
+
- `fortress.iam.addPermissionCheckObserver(listener)` — every allow/deny check.
|
|
284
|
+
|
|
285
|
+
The `audit-log` plugin subscribes to `addIamObserver` automatically so IAM events flow into the audit-log table without extra wiring. Wire your own `addAuthObserver` if you want auth events in the same log.
|
|
286
|
+
|
|
287
|
+
### Auth events (`AuthEvent.eventType`)
|
|
288
|
+
|
|
289
|
+
| Event | Outcome | Security weight | Recommended alert |
|
|
290
|
+
|---|---|---|---|
|
|
291
|
+
| `LOGIN_SUCCESS` | `success` | low | none |
|
|
292
|
+
| `LOGIN_FAILURE` | `invalid_credentials` \| `account_locked` \| `account_inactive` \| `2fa_required` \| ... | medium | spike alert (>N per window per IP) |
|
|
293
|
+
| `REGISTER` | `success` | low | spike alert (>N per window per IP) |
|
|
294
|
+
| `LOGOUT` | `success` | low | none |
|
|
295
|
+
| `TOKEN_REFRESH` | `success` \| `invalid` \| `expired` | low | none |
|
|
296
|
+
| `TOKEN_REUSE_DETECTED` | `success` (revocation succeeded) | **high** | page on-call — indicates stolen refresh token |
|
|
297
|
+
| `TOKEN_FINGERPRINT_MISMATCH` | `success` (revocation succeeded) | **high** | page on-call — indicates session hijack |
|
|
298
|
+
| `IMPERSONATE` | `success` | medium | log to immutable trail |
|
|
299
|
+
|
|
300
|
+
### IAM events (`IamEvent.eventType`)
|
|
301
|
+
|
|
302
|
+
| Event | Where | Recommended alert |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| `ROLE_CREATED` / `ROLE_DELETED` / `ROLE_UPDATED` | `fortress.iam.createRole / deleteRole / updateRole` | log only |
|
|
305
|
+
| `ROLE_PERMISSION_ADDED` / `ROLE_PERMISSION_REMOVED` | role-permission diff | log only |
|
|
306
|
+
| `ROLE_BOUND` / `ROLE_UNBOUND` | role-binding mutations | log only |
|
|
307
|
+
| `PERMISSION_CREATED` / `PERMISSION_DELETED` | permission catalog mutations | log only |
|
|
308
|
+
| `PERMISSION_CHANGED` | direct-permission binding mutations | log only |
|
|
309
|
+
| `GROUP_CREATED` / `GROUP_UPDATED` / `GROUP_DELETED` | group lifecycle mutations | log only |
|
|
310
|
+
| `GROUP_MEMBER_ADDED` / `GROUP_MEMBER_REMOVED` | group membership mutations | log only |
|
|
311
|
+
| `SERVICE_ACCOUNT_CREATED` / `SERVICE_ACCOUNT_UPDATED` / `SERVICE_ACCOUNT_DELETED` | service-account mutations | log only |
|
|
312
|
+
|
|
313
|
+
All IAM mutation events carry `targetType`, `targetId`, and a `metadata` object suitable for audit-log persistence.
|
|
314
|
+
|
|
315
|
+
### Permission-check events (`PermissionCheckEvent`)
|
|
316
|
+
|
|
317
|
+
| Field | Description |
|
|
318
|
+
|---|---|
|
|
319
|
+
| `subjectType` | `'USER'` \| `'GROUP'` \| `'SERVICE_ACCOUNT'` |
|
|
320
|
+
| `subjectId` | string id (kept off metric attributes by Fortress — stays on the in-process event) |
|
|
321
|
+
| `resource`, `action` | what was checked |
|
|
322
|
+
| `allowed` | final decision |
|
|
323
|
+
| `cached` | whether the result was served from the permission cache |
|
|
324
|
+
| `durationSeconds` | check latency |
|
|
325
|
+
|
|
326
|
+
The handler is **synchronous and hot-path**. Keep it bounded — update a counter, push to an in-memory channel, never await a network call.
|
|
327
|
+
|
|
328
|
+
## 5. Recommended dashboards and alerts
|
|
329
|
+
|
|
330
|
+
### Auth dashboard
|
|
331
|
+
|
|
332
|
+
- **Login success rate** = `rate(fortress.auth.events.total{event="LOGIN_SUCCESS"}[5m]) / rate(fortress.auth.events.total{event=~"LOGIN_.*"}[5m])`.
|
|
333
|
+
- **Failure breakdown** by `outcome` attribute. A sudden spike in `invalid_credentials` from one IP range is credential-stuffing.
|
|
334
|
+
- **Refresh churn** = `rate(fortress.auth.events.total{event="TOKEN_REFRESH",outcome="success"}[5m])`. Higher than expected (multiple per minute per user) indicates a misbehaving client.
|
|
335
|
+
- **Token reuse** = `increase(fortress.auth.events.total{event="TOKEN_REUSE_DETECTED"}[1h])`. Any non-zero value is a paging incident — a refresh token was used after rotation.
|
|
336
|
+
- **Impersonation** = `increase(fortress.auth.events.total{event="IMPERSONATE"}[1d])`. Audit weekly.
|
|
337
|
+
|
|
338
|
+
### IAM dashboard
|
|
339
|
+
|
|
340
|
+
- **Permission check rate**, separated by `result` (`allow` vs `deny`). A deny ratio over ~5% sustained indicates either a misconfig or active probing.
|
|
341
|
+
- **Cache hit ratio** = `rate(fortress.iam.permission_check.cache.hits[5m]) / (rate(fortress.iam.permission_check.cache.hits[5m]) + rate(fortress.iam.permission_check.cache.misses[5m]))`. Healthy services see >90% after warm-up; a drop indicates churn (frequent role changes, cache invalidations).
|
|
342
|
+
- **p99 check latency** = `histogram_quantile(0.99, sum by (le)(rate(fortress.iam.permission_check.duration_bucket[5m])))`. Target <10ms with cache, <100ms cache-miss.
|
|
343
|
+
|
|
344
|
+
### OAuth dashboard (if mounted)
|
|
345
|
+
|
|
346
|
+
- **Token-endpoint error rate** by `error` label from the response body shape; spike alert on `invalid_client`, `invalid_grant`.
|
|
347
|
+
- **PKCE rejections** = audit-log `OAUTH_PKCE_REQUIRED` rows (if you mirror them into a counter via an `addAuthObserver`).
|
|
348
|
+
- **Refresh-token family revocations** = `TOKEN_REUSE_DETECTED` events scoped to OAuth refresh tokens.
|
|
349
|
+
|
|
350
|
+
### Database dashboard
|
|
351
|
+
|
|
352
|
+
- **p99 query latency** = `histogram_quantile(0.99, sum by (le, db_operation_name)(rate(db_client_operation_duration_bucket[5m])))`. Triage slow queries by `db.operation.name` attribute.
|
|
353
|
+
- **Operation rate** by `db.collection.name`. Useful when adding indexes or tuning the IAM cache.
|
|
354
|
+
|
|
355
|
+
### Suggested alerts (production)
|
|
356
|
+
|
|
357
|
+
| Alert | Expression / rule | Severity |
|
|
358
|
+
|---|---|---|
|
|
359
|
+
| Token-reuse detected | any `TOKEN_REUSE_DETECTED` event in 5m | **page** |
|
|
360
|
+
| Token-fingerprint mismatch | any `TOKEN_FINGERPRINT_MISMATCH` event in 5m | **page** |
|
|
361
|
+
| Login failure surge (per IP) | >100 failures from one IP in 10m | warn |
|
|
362
|
+
| Permission-check deny ratio | `deny / total > 0.20` over 15m | warn |
|
|
363
|
+
| Permission-check p99 latency | `p99 > 100ms` over 15m | warn |
|
|
364
|
+
| Audit-log chain broken | `verifyChain().ok === false` from a scheduled job | **page** |
|
|
365
|
+
| OAuth `invalid_client` surge | `>20/m` over 10m | warn |
|
|
366
|
+
| Schema-version drift | `fortress migrate:check` non-zero exit in deploy preflight | **page** |
|
|
367
|
+
|
|
368
|
+
All thresholds are starting points — tune to your traffic shape after a baseline week.
|
|
369
|
+
|
|
370
|
+
## 6. Audit-log integration
|
|
371
|
+
|
|
372
|
+
The `audit-log` plugin persists every `IamEvent` to `fortress_audit_log` automatically and can be configured to hash-chain entries (`auditLog({ hashChain: true })`) so deletions are detectable. Recommended pattern:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { auditLog } from '@bajustone/fortress/plugins/audit-log';
|
|
376
|
+
|
|
377
|
+
createFortress({
|
|
378
|
+
// ...
|
|
379
|
+
plugins: [auditLog({ hashChain: true })],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Mirror auth events into the same log:
|
|
383
|
+
fortress.auth.addAuthObserver((event) => {
|
|
384
|
+
fortress.plugins['audit-log'].logCustomEvent(event).catch((err) => {
|
|
385
|
+
fortress.logger.error({ err, event }, 'failed to persist auth event');
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Schedule a verifier (e.g. nightly cron):
|
|
390
|
+
const result = await fortress.plugins['audit-log'].verifyChain();
|
|
391
|
+
if (!result.ok) {
|
|
392
|
+
// Page on-call — someone tampered with the audit table.
|
|
393
|
+
}
|
|
394
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Account Lockout Plugin
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `account-lockout` plugin adds progressive account lockout to Fortress. After a configurable number of failed login attempts, the account is temporarily locked. With escalation enabled, each successive lockout doubles the duration up to a configurable cap.
|
|
6
|
+
|
|
7
|
+
Lockout sequence with defaults:
|
|
8
|
+
- 1st lockout: 15 minutes
|
|
9
|
+
- 2nd lockout: 30 minutes
|
|
10
|
+
- 3rd lockout: 60 minutes (capped at `maxLockoutSeconds`)
|
|
11
|
+
|
|
12
|
+
On successful login the failed attempt counter resets to zero.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Import the `accountLockout` factory and pass it in the `plugins` array when creating a Fortress instance:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createFortress } from '@bajustone/fortress';
|
|
20
|
+
import { accountLockout } from '@bajustone/fortress/plugins/account-lockout';
|
|
21
|
+
|
|
22
|
+
const fortress = createFortress({
|
|
23
|
+
jwt: { key: 'your-secret-at-least-32-bytes!!' },
|
|
24
|
+
database: adapter,
|
|
25
|
+
plugins: [
|
|
26
|
+
accountLockout({
|
|
27
|
+
maxFailedAttempts: 5,
|
|
28
|
+
lockoutDurationSeconds: 900,
|
|
29
|
+
escalation: true,
|
|
30
|
+
maxLockoutSeconds: 3600,
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Once registered, methods are available at `fortress.plugins['account-lockout']` with full type safety.
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
All fields on `AccountLockoutConfig` are optional:
|
|
41
|
+
|
|
42
|
+
| Option | Type | Default | Description |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
| `maxFailedAttempts` | `number` | `5` | Number of consecutive failed login attempts before the account is locked. |
|
|
45
|
+
| `lockoutDurationSeconds` | `number` | `900` (15 min) | Duration of the first lockout in seconds. |
|
|
46
|
+
| `escalation` | `boolean` | `true` | When `true`, each successive lockout doubles the duration (exponential backoff). |
|
|
47
|
+
| `maxLockoutSeconds` | `number` | `3600` (1 hour) | Maximum lockout duration in seconds. Escalation never exceeds this cap. |
|
|
48
|
+
|
|
49
|
+
## How It Works
|
|
50
|
+
|
|
51
|
+
The plugin uses three lifecycle hooks -- no manual calls are needed for the lockout logic itself:
|
|
52
|
+
|
|
53
|
+
1. **`beforeLogin`** -- Checks if the identifier (email) is currently locked. If `lockedUntil` is in the future, a `FortressError` with code `UNAUTHORIZED` and message "Account temporarily locked. Try again later." is thrown.
|
|
54
|
+
2. **`onLoginFailure`** -- Increments the failed attempt counter. When the counter reaches `maxFailedAttempts`, the account is locked for the calculated duration and `lockoutCount` is incremented.
|
|
55
|
+
3. **`afterLogin`** -- Resets the failed attempt counter and clears the lock on successful login.
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
| Method | Signature | Returns |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `getLockoutStatus` | `(identifier: string)` | `Promise<LockoutStatus>` |
|
|
62
|
+
| `resetLockout` | `(identifier: string)` | `Promise<void>` |
|
|
63
|
+
|
|
64
|
+
### getLockoutStatus
|
|
65
|
+
|
|
66
|
+
Returns the current lockout status for an identifier (typically the user's email):
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const status = await fortress.plugins['account-lockout'].getLockoutStatus('alice@example.com');
|
|
70
|
+
|
|
71
|
+
console.log(status.isLocked); // boolean
|
|
72
|
+
console.log(status.failedAttempts); // number
|
|
73
|
+
console.log(status.lockoutCount); // number
|
|
74
|
+
console.log(status.lockedUntil); // Date | null
|
|
75
|
+
console.log(status.lastFailedAt); // Date | null
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `LockoutStatus` type:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
interface LockoutStatus {
|
|
82
|
+
identifier: string;
|
|
83
|
+
failedAttempts: number;
|
|
84
|
+
lockoutCount: number;
|
|
85
|
+
lockedUntil: Date | null;
|
|
86
|
+
lastFailedAt: Date | null;
|
|
87
|
+
isLocked: boolean;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If no lockout record exists for the identifier, all counters are `0` and `isLocked` is `false`.
|
|
92
|
+
|
|
93
|
+
### resetLockout
|
|
94
|
+
|
|
95
|
+
Manually resets all lockout state for an identifier. Useful for admin-initiated unlocks:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
await fortress.plugins['account-lockout'].resetLockout('alice@example.com');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This clears `failedAttempts`, `lockedUntil`, `lockoutCount`, and `lastFailedAt`.
|
|
102
|
+
|
|
103
|
+
## Example
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { createFortress } from '@bajustone/fortress';
|
|
107
|
+
import { accountLockout } from '@bajustone/fortress/plugins/account-lockout';
|
|
108
|
+
|
|
109
|
+
const fortress = createFortress({
|
|
110
|
+
jwt: { key: 'your-secret-at-least-32-bytes!!' },
|
|
111
|
+
database: adapter,
|
|
112
|
+
plugins: [
|
|
113
|
+
accountLockout({
|
|
114
|
+
maxFailedAttempts: 3,
|
|
115
|
+
lockoutDurationSeconds: 60,
|
|
116
|
+
escalation: true,
|
|
117
|
+
maxLockoutSeconds: 600,
|
|
118
|
+
}),
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// After 3 failed logins, the account is locked for 60 seconds.
|
|
123
|
+
// If the user fails again after unlock, the next lockout is 120 seconds.
|
|
124
|
+
// The maximum lockout is capped at 600 seconds (10 minutes).
|
|
125
|
+
|
|
126
|
+
// Admin can check status or manually unlock:
|
|
127
|
+
const status = await fortress.plugins['account-lockout'].getLockoutStatus('alice@example.com');
|
|
128
|
+
if (status.isLocked) {
|
|
129
|
+
await fortress.plugins['account-lockout'].resetLockout('alice@example.com');
|
|
130
|
+
}
|
|
131
|
+
```
|