@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,786 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/plugins/webauthn/index.ts
|
|
21
|
+
var webauthn_exports = {};
|
|
22
|
+
__export(webauthn_exports, {
|
|
23
|
+
webauthn: () => webauthn
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(webauthn_exports);
|
|
26
|
+
var import_server = require("@simplewebauthn/server");
|
|
27
|
+
|
|
28
|
+
// src/core/errors.ts
|
|
29
|
+
var FortressError = class extends Error {
|
|
30
|
+
code;
|
|
31
|
+
statusCode;
|
|
32
|
+
retryAfter;
|
|
33
|
+
details;
|
|
34
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
35
|
+
oauthError;
|
|
36
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
37
|
+
oauthDescription;
|
|
38
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
39
|
+
oauthErrorUri;
|
|
40
|
+
constructor(code, message, statusCode, options) {
|
|
41
|
+
super(message, { cause: options?.cause });
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.statusCode = statusCode;
|
|
44
|
+
this.retryAfter = options?.retryAfter;
|
|
45
|
+
this.details = options?.details;
|
|
46
|
+
this.oauthError = options?.oauthError;
|
|
47
|
+
this.oauthDescription = options?.oauthDescription;
|
|
48
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
49
|
+
}
|
|
50
|
+
toJSON() {
|
|
51
|
+
return {
|
|
52
|
+
code: this.code,
|
|
53
|
+
message: this.message,
|
|
54
|
+
statusCode: this.statusCode,
|
|
55
|
+
...this.details !== void 0 && { details: this.details }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var Errors = {
|
|
60
|
+
unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
|
|
61
|
+
tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
|
|
62
|
+
sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
|
|
63
|
+
sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
|
|
64
|
+
forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
|
|
65
|
+
badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
|
|
66
|
+
notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
|
|
67
|
+
conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
|
|
68
|
+
unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
|
|
69
|
+
rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
|
|
70
|
+
database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
|
|
71
|
+
serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
|
|
72
|
+
validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
|
|
73
|
+
/**
|
|
74
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
75
|
+
*
|
|
76
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
77
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
78
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
79
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
80
|
+
* on the machine-readable `error` field.
|
|
81
|
+
*
|
|
82
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
83
|
+
* - `invalid_client` → 401
|
|
84
|
+
* - everything else → 400
|
|
85
|
+
*/
|
|
86
|
+
oauth: (error, description, options) => {
|
|
87
|
+
const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
|
|
88
|
+
const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
|
|
89
|
+
return new FortressError(code, description ?? error, status, {
|
|
90
|
+
oauthError: error,
|
|
91
|
+
oauthDescription: description,
|
|
92
|
+
oauthErrorUri: options?.errorUri,
|
|
93
|
+
cause: options?.cause
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
98
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
99
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
100
|
+
* service-method call would produce.
|
|
101
|
+
*
|
|
102
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
103
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
104
|
+
* failures still become structured `FortressError`s.
|
|
105
|
+
*/
|
|
106
|
+
fromHttpResponse: (status, body) => {
|
|
107
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
108
|
+
const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
|
|
109
|
+
const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
|
|
110
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function isFortressErrorCode(code) {
|
|
114
|
+
return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
|
|
115
|
+
}
|
|
116
|
+
function statusToErrorCode(status) {
|
|
117
|
+
if (status === 401)
|
|
118
|
+
return "UNAUTHORIZED";
|
|
119
|
+
if (status === 403)
|
|
120
|
+
return "FORBIDDEN";
|
|
121
|
+
if (status === 404)
|
|
122
|
+
return "NOT_FOUND";
|
|
123
|
+
if (status === 409)
|
|
124
|
+
return "CONFLICT";
|
|
125
|
+
if (status === 422)
|
|
126
|
+
return "UNPROCESSABLE_ENTITY";
|
|
127
|
+
if (status === 429)
|
|
128
|
+
return "RATE_LIMITED";
|
|
129
|
+
if (status === 503)
|
|
130
|
+
return "SERVICE_UNAVAILABLE";
|
|
131
|
+
if (status >= 500)
|
|
132
|
+
return "DATABASE_ERROR";
|
|
133
|
+
return "BAD_REQUEST";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/core/schema-builder.ts
|
|
137
|
+
var import_openapi = require("@bajustone/fetcher/openapi");
|
|
138
|
+
var import_schema = require("@bajustone/fetcher/schema");
|
|
139
|
+
function collectRefNames(schema, acc) {
|
|
140
|
+
if (!schema || typeof schema !== "object")
|
|
141
|
+
return acc;
|
|
142
|
+
if (typeof schema.$ref === "string") {
|
|
143
|
+
const i = schema.$ref.lastIndexOf("/");
|
|
144
|
+
acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
|
|
145
|
+
}
|
|
146
|
+
if (schema.properties) {
|
|
147
|
+
for (const key of Object.keys(schema.properties))
|
|
148
|
+
collectRefNames(schema.properties[key], acc);
|
|
149
|
+
}
|
|
150
|
+
collectRefNames(schema.items, acc);
|
|
151
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
152
|
+
const variants = schema[key];
|
|
153
|
+
if (variants) {
|
|
154
|
+
for (const variant of variants)
|
|
155
|
+
collectRefNames(variant, acc);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object")
|
|
159
|
+
collectRefNames(schema.additionalProperties, acc);
|
|
160
|
+
return acc;
|
|
161
|
+
}
|
|
162
|
+
var refDefinitionContexts = /* @__PURE__ */ new WeakMap();
|
|
163
|
+
function refName(refPath) {
|
|
164
|
+
const i = refPath.lastIndexOf("/");
|
|
165
|
+
return i >= 0 ? refPath.slice(i + 1) : refPath;
|
|
166
|
+
}
|
|
167
|
+
function resolveRefDefs(schema) {
|
|
168
|
+
if (collectRefNames(schema, /* @__PURE__ */ new Set()).size === 0)
|
|
169
|
+
return void 0;
|
|
170
|
+
const defs = /* @__PURE__ */ Object.create(null);
|
|
171
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
172
|
+
const hasOwn = (value, key) => Object.hasOwn(value, key);
|
|
173
|
+
const visit = (node, inherited) => {
|
|
174
|
+
if (!node || typeof node !== "object")
|
|
175
|
+
return;
|
|
176
|
+
const context = refDefinitionContexts.get(node) ?? inherited;
|
|
177
|
+
if (typeof node.$ref === "string") {
|
|
178
|
+
const name = refName(node.$ref);
|
|
179
|
+
const component = context && hasOwn(context, name) ? context[name] : void 0;
|
|
180
|
+
if (component) {
|
|
181
|
+
if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
|
|
182
|
+
throw new Error(`Conflicting component definitions for $ref '${name}'`);
|
|
183
|
+
}
|
|
184
|
+
const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
|
|
185
|
+
defs[name] = component;
|
|
186
|
+
if (firstResolution)
|
|
187
|
+
visit(component, context);
|
|
188
|
+
} else if (!hasOwn(defs, name)) {
|
|
189
|
+
defs[name] = {};
|
|
190
|
+
unresolved.add(name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (node.properties) {
|
|
194
|
+
for (const child of Object.values(node.properties))
|
|
195
|
+
visit(child, context);
|
|
196
|
+
}
|
|
197
|
+
visit(node.items, context);
|
|
198
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
199
|
+
for (const child of node[key] ?? [])
|
|
200
|
+
visit(child, context);
|
|
201
|
+
}
|
|
202
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object")
|
|
203
|
+
visit(node.additionalProperties, context);
|
|
204
|
+
};
|
|
205
|
+
visit(schema);
|
|
206
|
+
return defs;
|
|
207
|
+
}
|
|
208
|
+
function createStandardProps(schema) {
|
|
209
|
+
let validateFn;
|
|
210
|
+
return {
|
|
211
|
+
version: 1,
|
|
212
|
+
vendor: "fortress",
|
|
213
|
+
validate(value) {
|
|
214
|
+
if (!validateFn) {
|
|
215
|
+
const defs = resolveRefDefs(schema);
|
|
216
|
+
validateFn = (0, import_openapi.fromJSONSchema)(schema, defs)["~standard"].validate;
|
|
217
|
+
}
|
|
218
|
+
return validateFn(value);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function toFortressSchema(schema, refDefinitions, standardProps) {
|
|
223
|
+
if (refDefinitions)
|
|
224
|
+
refDefinitionContexts.set(schema, refDefinitions);
|
|
225
|
+
return Object.assign(schema, {
|
|
226
|
+
"~standard": standardProps ?? createStandardProps(schema)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function str(opts) {
|
|
230
|
+
const o = typeof opts === "string" ? { description: opts } : opts ?? {};
|
|
231
|
+
const s = { type: "string" };
|
|
232
|
+
if (o.description)
|
|
233
|
+
s.description = o.description;
|
|
234
|
+
if (o.min !== void 0)
|
|
235
|
+
s.minLength = o.min;
|
|
236
|
+
if (o.max !== void 0)
|
|
237
|
+
s.maxLength = o.max;
|
|
238
|
+
if (o.pattern !== void 0)
|
|
239
|
+
s.pattern = o.pattern;
|
|
240
|
+
if (o.format !== void 0)
|
|
241
|
+
s.format = o.format;
|
|
242
|
+
return toFortressSchema(s);
|
|
243
|
+
}
|
|
244
|
+
function int(opts) {
|
|
245
|
+
const o = typeof opts === "string" ? { description: opts } : opts ?? {};
|
|
246
|
+
const s = { type: "integer" };
|
|
247
|
+
if (o.description)
|
|
248
|
+
s.description = o.description;
|
|
249
|
+
if (o.min !== void 0)
|
|
250
|
+
s.minimum = o.min;
|
|
251
|
+
if (o.max !== void 0)
|
|
252
|
+
s.maximum = o.max;
|
|
253
|
+
return toFortressSchema(s);
|
|
254
|
+
}
|
|
255
|
+
function id(description) {
|
|
256
|
+
const s = { type: "string", minLength: 1 };
|
|
257
|
+
if (description)
|
|
258
|
+
s.description = description;
|
|
259
|
+
return toFortressSchema(s);
|
|
260
|
+
}
|
|
261
|
+
function bool(description) {
|
|
262
|
+
const s = { type: "boolean" };
|
|
263
|
+
if (description)
|
|
264
|
+
s.description = description;
|
|
265
|
+
return toFortressSchema(s);
|
|
266
|
+
}
|
|
267
|
+
function obj(properties, ...required) {
|
|
268
|
+
const s = { type: "object", properties };
|
|
269
|
+
if (required.length > 0)
|
|
270
|
+
s.required = required;
|
|
271
|
+
return toFortressSchema(s);
|
|
272
|
+
}
|
|
273
|
+
function record(description) {
|
|
274
|
+
const s = { type: "object", additionalProperties: true };
|
|
275
|
+
if (description)
|
|
276
|
+
s.description = description;
|
|
277
|
+
return toFortressSchema(s);
|
|
278
|
+
}
|
|
279
|
+
function makeFormatBuilder(factory) {
|
|
280
|
+
const f = factory();
|
|
281
|
+
return (description) => {
|
|
282
|
+
const s = { type: "string" };
|
|
283
|
+
if (f.format)
|
|
284
|
+
s.format = f.format;
|
|
285
|
+
if (f.pattern)
|
|
286
|
+
s.pattern = f.pattern;
|
|
287
|
+
if (description)
|
|
288
|
+
s.description = description;
|
|
289
|
+
return toFortressSchema(s);
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
var email = makeFormatBuilder(import_schema.email);
|
|
293
|
+
var uuid = makeFormatBuilder(import_schema.uuid);
|
|
294
|
+
var url = makeFormatBuilder(import_schema.url);
|
|
295
|
+
var datetime = makeFormatBuilder(import_schema.datetime);
|
|
296
|
+
var date = makeFormatBuilder(import_schema.date);
|
|
297
|
+
var time = makeFormatBuilder(import_schema.time);
|
|
298
|
+
function isStandardSchema(value) {
|
|
299
|
+
return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
|
|
300
|
+
}
|
|
301
|
+
function isFortressSchema(value) {
|
|
302
|
+
return isStandardSchema(value) && value["~standard"].vendor === "fortress" && ("type" in value || "$ref" in value || "oneOf" in value || "anyOf" in value);
|
|
303
|
+
}
|
|
304
|
+
var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
|
|
305
|
+
function hasJsonSchemaShape(value) {
|
|
306
|
+
if (typeof value !== "object" || value === null)
|
|
307
|
+
return false;
|
|
308
|
+
const v = value;
|
|
309
|
+
if ("$ref" in v || "oneOf" in v || "anyOf" in v || "allOf" in v)
|
|
310
|
+
return true;
|
|
311
|
+
if (typeof v.type === "string" && JSON_SCHEMA_TYPES.has(v.type))
|
|
312
|
+
return true;
|
|
313
|
+
if (Array.isArray(v.type) && v.type.every((t) => typeof t === "string" && JSON_SCHEMA_TYPES.has(t)))
|
|
314
|
+
return true;
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
function extractJsonSchema(schema) {
|
|
318
|
+
if (isFortressSchema(schema)) {
|
|
319
|
+
return schema;
|
|
320
|
+
}
|
|
321
|
+
if (isStandardSchema(schema)) {
|
|
322
|
+
const std = schema["~standard"];
|
|
323
|
+
if (std?.jsonSchema?.input) {
|
|
324
|
+
return std.jsonSchema.input({ target: "draft-2020-12" });
|
|
325
|
+
}
|
|
326
|
+
if (hasJsonSchemaShape(schema)) {
|
|
327
|
+
return schema;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return {};
|
|
331
|
+
}
|
|
332
|
+
var ErrorEnvelope = obj({
|
|
333
|
+
code: str("Machine-readable error code"),
|
|
334
|
+
message: str("Human-readable error message"),
|
|
335
|
+
statusCode: int("HTTP status code"),
|
|
336
|
+
details: toFortressSchema({ description: "Optional structured error details" })
|
|
337
|
+
}, "code", "message", "statusCode");
|
|
338
|
+
var EndpointBuilder = class {
|
|
339
|
+
_method;
|
|
340
|
+
_path;
|
|
341
|
+
_handler = "";
|
|
342
|
+
_summary = "";
|
|
343
|
+
_description;
|
|
344
|
+
_tags = [];
|
|
345
|
+
_security = [];
|
|
346
|
+
_deprecated = false;
|
|
347
|
+
_permission;
|
|
348
|
+
_body;
|
|
349
|
+
_query;
|
|
350
|
+
_params;
|
|
351
|
+
_responses = {};
|
|
352
|
+
constructor(method, path) {
|
|
353
|
+
this._method = method;
|
|
354
|
+
this._path = path;
|
|
355
|
+
}
|
|
356
|
+
summary(s) {
|
|
357
|
+
this._summary = s;
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
description(s) {
|
|
361
|
+
this._description = s;
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
tags(...t) {
|
|
365
|
+
this._tags.push(...t);
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
security(...s) {
|
|
369
|
+
this._security.push(...s);
|
|
370
|
+
return this;
|
|
371
|
+
}
|
|
372
|
+
deprecated() {
|
|
373
|
+
this._deprecated = true;
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
permission(resource, action) {
|
|
377
|
+
this._permission = { resource, action };
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
body(schema) {
|
|
381
|
+
this._body = schema;
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
query(schema) {
|
|
385
|
+
this._query = schema;
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
params(schema) {
|
|
389
|
+
this._params = schema;
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
response(status, description, schema) {
|
|
393
|
+
const stored = { description };
|
|
394
|
+
if (schema !== void 0) {
|
|
395
|
+
stored.schema = isStandardSchema(schema) ? extractJsonSchema(schema) : schema;
|
|
396
|
+
}
|
|
397
|
+
this._responses[status] = stored;
|
|
398
|
+
return this;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Declare an error response that returns the canonical {@link ErrorEnvelope}
|
|
402
|
+
* body. Shorthand for `.response(status, description, ErrorEnvelope)`.
|
|
403
|
+
*
|
|
404
|
+
* Aligns host endpoint error responses with what Fortress's own routes
|
|
405
|
+
* emit, so a single client-side error parser works everywhere.
|
|
406
|
+
*
|
|
407
|
+
* ```ts
|
|
408
|
+
* endpoint('GET', '/schools/:id')
|
|
409
|
+
* .summary('Get a school')
|
|
410
|
+
* .params(obj({ id: str() }, 'id'))
|
|
411
|
+
* .response(200, 'OK', SchoolEnvelope)
|
|
412
|
+
* .errorResponse(404, 'School not found')
|
|
413
|
+
* .errorResponse(403, 'Forbidden')
|
|
414
|
+
* .build();
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
errorResponse(status, description) {
|
|
418
|
+
return this.response(status, description, ErrorEnvelope);
|
|
419
|
+
}
|
|
420
|
+
handler(name) {
|
|
421
|
+
this._handler = name;
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
build() {
|
|
425
|
+
const def = {
|
|
426
|
+
method: this._method,
|
|
427
|
+
path: this._path,
|
|
428
|
+
handler: this._handler
|
|
429
|
+
};
|
|
430
|
+
if (this._summary || this._tags.length > 0 || this._security.length > 0 || this._description || this._deprecated || this._permission) {
|
|
431
|
+
def.meta = {
|
|
432
|
+
summary: this._summary,
|
|
433
|
+
...this._description && { description: this._description },
|
|
434
|
+
...this._tags.length > 0 && { tags: this._tags },
|
|
435
|
+
...this._security.length > 0 && { security: this._security },
|
|
436
|
+
...this._deprecated && { deprecated: true },
|
|
437
|
+
...this._permission && { permission: this._permission }
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (this._body || this._query || this._params) {
|
|
441
|
+
def.input = {};
|
|
442
|
+
if (this._body) {
|
|
443
|
+
def.input.body = extractJsonSchema(this._body);
|
|
444
|
+
if (isStandardSchema(this._body))
|
|
445
|
+
def.input.bodySchema = this._body;
|
|
446
|
+
}
|
|
447
|
+
if (this._query) {
|
|
448
|
+
def.input.query = extractJsonSchema(this._query);
|
|
449
|
+
if (isStandardSchema(this._query))
|
|
450
|
+
def.input.querySchema = this._query;
|
|
451
|
+
}
|
|
452
|
+
if (this._params) {
|
|
453
|
+
def.input.params = extractJsonSchema(this._params);
|
|
454
|
+
if (isStandardSchema(this._params))
|
|
455
|
+
def.input.paramsSchema = this._params;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (Object.keys(this._responses).length > 0) {
|
|
459
|
+
def.responses = this._responses;
|
|
460
|
+
}
|
|
461
|
+
return def;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
function endpoint(method, path) {
|
|
465
|
+
return new EndpointBuilder(method, path);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/plugins/webauthn/index.ts
|
|
469
|
+
var RE_PLUS = /\+/g;
|
|
470
|
+
var RE_SLASH = /\//g;
|
|
471
|
+
var RE_TRAILING_EQ = /=+$/;
|
|
472
|
+
function base64urlToUint8Array(base64url) {
|
|
473
|
+
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
474
|
+
const pad = base64.length % 4;
|
|
475
|
+
const padded = pad ? base64 + "=".repeat(4 - pad) : base64;
|
|
476
|
+
const binary = atob(padded);
|
|
477
|
+
const bytes = new Uint8Array(binary.length);
|
|
478
|
+
for (let i = 0; i < binary.length; i++) {
|
|
479
|
+
bytes[i] = binary.charCodeAt(i);
|
|
480
|
+
}
|
|
481
|
+
return bytes;
|
|
482
|
+
}
|
|
483
|
+
function uint8ArrayToBase64url(bytes) {
|
|
484
|
+
let binary = "";
|
|
485
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
486
|
+
binary += String.fromCharCode(bytes[i]);
|
|
487
|
+
}
|
|
488
|
+
return btoa(binary).replace(RE_PLUS, "-").replace(RE_SLASH, "_").replace(RE_TRAILING_EQ, "");
|
|
489
|
+
}
|
|
490
|
+
var webauthnRoutes = {
|
|
491
|
+
generateRegistrationOptions: endpoint("POST", "/webauthn/register/options").summary("Generate WebAuthn registration options").description("Generate public key credential creation options for registering a new passkey against the authenticated caller.").tags("WebAuthn").security("bearer").body(obj({})).response(200, "Registration options", obj({
|
|
492
|
+
options: record("PublicKeyCredentialCreationOptions JSON")
|
|
493
|
+
}, "options")).response(400, "Bad request").response(401, "Not authenticated").response(404, "User not found").handler("generateRegistrationOptions").build(),
|
|
494
|
+
verifyRegistration: endpoint("POST", "/webauthn/register/verify").summary("Verify WebAuthn registration").description("Verify the registration response from the authenticator and store the new credential for the authenticated caller.").tags("WebAuthn").security("bearer").body(obj({
|
|
495
|
+
response: record("RegistrationResponseJSON from navigator.credentials.create()")
|
|
496
|
+
}, "response")).response(200, "Registration verified", obj({
|
|
497
|
+
verified: bool("Whether registration was successful"),
|
|
498
|
+
credentialId: str("Base64url-encoded credential ID"),
|
|
499
|
+
credentialDeviceType: str("singleDevice or multiDevice"),
|
|
500
|
+
credentialBackedUp: bool("Whether credential is backed up (synced passkey)")
|
|
501
|
+
}, "verified")).response(400, "Verification failed").handler("verifyRegistration").build(),
|
|
502
|
+
generateAuthenticationOptions: endpoint("POST", "/webauthn/authenticate/options").summary("Generate WebAuthn authentication options").description("Generate public key credential request options. Optionally pass userId for non-discoverable credential flow.").tags("WebAuthn").security("none").body(obj({ userId: id("Optional user ID") })).response(200, "Authentication options", obj({
|
|
503
|
+
options: record("PublicKeyCredentialRequestOptions JSON")
|
|
504
|
+
}, "options")).handler("generateAuthenticationOptions").build(),
|
|
505
|
+
verifyAuthentication: endpoint("POST", "/webauthn/authenticate/verify").summary("Verify WebAuthn authentication").description("Verify the authentication assertion. Returns tokens if passwordless mode is enabled.").tags("WebAuthn").security("none").body(obj({
|
|
506
|
+
response: record("AuthenticationResponseJSON from navigator.credentials.get()")
|
|
507
|
+
}, "response")).response(200, "Authentication verified", obj({
|
|
508
|
+
verified: bool("Whether authentication was successful"),
|
|
509
|
+
userId: id("Authenticated user ID"),
|
|
510
|
+
accessToken: str("JWT access token (if passwordless)"),
|
|
511
|
+
refreshToken: str("Refresh token (if passwordless)")
|
|
512
|
+
}, "verified", "userId")).response(401, "Authentication failed").handler("verifyAuthentication").build()
|
|
513
|
+
};
|
|
514
|
+
function webauthn(config) {
|
|
515
|
+
const rpName = config.rpName;
|
|
516
|
+
const rpID = config.rpID;
|
|
517
|
+
const origin = config.origin;
|
|
518
|
+
const attestation = config.attestation ?? "none";
|
|
519
|
+
const timeout = config.timeout ?? 6e4;
|
|
520
|
+
const challengeTTLSeconds = config.challengeTTLSeconds ?? 300;
|
|
521
|
+
const supportPasswordless = config.supportPasswordless ?? true;
|
|
522
|
+
const authenticatorSelection = {
|
|
523
|
+
residentKey: config.authenticatorSelection?.residentKey ?? "preferred",
|
|
524
|
+
userVerification: config.authenticatorSelection?.userVerification ?? "preferred",
|
|
525
|
+
...config.authenticatorSelection?.authenticatorAttachment ? { authenticatorAttachment: config.authenticatorSelection.authenticatorAttachment } : {}
|
|
526
|
+
};
|
|
527
|
+
async function verifyAuthenticationProof(db, response) {
|
|
528
|
+
const credentialRecord = await db.findOne({
|
|
529
|
+
model: "webauthn_credential",
|
|
530
|
+
where: [{ field: "credentialId", operator: "=", value: response.id }]
|
|
531
|
+
});
|
|
532
|
+
if (!credentialRecord)
|
|
533
|
+
throw Errors.unauthorized("Unknown credential");
|
|
534
|
+
let responseChallenge;
|
|
535
|
+
try {
|
|
536
|
+
const clientData = JSON.parse(
|
|
537
|
+
new TextDecoder().decode(base64urlToUint8Array(response.response.clientDataJSON))
|
|
538
|
+
);
|
|
539
|
+
if (typeof clientData.challenge !== "string" || clientData.challenge.length === 0)
|
|
540
|
+
throw new Error("missing challenge");
|
|
541
|
+
responseChallenge = clientData.challenge;
|
|
542
|
+
} catch {
|
|
543
|
+
throw Errors.unauthorized("Invalid WebAuthn client data");
|
|
544
|
+
}
|
|
545
|
+
const challenge = await db.findOne({
|
|
546
|
+
model: "webauthn_challenge",
|
|
547
|
+
where: [{ field: "challenge", operator: "=", value: responseChallenge }]
|
|
548
|
+
});
|
|
549
|
+
if (!challenge || challenge.userId !== null && challenge.userId !== credentialRecord.userId)
|
|
550
|
+
throw Errors.badRequest("No pending authentication challenge");
|
|
551
|
+
if (new Date(challenge.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
552
|
+
await db.delete({
|
|
553
|
+
model: "webauthn_challenge",
|
|
554
|
+
where: [{ field: "id", operator: "=", value: challenge.id }]
|
|
555
|
+
});
|
|
556
|
+
throw Errors.badRequest("Challenge expired");
|
|
557
|
+
}
|
|
558
|
+
const verification = await (0, import_server.verifyAuthenticationResponse)({
|
|
559
|
+
response,
|
|
560
|
+
expectedChallenge: challenge.challenge,
|
|
561
|
+
expectedOrigin: origin,
|
|
562
|
+
expectedRPID: rpID,
|
|
563
|
+
credential: {
|
|
564
|
+
id: credentialRecord.credentialId,
|
|
565
|
+
publicKey: base64urlToUint8Array(credentialRecord.publicKey),
|
|
566
|
+
counter: credentialRecord.counter,
|
|
567
|
+
transports: credentialRecord.transports ? JSON.parse(credentialRecord.transports) : void 0
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
if (!verification.verified)
|
|
571
|
+
throw Errors.unauthorized("Authentication verification failed");
|
|
572
|
+
const { newCounter } = verification.authenticationInfo;
|
|
573
|
+
if (newCounter > 0 && credentialRecord.counter > 0 && newCounter <= credentialRecord.counter)
|
|
574
|
+
throw Errors.unauthorized("Authenticator counter validation failed");
|
|
575
|
+
await db.update({
|
|
576
|
+
model: "webauthn_credential",
|
|
577
|
+
where: [{ field: "id", operator: "=", value: credentialRecord.id }],
|
|
578
|
+
data: { counter: newCounter }
|
|
579
|
+
});
|
|
580
|
+
await db.delete({
|
|
581
|
+
model: "webauthn_challenge",
|
|
582
|
+
where: [{ field: "id", operator: "=", value: challenge.id }]
|
|
583
|
+
});
|
|
584
|
+
return credentialRecord.userId;
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
name: "webauthn",
|
|
588
|
+
models: [
|
|
589
|
+
{
|
|
590
|
+
name: "webauthn_credential",
|
|
591
|
+
fields: {
|
|
592
|
+
id: { type: "number", required: true },
|
|
593
|
+
userId: { type: "number", required: true, references: { model: "user", field: "id" } },
|
|
594
|
+
credentialId: { type: "string", required: true, unique: true },
|
|
595
|
+
publicKey: { type: "string", required: true },
|
|
596
|
+
counter: { type: "number", required: true },
|
|
597
|
+
deviceType: { type: "string", required: true },
|
|
598
|
+
backedUp: { type: "boolean", required: true },
|
|
599
|
+
transports: { type: "string" },
|
|
600
|
+
createdAt: { type: "date", required: true }
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: "webauthn_challenge",
|
|
605
|
+
fields: {
|
|
606
|
+
id: { type: "number", required: true },
|
|
607
|
+
challenge: { type: "string", required: true, unique: true },
|
|
608
|
+
userId: { type: "number" },
|
|
609
|
+
expiresAt: { type: "date", required: true },
|
|
610
|
+
createdAt: { type: "date", required: true }
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
],
|
|
614
|
+
routes: webauthnRoutes,
|
|
615
|
+
hooks: supportPasswordless ? void 0 : {
|
|
616
|
+
postAuthGate: {
|
|
617
|
+
reason: "webauthn",
|
|
618
|
+
async evaluate(ctx) {
|
|
619
|
+
const credential = await ctx.db.findOne({
|
|
620
|
+
model: "webauthn_credential",
|
|
621
|
+
where: [{ field: "userId", operator: "=", value: ctx.user.id }]
|
|
622
|
+
});
|
|
623
|
+
return credential ? { pluginData: { requiresWebAuthn: true } } : void 0;
|
|
624
|
+
},
|
|
625
|
+
async verify(ctx, completion) {
|
|
626
|
+
if (!completion || typeof completion !== "object" || !("response" in completion))
|
|
627
|
+
throw Errors.unauthorized("Invalid WebAuthn completion");
|
|
628
|
+
const userId = await verifyAuthenticationProof(
|
|
629
|
+
ctx.db,
|
|
630
|
+
completion.response
|
|
631
|
+
);
|
|
632
|
+
if (userId !== ctx.user.id)
|
|
633
|
+
throw Errors.unauthorized("WebAuthn credential belongs to another user");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
methods: (ctx) => ({
|
|
638
|
+
async generateRegistrationOptions(_input, routeCtx) {
|
|
639
|
+
const userId = routeCtx?.userId;
|
|
640
|
+
if (userId == null)
|
|
641
|
+
throw Errors.unauthorized("User not authenticated");
|
|
642
|
+
const user = await ctx.db.findOne({
|
|
643
|
+
model: "user",
|
|
644
|
+
where: [{ field: "id", operator: "=", value: userId }]
|
|
645
|
+
});
|
|
646
|
+
if (!user)
|
|
647
|
+
throw Errors.notFound("User not found");
|
|
648
|
+
const existing = await ctx.db.findMany({
|
|
649
|
+
model: "webauthn_credential",
|
|
650
|
+
where: [{ field: "userId", operator: "=", value: userId }]
|
|
651
|
+
});
|
|
652
|
+
await ctx.db.delete({
|
|
653
|
+
model: "webauthn_challenge",
|
|
654
|
+
where: [{ field: "userId", operator: "=", value: userId }]
|
|
655
|
+
});
|
|
656
|
+
const options = await (0, import_server.generateRegistrationOptions)({
|
|
657
|
+
rpName,
|
|
658
|
+
rpID,
|
|
659
|
+
userName: user.email,
|
|
660
|
+
userDisplayName: user.name,
|
|
661
|
+
userID: new TextEncoder().encode(String(userId)),
|
|
662
|
+
attestationType: attestation === "indirect" ? "none" : attestation,
|
|
663
|
+
excludeCredentials: existing.map((cred) => ({
|
|
664
|
+
id: cred.credentialId,
|
|
665
|
+
transports: cred.transports ? JSON.parse(cred.transports) : void 0
|
|
666
|
+
})),
|
|
667
|
+
authenticatorSelection,
|
|
668
|
+
timeout
|
|
669
|
+
});
|
|
670
|
+
await ctx.db.create({
|
|
671
|
+
model: "webauthn_challenge",
|
|
672
|
+
data: {
|
|
673
|
+
challenge: options.challenge,
|
|
674
|
+
userId,
|
|
675
|
+
expiresAt: new Date(Date.now() + challengeTTLSeconds * 1e3),
|
|
676
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
return { options };
|
|
680
|
+
},
|
|
681
|
+
async verifyRegistration(input, routeCtx) {
|
|
682
|
+
const userId = routeCtx?.userId;
|
|
683
|
+
if (userId == null)
|
|
684
|
+
throw Errors.unauthorized("User not authenticated");
|
|
685
|
+
const { response } = input;
|
|
686
|
+
const challengeRecord = await ctx.db.findOne({
|
|
687
|
+
model: "webauthn_challenge",
|
|
688
|
+
where: [{ field: "userId", operator: "=", value: userId }]
|
|
689
|
+
});
|
|
690
|
+
if (!challengeRecord)
|
|
691
|
+
throw Errors.badRequest("No pending registration challenge");
|
|
692
|
+
if (new Date(challengeRecord.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
693
|
+
await ctx.db.delete({
|
|
694
|
+
model: "webauthn_challenge",
|
|
695
|
+
where: [{ field: "id", operator: "=", value: challengeRecord.id }]
|
|
696
|
+
});
|
|
697
|
+
throw Errors.badRequest("Challenge expired");
|
|
698
|
+
}
|
|
699
|
+
const verification = await (0, import_server.verifyRegistrationResponse)({
|
|
700
|
+
response,
|
|
701
|
+
expectedChallenge: challengeRecord.challenge,
|
|
702
|
+
expectedOrigin: origin,
|
|
703
|
+
expectedRPID: rpID
|
|
704
|
+
});
|
|
705
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
706
|
+
throw Errors.badRequest("Registration verification failed");
|
|
707
|
+
}
|
|
708
|
+
const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
|
709
|
+
await ctx.db.create({
|
|
710
|
+
model: "webauthn_credential",
|
|
711
|
+
data: {
|
|
712
|
+
userId,
|
|
713
|
+
credentialId: credential.id,
|
|
714
|
+
publicKey: uint8ArrayToBase64url(credential.publicKey),
|
|
715
|
+
counter: credential.counter,
|
|
716
|
+
deviceType: credentialDeviceType,
|
|
717
|
+
backedUp: credentialBackedUp,
|
|
718
|
+
transports: credential.transports ? JSON.stringify(credential.transports) : null,
|
|
719
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
await ctx.db.delete({
|
|
723
|
+
model: "webauthn_challenge",
|
|
724
|
+
where: [{ field: "id", operator: "=", value: challengeRecord.id }]
|
|
725
|
+
});
|
|
726
|
+
return {
|
|
727
|
+
verified: true,
|
|
728
|
+
credentialId: credential.id,
|
|
729
|
+
credentialDeviceType,
|
|
730
|
+
credentialBackedUp
|
|
731
|
+
};
|
|
732
|
+
},
|
|
733
|
+
async generateAuthenticationOptions(input) {
|
|
734
|
+
const { userId } = input;
|
|
735
|
+
let allowCredentials;
|
|
736
|
+
if (userId) {
|
|
737
|
+
await ctx.db.delete({
|
|
738
|
+
model: "webauthn_challenge",
|
|
739
|
+
where: [{ field: "userId", operator: "=", value: userId }]
|
|
740
|
+
});
|
|
741
|
+
const credentials = await ctx.db.findMany({
|
|
742
|
+
model: "webauthn_credential",
|
|
743
|
+
where: [{ field: "userId", operator: "=", value: userId }]
|
|
744
|
+
});
|
|
745
|
+
allowCredentials = credentials.map((cred) => ({
|
|
746
|
+
id: cred.credentialId,
|
|
747
|
+
transports: cred.transports ? JSON.parse(cred.transports) : void 0
|
|
748
|
+
}));
|
|
749
|
+
}
|
|
750
|
+
const options = await (0, import_server.generateAuthenticationOptions)({
|
|
751
|
+
rpID,
|
|
752
|
+
allowCredentials,
|
|
753
|
+
userVerification: authenticatorSelection.userVerification,
|
|
754
|
+
timeout
|
|
755
|
+
});
|
|
756
|
+
await ctx.db.create({
|
|
757
|
+
model: "webauthn_challenge",
|
|
758
|
+
data: {
|
|
759
|
+
challenge: options.challenge,
|
|
760
|
+
userId: userId ?? null,
|
|
761
|
+
expiresAt: new Date(Date.now() + challengeTTLSeconds * 1e3),
|
|
762
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
return { options };
|
|
766
|
+
},
|
|
767
|
+
async completeAuthentication(continuationToken, response, meta) {
|
|
768
|
+
if (!ctx.auth)
|
|
769
|
+
throw Errors.badRequest("Auth service is unavailable");
|
|
770
|
+
return ctx.auth.completePendingAuth(continuationToken, { response }, meta);
|
|
771
|
+
},
|
|
772
|
+
async verifyAuthentication(input, meta) {
|
|
773
|
+
const userId = await verifyAuthenticationProof(ctx.db, input.response);
|
|
774
|
+
if (!supportPasswordless)
|
|
775
|
+
throw Errors.badRequest("Use completeAuthentication for second-factor WebAuthn");
|
|
776
|
+
if (!ctx.auth)
|
|
777
|
+
throw Errors.badRequest("Auth service is unavailable");
|
|
778
|
+
return ctx.auth.completePluginAuth(userId, "webauthn", meta);
|
|
779
|
+
}
|
|
780
|
+
})
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
784
|
+
0 && (module.exports = {
|
|
785
|
+
webauthn
|
|
786
|
+
});
|