@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,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint dispatch — body parsing and handler invocation.
|
|
3
|
+
*
|
|
4
|
+
* Given a matched {@link EndpointDefinition}, this module:
|
|
5
|
+
*
|
|
6
|
+
* 1. Parses the request body (JSON, or `application/x-www-form-urlencoded`
|
|
7
|
+
* for OAuth endpoints).
|
|
8
|
+
* 2. Invokes the right service or plugin method by name. Auth + IAM
|
|
9
|
+
* endpoints dispatch through hardcoded switches that mirror the
|
|
10
|
+
* positional argument shapes of `fortress.auth.*` / `fortress.iam.*`.
|
|
11
|
+
* Plugin endpoints dispatch via `fortress.plugins[name][handler]` with
|
|
12
|
+
* the merged `{ ...body, ...params }` shape used by the existing
|
|
13
|
+
* adapter route mounts.
|
|
14
|
+
* 3. Serializes the result into a `Response` (JSON by default; HTML for
|
|
15
|
+
* the OpenAPI Scalar UI plugin).
|
|
16
|
+
*
|
|
17
|
+
* OAuth has special-case handling because RFC 6749 requires
|
|
18
|
+
* form-encoded bodies and Basic-auth client authentication.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ClientAuth } from '../../plugins/oauth';
|
|
22
|
+
import type { EndpointDefinition } from '../endpoint';
|
|
23
|
+
import type { Fortress } from '../fortress';
|
|
24
|
+
import type { FortressPlugin, PluginRouteContext } from '../plugin';
|
|
25
|
+
import type { RequestMeta, Subject, TokenClaims } from '../types';
|
|
26
|
+
import { Errors, FortressError } from '../errors';
|
|
27
|
+
|
|
28
|
+
/** Auth context resolved by `handleRequest` before dispatch. */
|
|
29
|
+
export interface DispatchAuth {
|
|
30
|
+
/** Resolved request principal — USER or SERVICE_ACCOUNT. Present iff authenticated. */
|
|
31
|
+
subject?: Subject;
|
|
32
|
+
/** Convenience alias for `subject?.id` when the subject is a USER. */
|
|
33
|
+
userId?: string;
|
|
34
|
+
claims?: TokenClaims;
|
|
35
|
+
/** Credential-level narrowing scopes (e.g. API-key scopes). */
|
|
36
|
+
scopes?: string[] | null;
|
|
37
|
+
meta?: RequestMeta;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Result of dispatching a request — a fully-formed `Response`. */
|
|
41
|
+
export type DispatchResult = Response;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dispatch a matched endpoint to the right service or plugin method and
|
|
45
|
+
* return a `Response`. Body parsing is performed here so the right
|
|
46
|
+
* content-type strategy is picked per endpoint family (JSON for most,
|
|
47
|
+
* `application/x-www-form-urlencoded` for OAuth).
|
|
48
|
+
*
|
|
49
|
+
* The caller is expected to have already run plugin middleware, token
|
|
50
|
+
* verification, validation, and {@link enforceFortressPermission}.
|
|
51
|
+
*/
|
|
52
|
+
export async function dispatchEndpoint(
|
|
53
|
+
fortress: Fortress,
|
|
54
|
+
request: Request,
|
|
55
|
+
endpoint: EndpointDefinition,
|
|
56
|
+
pathParams: Record<string, string>,
|
|
57
|
+
auth: DispatchAuth,
|
|
58
|
+
): Promise<DispatchResult> {
|
|
59
|
+
// OAuth endpoints get form-encoded body parsing + Basic auth handling.
|
|
60
|
+
const owningPlugin = findOwningPlugin(fortress, endpoint);
|
|
61
|
+
if (owningPlugin?.name === 'oauth') {
|
|
62
|
+
return dispatchOAuth(fortress, request, endpoint, auth);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parse body / query into a generic object.
|
|
66
|
+
const body = await parseBodyOrQuery(request);
|
|
67
|
+
|
|
68
|
+
// Plugin route (non-oauth)
|
|
69
|
+
if (owningPlugin) {
|
|
70
|
+
return dispatchPlugin(fortress, owningPlugin, endpoint, body, pathParams, request, auth);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Core auth / IAM dispatch
|
|
74
|
+
if (endpoint.path.startsWith('/auth/')) {
|
|
75
|
+
const result = await invokeAuthHandler(fortress, endpoint.handler, body, pathParams, auth);
|
|
76
|
+
return jsonResponse(result, successStatus(endpoint));
|
|
77
|
+
}
|
|
78
|
+
if (endpoint.path.startsWith('/iam/')) {
|
|
79
|
+
const result = await invokeIamHandler(fortress, endpoint.handler, body, pathParams);
|
|
80
|
+
return jsonResponse(result, successStatus(endpoint));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Top-level host routes are metadata-only and have no Fortress handler.
|
|
84
|
+
// Framework adapters exclude them from their mounted route table so the
|
|
85
|
+
// host handler runs; direct core dispatch fails closed instead of inventing
|
|
86
|
+
// a successful response.
|
|
87
|
+
throw Errors.notFound(`No Fortress handler is mounted for ${endpoint.method} ${endpoint.path}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Body parsing ─────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse the request body as JSON for write methods, or fall back to query
|
|
94
|
+
* params for GET. Returns an empty object if parsing fails or there is no
|
|
95
|
+
* body — handlers tolerate missing fields.
|
|
96
|
+
*/
|
|
97
|
+
async function parseBodyOrQuery(request: Request): Promise<Record<string, unknown>> {
|
|
98
|
+
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
99
|
+
return Object.fromEntries(new URL(request.url).searchParams);
|
|
100
|
+
}
|
|
101
|
+
// Some requests have no body at all (e.g. DELETE without payload).
|
|
102
|
+
const contentType = request.headers.get('content-type') ?? '';
|
|
103
|
+
if (!contentType.includes('json')) {
|
|
104
|
+
// No body or non-JSON body for write methods — try query params.
|
|
105
|
+
return Object.fromEntries(new URL(request.url).searchParams);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return (await request.json()) as Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function parseFormBody(request: Request): Promise<Record<string, string>> {
|
|
116
|
+
const text = await request.text();
|
|
117
|
+
const params = new URLSearchParams(text);
|
|
118
|
+
const out: Record<string, string> = {};
|
|
119
|
+
for (const [k, v] of params) out[k] = v;
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseBasicAuth(header: string | null): ClientAuth | undefined {
|
|
124
|
+
if (!header?.startsWith('Basic '))
|
|
125
|
+
return undefined;
|
|
126
|
+
let decoded: string;
|
|
127
|
+
try {
|
|
128
|
+
decoded = atob(header.slice(6));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const colon = decoded.indexOf(':');
|
|
134
|
+
if (colon === -1)
|
|
135
|
+
return undefined;
|
|
136
|
+
return {
|
|
137
|
+
clientId: decoded.slice(0, colon),
|
|
138
|
+
clientSecret: decoded.slice(colon + 1),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseBearerToken(header: string | null): string | undefined {
|
|
143
|
+
if (!header?.startsWith('Bearer '))
|
|
144
|
+
return undefined;
|
|
145
|
+
return header.slice(7);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Plugin dispatch (non-oauth) ─────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** Find the plugin (if any) that owns the given endpoint. */
|
|
151
|
+
function findOwningPlugin(fortress: Fortress, endpoint: EndpointDefinition): FortressPlugin | undefined {
|
|
152
|
+
const plugins = fortress.config.plugins ?? [];
|
|
153
|
+
for (const plugin of plugins) {
|
|
154
|
+
if (!plugin.routes)
|
|
155
|
+
continue;
|
|
156
|
+
const routes = Object.values(plugin.routes) as EndpointDefinition[];
|
|
157
|
+
if (routes.some(r => r.method === endpoint.method && r.path === endpoint.path)) {
|
|
158
|
+
return plugin;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function dispatchPlugin(
|
|
165
|
+
fortress: Fortress,
|
|
166
|
+
plugin: FortressPlugin,
|
|
167
|
+
endpoint: EndpointDefinition,
|
|
168
|
+
body: Record<string, unknown>,
|
|
169
|
+
pathParams: Record<string, string>,
|
|
170
|
+
request: Request,
|
|
171
|
+
auth: DispatchAuth,
|
|
172
|
+
): Promise<Response> {
|
|
173
|
+
const methods = (fortress.plugins as Record<string, Record<string, (...args: unknown[]) => unknown>>)[plugin.name];
|
|
174
|
+
if (!methods?.[endpoint.handler]) {
|
|
175
|
+
throw Errors.notFound(`Plugin handler '${plugin.name}.${endpoint.handler}' not found`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Call as `methods.<handler>(...)` so the `this` binding is preserved —
|
|
179
|
+
// some plugin methods reference sibling helpers via `this`.
|
|
180
|
+
// OpenAPI Scalar UI returns HTML, not JSON.
|
|
181
|
+
if (plugin.name === 'openapi' && endpoint.handler === 'getUI') {
|
|
182
|
+
const html = methods.getUI() as string;
|
|
183
|
+
return new Response(html, {
|
|
184
|
+
status: 200,
|
|
185
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const ctx: PluginRouteContext = {
|
|
190
|
+
subject: auth.subject,
|
|
191
|
+
userId: auth.subject?.type === 'USER' ? auth.subject.id : undefined,
|
|
192
|
+
claims: auth.claims,
|
|
193
|
+
scopes: auth.scopes,
|
|
194
|
+
meta: auth.meta,
|
|
195
|
+
request,
|
|
196
|
+
};
|
|
197
|
+
const result = await methods[endpoint.handler]({ ...body, ...pathParams }, ctx);
|
|
198
|
+
// Allow handlers to opt into HTML by returning a string starting with `<!`.
|
|
199
|
+
if (typeof result === 'string' && result.trimStart().startsWith('<!')) {
|
|
200
|
+
return new Response(result, {
|
|
201
|
+
status: 200,
|
|
202
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return jsonResponse(result ?? { ok: true }, successStatus(endpoint));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── OAuth special case ──────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
async function dispatchOAuth(
|
|
211
|
+
fortress: Fortress,
|
|
212
|
+
request: Request,
|
|
213
|
+
endpoint: EndpointDefinition,
|
|
214
|
+
auth: DispatchAuth,
|
|
215
|
+
): Promise<Response> {
|
|
216
|
+
const methods = (fortress.plugins as Record<string, Record<string, (...args: unknown[]) => unknown>>).oauth;
|
|
217
|
+
if (!methods)
|
|
218
|
+
throw Errors.notFound(`OAuth plugin not registered`);
|
|
219
|
+
const handlerName = endpoint.handler;
|
|
220
|
+
if (!methods[handlerName])
|
|
221
|
+
throw Errors.notFound(`OAuth handler '${handlerName}' not found`);
|
|
222
|
+
|
|
223
|
+
// IMPORTANT: invoke as `methods.<name>(...)` so the `this` binding is the
|
|
224
|
+
// methods object — the OAuth plugin's `handleTokenRequest` calls
|
|
225
|
+
// `this.clientCredentialsGrant(...)` and friends, which would otherwise
|
|
226
|
+
// throw "Cannot read properties of undefined" if called bare.
|
|
227
|
+
const m = methods as Record<string, (...args: unknown[]) => Promise<unknown> | unknown>;
|
|
228
|
+
const authHeader = request.headers.get('authorization');
|
|
229
|
+
|
|
230
|
+
switch (handlerName) {
|
|
231
|
+
case 'handleTokenRequest': {
|
|
232
|
+
const body = await parseFormBody(request);
|
|
233
|
+
const clientAuth = parseBasicAuth(authHeader);
|
|
234
|
+
const result = await m.handleTokenRequest(body, clientAuth);
|
|
235
|
+
return jsonResponse(result, 200);
|
|
236
|
+
}
|
|
237
|
+
case 'handleIntrospectRequest': {
|
|
238
|
+
const body = await parseFormBody(request);
|
|
239
|
+
const clientAuth = parseBasicAuth(authHeader);
|
|
240
|
+
if (!clientAuth) {
|
|
241
|
+
return jsonResponse(
|
|
242
|
+
{ error: 'invalid_client', error_description: 'Client authentication required' },
|
|
243
|
+
401,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const result = await m.handleIntrospectRequest({ token: body.token }, clientAuth);
|
|
247
|
+
return jsonResponse(result, 200);
|
|
248
|
+
}
|
|
249
|
+
case 'handleRevokeRequest': {
|
|
250
|
+
const body = await parseFormBody(request);
|
|
251
|
+
const clientAuth = parseBasicAuth(authHeader);
|
|
252
|
+
if (!clientAuth) {
|
|
253
|
+
return jsonResponse(
|
|
254
|
+
{ error: 'invalid_client', error_description: 'Client authentication required' },
|
|
255
|
+
401,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
await m.handleRevokeRequest({ token: body.token }, clientAuth);
|
|
259
|
+
return jsonResponse({}, 200);
|
|
260
|
+
}
|
|
261
|
+
case 'handleUserInfoRequest': {
|
|
262
|
+
// RFC 6750 §2.1: bearer token from Authorization header.
|
|
263
|
+
// The plugin's `handleUserInfoRequest` now returns OIDC-Core-§5.3
|
|
264
|
+
// shaped claims directly (sub-as-string, scope-gated standard
|
|
265
|
+
// claims, optional userinfoClaims hook output) and throws 401 for
|
|
266
|
+
// an invalid / expired token — which the standard error mapper
|
|
267
|
+
// serialises to a `{ code, message }` body. We just pass the result
|
|
268
|
+
// through.
|
|
269
|
+
const bearer = parseBearerToken(authHeader);
|
|
270
|
+
if (!bearer) {
|
|
271
|
+
return jsonResponse(
|
|
272
|
+
{ error: 'invalid_token', error_description: 'Bearer token required' },
|
|
273
|
+
401,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const claims = await m.handleUserInfoRequest(bearer) as Record<string, unknown>;
|
|
277
|
+
return jsonResponse(claims, 200);
|
|
278
|
+
}
|
|
279
|
+
case 'handleDiscovery': {
|
|
280
|
+
const result = m.handleDiscovery();
|
|
281
|
+
return jsonResponse(result, 200);
|
|
282
|
+
}
|
|
283
|
+
case 'handleJwksRequest': {
|
|
284
|
+
// RFC 7517 / OIDC Discovery: public JWKS for id_token verification.
|
|
285
|
+
// Cacheable for a short window — long enough that key rotation
|
|
286
|
+
// doesn't immediately bust every RP, short enough that a rotated
|
|
287
|
+
// key reaches them within the grace period.
|
|
288
|
+
const result = await m.handleJwksRequest();
|
|
289
|
+
return new Response(JSON.stringify(result), {
|
|
290
|
+
status: 200,
|
|
291
|
+
headers: {
|
|
292
|
+
'Content-Type': 'application/json',
|
|
293
|
+
'Cache-Control': 'public, max-age=300',
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
case 'handleAuthorizeRequest': {
|
|
298
|
+
// GET /oauth/authorize — front door for the auth-code flow.
|
|
299
|
+
// Reads query params, optionally identifies the user, then 302s to
|
|
300
|
+
// either the configured loginUrl or consentUrl with `?flow=<id>`.
|
|
301
|
+
const query = Object.fromEntries(new URL(request.url).searchParams);
|
|
302
|
+
const result = (await m.handleAuthorizeRequest(query, { userId: auth.userId })) as {
|
|
303
|
+
redirectUrl: string;
|
|
304
|
+
};
|
|
305
|
+
return new Response(null, {
|
|
306
|
+
status: 302,
|
|
307
|
+
headers: { Location: result.redirectUrl },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
case 'handleGetFlow': {
|
|
311
|
+
// H6 fix — every consent-flow endpoint requires authentication so the
|
|
312
|
+
// owner check has someone to compare against. Without this guard, an
|
|
313
|
+
// unauthenticated caller could read another user's pending flow.
|
|
314
|
+
if (auth.userId === undefined) {
|
|
315
|
+
return jsonResponse(
|
|
316
|
+
{ error: 'unauthorized', error_description: 'Authentication required' },
|
|
317
|
+
401,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const flowId = consentFlowIdFromUrl(request.url);
|
|
321
|
+
const result = await m.handleGetFlow(flowId, { userId: auth.userId });
|
|
322
|
+
return jsonResponse(result, 200);
|
|
323
|
+
}
|
|
324
|
+
case 'handleApproveFlow': {
|
|
325
|
+
if (auth.userId === undefined) {
|
|
326
|
+
return jsonResponse(
|
|
327
|
+
{ error: 'unauthorized', error_description: 'Authentication required' },
|
|
328
|
+
401,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const flowId = consentFlowIdFromUrl(request.url);
|
|
332
|
+
const result = await m.handleApproveFlow(flowId, { userId: auth.userId });
|
|
333
|
+
return jsonResponse(result, 200);
|
|
334
|
+
}
|
|
335
|
+
case 'handleDenyFlow': {
|
|
336
|
+
if (auth.userId === undefined) {
|
|
337
|
+
return jsonResponse(
|
|
338
|
+
{ error: 'unauthorized', error_description: 'Authentication required' },
|
|
339
|
+
401,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const flowId = consentFlowIdFromUrl(request.url);
|
|
343
|
+
const result = await m.handleDenyFlow(flowId, { userId: auth.userId });
|
|
344
|
+
return jsonResponse(result, 200);
|
|
345
|
+
}
|
|
346
|
+
default: {
|
|
347
|
+
// Authorize endpoint and friends — JSON body. Call through `m` so the
|
|
348
|
+
// `this` binding survives.
|
|
349
|
+
const body = (await request.json().catch(() => ({}))) as unknown;
|
|
350
|
+
const result = await m[handlerName](body);
|
|
351
|
+
return jsonResponse(result ?? { ok: true }, successStatus(endpoint));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const FLOW_ID_PATTERN = /\/oauth\/flows\/([^/]+)/;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Extract the opaque `:flowId` path segment from a
|
|
360
|
+
* `/oauth/flows/:flowId[/...]` URL. Throws if the segment is missing.
|
|
361
|
+
*/
|
|
362
|
+
function consentFlowIdFromUrl(url: string): string {
|
|
363
|
+
const path = new URL(url).pathname;
|
|
364
|
+
const match = FLOW_ID_PATTERN.exec(path);
|
|
365
|
+
if (!match)
|
|
366
|
+
throw Errors.badRequest('Invalid flow id');
|
|
367
|
+
return decodeURIComponent(match[1]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Auth / IAM hardcoded dispatch ────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
async function invokeAuthHandler(
|
|
373
|
+
fortress: Fortress,
|
|
374
|
+
handler: string,
|
|
375
|
+
body: Record<string, unknown>,
|
|
376
|
+
params: Record<string, string>,
|
|
377
|
+
auth: DispatchAuth,
|
|
378
|
+
): Promise<unknown> {
|
|
379
|
+
const meta = auth.meta;
|
|
380
|
+
switch (handler) {
|
|
381
|
+
case 'login':
|
|
382
|
+
return fortress.auth.login(String(body.identifier ?? ''), String(body.password ?? ''), {
|
|
383
|
+
...meta,
|
|
384
|
+
...(typeof body.trustedDeviceToken === 'string' ? { trustedDeviceToken: body.trustedDeviceToken } : {}),
|
|
385
|
+
});
|
|
386
|
+
case 'verifyTwoFactor': {
|
|
387
|
+
const methods = (fortress.plugins as Record<string, Record<string, (...args: unknown[]) => unknown>>)['two-factor'];
|
|
388
|
+
if (!methods?.verify)
|
|
389
|
+
throw Errors.badRequest('Two-factor plugin is not configured');
|
|
390
|
+
return methods.verify(String(body.continuationToken ?? ''), String(body.code ?? ''), {
|
|
391
|
+
...meta,
|
|
392
|
+
...(body.rememberDevice === true ? { rememberDevice: true } : {}),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
case 'verifyMagicLink': {
|
|
396
|
+
const methods = (fortress.plugins as Record<string, Record<string, (...args: unknown[]) => unknown>>)['magic-link'];
|
|
397
|
+
if (!methods?.verify)
|
|
398
|
+
throw Errors.badRequest('Magic-link plugin is not configured');
|
|
399
|
+
return methods.verify(String(body.token ?? ''), meta);
|
|
400
|
+
}
|
|
401
|
+
case 'createUser':
|
|
402
|
+
return fortress.auth.createUser(body as never);
|
|
403
|
+
case 'refresh':
|
|
404
|
+
return fortress.auth.refresh(String(body.refreshToken ?? ''), meta);
|
|
405
|
+
case 'logout':
|
|
406
|
+
await fortress.auth.logout(String(body.refreshToken ?? ''));
|
|
407
|
+
return { ok: true };
|
|
408
|
+
case 'me':
|
|
409
|
+
return fortress.auth.me(requireUserId(auth));
|
|
410
|
+
case 'listSessions':
|
|
411
|
+
return fortress.auth.listSessions(requireUserId(auth));
|
|
412
|
+
case 'revokeSession':
|
|
413
|
+
await fortress.auth.revokeSession(requireUserId(auth), params.id);
|
|
414
|
+
return { ok: true };
|
|
415
|
+
case 'revokeAllOtherSessions':
|
|
416
|
+
await fortress.auth.revokeAllOtherSessions(requireUserId(auth), String(body.currentTokenId ?? ''));
|
|
417
|
+
return { ok: true };
|
|
418
|
+
case 'addLoginIdentifier':
|
|
419
|
+
await fortress.auth.addLoginIdentifier(
|
|
420
|
+
requireUserId(auth),
|
|
421
|
+
body.type as never,
|
|
422
|
+
String(body.value ?? ''),
|
|
423
|
+
);
|
|
424
|
+
return { ok: true };
|
|
425
|
+
case 'removeLoginIdentifier':
|
|
426
|
+
await fortress.auth.removeLoginIdentifier(
|
|
427
|
+
requireUserId(auth),
|
|
428
|
+
body.type as never,
|
|
429
|
+
String(body.value ?? ''),
|
|
430
|
+
);
|
|
431
|
+
return { ok: true };
|
|
432
|
+
case 'getLoginIdentifiers':
|
|
433
|
+
return fortress.auth.getLoginIdentifiers(requireUserId(auth));
|
|
434
|
+
case 'impersonate':
|
|
435
|
+
return fortress.auth.impersonate(requireUserId(auth), String(body.targetUserId ?? ''), {
|
|
436
|
+
reason: body.reason as string | undefined,
|
|
437
|
+
expirySeconds: body.expirySeconds as number | undefined,
|
|
438
|
+
});
|
|
439
|
+
default:
|
|
440
|
+
throw Errors.notFound(`Auth handler '${handler}' not found`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function invokeIamHandler(
|
|
445
|
+
fortress: Fortress,
|
|
446
|
+
handler: string,
|
|
447
|
+
body: Record<string, unknown>,
|
|
448
|
+
params: Record<string, string>,
|
|
449
|
+
): Promise<unknown> {
|
|
450
|
+
switch (handler) {
|
|
451
|
+
case 'getResources':
|
|
452
|
+
return fortress.iam.getResources();
|
|
453
|
+
case 'getRoles':
|
|
454
|
+
return fortress.iam.getRoles();
|
|
455
|
+
case 'createRole':
|
|
456
|
+
return fortress.iam.createRole(
|
|
457
|
+
String(body.name ?? ''),
|
|
458
|
+
body.permissions as never,
|
|
459
|
+
body.description as string | undefined,
|
|
460
|
+
);
|
|
461
|
+
case 'deleteRole':
|
|
462
|
+
await fortress.iam.deleteRole(params.id);
|
|
463
|
+
return { ok: true };
|
|
464
|
+
case 'bindRoleToUser':
|
|
465
|
+
await fortress.iam.bindRoleToUser(
|
|
466
|
+
String(body.userId ?? ''),
|
|
467
|
+
params.id,
|
|
468
|
+
body.tenantId as string | undefined,
|
|
469
|
+
);
|
|
470
|
+
return { ok: true };
|
|
471
|
+
case 'bindRoleToGroup':
|
|
472
|
+
await fortress.iam.bindRoleToGroup(
|
|
473
|
+
String(body.groupId ?? ''),
|
|
474
|
+
params.id,
|
|
475
|
+
body.tenantId as string | undefined,
|
|
476
|
+
);
|
|
477
|
+
return { ok: true };
|
|
478
|
+
case 'unbindRole':
|
|
479
|
+
await fortress.iam.unbindRole(
|
|
480
|
+
body.subjectType as never,
|
|
481
|
+
String(body.subjectId ?? ''),
|
|
482
|
+
params.id,
|
|
483
|
+
body.tenantId as string | undefined,
|
|
484
|
+
);
|
|
485
|
+
return { ok: true };
|
|
486
|
+
case 'createGroup':
|
|
487
|
+
return fortress.iam.createGroup(
|
|
488
|
+
String(body.name ?? ''),
|
|
489
|
+
body.description as string | undefined,
|
|
490
|
+
);
|
|
491
|
+
case 'addUserToGroup':
|
|
492
|
+
await fortress.iam.addUserToGroup(params.id, String(body.userId ?? ''));
|
|
493
|
+
return { ok: true };
|
|
494
|
+
case 'removeUserFromGroup':
|
|
495
|
+
await fortress.iam.removeUserFromGroup(params.id, params.userId);
|
|
496
|
+
return { ok: true };
|
|
497
|
+
case 'getUserPermissions':
|
|
498
|
+
return fortress.iam.getPermissionsForSubject(
|
|
499
|
+
{ type: 'USER', id: params.id },
|
|
500
|
+
body.tenantId as string | undefined,
|
|
501
|
+
);
|
|
502
|
+
case 'checkPermission': {
|
|
503
|
+
// Accept either the new `{ subject: { type, id } }` shape or the
|
|
504
|
+
// legacy `{ userId }` shape. Defaulting to USER keeps this backwards
|
|
505
|
+
// compatible for callers that haven't migrated yet — step 10 updates
|
|
506
|
+
// the endpoint body schema to the new shape.
|
|
507
|
+
const subjectIn = body.subject as { type?: string; id?: string } | undefined;
|
|
508
|
+
const subject: Subject = subjectIn?.type && subjectIn?.id != null
|
|
509
|
+
? { type: subjectIn.type as 'USER' | 'GROUP' | 'SERVICE_ACCOUNT', id: String(subjectIn.id) }
|
|
510
|
+
: { type: 'USER' as const, id: String(body.userId ?? '') };
|
|
511
|
+
// M7 fix: sanitize caller-supplied context before forwarding to the
|
|
512
|
+
// evaluator. `/iam/check` is an admin diagnostic; we must NOT let a
|
|
513
|
+
// caller widen credentialScopes (which would let any caller answer
|
|
514
|
+
// "yes" by passing scopes:['*']) nor satisfy ownership conditions by
|
|
515
|
+
// forging `resource.ownerId === user.id`. The narrowed shape allows
|
|
516
|
+
// only `request.*` (truly request-scoped data the caller could have
|
|
517
|
+
// supplied in a real request anyway).
|
|
518
|
+
const rawCtx = body.context as Record<string, unknown> | undefined;
|
|
519
|
+
const safeCtx: { tenantId?: string; request?: Record<string, unknown> } = {};
|
|
520
|
+
if (rawCtx?.tenantId !== undefined && typeof rawCtx.tenantId === 'string')
|
|
521
|
+
safeCtx.tenantId = rawCtx.tenantId;
|
|
522
|
+
if (rawCtx?.request && typeof rawCtx.request === 'object')
|
|
523
|
+
safeCtx.request = rawCtx.request as Record<string, unknown>;
|
|
524
|
+
const allowed = await fortress.iam.checkPermission(
|
|
525
|
+
subject,
|
|
526
|
+
String(body.resource ?? ''),
|
|
527
|
+
String(body.action ?? ''),
|
|
528
|
+
safeCtx as never,
|
|
529
|
+
);
|
|
530
|
+
return { allowed };
|
|
531
|
+
}
|
|
532
|
+
case 'bindPermissionToUser':
|
|
533
|
+
await fortress.iam.bindPermissionToUser(
|
|
534
|
+
String(body.userId ?? ''),
|
|
535
|
+
body.permission as never,
|
|
536
|
+
body.tenantId as string | undefined,
|
|
537
|
+
);
|
|
538
|
+
return { ok: true };
|
|
539
|
+
case 'bindPermissionToGroup':
|
|
540
|
+
await fortress.iam.bindPermissionToGroup(
|
|
541
|
+
String(body.groupId ?? ''),
|
|
542
|
+
body.permission as never,
|
|
543
|
+
body.tenantId as string | undefined,
|
|
544
|
+
);
|
|
545
|
+
return { ok: true };
|
|
546
|
+
case 'unbindPermissionFromUser':
|
|
547
|
+
await fortress.iam.unbindPermissionFromUser(
|
|
548
|
+
String(body.userId ?? ''),
|
|
549
|
+
String(body.permissionId ?? ''),
|
|
550
|
+
body.tenantId as string | undefined,
|
|
551
|
+
);
|
|
552
|
+
return { ok: true };
|
|
553
|
+
case 'unbindPermissionFromGroup':
|
|
554
|
+
await fortress.iam.unbindPermissionFromGroup(
|
|
555
|
+
String(body.groupId ?? ''),
|
|
556
|
+
String(body.permissionId ?? ''),
|
|
557
|
+
body.tenantId as string | undefined,
|
|
558
|
+
);
|
|
559
|
+
return { ok: true };
|
|
560
|
+
|
|
561
|
+
// ── Service Accounts ──────────────────────────────────────────
|
|
562
|
+
case 'createServiceAccount':
|
|
563
|
+
return fortress.iam.createServiceAccount({
|
|
564
|
+
name: String(body.name ?? ''),
|
|
565
|
+
displayName: body.displayName as string | undefined,
|
|
566
|
+
description: body.description as string | undefined,
|
|
567
|
+
});
|
|
568
|
+
case 'listServiceAccounts':
|
|
569
|
+
return fortress.iam.listServiceAccounts({
|
|
570
|
+
limit: body.limit != null ? Number(body.limit) : undefined,
|
|
571
|
+
offset: body.offset != null ? Number(body.offset) : undefined,
|
|
572
|
+
});
|
|
573
|
+
case 'getServiceAccount':
|
|
574
|
+
return fortress.iam.getServiceAccount(params.id);
|
|
575
|
+
case 'updateServiceAccount':
|
|
576
|
+
return fortress.iam.updateServiceAccount(params.id, {
|
|
577
|
+
displayName: body.displayName as string | null | undefined,
|
|
578
|
+
description: body.description as string | null | undefined,
|
|
579
|
+
isActive: body.isActive as boolean | undefined,
|
|
580
|
+
});
|
|
581
|
+
case 'deleteServiceAccount':
|
|
582
|
+
await fortress.iam.deleteServiceAccount(params.id);
|
|
583
|
+
return { ok: true };
|
|
584
|
+
case 'getServiceAccountPermissions':
|
|
585
|
+
return fortress.iam.getPermissionsForSubject(
|
|
586
|
+
{ type: 'SERVICE_ACCOUNT', id: params.id },
|
|
587
|
+
body.tenantId as string | undefined,
|
|
588
|
+
);
|
|
589
|
+
case 'bindRoleToServiceAccount':
|
|
590
|
+
await fortress.iam.bindRoleToServiceAccount(
|
|
591
|
+
String(body.serviceAccountId ?? ''),
|
|
592
|
+
params.id,
|
|
593
|
+
body.tenantId as string | undefined,
|
|
594
|
+
);
|
|
595
|
+
return { ok: true };
|
|
596
|
+
case 'unbindRoleFromServiceAccount':
|
|
597
|
+
await fortress.iam.unbindRoleFromServiceAccount(
|
|
598
|
+
String(body.serviceAccountId ?? ''),
|
|
599
|
+
params.id,
|
|
600
|
+
body.tenantId as string | undefined,
|
|
601
|
+
);
|
|
602
|
+
return { ok: true };
|
|
603
|
+
case 'bindPermissionToServiceAccount':
|
|
604
|
+
await fortress.iam.bindPermissionToServiceAccount(
|
|
605
|
+
String(body.serviceAccountId ?? ''),
|
|
606
|
+
body.permission as never,
|
|
607
|
+
body.tenantId as string | undefined,
|
|
608
|
+
);
|
|
609
|
+
return { ok: true };
|
|
610
|
+
case 'unbindPermissionFromServiceAccount':
|
|
611
|
+
await fortress.iam.unbindPermissionFromServiceAccount(
|
|
612
|
+
String(body.serviceAccountId ?? ''),
|
|
613
|
+
String(body.permissionId ?? ''),
|
|
614
|
+
body.tenantId as string | undefined,
|
|
615
|
+
);
|
|
616
|
+
return { ok: true };
|
|
617
|
+
|
|
618
|
+
default:
|
|
619
|
+
throw Errors.notFound(`IAM handler '${handler}' not found`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function requireUserId(auth: DispatchAuth): string {
|
|
624
|
+
if (!auth.userId)
|
|
625
|
+
throw new FortressError('UNAUTHORIZED', 'User not authenticated', 401);
|
|
626
|
+
return auth.userId;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── Response helpers ────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Pick the first 2xx status declared in `endpoint.responses` (e.g. `201`
|
|
633
|
+
* for `POST /auth/register`), defaulting to `200`.
|
|
634
|
+
*/
|
|
635
|
+
function successStatus(endpoint: EndpointDefinition): number {
|
|
636
|
+
if (!endpoint.responses)
|
|
637
|
+
return 200;
|
|
638
|
+
for (const code of Object.keys(endpoint.responses)) {
|
|
639
|
+
const num = Number(code);
|
|
640
|
+
if (num >= 200 && num < 300)
|
|
641
|
+
return num;
|
|
642
|
+
}
|
|
643
|
+
return 200;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Build a JSON `Response` with the right `Content-Type`. */
|
|
647
|
+
export function jsonResponse(body: unknown, status: number): Response {
|
|
648
|
+
return new Response(JSON.stringify(body ?? { ok: true }), {
|
|
649
|
+
status,
|
|
650
|
+
headers: { 'Content-Type': 'application/json' },
|
|
651
|
+
});
|
|
652
|
+
}
|