@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,75 @@
|
|
|
1
|
+
import type { ApiKeyMethods } from '../plugins/api-key';
|
|
2
|
+
import type { AuditLogMethods } from '../plugins/audit-log';
|
|
3
|
+
import type { DataIsolationMethods } from '../plugins/data-isolation';
|
|
4
|
+
import type { EmailVerificationMethods } from '../plugins/email-verification';
|
|
5
|
+
import type { MagicLinkMethods } from '../plugins/magic-link';
|
|
6
|
+
import type { OAuthMethods } from '../plugins/oauth';
|
|
7
|
+
import type { OpenAPIMethods } from '../plugins/openapi';
|
|
8
|
+
import type { SocialLoginMethods } from '../plugins/social-login';
|
|
9
|
+
import type { TenancyMethods } from '../plugins/tenancy';
|
|
10
|
+
import type { TwoFactorMethods } from '../plugins/two-factor';
|
|
11
|
+
import type { WebAuthnMethods } from '../plugins/webauthn';
|
|
12
|
+
import type { EndpointDefinition, InferEndpointCallInput, InferEndpointSuccessResponse } from './endpoint';
|
|
13
|
+
import type { CallOptions } from './http/call';
|
|
14
|
+
import type { FortressPlugin } from './plugin';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type-level map of every built-in plugin name to its method-surface
|
|
18
|
+
* interface. {@link InferPlugins} uses this to expose typed plugin methods
|
|
19
|
+
* on the fortress instance.
|
|
20
|
+
*/
|
|
21
|
+
export interface PluginMethodsMap {
|
|
22
|
+
'api-key': ApiKeyMethods;
|
|
23
|
+
'audit-log': AuditLogMethods;
|
|
24
|
+
'data-isolation': DataIsolationMethods;
|
|
25
|
+
'email-verification': EmailVerificationMethods;
|
|
26
|
+
'magic-link': MagicLinkMethods;
|
|
27
|
+
'oauth': OAuthMethods;
|
|
28
|
+
'openapi': OpenAPIMethods;
|
|
29
|
+
'social-login': SocialLoginMethods;
|
|
30
|
+
'tenancy': TenancyMethods;
|
|
31
|
+
'two-factor': TwoFactorMethods;
|
|
32
|
+
'webauthn': WebAuthnMethods;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Infer the typed plugin-methods record from a `plugins` tuple passed to {@link createFortress}. */
|
|
36
|
+
export type InferPlugins<T extends readonly FortressPlugin[]> = {
|
|
37
|
+
[P in T[number] as P['name']]: P['name'] extends keyof PluginMethodsMap
|
|
38
|
+
? PluginMethodsMap[P['name']]
|
|
39
|
+
: Record<string, (...args: any[]) => any>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ── Typed call map helpers ──────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** Distributes a union into an intersection — the classic TS trick. Used by {@link InferPluginCallMap}. */
|
|
45
|
+
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Turn a record of {@link EndpointDefinition}s into a record of typed
|
|
49
|
+
* callables: each key becomes a function whose input is the endpoint's
|
|
50
|
+
* inferred body+query+params intersection and whose output is the inferred
|
|
51
|
+
* success-response body.
|
|
52
|
+
*/
|
|
53
|
+
export type CallableForEndpoints<E> = E extends Record<string, EndpointDefinition<any, any, any, any>>
|
|
54
|
+
? {
|
|
55
|
+
[K in keyof E]: (
|
|
56
|
+
input: InferEndpointCallInput<E[K]>,
|
|
57
|
+
options?: CallOptions,
|
|
58
|
+
) => Promise<InferEndpointSuccessResponse<E[K]>>;
|
|
59
|
+
}
|
|
60
|
+
: Record<string, never>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Infer the typed call surface contributed by a plugins tuple. Walks every
|
|
64
|
+
* plugin's `routes` record (if present) and unions the callables into one
|
|
65
|
+
* flat object keyed by handler name.
|
|
66
|
+
*/
|
|
67
|
+
export type InferPluginCallMap<T extends readonly FortressPlugin[]> = UnionToIntersection<
|
|
68
|
+
T[number] extends infer P
|
|
69
|
+
? P extends { routes?: infer R }
|
|
70
|
+
? R extends Record<string, EndpointDefinition<any, any, any, any>>
|
|
71
|
+
? CallableForEndpoints<R>
|
|
72
|
+
: Record<string, never>
|
|
73
|
+
: Record<string, never>
|
|
74
|
+
: Record<string, never>
|
|
75
|
+
>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* eslint-disable ts/no-unsafe-function-type -- test uses Function type for plugin methods */
|
|
2
|
+
import type { DatabaseAdapter } from '../adapters/database';
|
|
3
|
+
import type { FortressConfig } from './config';
|
|
4
|
+
import type { FortressPlugin } from './plugin';
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
chainAdapterWrappers,
|
|
9
|
+
collectScopeRules,
|
|
10
|
+
executePluginMiddleware,
|
|
11
|
+
mergeTokenClaims,
|
|
12
|
+
processPlugins,
|
|
13
|
+
wrapAdapterWithScopeRules,
|
|
14
|
+
} from './plugin-runner';
|
|
15
|
+
|
|
16
|
+
const mockDb = {} as DatabaseAdapter;
|
|
17
|
+
const mockConfig = { jwt: { key: 'plugin-runner-test-secret-32ch!!' }, database: mockDb } as FortressConfig;
|
|
18
|
+
|
|
19
|
+
function testPlugin(overrides: Partial<FortressPlugin> = {}): FortressPlugin {
|
|
20
|
+
return { name: 'test-plugin', ...overrides };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('processPlugins', () => {
|
|
24
|
+
it('returns empty object for plugins with no methods', () => {
|
|
25
|
+
const result = processPlugins([testPlugin()], mockDb, mockConfig);
|
|
26
|
+
expect(result['test-plugin']).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('exposes plugin methods by plugin name', () => {
|
|
30
|
+
const plugin = testPlugin({
|
|
31
|
+
name: 'my-plugin',
|
|
32
|
+
methods: () => ({
|
|
33
|
+
greet: (name: string) => `hello ${name}`,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
const result = processPlugins([plugin], mockDb, mockConfig);
|
|
37
|
+
expect((result['my-plugin'].greet as Function)('world')).toBe('hello world');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('passes PluginContext to methods factory', () => {
|
|
41
|
+
const methodsFn = vi.fn(() => ({}));
|
|
42
|
+
processPlugins([testPlugin({ methods: methodsFn })], mockDb, mockConfig);
|
|
43
|
+
expect(methodsFn).toHaveBeenCalledWith({ db: mockDb, config: mockConfig });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('handles multiple plugins', () => {
|
|
47
|
+
const plugins = [
|
|
48
|
+
testPlugin({ name: 'a', methods: () => ({ foo: () => 1 }) }),
|
|
49
|
+
testPlugin({ name: 'b', methods: () => ({ bar: () => 2 }) }),
|
|
50
|
+
];
|
|
51
|
+
const result = processPlugins(plugins, mockDb, mockConfig);
|
|
52
|
+
expect(Object.keys(result)).toEqual(['a', 'b']);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('chainAdapterWrappers', () => {
|
|
57
|
+
it('returns base adapter when no plugins wrap', () => {
|
|
58
|
+
const result = chainAdapterWrappers([testPlugin()], mockDb, {});
|
|
59
|
+
expect(result).toBe(mockDb);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('chains wrappers in registration order', () => {
|
|
63
|
+
const calls: string[] = [];
|
|
64
|
+
const plugins = [
|
|
65
|
+
testPlugin({
|
|
66
|
+
name: 'first',
|
|
67
|
+
wrapAdapter: (adapter) => {
|
|
68
|
+
calls.push('first');
|
|
69
|
+
return { ...adapter, _first: true } as unknown as DatabaseAdapter;
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
testPlugin({
|
|
73
|
+
name: 'second',
|
|
74
|
+
wrapAdapter: (adapter) => {
|
|
75
|
+
calls.push('second');
|
|
76
|
+
return { ...adapter, _second: true } as unknown as DatabaseAdapter;
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const result = chainAdapterWrappers(plugins, mockDb, {}) as unknown as Record<string, unknown>;
|
|
82
|
+
expect(calls).toEqual(['first', 'second']);
|
|
83
|
+
expect(result._first).toBe(true);
|
|
84
|
+
expect(result._second).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('passes request context to wrapAdapter', () => {
|
|
88
|
+
const wrapFn = vi.fn(adapter => adapter);
|
|
89
|
+
const plugin = testPlugin({ wrapAdapter: wrapFn });
|
|
90
|
+
const ctx = { tenantId: '5' };
|
|
91
|
+
|
|
92
|
+
chainAdapterWrappers([plugin], mockDb, ctx);
|
|
93
|
+
expect(wrapFn).toHaveBeenCalledWith(mockDb, ctx);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('mergeTokenClaims', () => {
|
|
98
|
+
it('returns empty object when no plugins enrich claims', async () => {
|
|
99
|
+
const result = await mergeTokenClaims([testPlugin()], '1', { db: mockDb, config: mockConfig });
|
|
100
|
+
expect(result).toEqual({});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('merges claims from multiple plugins', async () => {
|
|
104
|
+
const plugins = [
|
|
105
|
+
testPlugin({
|
|
106
|
+
name: 'tenancy',
|
|
107
|
+
enrichTokenClaims: async () => ({ tenantId: '5' }),
|
|
108
|
+
}),
|
|
109
|
+
testPlugin({
|
|
110
|
+
name: 'custom',
|
|
111
|
+
enrichTokenClaims: async () => ({ role: 'admin' }),
|
|
112
|
+
}),
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const result = await mergeTokenClaims(plugins, '1', { db: mockDb, config: mockConfig });
|
|
116
|
+
expect(result).toEqual({ tenantId: '5', role: 'admin' });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('later plugin wins on key conflict', async () => {
|
|
120
|
+
const plugins = [
|
|
121
|
+
testPlugin({
|
|
122
|
+
name: 'a',
|
|
123
|
+
enrichTokenClaims: async () => ({ key: 'first' }),
|
|
124
|
+
}),
|
|
125
|
+
testPlugin({
|
|
126
|
+
name: 'b',
|
|
127
|
+
enrichTokenClaims: async () => ({ key: 'second' }),
|
|
128
|
+
}),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const result = await mergeTokenClaims(plugins, '1', { db: mockDb, config: mockConfig });
|
|
132
|
+
expect(result.key).toBe('second');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('executePluginMiddleware', () => {
|
|
137
|
+
it('canonicalizes double and trailing slashes before path matching', async () => {
|
|
138
|
+
const handler = vi.fn(async (_ctx, _request, next) => next());
|
|
139
|
+
const plugin = testPlugin({
|
|
140
|
+
middleware: [{ position: 'before-auth', path: '/auth/login', handler }],
|
|
141
|
+
});
|
|
142
|
+
const request = {
|
|
143
|
+
request: new Request('http://localhost/auth//login/'),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await executePluginMiddleware(
|
|
147
|
+
[plugin],
|
|
148
|
+
'before-auth',
|
|
149
|
+
'/auth//login/',
|
|
150
|
+
{ db: mockDb, config: mockConfig },
|
|
151
|
+
request,
|
|
152
|
+
);
|
|
153
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('collectScopeRules', () => {
|
|
158
|
+
it('returns null when no plugins have scope rules', async () => {
|
|
159
|
+
const result = await collectScopeRules([testPlugin()], '1', 'sale', { db: mockDb, config: mockConfig });
|
|
160
|
+
expect(result).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns null when plugin returns null for the model', async () => {
|
|
164
|
+
const plugin = testPlugin({
|
|
165
|
+
scopeRules: async () => null,
|
|
166
|
+
});
|
|
167
|
+
const result = await collectScopeRules([plugin], '1', 'sale', { db: mockDb, config: mockConfig });
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('collects filters and defaults from a single plugin', async () => {
|
|
172
|
+
const plugin = testPlugin({
|
|
173
|
+
scopeRules: async () => ({
|
|
174
|
+
filters: [{ field: 'siteId', operator: '=', value: 3 }],
|
|
175
|
+
defaults: { siteId: 3 },
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = await collectScopeRules([plugin], '1', 'sale', { db: mockDb, config: mockConfig });
|
|
180
|
+
expect(result).toEqual({
|
|
181
|
+
filters: [{ field: 'siteId', operator: '=', value: 3 }],
|
|
182
|
+
defaults: { siteId: 3 },
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('stacks filters and merges defaults from multiple plugins', async () => {
|
|
187
|
+
const plugins = [
|
|
188
|
+
testPlugin({
|
|
189
|
+
name: 'org',
|
|
190
|
+
scopeRules: async () => ({
|
|
191
|
+
filters: [{ field: 'orgId', operator: '=', value: 7 }],
|
|
192
|
+
defaults: { orgId: 7 },
|
|
193
|
+
}),
|
|
194
|
+
}),
|
|
195
|
+
testPlugin({
|
|
196
|
+
name: 'site',
|
|
197
|
+
scopeRules: async () => ({
|
|
198
|
+
filters: [{ field: 'siteId', operator: '=', value: 3 }],
|
|
199
|
+
defaults: { siteId: 3 },
|
|
200
|
+
}),
|
|
201
|
+
}),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const result = await collectScopeRules(plugins, '1', 'sale', { db: mockDb, config: mockConfig });
|
|
205
|
+
expect(result?.filters).toHaveLength(2);
|
|
206
|
+
expect(result?.defaults).toEqual({ orgId: 7, siteId: 3 });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('wrapAdapterWithScopeRules', () => {
|
|
211
|
+
it('forces scoped defaults on create and rejects scoped-field updates', async () => {
|
|
212
|
+
const adapter = {
|
|
213
|
+
...mockDb,
|
|
214
|
+
create: vi.fn(async params => params.data),
|
|
215
|
+
update: vi.fn(async params => params.data),
|
|
216
|
+
transaction: async (fn: (tx: DatabaseAdapter) => Promise<unknown>) => fn(adapter as unknown as DatabaseAdapter),
|
|
217
|
+
} as unknown as DatabaseAdapter & { create: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
|
|
218
|
+
|
|
219
|
+
const scoped = wrapAdapterWithScopeRules(adapter, {
|
|
220
|
+
filters: [{ field: 'orgId', operator: '=', value: 'org-1' }],
|
|
221
|
+
defaults: { orgId: 'org-1' },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await scoped.create({ model: 'post', data: { title: 'hello', orgId: 'attacker-org' } });
|
|
225
|
+
expect(adapter.create).toHaveBeenCalledWith({ model: 'post', data: { title: 'hello', orgId: 'org-1' } });
|
|
226
|
+
|
|
227
|
+
await expect(scoped.update({
|
|
228
|
+
model: 'post',
|
|
229
|
+
where: [{ field: 'id', operator: '=', value: '1' }],
|
|
230
|
+
data: { orgId: 'attacker-org' },
|
|
231
|
+
})).rejects.toThrow('Cannot update scoped field \'orgId\'');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../adapters/database';
|
|
2
|
+
import type { ScopeRule, WhereClause } from '../adapters/database/types';
|
|
3
|
+
import type { AuthService } from './auth/auth-service';
|
|
4
|
+
import type { FortressConfig } from './config';
|
|
5
|
+
import type { PluginRequestContext } from './http/plugin-middleware';
|
|
6
|
+
import type { IamService } from './iam/iam-service';
|
|
7
|
+
import type { FortressLogger } from './observability/logger';
|
|
8
|
+
import type { FortressPlugin, MiddlewareDefinition, PluginContext } from './plugin';
|
|
9
|
+
import { Errors } from './errors';
|
|
10
|
+
import { canonicalizePath } from './http/match';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Process registered plugins and return their exposed methods.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function processPlugins(
|
|
17
|
+
plugins: readonly FortressPlugin[],
|
|
18
|
+
db: DatabaseAdapter,
|
|
19
|
+
config: FortressConfig,
|
|
20
|
+
auth?: AuthService,
|
|
21
|
+
iam?: IamService,
|
|
22
|
+
logger?: FortressLogger,
|
|
23
|
+
// eslint-disable-next-line ts/no-unsafe-function-type -- plugin methods are dynamically typed
|
|
24
|
+
): Record<string, Record<string, Function>> {
|
|
25
|
+
const ctx: PluginContext = { db, config, auth, iam, logger };
|
|
26
|
+
// eslint-disable-next-line ts/no-unsafe-function-type -- plugin methods are dynamically typed
|
|
27
|
+
const result: Record<string, Record<string, Function>> = {};
|
|
28
|
+
|
|
29
|
+
for (const plugin of plugins) {
|
|
30
|
+
result[plugin.name] = plugin.methods?.(ctx) ?? {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Chain wrapAdapter from all plugins in registration order.
|
|
38
|
+
* Each wrapper receives the result of the previous.
|
|
39
|
+
*/
|
|
40
|
+
export function chainAdapterWrappers(
|
|
41
|
+
plugins: readonly FortressPlugin[],
|
|
42
|
+
baseAdapter: DatabaseAdapter,
|
|
43
|
+
requestContext: Record<string, unknown>,
|
|
44
|
+
): DatabaseAdapter {
|
|
45
|
+
let adapter = baseAdapter;
|
|
46
|
+
|
|
47
|
+
for (const plugin of plugins) {
|
|
48
|
+
if (plugin.wrapAdapter) {
|
|
49
|
+
adapter = plugin.wrapAdapter(adapter, requestContext);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return adapter;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Collect and merge enrichTokenClaims from all plugins.
|
|
58
|
+
* Later plugins override earlier ones on key conflicts.
|
|
59
|
+
*/
|
|
60
|
+
export async function mergeTokenClaims(
|
|
61
|
+
plugins: readonly FortressPlugin[],
|
|
62
|
+
userId: string,
|
|
63
|
+
ctx: PluginContext,
|
|
64
|
+
): Promise<Record<string, unknown>> {
|
|
65
|
+
const merged: Record<string, unknown> = {};
|
|
66
|
+
|
|
67
|
+
for (const plugin of plugins) {
|
|
68
|
+
if (plugin.enrichTokenClaims) {
|
|
69
|
+
const claims = await plugin.enrichTokenClaims(userId, ctx);
|
|
70
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
71
|
+
for (const key of Object.keys(claims)) {
|
|
72
|
+
if (key in merged) {
|
|
73
|
+
ctx.logger?.warn(
|
|
74
|
+
{ plugin: plugin.name, claim: key },
|
|
75
|
+
`plugin overwrites token claim '${key}'`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
Object.assign(merged, claims);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return merged;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collect and stack scopeRules from all plugins for a given model.
|
|
89
|
+
* All filters are AND'd together. All defaults are merged.
|
|
90
|
+
*/
|
|
91
|
+
export async function collectScopeRules(
|
|
92
|
+
plugins: readonly FortressPlugin[],
|
|
93
|
+
userId: string,
|
|
94
|
+
model: string,
|
|
95
|
+
ctx: PluginContext,
|
|
96
|
+
): Promise<ScopeRule | null> {
|
|
97
|
+
const allFilters: WhereClause[] = [];
|
|
98
|
+
const allDefaults: Record<string, unknown> = {};
|
|
99
|
+
|
|
100
|
+
for (const plugin of plugins) {
|
|
101
|
+
if (!plugin.scopeRules) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const rule = await plugin.scopeRules(userId, model, ctx);
|
|
105
|
+
if (!rule) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
allFilters.push(...rule.filters);
|
|
109
|
+
Object.assign(allDefaults, rule.defaults);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (allFilters.length === 0 && Object.keys(allDefaults).length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { filters: allFilters, defaults: allDefaults };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Wrap a DatabaseAdapter to auto-apply scope rules on every operation.
|
|
121
|
+
* Reads (findOne, findMany, count) get extra WHERE clauses.
|
|
122
|
+
* Writes (create) get default values merged into data.
|
|
123
|
+
* Mutations (update, delete) get extra WHERE clauses to prevent cross-scope changes.
|
|
124
|
+
*/
|
|
125
|
+
export function wrapAdapterWithScopeRules(
|
|
126
|
+
adapter: DatabaseAdapter,
|
|
127
|
+
scopeRule: ScopeRule,
|
|
128
|
+
): DatabaseAdapter {
|
|
129
|
+
const { filters, defaults } = scopeRule;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
...adapter,
|
|
133
|
+
|
|
134
|
+
create: <T>(params: {
|
|
135
|
+
model: string;
|
|
136
|
+
data: Record<string, unknown>;
|
|
137
|
+
}): Promise<T> =>
|
|
138
|
+
adapter.create<T>({
|
|
139
|
+
...params,
|
|
140
|
+
data: { ...params.data, ...defaults },
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
findOne: <T>(params: {
|
|
144
|
+
model: string;
|
|
145
|
+
where: WhereClause[];
|
|
146
|
+
}): Promise<T | null> =>
|
|
147
|
+
adapter.findOne<T>({
|
|
148
|
+
...params,
|
|
149
|
+
where: [...params.where, ...filters],
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
findMany: <T>(params: {
|
|
153
|
+
model: string;
|
|
154
|
+
where?: WhereClause[];
|
|
155
|
+
limit?: number;
|
|
156
|
+
offset?: number;
|
|
157
|
+
sortBy?: { field: string; direction: 'asc' | 'desc' };
|
|
158
|
+
}): Promise<T[]> =>
|
|
159
|
+
adapter.findMany<T>({
|
|
160
|
+
...params,
|
|
161
|
+
where: [...(params.where ?? []), ...filters],
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
update: <T>(params: {
|
|
165
|
+
model: string;
|
|
166
|
+
where: WhereClause[];
|
|
167
|
+
data: Record<string, unknown>;
|
|
168
|
+
}): Promise<T | null> => Promise.resolve().then(() => {
|
|
169
|
+
for (const key of Object.keys(defaults)) {
|
|
170
|
+
if (Object.hasOwn(params.data, key))
|
|
171
|
+
throw Errors.forbidden(`Cannot update scoped field '${key}'`);
|
|
172
|
+
}
|
|
173
|
+
return adapter.update<T>({
|
|
174
|
+
...params,
|
|
175
|
+
where: [...params.where, ...filters],
|
|
176
|
+
});
|
|
177
|
+
}),
|
|
178
|
+
|
|
179
|
+
delete: (params: { model: string; where: WhereClause[] }): Promise<void> =>
|
|
180
|
+
adapter.delete({
|
|
181
|
+
...params,
|
|
182
|
+
where: [...params.where, ...filters],
|
|
183
|
+
}),
|
|
184
|
+
|
|
185
|
+
count: (params: {
|
|
186
|
+
model: string;
|
|
187
|
+
where?: WhereClause[];
|
|
188
|
+
}): Promise<number> =>
|
|
189
|
+
adapter.count({
|
|
190
|
+
...params,
|
|
191
|
+
where: [...(params.where ?? []), ...filters],
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
transaction: <T>(fn: (tx: DatabaseAdapter) => Promise<T>): Promise<T> =>
|
|
195
|
+
adapter.transaction(tx => fn(wrapAdapterWithScopeRules(tx, scopeRule))),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get all model definitions declared by plugins.
|
|
201
|
+
*/
|
|
202
|
+
export function collectPluginModels(
|
|
203
|
+
plugins: readonly FortressPlugin[],
|
|
204
|
+
): { pluginName: string; models: FortressPlugin['models'] }[] {
|
|
205
|
+
return plugins
|
|
206
|
+
.filter(p => p.models && p.models.length > 0)
|
|
207
|
+
.map(p => ({ pluginName: p.name, models: p.models }));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Convert a middleware path pattern to a regex.
|
|
212
|
+
* Supports `:param` (single segment) and `*` (wildcard).
|
|
213
|
+
*/
|
|
214
|
+
function middlewarePathToRegex(pattern: string): RegExp {
|
|
215
|
+
const regexStr = canonicalizePath(pattern)
|
|
216
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
217
|
+
.replace(/:[^/]+/g, '[^/]+')
|
|
218
|
+
.replace(/\\\*/g, '.*')
|
|
219
|
+
.replace(/\//g, '\\/');
|
|
220
|
+
return new RegExp(`^${regexStr}$`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Collect all middleware definitions from plugins matching a given position.
|
|
225
|
+
* Returns them in plugin registration order.
|
|
226
|
+
*/
|
|
227
|
+
export function collectPluginMiddleware(
|
|
228
|
+
plugins: readonly FortressPlugin[],
|
|
229
|
+
position: MiddlewareDefinition['position'],
|
|
230
|
+
): { plugin: FortressPlugin; middleware: MiddlewareDefinition }[] {
|
|
231
|
+
const result: { plugin: FortressPlugin; middleware: MiddlewareDefinition }[] = [];
|
|
232
|
+
for (const plugin of plugins) {
|
|
233
|
+
if (!plugin.middleware)
|
|
234
|
+
continue;
|
|
235
|
+
for (const mw of plugin.middleware) {
|
|
236
|
+
if (mw.position === position) {
|
|
237
|
+
result.push({ plugin, middleware: mw });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Execute plugin middleware for a given position and request path.
|
|
246
|
+
*
|
|
247
|
+
* Iterates through all plugins in registration order, filters by position,
|
|
248
|
+
* matches the middleware path pattern against the request path, and chains
|
|
249
|
+
* handlers so each `next()` invokes the next matching middleware.
|
|
250
|
+
*/
|
|
251
|
+
export async function executePluginMiddleware(
|
|
252
|
+
plugins: readonly FortressPlugin[],
|
|
253
|
+
position: MiddlewareDefinition['position'],
|
|
254
|
+
requestPath: string,
|
|
255
|
+
ctx: PluginContext,
|
|
256
|
+
request: PluginRequestContext,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
const canonicalRequestPath = canonicalizePath(requestPath);
|
|
259
|
+
const matching = collectPluginMiddleware(plugins, position)
|
|
260
|
+
.filter(({ middleware: mw }) => middlewarePathToRegex(mw.path).test(canonicalRequestPath));
|
|
261
|
+
|
|
262
|
+
if (matching.length === 0)
|
|
263
|
+
return;
|
|
264
|
+
|
|
265
|
+
// Build a chain where each handler's `next` calls the next middleware
|
|
266
|
+
let index = 0;
|
|
267
|
+
|
|
268
|
+
async function runNext(): Promise<void> {
|
|
269
|
+
if (index >= matching.length)
|
|
270
|
+
return;
|
|
271
|
+
const current = matching[index++];
|
|
272
|
+
await current.middleware.handler(ctx, request, runNext);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await runNext();
|
|
276
|
+
}
|