@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,327 @@
|
|
|
1
|
+
# API Key Plugin
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `api-key` plugin adds scoped API key management to Fortress. It is designed for service accounts, CI/CD pipelines, mobile devices, and any scenario where long-lived bearer tokens are preferable to short-lived JWTs.
|
|
6
|
+
|
|
7
|
+
Keys are generated with a configurable prefix, hashed with SHA-256 before storage, and can be scoped to limit what actions a key is allowed to perform.
|
|
8
|
+
|
|
9
|
+
**Polymorphic ownership.** Keys are owned by a `Subject` — either a `USER` or a `SERVICE_ACCOUNT`. The api-key schema stores `(subject_type, subject_id)` instead of a hard FK to `users.id`, mirroring `role_binding` and `direct_permission_binding`. Authentication flows support both subject types transparently.
|
|
10
|
+
|
|
11
|
+
**Automatic request principal resolution.** The plugin implements Fortress's `resolvePrincipal` capability: when a request arrives with an `Authorization: ApiKey <key>` or `X-API-Key: <key>` header, the plugin resolves the key to its owning subject and that subject becomes the request principal for RBAC. No middleware setup required — just register the plugin.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Import the `apiKey` factory and pass it in the `plugins` array when creating a Fortress instance:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createFortress } from '@bajustone/fortress';
|
|
19
|
+
import { apiKey } from '@bajustone/fortress/plugins/api-key';
|
|
20
|
+
|
|
21
|
+
const fortress = createFortress({
|
|
22
|
+
jwt: { key: 'your-secret-at-least-32-bytes!!' },
|
|
23
|
+
database: adapter,
|
|
24
|
+
plugins: [
|
|
25
|
+
apiKey({
|
|
26
|
+
prefix: 'myapp',
|
|
27
|
+
defaultExpirySeconds: 90 * 24 * 60 * 60, // 90 days
|
|
28
|
+
maxKeysPerSubject: 5,
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Once registered, methods are available at `fortress.plugins['api-key']` with full type safety.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
All fields on `ApiKeyConfig` are optional:
|
|
39
|
+
|
|
40
|
+
| Option | Type | Default | Description |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `prefix` | `string` | `'fortress'` | Prefix prepended to generated keys. Keys are formatted as `{prefix}_sk_{hex}`. |
|
|
43
|
+
| `defaultExpirySeconds` | `number \| null` | `null` | Default time-to-live for new keys, in seconds. `null` means keys never expire unless an explicit `expiresAt` is provided at creation time. |
|
|
44
|
+
| `maxKeysPerSubject` | `number` | `10` | Maximum number of active (non-revoked) keys a single subject (user or service account) can hold. Revoked keys do not count toward this limit. |
|
|
45
|
+
| `routes` | `boolean` | `false` | Mount self-service HTTP routes under `/api-key/keys/*`. The programmatic methods on `fortress.plugins['api-key']` are always available regardless of this flag. |
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Creating a key
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// For a user
|
|
53
|
+
const { key, id } = await fortress.plugins['api-key'].createKey({
|
|
54
|
+
subject: { type: 'USER', id: userId },
|
|
55
|
+
name: 'CI deploy token',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// For a service account (see IAM docs for createServiceAccount)
|
|
59
|
+
const { key: saKey } = await fortress.plugins['api-key'].createKey({
|
|
60
|
+
subject: { type: 'SERVICE_ACCOUNT', id: serviceAccountId },
|
|
61
|
+
name: 'ci-deploy-key',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// key = "myapp_sk_a1b2c3d4..." (the raw secret -- only returned once)
|
|
65
|
+
// id = 42 (database record ID)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Store or display the raw `key` immediately. Fortress stores only the SHA-256 hash; the plaintext cannot be retrieved later.
|
|
69
|
+
|
|
70
|
+
You can set an explicit expiry:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const { key } = await fortress.plugins['api-key'].createKey({
|
|
74
|
+
subject: { type: 'USER', id: userId },
|
|
75
|
+
name: 'Temp key',
|
|
76
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If neither `expiresAt` nor `defaultExpirySeconds` is set, the key never expires.
|
|
81
|
+
|
|
82
|
+
### Listing keys
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const keys = await fortress.plugins['api-key'].listKeys({
|
|
86
|
+
subject: { type: 'USER', id: userId },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const k of keys) {
|
|
90
|
+
console.log(k.id, k.name, k.keyPrefix, k.scopes, k.expiresAt, k.lastUsedAt);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Returns only active (non-revoked) keys scoped to the given subject. Each entry includes a short `keyPrefix` (first 12 characters) for identification but never exposes the full key or its hash.
|
|
95
|
+
|
|
96
|
+
### Revoking a key
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await fortress.plugins['api-key'].revokeKey({
|
|
100
|
+
subject: { type: 'USER', id: userId },
|
|
101
|
+
id: keyId,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Marks the key as revoked. Revoked keys cannot be resolved and do not count toward the per-subject limit. The `subject` parameter is checked against the key's owner — attempting to revoke a key that belongs to a different subject throws a `NotFound` error.
|
|
106
|
+
|
|
107
|
+
### Rotating a key
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const { key: newKey, id: newId } = await fortress.plugins['api-key'].rotateKey({
|
|
111
|
+
subject: { type: 'USER', id: userId },
|
|
112
|
+
id: keyId,
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Rotation is a single atomic operation that:
|
|
117
|
+
|
|
118
|
+
1. Revokes the existing key.
|
|
119
|
+
2. Creates a new key with the same `name`, `scopes`, and `expiresAt` as the original.
|
|
120
|
+
|
|
121
|
+
The old key stops working immediately. The new raw key is returned and must be stored by the caller.
|
|
122
|
+
|
|
123
|
+
### Resolving a key for authentication
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const result = await fortress.plugins['api-key'].resolveKey(rawKey);
|
|
127
|
+
|
|
128
|
+
if (!result) {
|
|
129
|
+
// Key is invalid, revoked, expired, or owned by a disabled service account.
|
|
130
|
+
throw new Error('Unauthorized');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(result.subject); // { type: 'USER' | 'SERVICE_ACCOUNT', id: string }
|
|
134
|
+
console.log(result.scopes); // string[] | null
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`resolveKey` hashes the provided raw key and looks it up in the database. It returns `null` when the key is unknown, revoked, past its `expiresAt`, or owned by a service account with `isActive: false`. On success it also updates the key's `lastUsedAt` timestamp.
|
|
138
|
+
|
|
139
|
+
### Authenticating incoming requests
|
|
140
|
+
|
|
141
|
+
You don't need to call `resolveKey` yourself — the api-key plugin implements Fortress's `resolvePrincipal` capability, so both `fortress.handleRequest` *and* the Hono / Express / SvelteKit user-route auth middleware automatically resolve requests bearing an api-key header into a subject principal. Two header formats are accepted:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
Authorization: ApiKey myapp_sk_a1b2c3d4...
|
|
145
|
+
X-API-Key: myapp_sk_a1b2c3d4...
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
If neither header is present, the pipeline falls back to the JWT path. If both a JWT and an api-key are present, the api-key wins (resolvers run before the JWT fallback).
|
|
149
|
+
|
|
150
|
+
> Note: api-key authentication works uniformly on Fortress-owned routes (`/auth/*`, `/iam/*`, plugin routes) *and* your own custom routes — any route protected by the adapter's auth middleware goes through `fortress.resolvePrincipal`, which tries the plugin chain before the JWT fallback. The resolved principal is available as `fortressSubject` on the adapter request context (Hono `c.get('fortressSubject')` / Express `req.fortressSubject` / SvelteKit `event.locals.fortress.subject`).
|
|
151
|
+
|
|
152
|
+
Once resolved, the principal flows through the same RBAC machinery as a JWT-authenticated request:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// Inside a fortress-managed route or an adapter middleware RBAC check
|
|
156
|
+
await fortress.iam.checkPermission(
|
|
157
|
+
ctx.subject, // { type: 'USER' | 'SERVICE_ACCOUNT', id }
|
|
158
|
+
'deploy',
|
|
159
|
+
'run',
|
|
160
|
+
);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Scoped keys
|
|
164
|
+
|
|
165
|
+
Scopes let you restrict what an API key is allowed to do. Pass a `scopes` array at creation time:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const { key } = await fortress.plugins['api-key'].createKey({
|
|
169
|
+
subject: { type: 'USER', id: userId },
|
|
170
|
+
name: 'Read-only analytics',
|
|
171
|
+
scopes: ['analytics:read', 'reports:list'],
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
When the key is resolved, the scopes are returned so your application can enforce them:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const result = await fortress.plugins['api-key'].resolveKey(rawKey);
|
|
179
|
+
|
|
180
|
+
if (result && result.scopes && !result.scopes.includes('analytics:read')) {
|
|
181
|
+
throw new Error('Forbidden: insufficient scope');
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If no scopes are provided, `resolveKey` returns `scopes: null`, which you can treat as "unrestricted" or reject based on your security policy.
|
|
186
|
+
|
|
187
|
+
## API Reference
|
|
188
|
+
|
|
189
|
+
| Method | Signature | Returns |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `createKey` | `(input: { subject?: Subject; name: string; scopes?: string[]; expiresAt?: Date }, routeCtx?: PluginRouteContext)` | `Promise<{ key: string; id: string }>` |
|
|
192
|
+
| `listKeys` | `(input: { subject?: Subject }, routeCtx?: PluginRouteContext)` | `Promise<ApiKeyInfo[]>` |
|
|
193
|
+
| `revokeKey` | `(input: { subject?: Subject; id: string \| string }, routeCtx?: PluginRouteContext)` | `Promise<{ ok: true }>` |
|
|
194
|
+
| `rotateKey` | `(input: { subject?: Subject; id: string \| string }, routeCtx?: PluginRouteContext)` | `Promise<{ key: string; id: string }>` |
|
|
195
|
+
| `resolveKey` | `(rawKey: string)` | `Promise<{ subject: Subject; scopes: string[] \| null } \| null>` |
|
|
196
|
+
|
|
197
|
+
**Dual-mode `subject`.** When called from an HTTP route handler with a `routeCtx`, the plugin uses `routeCtx.subject` and ignores any `subject` supplied in `input` — clients can't pick which subject a key is created for. When called programmatically (no `routeCtx`), the plugin uses `input.subject`; programmatic callers are trusted.
|
|
198
|
+
|
|
199
|
+
The `ApiKeyInfo` type returned by `listKeys`:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
interface ApiKeyInfo {
|
|
203
|
+
id: string;
|
|
204
|
+
name: string;
|
|
205
|
+
keyPrefix: string; // first 12 characters of the raw key
|
|
206
|
+
scopes: string[] | null;
|
|
207
|
+
expiresAt: string | null;
|
|
208
|
+
lastUsedAt: string | null;
|
|
209
|
+
createdAt: string;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Service account flow (end-to-end)
|
|
214
|
+
|
|
215
|
+
A complete walkthrough of setting up a CI deploy bot as a service account, granting it permissions, and authenticating its requests. Service accounts are first-class IAM principals — see the [README IAM section](../../README.md#use-service-accounts) for the conceptual overview.
|
|
216
|
+
|
|
217
|
+
### 1. Create the service account
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const ci = await fortress.iam.createServiceAccount({
|
|
221
|
+
name: 'ci-deploy', // machine identifier, immutable
|
|
222
|
+
displayName: 'CI Deploy',
|
|
223
|
+
description: 'Runs production deploys from GitHub Actions',
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 2. Grant it permissions
|
|
228
|
+
|
|
229
|
+
Either bind a role or a direct permission. Both work exactly as they do for users:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const deployer = await fortress.iam.createRole('deployer', [
|
|
233
|
+
{ resource: 'deploy', action: 'run' },
|
|
234
|
+
{ resource: 'deploy', action: 'rollback' },
|
|
235
|
+
]);
|
|
236
|
+
await fortress.iam.bindRoleToServiceAccount(ci.id, deployer.id);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 3. Mint an API key for the service account
|
|
240
|
+
|
|
241
|
+
Programmatically:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const { key } = await fortress.plugins['api-key'].createKey({
|
|
245
|
+
subject: { type: 'SERVICE_ACCOUNT', id: ci.id },
|
|
246
|
+
name: 'ci-deploy-github-actions',
|
|
247
|
+
});
|
|
248
|
+
// store `key` in your CI secrets store NOW — it is never returned again
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Or via HTTP, using the admin plugin's mint endpoint — the only supported HTTP path for bootstrapping a service account's first credential (a fresh SA has no login flow, so it can't self-mint via the `/api-key/keys` self-service route):
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
curl -X POST http://localhost:3000/admin/service-accounts/$SA_ID/api-keys \
|
|
255
|
+
-H "Authorization: Bearer $ADMIN_JWT" \
|
|
256
|
+
-H "Content-Type: application/json" \
|
|
257
|
+
-d '{"name":"ci-deploy-github-actions"}'
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
This requires `admin({ apiKeyRoutes: true })` registered alongside `apiKey()`, and the caller must hold the `apiKey:manage` permission (auto-registered by bootstrap). See [docs/plugins/admin.md — API key management](./admin.md#api-key-management) for the full admin surface.
|
|
261
|
+
|
|
262
|
+
### 4. Authenticate requests using the key
|
|
263
|
+
|
|
264
|
+
The client sends the key in either header. Fortress's `resolvePrincipal` plugin hook — which `api-key` implements — inspects each incoming request and turns the header into a subject principal *before* RBAC runs:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
POST /deploy/run HTTP/1.1
|
|
268
|
+
Authorization: ApiKey fortress_sk_a1b2c3d4...
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
or
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
POST /deploy/run HTTP/1.1
|
|
275
|
+
X-API-Key: fortress_sk_a1b2c3d4...
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Inside your route, the authenticated principal is the service account:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// On a fortress-managed route, routeCtx.subject is populated
|
|
282
|
+
async handler(input, routeCtx) {
|
|
283
|
+
// routeCtx.subject is { type: 'SERVICE_ACCOUNT', id: ci.id }
|
|
284
|
+
const allowed = await fortress.iam.checkPermission(
|
|
285
|
+
routeCtx.subject!,
|
|
286
|
+
'deploy',
|
|
287
|
+
'run',
|
|
288
|
+
);
|
|
289
|
+
if (!allowed) throw Errors.forbidden();
|
|
290
|
+
// ... run the deploy
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Fortress-managed routes (like the `/iam/service-accounts/*` admin endpoints) automatically enforce the subject's permissions via `enforceFortressPermission`.
|
|
295
|
+
|
|
296
|
+
### 5. Kill-switch: deactivate when compromised
|
|
297
|
+
|
|
298
|
+
If a key leaks, you have two layers of defense. Revoke the individual key, or deactivate the entire service account — the latter instantly stops every key the account owns from resolving, and drives `checkPermission` to return `false` for that subject:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
// Option A: revoke just this key
|
|
302
|
+
await fortress.plugins['api-key'].revokeKey({
|
|
303
|
+
subject: { type: 'SERVICE_ACCOUNT', id: ci.id },
|
|
304
|
+
id: keyId,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Option B: kill-switch the whole service account
|
|
308
|
+
await fortress.iam.updateServiceAccount(ci.id, { isActive: false });
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 6. Decommission: delete the service account
|
|
312
|
+
|
|
313
|
+
`deleteServiceAccount` is a hard delete with cascade — the account row, all role bindings, all direct permission bindings, and every API key owned by the account are removed in one operation.
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
await fortress.iam.deleteServiceAccount(ci.id);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## How It Works
|
|
322
|
+
|
|
323
|
+
1. **Key generation** -- `crypto.getRandomValues` produces 32 random bytes, hex-encoded and prepended with `{prefix}_sk_` to form the raw key.
|
|
324
|
+
2. **Hashed storage** -- The raw key is hashed with SHA-256 (via the same `hashToken` used for refresh tokens). Only the hash is persisted; the plaintext is returned once at creation time.
|
|
325
|
+
3. **Prefix-based identification** -- The first 12 characters of the raw key are stored as `keyPrefix`, allowing users and admins to identify keys in listings without exposing the secret.
|
|
326
|
+
4. **Resolution** -- `resolveKey` hashes the incoming raw key and performs a database lookup on `keyHash`. It rejects revoked and expired keys, and updates `lastUsedAt` on success.
|
|
327
|
+
5. **Scopes** -- Stored as a JSON-serialized string array in the database and parsed back to `string[]` on read. Scope enforcement is left to the application layer.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Audit Log Plugin
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The audit-log plugin provides a hash-chain integrity trail for authentication events. Every login, logout, registration, token refresh, and failed login attempt is recorded with actor, timestamp, IP address, and user-agent metadata.
|
|
6
|
+
|
|
7
|
+
An optional SHA-256 hash chain links each entry to its predecessor, making retroactive tampering detectable. This is designed for organizations that need to demonstrate compliance with SOC 2, HIPAA, PCI-DSS, or similar frameworks that require immutable, verifiable logs of access events.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createFortress } from '@bajustone/fortress';
|
|
13
|
+
import { auditLog } from '@bajustone/fortress/plugins/audit-log';
|
|
14
|
+
|
|
15
|
+
const fortress = createFortress({
|
|
16
|
+
jwt: { key: process.env.JWT_SECRET! },
|
|
17
|
+
database: adapter,
|
|
18
|
+
plugins: [auditLog()],
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The bundled Fortress migrations create `fortress_audit_log`; run `fortress.migrate()` before enabling the plugin.
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
Pass an `AuditLogConfig` object to the `auditLog()` factory:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
auditLog({
|
|
30
|
+
events: ['LOGIN_SUCCESS', 'LOGIN_FAILURE', 'LOGOUT'],
|
|
31
|
+
hashChain: true,
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Option | Type | Default | Description |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `events` | `AuditEventType[]` | `undefined` (all events) | Restrict which events are captured. When omitted, all event types are logged. |
|
|
38
|
+
| `hashChain` | `boolean` | `false` | Enable SHA-256 hash chain linking each entry to its predecessor for tamper detection. |
|
|
39
|
+
|
|
40
|
+
### Event Types
|
|
41
|
+
|
|
42
|
+
| Event | Trigger | Actor |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `LOGIN_SUCCESS` | Successful `auth.login()` call | The authenticated user |
|
|
45
|
+
| `LOGIN_FAILURE` | Failed `auth.login()` call (wrong password, unknown user) | Anonymous (actorId is null) |
|
|
46
|
+
| `LOGOUT` | `auth.logout()` call | User (resolved from token context) |
|
|
47
|
+
| `REGISTER` | `auth.createUser()` call | The newly created user |
|
|
48
|
+
| `TOKEN_REFRESH` | `auth.refresh()` call | User (from refresh token context) |
|
|
49
|
+
| `TOKEN_REUSE` | Refresh token replay detected | Reserved for future hook support |
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Automatic Event Capture
|
|
54
|
+
|
|
55
|
+
Once the plugin is registered, events are captured automatically through Fortress lifecycle hooks. No additional code is needed for logging -- every `auth.login()`, `auth.createUser()`, `auth.logout()`, and `auth.refresh()` call writes an audit entry.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// These calls automatically produce audit log entries:
|
|
59
|
+
const user = await fortress.auth.createUser({
|
|
60
|
+
email: 'alice@example.com',
|
|
61
|
+
name: 'Alice',
|
|
62
|
+
password: 'secure-password-123',
|
|
63
|
+
});
|
|
64
|
+
// -> REGISTER entry logged
|
|
65
|
+
|
|
66
|
+
const { accessToken, refreshToken } = await fortress.auth.login(
|
|
67
|
+
'alice@example.com',
|
|
68
|
+
'secure-password-123',
|
|
69
|
+
);
|
|
70
|
+
// -> LOGIN_SUCCESS entry logged
|
|
71
|
+
|
|
72
|
+
await fortress.auth.refresh(refreshToken);
|
|
73
|
+
// -> TOKEN_REFRESH entry logged
|
|
74
|
+
|
|
75
|
+
await fortress.auth.logout(refreshToken);
|
|
76
|
+
// -> LOGOUT entry logged
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Failed logins are also captured. The entry includes the attempted identifier and error message in the `metadata` field as JSON:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
await fortress.auth.login('alice@example.com', 'wrong-password').catch(() => {});
|
|
83
|
+
// -> LOGIN_FAILURE entry logged with metadata: {"identifier":"alice@example.com","error":"..."}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Passing Request Metadata
|
|
87
|
+
|
|
88
|
+
To capture IP address and user-agent in audit entries, pass `RequestMeta` through the auth methods that support it. If you are using the Hono adapter, this is handled automatically. Otherwise, pass `meta` through your hook context.
|
|
89
|
+
|
|
90
|
+
### Querying the Audit Log
|
|
91
|
+
|
|
92
|
+
Use the type-safe plugin access to retrieve entries:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const entries = await fortress.plugins['audit-log'].getAuditLog();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The `getAuditLog` method accepts an `AuditLogQueryOptions` object for filtering and pagination:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// All login failures in the last 24 hours
|
|
102
|
+
const failures = await fortress.plugins['audit-log'].getAuditLog({
|
|
103
|
+
eventType: 'LOGIN_FAILURE',
|
|
104
|
+
from: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// All events for a specific user, paginated
|
|
108
|
+
const userHistory = await fortress.plugins['audit-log'].getAuditLog({
|
|
109
|
+
userId: '42',
|
|
110
|
+
limit: 50,
|
|
111
|
+
offset: 0,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Events within a date range
|
|
115
|
+
const report = await fortress.plugins['audit-log'].getAuditLog({
|
|
116
|
+
from: new Date('2026-01-01'),
|
|
117
|
+
to: new Date('2026-03-31'),
|
|
118
|
+
limit: 1000,
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Results are returned in reverse chronological order (`timestamp DESC`).
|
|
123
|
+
|
|
124
|
+
#### Query Options
|
|
125
|
+
|
|
126
|
+
| Option | Type | Description |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `userId` | `string` | Filter by `actorId` |
|
|
129
|
+
| `eventType` | `AuditEventType` | Filter by event type |
|
|
130
|
+
| `from` | `Date` | Entries on or after this timestamp |
|
|
131
|
+
| `to` | `Date` | Entries on or before this timestamp |
|
|
132
|
+
| `limit` | `number` | Maximum entries to return |
|
|
133
|
+
| `offset` | `number` | Number of entries to skip (for pagination) |
|
|
134
|
+
|
|
135
|
+
### Hash Chain for Tamper Detection
|
|
136
|
+
|
|
137
|
+
When `hashChain: true` is set, each audit entry includes a `previousHash` field containing the SHA-256 hash of the preceding entry. The digest uses an unambiguous serialization of all 13 stored fields, including the predecessor's own `previousHash`, so changes to actor, target, request, outcome, metadata, or timestamp fields break verification. This is an unkeyed integrity check, not proof against an attacker who can rewrite the complete database: such an attacker can recompute the chain and reset its in-database anchor. Use external attestation or a keyed/external anchor when database-write attackers are in scope.
|
|
138
|
+
|
|
139
|
+
The first entry has `previousHash: null`. Every subsequent entry references the one before it. A permanent singleton `fortress_audit_chain_state` anchor starts at `{ lastHash: null, entryCount: 0 }` and stores the expected terminal hash/count after each append, making deletion or mutation of the final row—or deletion of the entire chain—detectable as well. Each append reads this anchor, inserts the new row using `anchor.lastHash`, and advances the anchor in the same serialized transaction, so append work remains constant as the log grows. A zero anchor performs one bounded (`LIMIT 1`) existence probe to reject an untransformed legacy log safely; no history is hashed or traversed. SQLite transactions are queued per adapter instance to accommodate synchronous drivers; PostgreSQL uses a transaction-scoped advisory lock across application instances and is not placed on an in-process/global queue. `verifyChain()` performs the separate full historical scan.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const fortress = createFortress({
|
|
143
|
+
jwt: { key: process.env.JWT_SECRET! },
|
|
144
|
+
database: adapter,
|
|
145
|
+
plugins: [auditLog({ hashChain: true })],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// After several auth events...
|
|
149
|
+
const entries = await fortress.plugins['audit-log'].getAuditLog();
|
|
150
|
+
|
|
151
|
+
// Verify chain integrity without relying on identifier ordering
|
|
152
|
+
const verification = await fortress.plugins['audit-log'].verifyChain();
|
|
153
|
+
if (!verification.valid)
|
|
154
|
+
throw new Error(`Broken audit chain: ${JSON.stringify(verification.brokenLinks)}`);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If a row is deleted or altered, either an internal link or the persisted terminal anchor breaks. Auditors can verify both the chain graph and its expected terminal state through `verifyChain()`. Appends intentionally do not rescan history; schedule `verifyChain()` independently and alert on an invalid result.
|
|
158
|
+
|
|
159
|
+
### Enabling hash chaining on an existing log
|
|
160
|
+
|
|
161
|
+
A non-empty log written with `hashChain: false` must be explicitly transitioned before normal auth traffic starts using `hashChain: true`. Archive and externally attest the legacy rows first: rebaselining links the stored rows from that point forward but cannot prove that their earlier history was untampered.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const audit = fortress.plugins['audit-log'];
|
|
165
|
+
const result = await audit.rebaselineChain();
|
|
166
|
+
if (!result.valid)
|
|
167
|
+
throw new Error('Audit rebaseline failed');
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`rebaselineChain()` runs under the same transaction/advisory lock as append, accepts only the migration-seeded zero anchor plus rows whose `previousHash` values are all null, orders those rows by `createdAt` and then string `id`, rewrites their links, advances the anchor, and verifies the result before commit. It rejects an already or partially chained log, so it cannot be used as a general-purpose way to erase evidence of detected tampering. Run it once during a maintenance window after migrations, before exposing login/register/refresh/logout traffic on the hash-chain-enabled instance.
|
|
171
|
+
|
|
172
|
+
Automatic audit hook writes are best-effort after auth state changes: an audit storage/anchor failure is sent to the configured logger and does not turn a committed login, registration, refresh, or logout into a reported auth failure. Direct `logCustomEvent()` and maintenance methods still reject on errors, allowing operators to monitor and fail their own workflows explicitly.
|
|
173
|
+
|
|
174
|
+
**Performance note:** Chain-enabled append is O(1) with respect to historical row count. `verifyChain()` and the one-time `rebaselineChain()` are O(n) maintenance operations and hold the chain lock while they run.
|
|
175
|
+
|
|
176
|
+
### Exporting for compliance
|
|
177
|
+
|
|
178
|
+
`exportEntries` serializes audit entries to a string for retention,
|
|
179
|
+
hand-off to a SIEM, or a compliance request. It accepts the same
|
|
180
|
+
`AuditLogQueryOptions` as `getAuditLog`, so you can scope the export by
|
|
181
|
+
user, event type, or time window.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const audit = fortress.plugins['audit-log'];
|
|
185
|
+
|
|
186
|
+
// Full JSON export (default format)
|
|
187
|
+
const json = await audit.exportEntries();
|
|
188
|
+
|
|
189
|
+
// CSV export of one user's last quarter, for a data-subject request
|
|
190
|
+
const csv = await audit.exportEntries('csv', {
|
|
191
|
+
userId: '42',
|
|
192
|
+
from: new Date('2026-01-01'),
|
|
193
|
+
to: new Date('2026-03-31'),
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The CSV output is RFC 4180-compliant: a fixed header row, one row per
|
|
198
|
+
entry, with cells containing commas, quotes, or newlines quoted and their
|
|
199
|
+
embedded quotes doubled. Cells beginning with `=`, `+`, `-`, `@`, tab, or
|
|
200
|
+
carriage return are prefixed with an apostrophe to prevent spreadsheet-formula
|
|
201
|
+
injection. `Date` columns are emitted as ISO 8601 strings.
|
|
202
|
+
Both formats stream the rows in the same order as `getAuditLog` (newest
|
|
203
|
+
first).
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### Factory
|
|
208
|
+
|
|
209
|
+
| Function | Signature | Description |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `auditLog` | `(config?: AuditLogConfig) => FortressPlugin` | Creates the audit-log plugin instance |
|
|
212
|
+
|
|
213
|
+
### Methods (via `fortress.plugins['audit-log']`)
|
|
214
|
+
|
|
215
|
+
| Method | Signature | Description |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `getAuditLog` | `(options?: AuditLogQueryOptions) => Promise<AuditLogEntry[]>` | Query audit log entries with optional filters |
|
|
218
|
+
| `logCustomEvent` | `(event: CustomAuditEvent) => Promise<void>` | Append an application-defined event |
|
|
219
|
+
| `verifyChain` | `() => Promise<ChainVerificationResult>` | Walk the hash chain and report broken links |
|
|
220
|
+
| `rebaselineChain` | `() => Promise<ChainVerificationResult>` | One-time transactional conversion of an unchained legacy log |
|
|
221
|
+
| `exportEntries` | `(format?: 'json' \| 'csv', options?: AuditLogQueryOptions) => Promise<string>` | Serialize entries for compliance export (defaults to `json`) |
|
|
222
|
+
|
|
223
|
+
### Types
|
|
224
|
+
|
|
225
|
+
**`AuditLogEntry`** -- a single audit log record:
|
|
226
|
+
|
|
227
|
+
| Field | Type | Description |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| `id` | `string` | Adapter-generated entry ID |
|
|
230
|
+
| `timestamp` | `Date` | Event timestamp |
|
|
231
|
+
| `eventType` | `AuditEventType` | One of the six event types |
|
|
232
|
+
| `actorId` | `string \| null` | User ID of the actor, or null for anonymous events |
|
|
233
|
+
| `actorType` | `string` | `"user"` or `"anonymous"` |
|
|
234
|
+
| `targetId` | `string \| null` | ID of the affected resource (e.g., the new user on REGISTER) |
|
|
235
|
+
| `targetType` | `string \| null` | Type of the affected resource (e.g., `"user"`) |
|
|
236
|
+
| `ipAddress` | `string \| null` | Client IP if available via `RequestMeta` |
|
|
237
|
+
| `userAgent` | `string \| null` | Client user-agent if available via `RequestMeta` |
|
|
238
|
+
| `outcome` | `string` | `"success"` or `"failure"` |
|
|
239
|
+
| `metadata` | `string \| null` | JSON string with event-specific details (e.g., error message on failure) |
|
|
240
|
+
| `previousHash` | `string \| null` | SHA-256 hash of the preceding entry (only when `hashChain` is enabled) |
|
|
241
|
+
| `createdAt` | `Date` | Creation timestamp |
|
|
242
|
+
|
|
243
|
+
## How It Works
|
|
244
|
+
|
|
245
|
+
The plugin uses Fortress's hook system to intercept auth lifecycle events without modifying core code.
|
|
246
|
+
|
|
247
|
+
**Hooks registered:**
|
|
248
|
+
|
|
249
|
+
| Hook | Event Logged |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `afterLogin` | `LOGIN_SUCCESS` |
|
|
252
|
+
| `onLoginFailure` | `LOGIN_FAILURE` |
|
|
253
|
+
| `beforeLogout` | `LOGOUT` |
|
|
254
|
+
| `afterRegister` | `REGISTER` |
|
|
255
|
+
| `afterTokenRefresh` | `TOKEN_REFRESH` |
|
|
256
|
+
|
|
257
|
+
Each hook writes a row to the `audit_log` model via the database adapter. When hash chaining is enabled, Fortress reads the persisted terminal anchor, stores its `lastHash` as the new entry's `previousHash`, hashes the inserted entry, and advances the anchor in the same serialized transaction. Full graph traversal and all-field digest verification are reserved for `verifyChain()`.
|
|
258
|
+
|
|
259
|
+
The `getAuditLog` method builds `WhereClause` filters from the query options and delegates to `db.findMany`, keeping the plugin fully database-agnostic.
|
|
260
|
+
|
|
261
|
+
All hooks run inline with the auth flow. Audit-plugin write failures are logged and contained so they cannot report a committed auth state change as failed. Failures from other plugins are unaffected, and explicit audit method calls continue to reject on storage or chain-maintenance errors.
|