@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,435 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../adapters/database';
|
|
2
|
+
import type {
|
|
3
|
+
FortressUser,
|
|
4
|
+
LoginIdentifier,
|
|
5
|
+
PendingReason,
|
|
6
|
+
Permission,
|
|
7
|
+
PermissionInput,
|
|
8
|
+
ServiceAccount,
|
|
9
|
+
Subject,
|
|
10
|
+
} from './types';
|
|
11
|
+
import { normalizeEmail } from './auth/email';
|
|
12
|
+
|
|
13
|
+
// --- Stored types for typed queries ---
|
|
14
|
+
|
|
15
|
+
export interface StoredRefreshToken {
|
|
16
|
+
id: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
tokenFamily: string;
|
|
19
|
+
familyCreatedAt: Date;
|
|
20
|
+
successorTokenHash: string | null;
|
|
21
|
+
rotatedAt: Date | null;
|
|
22
|
+
isRevoked: boolean;
|
|
23
|
+
expiresAt: Date;
|
|
24
|
+
ipAddress: string | null;
|
|
25
|
+
userAgent: string | null;
|
|
26
|
+
deviceName: string | null;
|
|
27
|
+
lastActiveAt: Date | null;
|
|
28
|
+
fingerprintHash: string | null;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Hashed, short-lived state for completing a pending authentication flow. */
|
|
33
|
+
export interface StoredContinuation {
|
|
34
|
+
id: string;
|
|
35
|
+
userId: string;
|
|
36
|
+
tokenHash: string;
|
|
37
|
+
reason: PendingReason;
|
|
38
|
+
expiresAt: Date;
|
|
39
|
+
consumedAt: Date | null;
|
|
40
|
+
failedAttempts: number;
|
|
41
|
+
lastFailedAt: Date | null;
|
|
42
|
+
invalidatedAt: Date | null;
|
|
43
|
+
maxAttempts: number;
|
|
44
|
+
cooldownSeconds: number;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Interface ---
|
|
49
|
+
|
|
50
|
+
export interface InternalAdapter {
|
|
51
|
+
/** Resolve user via login_identifier lookup, falling back to direct email match */
|
|
52
|
+
findUserByIdentifier: (identifier: string) => Promise<(FortressUser & { passwordHash: string | null }) | null>;
|
|
53
|
+
/** Get group names for a user via group_user → group resolution */
|
|
54
|
+
getUserGroups: (userId: string) => Promise<string[]>;
|
|
55
|
+
/** Find a refresh token by its SHA-256 hash */
|
|
56
|
+
findRefreshTokenByHash: (tokenHash: string) => Promise<StoredRefreshToken | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve every permission that applies to a subject. For `USER` subjects,
|
|
59
|
+
* walks group memberships and unions their bindings. For `SERVICE_ACCOUNT`
|
|
60
|
+
* and other non-user subjects, only direct bindings on that subject are
|
|
61
|
+
* considered. Missing or inactive users and service accounts return an empty
|
|
62
|
+
* list (the existence/isActive gate is a second layer of defense alongside
|
|
63
|
+
* credential resolution).
|
|
64
|
+
*/
|
|
65
|
+
getSubjectPermissions: (subject: Subject, tenantId?: string) => Promise<Permission[]>;
|
|
66
|
+
/** Find an existing permission without creating it. */
|
|
67
|
+
findPermission: (input: PermissionInput) => Promise<Permission | null>;
|
|
68
|
+
/** Find an existing permission or create it if missing */
|
|
69
|
+
findOrCreatePermission: (input: PermissionInput) => Promise<Permission>;
|
|
70
|
+
/** Internal mutation variant that reports whether this call inserted the row. */
|
|
71
|
+
findOrCreatePermissionWithStatus: (input: PermissionInput) => Promise<{ permission: Permission; created: boolean }>;
|
|
72
|
+
/** Ensure a resource exists (no-op if already present) */
|
|
73
|
+
ensureResource: (name: string) => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Factory ---
|
|
77
|
+
|
|
78
|
+
function normalizePermission(permission: Permission): Permission {
|
|
79
|
+
return {
|
|
80
|
+
...permission,
|
|
81
|
+
conditions: typeof permission.conditions === 'string'
|
|
82
|
+
? JSON.parse(permission.conditions)
|
|
83
|
+
: permission.conditions,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stableJson(value: unknown): unknown {
|
|
88
|
+
if (Array.isArray(value))
|
|
89
|
+
return value.map(stableJson);
|
|
90
|
+
if (value && typeof value === 'object') {
|
|
91
|
+
const out: Record<string, unknown> = {};
|
|
92
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort())
|
|
93
|
+
out[key] = stableJson((value as Record<string, unknown>)[key]);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function serializePermissionConditions(
|
|
100
|
+
db: DatabaseAdapter,
|
|
101
|
+
conditions: PermissionInput['conditions'],
|
|
102
|
+
): string | PermissionInput['conditions'] | null {
|
|
103
|
+
if (!conditions)
|
|
104
|
+
return null;
|
|
105
|
+
// M8: normalize JSON key order before storage / lookup. SQLite compares
|
|
106
|
+
// text byte-for-byte; PostgreSQL JSONB normalizes object key order on the
|
|
107
|
+
// server, but returning a stable object here keeps adapter behavior
|
|
108
|
+
// consistent and makes tests deterministic.
|
|
109
|
+
const normalized = stableJson(conditions) as PermissionInput['conditions'];
|
|
110
|
+
// PostgreSQL schema stores JSONB; SQLite stores text.
|
|
111
|
+
return db.dialect === 'pg' ? normalized : JSON.stringify(normalized);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function findOrCreatePermissionWithStatus(
|
|
115
|
+
db: DatabaseAdapter,
|
|
116
|
+
input: PermissionInput,
|
|
117
|
+
): Promise<{ permission: Permission; created: boolean }> {
|
|
118
|
+
const conditions = serializePermissionConditions(db, input.conditions);
|
|
119
|
+
const where = [
|
|
120
|
+
{ field: 'resource', operator: '=' as const, value: input.resource },
|
|
121
|
+
{ field: 'action', operator: '=' as const, value: input.action },
|
|
122
|
+
{ field: 'effect', operator: '=' as const, value: input.effect ?? 'ALLOW' },
|
|
123
|
+
conditions == null
|
|
124
|
+
? { field: 'conditions', operator: 'isNull' as const, value: null }
|
|
125
|
+
: { field: 'conditions', operator: '=' as const, value: conditions },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const existing = await db.findOne<Permission>({ model: 'permission', where });
|
|
129
|
+
if (existing)
|
|
130
|
+
return { permission: normalizePermission(existing), created: false };
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const created = await db.create<Permission>({
|
|
134
|
+
model: 'permission',
|
|
135
|
+
data: {
|
|
136
|
+
resource: input.resource,
|
|
137
|
+
action: input.action,
|
|
138
|
+
effect: input.effect ?? 'ALLOW',
|
|
139
|
+
conditions,
|
|
140
|
+
description: `${input.action} ${input.resource}`,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return { permission: normalizePermission(created), created: true };
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
// A concurrent caller may win the unique-index race. Re-read its row;
|
|
147
|
+
// only the winner reports a creation event.
|
|
148
|
+
const winner = await db.findOne<Permission>({ model: 'permission', where });
|
|
149
|
+
if (winner)
|
|
150
|
+
return { permission: normalizePermission(winner), created: false };
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createInternalAdapter(db: DatabaseAdapter): InternalAdapter {
|
|
156
|
+
return {
|
|
157
|
+
async findUserByIdentifier(identifier: string): Promise<(FortressUser & { passwordHash: string | null }) | null> {
|
|
158
|
+
// Preserve case-sensitive username/phone semantics by trying the exact
|
|
159
|
+
// identifier first. Email gets a second canonical lookup so mixed-case
|
|
160
|
+
// and canonically-equivalent Unicode input resolves the stored identity.
|
|
161
|
+
let loginId = await db.findOne<LoginIdentifier>({
|
|
162
|
+
model: 'login_identifier',
|
|
163
|
+
where: [{ field: 'value', operator: '=', value: identifier }],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const canonicalEmail = normalizeEmail(identifier);
|
|
167
|
+
if (!loginId && canonicalEmail !== identifier) {
|
|
168
|
+
loginId = await db.findOne<LoginIdentifier>({
|
|
169
|
+
model: 'login_identifier',
|
|
170
|
+
where: [
|
|
171
|
+
{ field: 'type', operator: '=', value: 'email' },
|
|
172
|
+
{ field: 'value', operator: '=', value: canonicalEmail },
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (loginId) {
|
|
178
|
+
return db.findOne<FortressUser & { passwordHash: string | null }>({
|
|
179
|
+
model: 'user',
|
|
180
|
+
where: [{ field: 'id', operator: '=', value: loginId.userId }],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fallback for social-only users without a login_identifier row.
|
|
185
|
+
return db.findOne<FortressUser & { passwordHash: string | null }>({
|
|
186
|
+
model: 'user',
|
|
187
|
+
where: [{ field: 'email', operator: '=', value: canonicalEmail }],
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async getUserGroups(userId: string): Promise<string[]> {
|
|
192
|
+
const memberships = await db.findMany<{ groupId: string }>({
|
|
193
|
+
model: 'group_user',
|
|
194
|
+
where: [{ field: 'userId', operator: '=', value: userId }],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (memberships.length === 0)
|
|
198
|
+
return [];
|
|
199
|
+
|
|
200
|
+
const groupIds = memberships.map(m => m.groupId);
|
|
201
|
+
const groups = await db.findMany<{ name: string }>({
|
|
202
|
+
model: 'group',
|
|
203
|
+
where: [{ field: 'id', operator: 'in', value: groupIds }],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return groups.map(g => g.name);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async findRefreshTokenByHash(tokenHash: string): Promise<StoredRefreshToken | null> {
|
|
210
|
+
return db.findOne<StoredRefreshToken>({
|
|
211
|
+
model: 'refresh_token',
|
|
212
|
+
where: [{ field: 'tokenHash', operator: '=', value: tokenHash }],
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
async getSubjectPermissions(subject: Subject, tenantId?: string): Promise<Permission[]> {
|
|
217
|
+
const tenantFilter = tenantId != null;
|
|
218
|
+
const tenantlessFilter = tenantId == null;
|
|
219
|
+
const isUser = subject.type === 'USER';
|
|
220
|
+
|
|
221
|
+
// Missing or inactive principals never resolve permissions. This is a
|
|
222
|
+
// second line of defense behind credential resolution and also protects
|
|
223
|
+
// callers that invoke IAM APIs directly.
|
|
224
|
+
if (subject.type === 'USER') {
|
|
225
|
+
const user = await db.findOne<FortressUser>({
|
|
226
|
+
model: 'user',
|
|
227
|
+
where: [{ field: 'id', operator: '=', value: subject.id }],
|
|
228
|
+
});
|
|
229
|
+
if (!user || !user.isActive)
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
else if (subject.type === 'SERVICE_ACCOUNT') {
|
|
233
|
+
const sa = await db.findOne<ServiceAccount>({
|
|
234
|
+
model: 'service_account',
|
|
235
|
+
where: [{ field: 'id', operator: '=', value: subject.id }],
|
|
236
|
+
});
|
|
237
|
+
if (!sa || !sa.isActive)
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Optimized path: single JOIN query when rawQuery is available
|
|
242
|
+
if (db.rawQuery) {
|
|
243
|
+
const rbTenant = tenantFilter
|
|
244
|
+
? ' AND (rb.tenant_id = ? OR rb.tenant_id IS NULL)'
|
|
245
|
+
: ' AND rb.tenant_id IS NULL';
|
|
246
|
+
const dpbTenant = tenantFilter
|
|
247
|
+
? ' AND (dpb.tenant_id = ? OR dpb.tenant_id IS NULL)'
|
|
248
|
+
: ' AND dpb.tenant_id IS NULL';
|
|
249
|
+
|
|
250
|
+
// Role-binding predicate: always match the bare subject.
|
|
251
|
+
// For USER subjects, also union group memberships.
|
|
252
|
+
const rbPredicate = isUser
|
|
253
|
+
? `((rb.subject_type = 'USER' AND rb.subject_id = ?)
|
|
254
|
+
OR (rb.subject_type = 'GROUP' AND rb.subject_id IN (
|
|
255
|
+
SELECT gu.group_id FROM fortress_group_user gu WHERE gu.user_id = ?
|
|
256
|
+
)))`
|
|
257
|
+
: `(rb.subject_type = ? AND rb.subject_id = ?)`;
|
|
258
|
+
|
|
259
|
+
const dpbPredicate = isUser
|
|
260
|
+
? `((dpb.subject_type = 'USER' AND dpb.subject_id = ?)
|
|
261
|
+
OR (dpb.subject_type = 'GROUP' AND dpb.subject_id IN (
|
|
262
|
+
SELECT gu.group_id FROM fortress_group_user gu WHERE gu.user_id = ?
|
|
263
|
+
)))`
|
|
264
|
+
: `(dpb.subject_type = ? AND dpb.subject_id = ?)`;
|
|
265
|
+
|
|
266
|
+
const rbParams = isUser
|
|
267
|
+
? [subject.id, subject.id]
|
|
268
|
+
: [subject.type, subject.id];
|
|
269
|
+
const dpbParams = isUser
|
|
270
|
+
? [subject.id, subject.id]
|
|
271
|
+
: [subject.type, subject.id];
|
|
272
|
+
|
|
273
|
+
const params: unknown[] = [];
|
|
274
|
+
params.push(...rbParams);
|
|
275
|
+
if (tenantFilter)
|
|
276
|
+
params.push(tenantId);
|
|
277
|
+
params.push(...dpbParams);
|
|
278
|
+
if (tenantFilter)
|
|
279
|
+
params.push(tenantId);
|
|
280
|
+
|
|
281
|
+
const rows = await db.rawQuery<Permission>(
|
|
282
|
+
`SELECT DISTINCT p.id, p.resource, p.action, p.effect, p.conditions, p.description
|
|
283
|
+
FROM fortress_permission p
|
|
284
|
+
WHERE p.id IN (
|
|
285
|
+
-- Role-based permissions
|
|
286
|
+
SELECT rp.permission_id FROM fortress_role_permission rp
|
|
287
|
+
JOIN fortress_role_binding rb ON rb.role_id = rp.role_id
|
|
288
|
+
WHERE ${rbPredicate}${rbTenant}
|
|
289
|
+
UNION
|
|
290
|
+
-- Direct permission bindings
|
|
291
|
+
SELECT dpb.permission_id FROM fortress_direct_permission_binding dpb
|
|
292
|
+
WHERE ${dpbPredicate}${dpbTenant}
|
|
293
|
+
)
|
|
294
|
+
ORDER BY p.id`,
|
|
295
|
+
params,
|
|
296
|
+
);
|
|
297
|
+
return rows.map(normalizePermission);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Fallback: sequential findMany queries
|
|
301
|
+
// Helper to filter bindings by tenant (global bindings always included)
|
|
302
|
+
function matchesTenant<T extends { tenantId?: string | null }>(bindings: T[]): T[] {
|
|
303
|
+
if (tenantlessFilter)
|
|
304
|
+
return bindings.filter(b => b.tenantId == null);
|
|
305
|
+
return bindings.filter(b => b.tenantId == null || b.tenantId === tenantId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 1. Group memberships — USER subjects only. Non-user subjects aren't
|
|
309
|
+
// members of groups, so skip this lookup entirely.
|
|
310
|
+
let groupIds: string[] = [];
|
|
311
|
+
if (isUser) {
|
|
312
|
+
const groupMemberships = await db.findMany<{ groupId: string }>({
|
|
313
|
+
model: 'group_user',
|
|
314
|
+
where: [{ field: 'userId', operator: '=', value: subject.id }],
|
|
315
|
+
});
|
|
316
|
+
groupIds = groupMemberships.map(m => m.groupId);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 2. Role-based permission IDs — direct bindings on the subject.
|
|
320
|
+
const directRoleBindings = matchesTenant(await db.findMany<{ roleId: string; tenantId?: string | null }>({
|
|
321
|
+
model: 'role_binding',
|
|
322
|
+
where: [
|
|
323
|
+
{ field: 'subjectType', operator: '=', value: subject.type },
|
|
324
|
+
{ field: 'subjectId', operator: '=', value: subject.id },
|
|
325
|
+
],
|
|
326
|
+
}));
|
|
327
|
+
|
|
328
|
+
let groupRoleBindings: { roleId: string; tenantId?: string | null }[] = [];
|
|
329
|
+
if (groupIds.length > 0) {
|
|
330
|
+
groupRoleBindings = matchesTenant(await db.findMany<{ roleId: string; tenantId?: string | null }>({
|
|
331
|
+
model: 'role_binding',
|
|
332
|
+
where: [
|
|
333
|
+
{ field: 'subjectType', operator: '=', value: 'GROUP' },
|
|
334
|
+
{ field: 'subjectId', operator: 'in', value: groupIds },
|
|
335
|
+
],
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const roleIds = [...new Set([
|
|
340
|
+
...directRoleBindings.map(b => b.roleId),
|
|
341
|
+
...groupRoleBindings.map(b => b.roleId),
|
|
342
|
+
])];
|
|
343
|
+
|
|
344
|
+
let rolePermissionIds: string[] = [];
|
|
345
|
+
if (roleIds.length > 0) {
|
|
346
|
+
const rolePerms = await db.findMany<{ permissionId: string }>({
|
|
347
|
+
model: 'role_permission',
|
|
348
|
+
where: [{ field: 'roleId', operator: 'in', value: roleIds }],
|
|
349
|
+
});
|
|
350
|
+
rolePermissionIds = rolePerms.map(rp => rp.permissionId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 3. Direct permission binding IDs — on the subject directly.
|
|
354
|
+
const directSubjectBindings = matchesTenant(await db.findMany<{ permissionId: string; tenantId?: string | null }>({
|
|
355
|
+
model: 'direct_permission_binding',
|
|
356
|
+
where: [
|
|
357
|
+
{ field: 'subjectType', operator: '=', value: subject.type },
|
|
358
|
+
{ field: 'subjectId', operator: '=', value: subject.id },
|
|
359
|
+
],
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
let directGroupBindings: { permissionId: string; tenantId?: string | null }[] = [];
|
|
363
|
+
if (groupIds.length > 0) {
|
|
364
|
+
directGroupBindings = matchesTenant(await db.findMany<{ permissionId: string; tenantId?: string | null }>({
|
|
365
|
+
model: 'direct_permission_binding',
|
|
366
|
+
where: [
|
|
367
|
+
{ field: 'subjectType', operator: '=', value: 'GROUP' },
|
|
368
|
+
{ field: 'subjectId', operator: 'in', value: groupIds },
|
|
369
|
+
],
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 4. Merge and deduplicate permission IDs
|
|
374
|
+
const allPermissionIds = [...new Set([
|
|
375
|
+
...rolePermissionIds,
|
|
376
|
+
...directSubjectBindings.map(b => b.permissionId),
|
|
377
|
+
...directGroupBindings.map(b => b.permissionId),
|
|
378
|
+
])];
|
|
379
|
+
|
|
380
|
+
if (allPermissionIds.length === 0)
|
|
381
|
+
return [];
|
|
382
|
+
|
|
383
|
+
// 5. Fetch actual permissions
|
|
384
|
+
const permissions = await db.findMany<Permission>({
|
|
385
|
+
model: 'permission',
|
|
386
|
+
where: [{ field: 'id', operator: 'in', value: allPermissionIds }],
|
|
387
|
+
});
|
|
388
|
+
return permissions.map(normalizePermission);
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
async findPermission(input: PermissionInput): Promise<Permission | null> {
|
|
392
|
+
const conditions = serializePermissionConditions(db, input.conditions);
|
|
393
|
+
const permission = await db.findOne<Permission>({
|
|
394
|
+
model: 'permission',
|
|
395
|
+
where: [
|
|
396
|
+
{ field: 'resource', operator: '=', value: input.resource },
|
|
397
|
+
{ field: 'action', operator: '=', value: input.action },
|
|
398
|
+
{ field: 'effect', operator: '=', value: input.effect ?? 'ALLOW' },
|
|
399
|
+
conditions == null
|
|
400
|
+
? { field: 'conditions', operator: 'isNull', value: null }
|
|
401
|
+
: { field: 'conditions', operator: '=', value: conditions },
|
|
402
|
+
],
|
|
403
|
+
});
|
|
404
|
+
return permission ? normalizePermission(permission) : null;
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
async findOrCreatePermission(input: PermissionInput): Promise<Permission> {
|
|
408
|
+
return (await findOrCreatePermissionWithStatus(db, input)).permission;
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
findOrCreatePermissionWithStatus(input: PermissionInput) {
|
|
412
|
+
return findOrCreatePermissionWithStatus(db, input);
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async ensureResource(name: string): Promise<void> {
|
|
416
|
+
const where = [{ field: 'name', operator: '=' as const, value: name }];
|
|
417
|
+
const existing = await db.findOne<{ name: string }>({ model: 'resource', where });
|
|
418
|
+
if (existing)
|
|
419
|
+
return;
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
await db.create({ model: 'resource', data: { name } });
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
// find-then-create is not atomic — a concurrent caller may have
|
|
426
|
+
// inserted the same resource name (unique). That's the desired end
|
|
427
|
+
// state, so swallow the conflict if the row now exists; re-throw
|
|
428
|
+
// otherwise.
|
|
429
|
+
const winner = await db.findOne<{ name: string }>({ model: 'resource', where });
|
|
430
|
+
if (!winner)
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema types (draft 2020-12 subset) + FortressSchema.
|
|
3
|
+
*
|
|
4
|
+
* FortressSchema<T> extends JSONSchema with Standard Schema V1 support,
|
|
5
|
+
* giving each schema: JSON Schema fields (OpenAPI), runtime validation,
|
|
6
|
+
* and TypeScript type inference — all from one object.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { StandardSchemaV1 } from './standard-schema';
|
|
10
|
+
|
|
11
|
+
/** A subset of JSON Schema (draft 2020-12) sufficient for fortress endpoint definitions and OpenAPI generation. */
|
|
12
|
+
export interface JSONSchema {
|
|
13
|
+
type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
|
|
14
|
+
properties?: Record<string, JSONSchema>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
items?: JSONSchema;
|
|
17
|
+
enum?: (string | number | boolean | null)[];
|
|
18
|
+
const?: string | number | boolean | null;
|
|
19
|
+
format?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
default?: unknown;
|
|
22
|
+
nullable?: boolean;
|
|
23
|
+
oneOf?: JSONSchema[];
|
|
24
|
+
anyOf?: JSONSchema[];
|
|
25
|
+
allOf?: JSONSchema[];
|
|
26
|
+
$ref?: string;
|
|
27
|
+
discriminator?: { propertyName: string; mapping?: Record<string, string> };
|
|
28
|
+
additionalProperties?: boolean | JSONSchema;
|
|
29
|
+
minimum?: number;
|
|
30
|
+
maximum?: number;
|
|
31
|
+
minLength?: number;
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
pattern?: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A JSON Schema object that also implements Standard Schema V1.
|
|
39
|
+
*
|
|
40
|
+
* - JSON Schema fields → OpenAPI 3.1 spec generation
|
|
41
|
+
* - `~standard.validate()` → runtime validation
|
|
42
|
+
* - `StandardSchemaV1.InferOutput<typeof schema>` → TypeScript type inference
|
|
43
|
+
*/
|
|
44
|
+
export type FortressSchema<T = unknown> = JSONSchema & StandardSchemaV1<T, T>;
|
|
45
|
+
|
|
46
|
+
/** Extract the TypeScript type from a FortressSchema or StandardSchemaV1. */
|
|
47
|
+
export type Infer<T> = T extends StandardSchemaV1<any, infer O> ? O : unknown;
|
|
48
|
+
|
|
49
|
+
/** Flatten intersection types for clean IDE tooltips. */
|
|
50
|
+
export type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`route manifest > snapshots the core auth manifest subset 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"classification": "authenticated",
|
|
7
|
+
"csrfApplicable": true,
|
|
8
|
+
"handler": "removeLoginIdentifier",
|
|
9
|
+
"method": "DELETE",
|
|
10
|
+
"mounted": true,
|
|
11
|
+
"path": "/auth/identifiers",
|
|
12
|
+
"plugin": "auth",
|
|
13
|
+
"rateLimited": false,
|
|
14
|
+
"security": [
|
|
15
|
+
"bearer",
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"classification": "authenticated",
|
|
20
|
+
"csrfApplicable": true,
|
|
21
|
+
"handler": "revokeAllOtherSessions",
|
|
22
|
+
"method": "DELETE",
|
|
23
|
+
"mounted": true,
|
|
24
|
+
"path": "/auth/sessions",
|
|
25
|
+
"plugin": "auth",
|
|
26
|
+
"rateLimited": false,
|
|
27
|
+
"security": [
|
|
28
|
+
"bearer",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"classification": "authenticated",
|
|
33
|
+
"csrfApplicable": true,
|
|
34
|
+
"handler": "revokeSession",
|
|
35
|
+
"method": "DELETE",
|
|
36
|
+
"mounted": true,
|
|
37
|
+
"path": "/auth/sessions/:id",
|
|
38
|
+
"plugin": "auth",
|
|
39
|
+
"rateLimited": false,
|
|
40
|
+
"security": [
|
|
41
|
+
"bearer",
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"classification": "authenticated",
|
|
46
|
+
"csrfApplicable": false,
|
|
47
|
+
"handler": "getLoginIdentifiers",
|
|
48
|
+
"method": "GET",
|
|
49
|
+
"mounted": true,
|
|
50
|
+
"path": "/auth/identifiers",
|
|
51
|
+
"plugin": "auth",
|
|
52
|
+
"rateLimited": false,
|
|
53
|
+
"security": [
|
|
54
|
+
"bearer",
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"classification": "authenticated",
|
|
59
|
+
"csrfApplicable": false,
|
|
60
|
+
"handler": "me",
|
|
61
|
+
"method": "GET",
|
|
62
|
+
"mounted": true,
|
|
63
|
+
"path": "/auth/me",
|
|
64
|
+
"plugin": "auth",
|
|
65
|
+
"rateLimited": false,
|
|
66
|
+
"security": [
|
|
67
|
+
"bearer",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"classification": "authenticated",
|
|
72
|
+
"csrfApplicable": false,
|
|
73
|
+
"handler": "listSessions",
|
|
74
|
+
"method": "GET",
|
|
75
|
+
"mounted": true,
|
|
76
|
+
"path": "/auth/sessions",
|
|
77
|
+
"plugin": "auth",
|
|
78
|
+
"rateLimited": false,
|
|
79
|
+
"security": [
|
|
80
|
+
"bearer",
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"classification": "public",
|
|
85
|
+
"csrfApplicable": true,
|
|
86
|
+
"handler": "verifyTwoFactor",
|
|
87
|
+
"method": "POST",
|
|
88
|
+
"mounted": true,
|
|
89
|
+
"path": "/auth/2fa/verify",
|
|
90
|
+
"plugin": "auth",
|
|
91
|
+
"rateLimited": false,
|
|
92
|
+
"security": [
|
|
93
|
+
"none",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"classification": "authenticated",
|
|
98
|
+
"csrfApplicable": true,
|
|
99
|
+
"handler": "addLoginIdentifier",
|
|
100
|
+
"method": "POST",
|
|
101
|
+
"mounted": true,
|
|
102
|
+
"path": "/auth/identifiers",
|
|
103
|
+
"plugin": "auth",
|
|
104
|
+
"rateLimited": false,
|
|
105
|
+
"security": [
|
|
106
|
+
"bearer",
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"classification": "rbac",
|
|
111
|
+
"csrfApplicable": true,
|
|
112
|
+
"handler": "impersonate",
|
|
113
|
+
"method": "POST",
|
|
114
|
+
"mounted": true,
|
|
115
|
+
"path": "/auth/impersonate",
|
|
116
|
+
"permission": {
|
|
117
|
+
"action": "impersonate",
|
|
118
|
+
"resource": "fortress",
|
|
119
|
+
},
|
|
120
|
+
"plugin": "auth",
|
|
121
|
+
"rateLimited": false,
|
|
122
|
+
"security": [
|
|
123
|
+
"bearer",
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"classification": "public",
|
|
128
|
+
"csrfApplicable": true,
|
|
129
|
+
"handler": "login",
|
|
130
|
+
"method": "POST",
|
|
131
|
+
"mounted": true,
|
|
132
|
+
"path": "/auth/login",
|
|
133
|
+
"plugin": "auth",
|
|
134
|
+
"rateLimited": false,
|
|
135
|
+
"security": [
|
|
136
|
+
"none",
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"classification": "public",
|
|
141
|
+
"csrfApplicable": true,
|
|
142
|
+
"handler": "logout",
|
|
143
|
+
"method": "POST",
|
|
144
|
+
"mounted": true,
|
|
145
|
+
"path": "/auth/logout",
|
|
146
|
+
"plugin": "auth",
|
|
147
|
+
"rateLimited": false,
|
|
148
|
+
"security": [
|
|
149
|
+
"none",
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"classification": "public",
|
|
154
|
+
"csrfApplicable": true,
|
|
155
|
+
"handler": "verifyMagicLink",
|
|
156
|
+
"method": "POST",
|
|
157
|
+
"mounted": true,
|
|
158
|
+
"path": "/auth/magic-link/verify",
|
|
159
|
+
"plugin": "auth",
|
|
160
|
+
"rateLimited": false,
|
|
161
|
+
"security": [
|
|
162
|
+
"none",
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"classification": "public",
|
|
167
|
+
"csrfApplicable": true,
|
|
168
|
+
"handler": "refresh",
|
|
169
|
+
"method": "POST",
|
|
170
|
+
"mounted": true,
|
|
171
|
+
"path": "/auth/refresh",
|
|
172
|
+
"plugin": "auth",
|
|
173
|
+
"rateLimited": false,
|
|
174
|
+
"security": [
|
|
175
|
+
"none",
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"classification": "public",
|
|
180
|
+
"csrfApplicable": true,
|
|
181
|
+
"handler": "createUser",
|
|
182
|
+
"method": "POST",
|
|
183
|
+
"mounted": true,
|
|
184
|
+
"path": "/auth/register",
|
|
185
|
+
"plugin": "auth",
|
|
186
|
+
"rateLimited": false,
|
|
187
|
+
"security": [
|
|
188
|
+
"none",
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
`;
|