@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,227 @@
|
|
|
1
|
+
# Admin and operator recipes
|
|
2
|
+
|
|
3
|
+
Fortress is API-first by design — there is no reference UI shipped with
|
|
4
|
+
the library. The `admin` plugin (`@bajustone/fortress/plugins/admin`)
|
|
5
|
+
already exposes IAM CRUD as HTTP routes; this page collects the
|
|
6
|
+
**operator-workflow recipes** that combine those routes (and a few
|
|
7
|
+
service-layer helpers) into common admin tasks.
|
|
8
|
+
|
|
9
|
+
Drop these into your own admin app (Next.js / SvelteKit / Hono) or wire
|
|
10
|
+
them to a CLI. Every recipe is plain HTTP or `fortress.iam.*` /
|
|
11
|
+
`fortress.plugins.*` calls — no extra dependencies beyond what's
|
|
12
|
+
already installed.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
Mount the admin plugin so the IAM endpoints exist:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { admin } from '@bajustone/fortress/plugins/admin';
|
|
20
|
+
import { apiKey } from '@bajustone/fortress/plugins/api-key';
|
|
21
|
+
|
|
22
|
+
createFortress({
|
|
23
|
+
// ...
|
|
24
|
+
plugins: [
|
|
25
|
+
admin({ apiKeyRoutes: true }), // also mounts admin api-key routes
|
|
26
|
+
apiKey({ prefix: 'fortress', routes: true }),
|
|
27
|
+
// ... rest of your plugins
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Bootstrap the first admin user once:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
curl -X POST https://your-app/iam/admin/bootstrap \
|
|
36
|
+
-H 'Authorization: Bearer <a freshly-issued JWT for the user>' \
|
|
37
|
+
-H 'Content-Type: application/json' \
|
|
38
|
+
-d '{}'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This creates the `fortress-admin` role with every `fortress:*`
|
|
42
|
+
permission and binds it to the calling user. Run once per deployment;
|
|
43
|
+
subsequent calls 409 unless the role is empty.
|
|
44
|
+
|
|
45
|
+
## Users
|
|
46
|
+
|
|
47
|
+
Standard auth endpoints handle most user lifecycle:
|
|
48
|
+
|
|
49
|
+
| Task | API |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Create user | `POST /auth/register` or `fortress.auth.createUser(...)` |
|
|
52
|
+
| Disable user | `fortress.auth.setUserActive(userId, false)` |
|
|
53
|
+
| Reset password (admin) | `fortress.auth.setUserPassword(userId, newPassword)` |
|
|
54
|
+
| Force email re-verification | clear `email_verified` row, call email-verification plugin's `send` method |
|
|
55
|
+
| Delete user | `fortress.auth.deleteUser(userId)` — cascades to sessions, role bindings, direct perms |
|
|
56
|
+
|
|
57
|
+
## Roles, groups, permissions
|
|
58
|
+
|
|
59
|
+
`@bajustone/fortress/plugins/admin` routes:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
POST /iam/roles create role
|
|
63
|
+
DELETE /iam/roles/:id delete role
|
|
64
|
+
GET /iam/roles list roles
|
|
65
|
+
GET /iam/permissions list permissions
|
|
66
|
+
POST /iam/permissions/bind/user bind a permission to a user
|
|
67
|
+
POST /iam/permissions/bind/group bind to a group
|
|
68
|
+
POST /iam/permissions/bind/service-account bind to a service account
|
|
69
|
+
DELETE /iam/permissions/bind/* unbind variants
|
|
70
|
+
POST /iam/role-bindings/user bind a role to a user
|
|
71
|
+
POST /iam/role-bindings/group bind a role to a group
|
|
72
|
+
POST /iam/role-bindings/service-account bind a role to a service account
|
|
73
|
+
DELETE /iam/role-bindings/* unbind variants
|
|
74
|
+
POST /iam/groups create group
|
|
75
|
+
POST /iam/groups/:id/members add user to group
|
|
76
|
+
DELETE /iam/groups/:id/members/:userId remove user from group
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
All require an authenticated user with the matching `fortress:*`
|
|
80
|
+
permission (e.g. `fortress:createRole`). The admin bootstrap binds them
|
|
81
|
+
to the first admin.
|
|
82
|
+
|
|
83
|
+
For declarative management (recommended for production), use
|
|
84
|
+
**policy-as-code** instead — see [docs/policy-as-code.md](./policy-as-code.md).
|
|
85
|
+
|
|
86
|
+
## Sessions and revocation
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// List sessions for a user (callable from an admin route).
|
|
90
|
+
const sessions = await fortress.auth.listSessions(userId);
|
|
91
|
+
|
|
92
|
+
// Revoke a single session.
|
|
93
|
+
await fortress.auth.revokeSession(userId, sessionId);
|
|
94
|
+
|
|
95
|
+
// Revoke every refresh token for a user (forces logout everywhere on
|
|
96
|
+
// next access-token expiry; access tokens are stateless so they remain
|
|
97
|
+
// valid until they expire — keep TTL short).
|
|
98
|
+
await fortress.auth.revokeAllSessions(userId);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
To force-logout immediately even before access-token expiry, set the
|
|
102
|
+
user inactive (`setUserActive(userId, false)`); the auth pipeline
|
|
103
|
+
rejects requests whose JWT subject resolves to an inactive user.
|
|
104
|
+
|
|
105
|
+
## Service accounts and API keys
|
|
106
|
+
|
|
107
|
+
Service accounts are first-class IAM subjects. They have no sessions —
|
|
108
|
+
they authenticate via API keys.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// Create the service account.
|
|
112
|
+
const sa = await fortress.iam.createServiceAccount({
|
|
113
|
+
name: 'ci-bot',
|
|
114
|
+
displayName: 'CI Bot',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Bind a role to it.
|
|
118
|
+
const role = (await fortress.iam.getRoles()).find(r => r.name === 'editor')!;
|
|
119
|
+
await fortress.iam.bindRoleToServiceAccount(sa.id, role.id);
|
|
120
|
+
|
|
121
|
+
// Mint an API key for the service account (admin plugin route, requires apiKey:manage).
|
|
122
|
+
const res = await fetch(`/admin/service-accounts/${sa.id}/api-keys`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${adminJwt}` },
|
|
125
|
+
body: JSON.stringify({ name: 'ci-pipeline' }),
|
|
126
|
+
});
|
|
127
|
+
const { key } = await res.json(); // returned ONCE; store in secret manager
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
API keys carry `subjectType: 'SERVICE_ACCOUNT'`; the resolver populates
|
|
131
|
+
the request principal so RBAC works the same as for users.
|
|
132
|
+
|
|
133
|
+
## OAuth clients
|
|
134
|
+
|
|
135
|
+
OAuth clients have secrets; manage via the OAuth plugin's admin
|
|
136
|
+
methods (`fortress.plugins.oauth.createClient(...)`,
|
|
137
|
+
`updateClient(...)`, `deleteClient(...)`). Secrets are returned **once**
|
|
138
|
+
on create — re-issue with `rotateClientSecret(clientId)` and update RPs
|
|
139
|
+
out-of-band.
|
|
140
|
+
|
|
141
|
+
For policy-as-code: OAuth clients are intentionally NOT covered by the
|
|
142
|
+
policy file (secrets do not belong in a checked-in JSON file). Track
|
|
143
|
+
them in your secret manager and run create/update via a one-off script.
|
|
144
|
+
|
|
145
|
+
## Audit logs
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Read a page of audit log entries.
|
|
149
|
+
const page = await fortress.plugins['audit-log'].listEntries({
|
|
150
|
+
limit: 100,
|
|
151
|
+
offset: 0,
|
|
152
|
+
// optional filters:
|
|
153
|
+
eventType: 'LOGIN_FAILURE',
|
|
154
|
+
userId,
|
|
155
|
+
after: new Date(Date.now() - 7 * 24 * 3600 * 1000),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Verify the hash chain (when hashChain: true was passed at plugin construction).
|
|
159
|
+
const verified = await fortress.plugins['audit-log'].verifyChain();
|
|
160
|
+
if (!verified.ok) {
|
|
161
|
+
console.error(`Chain broken at index ${verified.brokenAt}`);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The verifier walks every row and recomputes the SHA-256 chain. Run as a
|
|
166
|
+
scheduled job; alert when `ok === false`.
|
|
167
|
+
|
|
168
|
+
## Permission debugging — "why does user X have permission Y?"
|
|
169
|
+
|
|
170
|
+
Fortress ships `explainPermission` for this exact question:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { explainPermission } from '@bajustone/fortress';
|
|
174
|
+
|
|
175
|
+
const explain = await explainPermission(
|
|
176
|
+
fortress.config.database,
|
|
177
|
+
fortress.iam,
|
|
178
|
+
{ type: 'USER', id: 42 },
|
|
179
|
+
'article',
|
|
180
|
+
'delete',
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
console.log(explain.allowed); // true | false
|
|
184
|
+
console.log(explain.sources); // every grant + its source (role / direct / group)
|
|
185
|
+
console.log(explain.roleBindings); // roles the user is bound to
|
|
186
|
+
console.log(explain.groupMemberships); // groups the user belongs to
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Each source carries enough context to point a human at the misconfig:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
explain.sources[0]
|
|
193
|
+
// {
|
|
194
|
+
// via: 'role',
|
|
195
|
+
// role: 'editor',
|
|
196
|
+
// group: { id: 7, name: 'eng' }, // when inherited via group
|
|
197
|
+
// permission: { id, resource, action, effect, conditions, description }
|
|
198
|
+
// }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
With `rbac.evaluationMode: 'deny-overrides'`, any matching `DENY` source
|
|
202
|
+
flips `allowed` to `false` regardless of matching `ALLOW` sources. In the
|
|
203
|
+
default `allow-only` mode, DENY entries are intentionally ignored. Wildcard permissions
|
|
204
|
+
(`resource: '*'`, `action: '*'`) match every check; you'll see them
|
|
205
|
+
listed as sources when they apply.
|
|
206
|
+
|
|
207
|
+
Wrap this in an admin route and you have a one-click "why does this
|
|
208
|
+
permission resolve the way it does" debugger.
|
|
209
|
+
|
|
210
|
+
## Building an admin console UI
|
|
211
|
+
|
|
212
|
+
The library doesn't ship one. Wire the routes above to your framework
|
|
213
|
+
of choice; common starting points:
|
|
214
|
+
|
|
215
|
+
- **Hono + React/HTMX:** drop the existing admin plugin routes under
|
|
216
|
+
`/admin/*`, build a small SPA against them. The `mountFortress` call
|
|
217
|
+
already mounts every IAM route.
|
|
218
|
+
- **SvelteKit:** use the SvelteKit adapter; build admin pages under
|
|
219
|
+
`/routes/admin/+page.server.ts` that call `fortress.iam.*` directly.
|
|
220
|
+
No HTTP roundtrip needed.
|
|
221
|
+
- **CLI:** wrap the IAM methods in a thin commander/citty CLI; useful
|
|
222
|
+
for one-off ops and for environments without admin UI access.
|
|
223
|
+
|
|
224
|
+
In every case, gate the routes/pages with `permission('fortress', '...')`
|
|
225
|
+
declarations on the endpoint (`fortress` will RBAC-check before
|
|
226
|
+
dispatch) or `fortress.iam.checkPermission(subject, 'fortress', '...')`
|
|
227
|
+
in your handler.
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# Architecture and plugin authoring
|
|
2
|
+
|
|
3
|
+
## Request pipeline
|
|
4
|
+
|
|
5
|
+
Every framework adapter delegates Fortress-owned routes to:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
const response = await fortress.handleRequest(request);
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The pipeline runs in this order:
|
|
12
|
+
|
|
13
|
+
1. `before-auth` plugin middleware
|
|
14
|
+
2. CSRF for unsafe cookie-authenticated requests
|
|
15
|
+
3. route matching
|
|
16
|
+
4. plugin principal resolvers, then JWT fallback
|
|
17
|
+
5. `after-auth` plugin middleware
|
|
18
|
+
6. endpoint permission check
|
|
19
|
+
7. `after-rbac` plugin middleware
|
|
20
|
+
8. body, query, and parameter validation
|
|
21
|
+
9. endpoint dispatch
|
|
22
|
+
10. auth-cookie serialization
|
|
23
|
+
|
|
24
|
+
Use the same pipeline without an HTTP server:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
const result = await fortress.call.login({
|
|
28
|
+
identifier: 'alice@example.com',
|
|
29
|
+
password: 'correct-horse-battery-staple',
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Direct service calls skip HTTP middleware, CSRF, route permissions, and request schemas:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
await fortress.auth.login(identifier, password);
|
|
37
|
+
await fortress.iam.checkPermission(subject, 'post', 'read');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Auth and IAM service hooks and observers still run.
|
|
41
|
+
|
|
42
|
+
## Instance composition
|
|
43
|
+
|
|
44
|
+
`createFortress()`:
|
|
45
|
+
|
|
46
|
+
1. validates JWT keys and session options;
|
|
47
|
+
2. wraps the database with observability;
|
|
48
|
+
3. creates auth and IAM services;
|
|
49
|
+
4. registers plugin methods, hooks, middleware, routes, and principal resolvers;
|
|
50
|
+
5. merges core, plugin, and host endpoint metadata;
|
|
51
|
+
6. builds `manifest`, `handleRequest`, and `call`.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const fortress = createFortress({ database, jwt: { key }, plugins });
|
|
55
|
+
|
|
56
|
+
fortress.auth;
|
|
57
|
+
fortress.iam;
|
|
58
|
+
fortress.plugins;
|
|
59
|
+
fortress.call;
|
|
60
|
+
fortress.endpoints;
|
|
61
|
+
fortress.manifest;
|
|
62
|
+
fortress.handleRequest;
|
|
63
|
+
fortress.migrate;
|
|
64
|
+
fortress.syncPermissionsFromManifest;
|
|
65
|
+
fortress.toOpenAPI;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Startup rejects:
|
|
69
|
+
|
|
70
|
+
- JWT keys shorter than 32 UTF-8 bytes;
|
|
71
|
+
- non-positive `jwt.session` values;
|
|
72
|
+
- duplicate plugin names;
|
|
73
|
+
- duplicate routes or call keys between plugins;
|
|
74
|
+
- `security: ['none']` combined with a permission;
|
|
75
|
+
- use of the reserved `__host` plugin name when top-level `routes` are set.
|
|
76
|
+
|
|
77
|
+
A plugin may intentionally replace a core route. Two plugins cannot own the same route.
|
|
78
|
+
|
|
79
|
+
## Endpoint metadata
|
|
80
|
+
|
|
81
|
+
An endpoint definition supplies request schemas, response schemas, security, IAM permission, OpenAPI metadata, and a handler name:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { endpoint, obj, str } from '@bajustone/fortress';
|
|
85
|
+
|
|
86
|
+
const createPost = endpoint('POST', '/posts')
|
|
87
|
+
.summary('Create a post')
|
|
88
|
+
.security('bearer')
|
|
89
|
+
.permission('post', 'create')
|
|
90
|
+
.body(obj({ title: str() }, 'title'))
|
|
91
|
+
.response(201, 'Created', obj({ id: str() }, 'id'))
|
|
92
|
+
.handler('createPost')
|
|
93
|
+
.build();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Register application-owned metadata at the top level:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const fortress = createFortress({
|
|
100
|
+
database,
|
|
101
|
+
jwt: { key },
|
|
102
|
+
routes: { createPost },
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Top-level routes appear in `fortress.endpoints`, the manifest, OpenAPI, and protection helpers. The host router still dispatches them. They do not create `fortress.call` methods.
|
|
107
|
+
|
|
108
|
+
To create a mounted and callable endpoint, define a plugin with matching `routes` and `methods` keys.
|
|
109
|
+
|
|
110
|
+
## Write a route plugin
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import {
|
|
114
|
+
endpoint,
|
|
115
|
+
obj,
|
|
116
|
+
str,
|
|
117
|
+
type FortressPlugin,
|
|
118
|
+
} from '@bajustone/fortress';
|
|
119
|
+
|
|
120
|
+
const greetingEndpoint = endpoint('POST', '/greetings')
|
|
121
|
+
.security('bearer')
|
|
122
|
+
.permission('greeting', 'create')
|
|
123
|
+
.body(obj({ name: str() }, 'name'))
|
|
124
|
+
.response(200, 'Greeting', obj({ message: str() }, 'message'))
|
|
125
|
+
.handler('createGreeting')
|
|
126
|
+
.build();
|
|
127
|
+
|
|
128
|
+
export function greetings() {
|
|
129
|
+
return {
|
|
130
|
+
name: 'greetings',
|
|
131
|
+
routes: { createGreeting: greetingEndpoint },
|
|
132
|
+
methods: () => ({
|
|
133
|
+
async createGreeting(input: { name: string }) {
|
|
134
|
+
return { message: `Hello ${input.name}` };
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
} as const satisfies FortressPlugin;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const fortress = createFortress({
|
|
143
|
+
database,
|
|
144
|
+
jwt: { key },
|
|
145
|
+
plugins: [greetings()] as const,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await fortress.call.createGreeting(
|
|
149
|
+
{ name: 'Alice' },
|
|
150
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The route record key, endpoint handler, and method key must match.
|
|
155
|
+
|
|
156
|
+
## Add lifecycle hooks
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
export function loginPolicy(): FortressPlugin {
|
|
160
|
+
return {
|
|
161
|
+
name: 'login-policy',
|
|
162
|
+
hooks: {
|
|
163
|
+
async beforeLogin(ctx) {
|
|
164
|
+
if (await isBlocked(ctx.email)) {
|
|
165
|
+
return {
|
|
166
|
+
stop: true,
|
|
167
|
+
response: { error: 'ACCOUNT_BLOCKED' },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
async onLoginFailure(ctx) {
|
|
172
|
+
await recordFailure(ctx.identifier);
|
|
173
|
+
},
|
|
174
|
+
async afterLogin(ctx, result) {
|
|
175
|
+
await recordSuccess(result.user.id);
|
|
176
|
+
ctx.responseHeaders.set('X-Auth-Method', result.method);
|
|
177
|
+
return result;
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Hooks run in plugin registration order.
|
|
185
|
+
|
|
186
|
+
Use these before token issuance:
|
|
187
|
+
|
|
188
|
+
- `beforeLogin`
|
|
189
|
+
- `beforeRegister`
|
|
190
|
+
- `beforeTokenRefresh`
|
|
191
|
+
- `postAuthGate`
|
|
192
|
+
|
|
193
|
+
Use these for committed side effects:
|
|
194
|
+
|
|
195
|
+
- `afterLogin`
|
|
196
|
+
- `afterRegister`
|
|
197
|
+
- `afterTokenRefresh`
|
|
198
|
+
- `onLoginFailure`
|
|
199
|
+
|
|
200
|
+
A thrown `afterLogin`, `afterRegister`, or `afterTokenRefresh` hook is logged and skipped. It cannot roll back an issued session or created user. A plugin that must deny authentication belongs before token issuance.
|
|
201
|
+
|
|
202
|
+
## Add a pending authentication gate
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { Errors, type FortressPlugin } from '@bajustone/fortress';
|
|
206
|
+
|
|
207
|
+
export function securityQuestion(): FortressPlugin {
|
|
208
|
+
return {
|
|
209
|
+
name: 'security-question',
|
|
210
|
+
hooks: {
|
|
211
|
+
postAuthGate: {
|
|
212
|
+
reason: 'two-factor',
|
|
213
|
+
maxAttempts: 5,
|
|
214
|
+
cooldownSeconds: 60,
|
|
215
|
+
async evaluate({ user }) {
|
|
216
|
+
if (await requiresQuestion(user.id))
|
|
217
|
+
return { pluginData: { prompt: 'First school?' } };
|
|
218
|
+
},
|
|
219
|
+
async verify({ user }, completion) {
|
|
220
|
+
if (!await checkAnswer(user.id, completion))
|
|
221
|
+
throw Errors.unauthorized('Invalid answer');
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
A gate returns a pending auth result before tokens exist:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const result = await fortress.auth.login(email, password);
|
|
233
|
+
|
|
234
|
+
if (result.status === 'pending') {
|
|
235
|
+
const completed = await fortress.auth.completePendingAuth(
|
|
236
|
+
result.pending.continuationToken,
|
|
237
|
+
answer,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Use an existing `PendingReason` or add a new reason to the core union before publishing a plugin.
|
|
243
|
+
|
|
244
|
+
## Resolve a custom credential
|
|
245
|
+
|
|
246
|
+
Principal resolvers run in plugin order before JWT verification. Return `null` when the credential is absent so the next resolver can try.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
export function signedRequest(): FortressPlugin {
|
|
250
|
+
return {
|
|
251
|
+
name: 'signed-request',
|
|
252
|
+
async resolvePrincipal(request) {
|
|
253
|
+
const signature = request.headers.get('X-Signature');
|
|
254
|
+
if (!signature)
|
|
255
|
+
return null;
|
|
256
|
+
|
|
257
|
+
const serviceAccountId = await verifySignature(signature, request);
|
|
258
|
+
return {
|
|
259
|
+
subject: { type: 'SERVICE_ACCOUNT', id: serviceAccountId },
|
|
260
|
+
scopes: ['orders:write'],
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Scopes narrow the subject's IAM permissions. They never grant a permission the subject does not already have.
|
|
268
|
+
|
|
269
|
+
## Add request middleware
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
export function requestAudit(): FortressPlugin {
|
|
273
|
+
return {
|
|
274
|
+
name: 'request-audit',
|
|
275
|
+
middleware: [{
|
|
276
|
+
path: '/orders/*',
|
|
277
|
+
position: 'after-rbac',
|
|
278
|
+
async handler(_plugin, context, next) {
|
|
279
|
+
await audit(
|
|
280
|
+
context.fortressSubject,
|
|
281
|
+
new URL(context.request.url).pathname,
|
|
282
|
+
);
|
|
283
|
+
await next();
|
|
284
|
+
},
|
|
285
|
+
}],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Positions:
|
|
291
|
+
|
|
292
|
+
| Position | Identity | Permission result | Use |
|
|
293
|
+
|---|---:|---:|---|
|
|
294
|
+
| `before-auth` | no | no | rate limits, request normalization |
|
|
295
|
+
| `after-auth` | yes | no | account state, identity-aware controls |
|
|
296
|
+
| `after-rbac` | yes | yes | authorized audit and request context |
|
|
297
|
+
|
|
298
|
+
## Extend JWT claims
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
export function tenantClaims(): FortressPlugin {
|
|
302
|
+
return {
|
|
303
|
+
name: 'tenant-claims',
|
|
304
|
+
async enrichTokenClaims(userId, { db }) {
|
|
305
|
+
const membership = await findMembership(db, userId);
|
|
306
|
+
return {
|
|
307
|
+
tenantId: membership.id,
|
|
308
|
+
tenantCode: membership.code,
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Claims are shallow-merged in plugin order. Later plugins overwrite duplicate keys and emit a development warning.
|
|
316
|
+
|
|
317
|
+
## Scope database access
|
|
318
|
+
|
|
319
|
+
`scopeRules` adds model filters. `wrapAdapter` replaces the request-scoped adapter when a plugin needs transaction or schema behavior.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
export function organizationScope(): FortressPlugin {
|
|
323
|
+
return {
|
|
324
|
+
name: 'organization-scope',
|
|
325
|
+
async scopeRules(userId, model) {
|
|
326
|
+
if (model !== 'post')
|
|
327
|
+
return null;
|
|
328
|
+
|
|
329
|
+
const organizationId = await organizationFor(userId);
|
|
330
|
+
return {
|
|
331
|
+
filters: [{ field: 'organizationId', operator: '=', value: organizationId }],
|
|
332
|
+
defaults: { organizationId },
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Read the scoped adapter from framework context:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
const db = await getScopedDb(c, 'post');
|
|
343
|
+
const posts = await db.findMany({ model: 'post' });
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Use the bundled [data-isolation plugin](plugins/data-isolation.md) for row-level filtering and [tenancy plugin](plugins/tenancy.md) for PostgreSQL schema isolation.
|
|
347
|
+
|
|
348
|
+
## Database contract
|
|
349
|
+
|
|
350
|
+
Plugins use the same adapter as core:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
interface DatabaseAdapter {
|
|
354
|
+
create(args): Promise<unknown>;
|
|
355
|
+
findOne(args): Promise<unknown | null>;
|
|
356
|
+
findMany(args): Promise<unknown[]>;
|
|
357
|
+
update(args): Promise<unknown | null>;
|
|
358
|
+
delete(args): Promise<void>;
|
|
359
|
+
count(args): Promise<number>;
|
|
360
|
+
transaction<T>(fn: (tx: DatabaseAdapter) => Promise<T>): Promise<T>;
|
|
361
|
+
rawQuery?<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
362
|
+
dialect?: 'sqlite' | 'pg';
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Portable `rawQuery` SQL uses `?` placeholders. The adapter translates them for its driver.
|
|
367
|
+
|
|
368
|
+
Core operators:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
'=' | '!=' | 'in' | 'gt' | 'lt' | 'gte' | 'lte' | 'isNull'
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Adapters may support additional operators. The Drizzle adapter also supports `like`.
|
|
375
|
+
|
|
376
|
+
## Auth data flow
|
|
377
|
+
|
|
378
|
+
### Login
|
|
379
|
+
|
|
380
|
+
1. Run `beforeLogin` hooks.
|
|
381
|
+
2. Resolve the login identifier.
|
|
382
|
+
3. verify user state and password with timing-safe miss behavior.
|
|
383
|
+
4. run post-auth gates.
|
|
384
|
+
5. enrich claims.
|
|
385
|
+
6. create the access token and refresh-token family.
|
|
386
|
+
7. run failure-contained after hooks.
|
|
387
|
+
8. emit auth observers.
|
|
388
|
+
|
|
389
|
+
### Refresh
|
|
390
|
+
|
|
391
|
+
1. hash and find the refresh token;
|
|
392
|
+
2. reject expiry, inactivity, absolute lifetime, or fingerprint policy failures;
|
|
393
|
+
3. detect reuse and revoke the family;
|
|
394
|
+
4. rotate the token in the same family;
|
|
395
|
+
5. refresh claims and session metadata;
|
|
396
|
+
6. run failure-contained after hooks;
|
|
397
|
+
7. return the new pair.
|
|
398
|
+
|
|
399
|
+
The database stores refresh-token hashes, not raw tokens.
|
|
400
|
+
|
|
401
|
+
## IAM data flow
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
await fortress.iam.checkPermission(subject, resource, action, context);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
The check:
|
|
408
|
+
|
|
409
|
+
1. verifies that a user subject still exists and is active;
|
|
410
|
+
2. loads global permissions from the optional cache or tenant permissions from the database;
|
|
411
|
+
3. collects role, group, and direct grants;
|
|
412
|
+
4. narrows with credential scopes;
|
|
413
|
+
5. evaluates resource/action wildcards and conditions;
|
|
414
|
+
6. applies `allow-only` or `deny-overrides`;
|
|
415
|
+
7. emits a synchronous permission-check event.
|
|
416
|
+
|
|
417
|
+
Tenant checks bypass the global permission cache. User activation is checked before cached grants, so deactivation denies immediately.
|
|
418
|
+
|
|
419
|
+
## Package boundaries
|
|
420
|
+
|
|
421
|
+
| Entry point | Purpose |
|
|
422
|
+
|---|---|
|
|
423
|
+
| `@bajustone/fortress` | core, schemas, endpoints, policy, migrations |
|
|
424
|
+
| `@bajustone/fortress/hono` | Hono mount, middleware, validation, protection |
|
|
425
|
+
| `@bajustone/fortress/express` | Express mount, middleware, validation, protection |
|
|
426
|
+
| `@bajustone/fortress/sveltekit` | handle hook, actions, locals, validation, protection |
|
|
427
|
+
| `@bajustone/fortress/drizzle` | adapter, SQLite/PG schemas, DB error mapping |
|
|
428
|
+
| `@bajustone/fortress/drizzle/pg` | PostgreSQL schema only |
|
|
429
|
+
| `@bajustone/fortress/testing` | in-memory adapter and drift checks |
|
|
430
|
+
| `@bajustone/fortress/otel` | OpenTelemetry provider adapter |
|
|
431
|
+
| `@bajustone/fortress/fetcher` | outbound client and schema tooling |
|
|
432
|
+
| `@bajustone/fortress/plugins/*` | optional plugins |
|
|
433
|
+
|
|
434
|
+
Core does not import framework adapters. Framework adapters only translate their request/response APIs to the web-standard core.
|