@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,881 @@
|
|
|
1
|
+
import type { EndpointDefinition, EndpointResponse, HttpMethod, SecurityRequirement } from './endpoint';
|
|
2
|
+
import type { FortressSchema, Infer, JSONSchema, Simplify } from './json-schema';
|
|
3
|
+
import type { StandardSchemaV1 } from './standard-schema';
|
|
4
|
+
import { fromJSONSchema } from '@bajustone/fetcher/openapi';
|
|
5
|
+
import { date as fDate, datetime as fDatetime, email as fEmail, time as fTime, url as fUrl, uuid as fUuid } from '@bajustone/fetcher/schema';
|
|
6
|
+
|
|
7
|
+
/** Shorthand for the Standard Schema inferred output type. */
|
|
8
|
+
type InferSchema<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;
|
|
9
|
+
|
|
10
|
+
/** Distribute a union into an intersection — `A | B` → `A & B`. Used by {@link intersect}. */
|
|
11
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
12
|
+
|
|
13
|
+
/** Options accepted by the {@link str} builder (string constraints + annotations). */
|
|
14
|
+
export interface StringOptions {
|
|
15
|
+
description?: string;
|
|
16
|
+
/** Minimum length (`minLength`), enforced at runtime. */
|
|
17
|
+
min?: number;
|
|
18
|
+
/** Maximum length (`maxLength`), enforced at runtime. */
|
|
19
|
+
max?: number;
|
|
20
|
+
/** Regular-expression source (`pattern`), enforced at runtime. */
|
|
21
|
+
pattern?: string;
|
|
22
|
+
/** OpenAPI `format` annotation (e.g. `email`, `uuid`). Annotation-only unless paired with `pattern`. */
|
|
23
|
+
format?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Options accepted by the {@link num}/{@link int} builders (numeric bounds + annotations). */
|
|
27
|
+
export interface NumberOptions {
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Inclusive minimum (`minimum`), enforced at runtime. */
|
|
30
|
+
min?: number;
|
|
31
|
+
/** Inclusive maximum (`maximum`), enforced at runtime. */
|
|
32
|
+
max?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Standard Schema wiring ─────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Collect the component names referenced by `$ref` anywhere in a schema tree
|
|
39
|
+
* (the last path segment, e.g. `PermissionInput` for
|
|
40
|
+
* `#/components/schemas/PermissionInput`).
|
|
41
|
+
*/
|
|
42
|
+
function collectRefNames(schema: JSONSchema | undefined, acc: Set<string>): Set<string> {
|
|
43
|
+
if (!schema || typeof schema !== 'object')
|
|
44
|
+
return acc;
|
|
45
|
+
if (typeof schema.$ref === 'string') {
|
|
46
|
+
const i = schema.$ref.lastIndexOf('/');
|
|
47
|
+
acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
|
|
48
|
+
}
|
|
49
|
+
if (schema.properties) {
|
|
50
|
+
for (const key of Object.keys(schema.properties))
|
|
51
|
+
collectRefNames(schema.properties[key], acc);
|
|
52
|
+
}
|
|
53
|
+
collectRefNames(schema.items, acc);
|
|
54
|
+
for (const key of ['oneOf', 'anyOf', 'allOf'] as const) {
|
|
55
|
+
const variants = schema[key];
|
|
56
|
+
if (variants) {
|
|
57
|
+
for (const variant of variants)
|
|
58
|
+
collectRefNames(variant, acc);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object')
|
|
62
|
+
collectRefNames(schema.additionalProperties, acc);
|
|
63
|
+
return acc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Component definitions bound to a particular `$ref` schema. A WeakMap keeps
|
|
68
|
+
* this runtime-only context out of emitted JSON Schema and, unlike a global
|
|
69
|
+
* name registry, prevents two independent component maps that both contain a
|
|
70
|
+
* `User` schema from overwriting each other before lazy validation runs.
|
|
71
|
+
*/
|
|
72
|
+
const refDefinitionContexts = new WeakMap<object, Record<string, JSONSchema>>();
|
|
73
|
+
|
|
74
|
+
function refName(refPath: string): string {
|
|
75
|
+
const i = refPath.lastIndexOf('/');
|
|
76
|
+
return i >= 0 ? refPath.slice(i + 1) : refPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// OpenAPI 3.x component-map keys are restricted to this grammar. Rejecting
|
|
80
|
+
// unsupported names is safer than emitting an ambiguous JSON Pointer that
|
|
81
|
+
// silently resolves to a different final path segment.
|
|
82
|
+
const COMPONENT_NAME_RE = /^[\w.-]+$/;
|
|
83
|
+
|
|
84
|
+
function assertComponentName(name: string): void {
|
|
85
|
+
if (!COMPONENT_NAME_RE.test(name))
|
|
86
|
+
throw new Error(`Invalid OpenAPI component name '${name}'`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the definitions map fed to `fromJSONSchema`, resolving each `$ref`
|
|
91
|
+
* against the context bound by {@link defineComponents} / typed {@link ref}.
|
|
92
|
+
* Context is inherited while traversing a referenced component, which supports
|
|
93
|
+
* transitive and recursive refs. Bare refs without any same-name definition
|
|
94
|
+
* in the composed schema remain permissive (`{}`); when composed with a bound
|
|
95
|
+
* ref of the same OpenAPI component name, both correctly resolve to that one
|
|
96
|
+
* global component definition.
|
|
97
|
+
*/
|
|
98
|
+
function resolveRefDefs(schema: JSONSchema): Record<string, JSONSchema> | undefined {
|
|
99
|
+
if (collectRefNames(schema, new Set<string>()).size === 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
|
|
102
|
+
const defs = Object.create(null) as Record<string, JSONSchema>;
|
|
103
|
+
const unresolved = new Set<string>();
|
|
104
|
+
const hasOwn = (value: object, key: string): boolean => Object.hasOwn(value, key);
|
|
105
|
+
|
|
106
|
+
const visit = (node: JSONSchema | undefined, inherited?: Record<string, JSONSchema>): void => {
|
|
107
|
+
if (!node || typeof node !== 'object')
|
|
108
|
+
return;
|
|
109
|
+
const context = refDefinitionContexts.get(node) ?? inherited;
|
|
110
|
+
if (typeof node.$ref === 'string') {
|
|
111
|
+
const name = refName(node.$ref);
|
|
112
|
+
const component = context && hasOwn(context, name) ? context[name] : undefined;
|
|
113
|
+
if (component) {
|
|
114
|
+
if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
|
|
115
|
+
throw new Error(`Conflicting component definitions for $ref '${name}'`);
|
|
116
|
+
}
|
|
117
|
+
const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
|
|
118
|
+
defs[name] = component;
|
|
119
|
+
if (firstResolution)
|
|
120
|
+
visit(component, context);
|
|
121
|
+
}
|
|
122
|
+
else if (!hasOwn(defs, name)) {
|
|
123
|
+
defs[name] = {};
|
|
124
|
+
unresolved.add(name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (node.properties) {
|
|
128
|
+
for (const child of Object.values(node.properties))
|
|
129
|
+
visit(child, context);
|
|
130
|
+
}
|
|
131
|
+
visit(node.items, context);
|
|
132
|
+
for (const key of ['oneOf', 'anyOf', 'allOf'] as const) {
|
|
133
|
+
for (const child of node[key] ?? [])
|
|
134
|
+
visit(child, context);
|
|
135
|
+
}
|
|
136
|
+
if (node.additionalProperties && typeof node.additionalProperties === 'object')
|
|
137
|
+
visit(node.additionalProperties, context);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
visit(schema);
|
|
141
|
+
return defs;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the `~standard` props for a fortress schema. Runtime validation is
|
|
146
|
+
* delegated to `@bajustone/fetcher`'s `fromJSONSchema`, which compiles the
|
|
147
|
+
* JSON Schema object into a Standard Schema V1 validator (lazily, on first
|
|
148
|
+
* use, then memoized). fortress schemas keep `vendor: 'fortress'` and remain
|
|
149
|
+
* plain JSON Schema objects — only the validation engine is fetcher's.
|
|
150
|
+
*
|
|
151
|
+
* `$ref` nodes are resolved against context bound by {@link defineComponents}
|
|
152
|
+
* / typed {@link ref}: the referenced component's JSON
|
|
153
|
+
* Schema (and any components it transitively references) is threaded into the
|
|
154
|
+
* compiled validator so ref'd request bodies are enforced. A `$ref` whose
|
|
155
|
+
* component is unavailable anywhere in the composed schema stays permissive
|
|
156
|
+
* (compiled against `{}`), and surrounding inline fields are always validated
|
|
157
|
+
* strictly.
|
|
158
|
+
*/
|
|
159
|
+
function createStandardProps<T>(schema: JSONSchema): StandardSchemaV1<T, T>['~standard'] {
|
|
160
|
+
type ValidateFn = (value: unknown) => StandardSchemaV1.Result<T> | Promise<StandardSchemaV1.Result<T>>;
|
|
161
|
+
let validateFn: ValidateFn | undefined;
|
|
162
|
+
return {
|
|
163
|
+
version: 1,
|
|
164
|
+
vendor: 'fortress',
|
|
165
|
+
validate(value: unknown): StandardSchemaV1.Result<T> | Promise<StandardSchemaV1.Result<T>> {
|
|
166
|
+
if (!validateFn) {
|
|
167
|
+
const defs = resolveRefDefs(schema);
|
|
168
|
+
validateFn = fromJSONSchema<T>(schema, defs)['~standard'].validate as ValidateFn;
|
|
169
|
+
}
|
|
170
|
+
return validateFn(value);
|
|
171
|
+
},
|
|
172
|
+
} as StandardSchemaV1<T, T>['~standard'];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Standard Schema validation for builder `oneOf`: JSON Schema requires that
|
|
177
|
+
* exactly one branch succeeds, while some validator bridges treat it as an
|
|
178
|
+
* ordinary union. Validate each branch directly so this invariant is retained
|
|
179
|
+
* independently of the bridge, including for branches backed by `$ref`.
|
|
180
|
+
*/
|
|
181
|
+
function createExactOneOfProps<T>(schemas: readonly FortressSchema<any>[]): StandardSchemaV1<T, T>['~standard'] {
|
|
182
|
+
type Result = StandardSchemaV1.Result<T>;
|
|
183
|
+
type ValidateFn = (value: unknown) => Result | Promise<Result>;
|
|
184
|
+
const validate: ValidateFn = (value) => {
|
|
185
|
+
const results = schemas.map(schema => schema['~standard'].validate(value));
|
|
186
|
+
if (results.some(result => result instanceof Promise)) {
|
|
187
|
+
return Promise.all(results).then(resolved => exactOneResult(resolved));
|
|
188
|
+
}
|
|
189
|
+
return exactOneResult(results as Result[]);
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
version: 1,
|
|
193
|
+
vendor: 'fortress',
|
|
194
|
+
validate,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function exactOneResult<T>(results: readonly StandardSchemaV1.Result<T>[]): StandardSchemaV1.Result<T> {
|
|
199
|
+
const successes = results.filter((result): result is StandardSchemaV1.SuccessResult<T> => !result.issues);
|
|
200
|
+
if (successes.length === 1)
|
|
201
|
+
return { value: successes[0].value };
|
|
202
|
+
return {
|
|
203
|
+
issues: [{
|
|
204
|
+
message: successes.length === 0
|
|
205
|
+
? 'Value must match exactly one oneOf variant'
|
|
206
|
+
: 'Value must match exactly one oneOf variant; multiple variants matched',
|
|
207
|
+
}],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function toFortressSchema<T>(
|
|
212
|
+
schema: JSONSchema,
|
|
213
|
+
refDefinitions?: Record<string, JSONSchema>,
|
|
214
|
+
standardProps?: StandardSchemaV1<T, T>['~standard'],
|
|
215
|
+
): FortressSchema<T> {
|
|
216
|
+
if (refDefinitions)
|
|
217
|
+
refDefinitionContexts.set(schema, refDefinitions);
|
|
218
|
+
return Object.assign(schema, {
|
|
219
|
+
'~standard': standardProps ?? createStandardProps<T>(schema),
|
|
220
|
+
}) as FortressSchema<T>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Schema Builders ─────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build a string {@link FortressSchema}.
|
|
227
|
+
*
|
|
228
|
+
* Pass a string for just a description, or an options object to set
|
|
229
|
+
* `minLength`/`maxLength`/`pattern`/`format` — the length and pattern
|
|
230
|
+
* constraints are enforced at runtime by the validator.
|
|
231
|
+
*/
|
|
232
|
+
export function str(opts?: string | StringOptions): FortressSchema<string> {
|
|
233
|
+
const o = typeof opts === 'string' ? { description: opts } : opts ?? {};
|
|
234
|
+
const s: JSONSchema = { type: 'string' };
|
|
235
|
+
if (o.description)
|
|
236
|
+
s.description = o.description;
|
|
237
|
+
if (o.min !== undefined)
|
|
238
|
+
s.minLength = o.min;
|
|
239
|
+
if (o.max !== undefined)
|
|
240
|
+
s.maxLength = o.max;
|
|
241
|
+
if (o.pattern !== undefined)
|
|
242
|
+
s.pattern = o.pattern;
|
|
243
|
+
if (o.format !== undefined)
|
|
244
|
+
s.format = o.format;
|
|
245
|
+
return toFortressSchema<string>(s);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Build a number {@link FortressSchema} (any numeric, integer or float).
|
|
250
|
+
*
|
|
251
|
+
* Pass a string for just a description, or an options object to set
|
|
252
|
+
* `minimum`/`maximum` — both are enforced at runtime by the validator.
|
|
253
|
+
*/
|
|
254
|
+
export function num(opts?: string | NumberOptions): FortressSchema<number> {
|
|
255
|
+
const o = typeof opts === 'string' ? { description: opts } : opts ?? {};
|
|
256
|
+
const s: JSONSchema = { type: 'number' };
|
|
257
|
+
if (o.description)
|
|
258
|
+
s.description = o.description;
|
|
259
|
+
if (o.min !== undefined)
|
|
260
|
+
s.minimum = o.min;
|
|
261
|
+
if (o.max !== undefined)
|
|
262
|
+
s.maximum = o.max;
|
|
263
|
+
return toFortressSchema<number>(s);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Build an integer {@link FortressSchema}.
|
|
268
|
+
*
|
|
269
|
+
* Pass a string for just a description, or an options object to set
|
|
270
|
+
* `minimum`/`maximum` — both are enforced at runtime by the validator.
|
|
271
|
+
*/
|
|
272
|
+
export function int(opts?: string | NumberOptions): FortressSchema<number> {
|
|
273
|
+
const o = typeof opts === 'string' ? { description: opts } : opts ?? {};
|
|
274
|
+
const s: JSONSchema = { type: 'integer' };
|
|
275
|
+
if (o.description)
|
|
276
|
+
s.description = o.description;
|
|
277
|
+
if (o.min !== undefined)
|
|
278
|
+
s.minimum = o.min;
|
|
279
|
+
if (o.max !== undefined)
|
|
280
|
+
s.maximum = o.max;
|
|
281
|
+
return toFortressSchema<number>(s);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build a subject-id {@link FortressSchema}.
|
|
286
|
+
*
|
|
287
|
+
* IDs are opaque strings at the fortress API surface (RFC 7519 §4.1.2 for
|
|
288
|
+
* JWT `sub`). Numeric-keyed adapters stringify on read and parse on write
|
|
289
|
+
* at the adapter boundary; consumers never see the underlying representation.
|
|
290
|
+
*/
|
|
291
|
+
export function id(description?: string): FortressSchema<string> {
|
|
292
|
+
const s: JSONSchema = { type: 'string', minLength: 1 };
|
|
293
|
+
if (description)
|
|
294
|
+
s.description = description;
|
|
295
|
+
return toFortressSchema<string>(s);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Build a boolean {@link FortressSchema}. */
|
|
299
|
+
export function bool(description?: string): FortressSchema<boolean> {
|
|
300
|
+
const s: JSONSchema = { type: 'boolean' };
|
|
301
|
+
if (description)
|
|
302
|
+
s.description = description;
|
|
303
|
+
return toFortressSchema<boolean>(s);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Build an array {@link FortressSchema} whose items match the supplied schema. */
|
|
307
|
+
export function arr<T>(items: FortressSchema<T>, description?: string): FortressSchema<T[]> {
|
|
308
|
+
const s: JSONSchema = { type: 'array', items };
|
|
309
|
+
if (description)
|
|
310
|
+
s.description = description;
|
|
311
|
+
return toFortressSchema<T[]>(s);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build an object {@link FortressSchema}. Pass property names after `properties`
|
|
316
|
+
* to mark them as required — they become non-optional in the inferred type.
|
|
317
|
+
*/
|
|
318
|
+
export function obj<
|
|
319
|
+
P extends Record<string, FortressSchema<any>>,
|
|
320
|
+
K extends (keyof P & string)[] = [],
|
|
321
|
+
>(
|
|
322
|
+
properties: P,
|
|
323
|
+
...required: K
|
|
324
|
+
): FortressSchema<
|
|
325
|
+
Simplify<
|
|
326
|
+
{ [Key in K[number]]: Infer<P[Key]> }
|
|
327
|
+
& { [Key in Exclude<keyof P & string, K[number]>]?: Infer<P[Key]> }
|
|
328
|
+
>
|
|
329
|
+
> {
|
|
330
|
+
const s: JSONSchema = { type: 'object', properties: properties as Record<string, JSONSchema> };
|
|
331
|
+
if (required.length > 0)
|
|
332
|
+
s.required = required;
|
|
333
|
+
return toFortressSchema(s);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Wrap a schema so it also accepts `null`. */
|
|
337
|
+
export function nullable<T>(schema: FortressSchema<T>): FortressSchema<T | null> {
|
|
338
|
+
// Fetcher's JSON-Schema bridge resolves `$ref` before OpenAPI's legacy
|
|
339
|
+
// `nullable` sibling. Use a real union for refs so null is enforced
|
|
340
|
+
// correctly; keep the established compact shape for inline schemas.
|
|
341
|
+
if (typeof schema.$ref === 'string')
|
|
342
|
+
return toFortressSchema<T | null>({ oneOf: [schema, { type: 'null' }] });
|
|
343
|
+
const s: JSONSchema = { ...schema, nullable: true };
|
|
344
|
+
return toFortressSchema<T | null>(s, refDefinitionContexts.get(schema));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Build a discriminated-union schema (exactly one variant must match). */
|
|
348
|
+
export function oneOf<S extends FortressSchema<any>[]>(
|
|
349
|
+
...schemas: S
|
|
350
|
+
): FortressSchema<Infer<S[number]>> {
|
|
351
|
+
const schema: JSONSchema = { oneOf: schemas as JSONSchema[] };
|
|
352
|
+
return toFortressSchema<Infer<S[number]>>(
|
|
353
|
+
schema,
|
|
354
|
+
undefined,
|
|
355
|
+
createExactOneOfProps<Infer<S[number]>>(schemas),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Build a union schema (any one variant may match). */
|
|
360
|
+
export function anyOf<S extends FortressSchema<any>[]>(
|
|
361
|
+
...schemas: S
|
|
362
|
+
): FortressSchema<Infer<S[number]>> {
|
|
363
|
+
return toFortressSchema<Infer<S[number]>>({ anyOf: schemas as JSONSchema[] });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Build a `$ref` schema pointing at an OpenAPI component schema by name.
|
|
368
|
+
*
|
|
369
|
+
* Two forms:
|
|
370
|
+
* - `ref(name)` — untyped `$ref`, returns `FortressSchema<unknown>`. Use when
|
|
371
|
+
* the referenced component hasn't been declared yet (e.g. self-references
|
|
372
|
+
* inside a components literal).
|
|
373
|
+
* - `ref(name, schema)` — typed and runtime-bound `$ref`. The supplied schema
|
|
374
|
+
* provides both the inferred TypeScript type and the component definition
|
|
375
|
+
* used for runtime validation. Use when you already have the component
|
|
376
|
+
* schema and want downstream types and validation to flow through.
|
|
377
|
+
*
|
|
378
|
+
* For the common case (typed refs against a known components map), prefer
|
|
379
|
+
* {@link defineComponents} — it returns a bound `ref` that only needs a name.
|
|
380
|
+
*/
|
|
381
|
+
export function ref<T>(name: string, schema: FortressSchema<T>): FortressSchema<T>;
|
|
382
|
+
export function ref(name: string): FortressSchema<unknown>;
|
|
383
|
+
export function ref<T>(name: string, schema?: FortressSchema<T>): FortressSchema<T> {
|
|
384
|
+
assertComponentName(name);
|
|
385
|
+
// Typed refs bind the real component schema. Bare `ref(name)` has no local
|
|
386
|
+
// definition context; it stays permissive unless the composed schema binds
|
|
387
|
+
// the same global OpenAPI component name elsewhere.
|
|
388
|
+
const definitions = schema ? { [name]: schema } : undefined;
|
|
389
|
+
return toFortressSchema<T>({ $ref: `#/components/schemas/${name}` }, definitions);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Declare a typed registry of OpenAPI component schemas and return a bound
|
|
394
|
+
* `ref` function that preserves each component's inferred TypeScript type.
|
|
395
|
+
*
|
|
396
|
+
* ```ts
|
|
397
|
+
* const User = obj({ id: int(), email: str() }, 'id', 'email');
|
|
398
|
+
* const { components, ref } = defineComponents({ User });
|
|
399
|
+
*
|
|
400
|
+
* const ep = endpoint('GET', '/me')
|
|
401
|
+
* .response(200, 'Current user', ref('User'))
|
|
402
|
+
* // ^ FortressSchema<{ id: string; email: string }>
|
|
403
|
+
* .handler('me')
|
|
404
|
+
* .build();
|
|
405
|
+
* ```
|
|
406
|
+
*
|
|
407
|
+
* The returned `components` is the same object you passed in (useful for
|
|
408
|
+
* OpenAPI spec emission). The returned `ref` is a generic function that
|
|
409
|
+
* looks up each key in `T` and returns a `$ref` schema carrying the
|
|
410
|
+
* inferred type of the referenced component.
|
|
411
|
+
*/
|
|
412
|
+
export function defineComponents<T extends Record<string, FortressSchema<any>>>(
|
|
413
|
+
components: T,
|
|
414
|
+
): {
|
|
415
|
+
components: T;
|
|
416
|
+
ref: <K extends keyof T & string>(name: K) => FortressSchema<Infer<T[K]>>;
|
|
417
|
+
} {
|
|
418
|
+
for (const name of Object.keys(components))
|
|
419
|
+
assertComponentName(name);
|
|
420
|
+
// Snapshot the name→schema bindings now. Validators compile lazily, so
|
|
421
|
+
// retaining the caller's mutable registry would otherwise make replacement
|
|
422
|
+
// before first validation behave differently from replacement afterward.
|
|
423
|
+
const definitions = Object.assign(
|
|
424
|
+
Object.create(null) as Record<string, JSONSchema>,
|
|
425
|
+
components,
|
|
426
|
+
);
|
|
427
|
+
return {
|
|
428
|
+
components,
|
|
429
|
+
ref: <K extends keyof T & string>(name: K): FortressSchema<Infer<T[K]>> => {
|
|
430
|
+
assertComponentName(name);
|
|
431
|
+
return toFortressSchema<Infer<T[K]>>(
|
|
432
|
+
{ $ref: `#/components/schemas/${String(name)}` },
|
|
433
|
+
definitions,
|
|
434
|
+
);
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Build an enum {@link FortressSchema} from a fixed set of string or number literal values. */
|
|
440
|
+
export function enums<T extends string | number>(...values: T[]): FortressSchema<T> {
|
|
441
|
+
return toFortressSchema<T>({ enum: values });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Build a string {@link FortressSchema} with an OpenAPI `format` annotation (e.g. `email`, `uri`, `uuid`). */
|
|
445
|
+
export function strFormat(format: string, description?: string): FortressSchema<string> {
|
|
446
|
+
const s: JSONSchema = { type: 'string', format };
|
|
447
|
+
if (description)
|
|
448
|
+
s.description = description;
|
|
449
|
+
return toFortressSchema<string>(s);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Build a {@link FortressSchema} that only accepts the literal `null`. */
|
|
453
|
+
export function nullType(): FortressSchema<null> {
|
|
454
|
+
return toFortressSchema<null>({ type: 'null' });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** An object {@link FortressSchema} with unknown additional properties (e.g. plugin data, metadata). */
|
|
458
|
+
export function record(description?: string): FortressSchema<Record<string, unknown>> {
|
|
459
|
+
const s: JSONSchema = { type: 'object', additionalProperties: true };
|
|
460
|
+
if (description)
|
|
461
|
+
s.description = description;
|
|
462
|
+
return toFortressSchema<Record<string, unknown>>(s);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** An object {@link FortressSchema} where every property value matches the supplied schema. */
|
|
466
|
+
export function recordOf<T>(valueSchema: FortressSchema<T>, description?: string): FortressSchema<Record<string, T>> {
|
|
467
|
+
const s: JSONSchema = { type: 'object', additionalProperties: valueSchema as JSONSchema };
|
|
468
|
+
if (description)
|
|
469
|
+
s.description = description;
|
|
470
|
+
return toFortressSchema<Record<string, T>>(s);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Build a literal {@link FortressSchema} accepting exactly one constant value
|
|
475
|
+
* (`const`). The value is enforced at runtime and the inferred type narrows to
|
|
476
|
+
* the literal (e.g. `literal('admin')` infers `'admin'`).
|
|
477
|
+
*/
|
|
478
|
+
export function literal<const V extends string | number | boolean>(value: V, description?: string): FortressSchema<V> {
|
|
479
|
+
const s: JSONSchema = { const: value };
|
|
480
|
+
if (description)
|
|
481
|
+
s.description = description;
|
|
482
|
+
return toFortressSchema<V>(s);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Build an intersection {@link FortressSchema} (`allOf`) — a value must satisfy
|
|
487
|
+
* every supplied schema. The inferred type is the intersection of each schema's
|
|
488
|
+
* inferred type.
|
|
489
|
+
*/
|
|
490
|
+
export function intersect<S extends FortressSchema<any>[]>(
|
|
491
|
+
...schemas: S
|
|
492
|
+
): FortressSchema<UnionToIntersection<Infer<S[number]>>> {
|
|
493
|
+
return toFortressSchema<UnionToIntersection<Infer<S[number]>>>({ allOf: schemas as JSONSchema[] });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Close an object {@link FortressSchema} — sets `additionalProperties: false`,
|
|
498
|
+
* so the validator rejects any key not declared in `properties`. Use to harden
|
|
499
|
+
* request bodies against over-posting (mass assignment).
|
|
500
|
+
*/
|
|
501
|
+
export function strict<T>(schema: FortressSchema<T>): FortressSchema<T> {
|
|
502
|
+
return toFortressSchema<T>(
|
|
503
|
+
{ ...schema, additionalProperties: false },
|
|
504
|
+
refDefinitionContexts.get(schema),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Build a discriminated-union {@link FortressSchema}. Emits `oneOf` plus an
|
|
510
|
+
* OpenAPI `discriminator`, which the validator uses to dispatch on
|
|
511
|
+
* `propertyName` (faster, with precise per-variant errors) instead of trying
|
|
512
|
+
* every branch.
|
|
513
|
+
*/
|
|
514
|
+
export function discriminatedUnion<S extends FortressSchema<any>[]>(
|
|
515
|
+
propertyName: string,
|
|
516
|
+
...variants: S
|
|
517
|
+
): FortressSchema<Infer<S[number]>> {
|
|
518
|
+
return toFortressSchema<Infer<S[number]>>({
|
|
519
|
+
oneOf: variants as JSONSchema[],
|
|
520
|
+
discriminator: { propertyName },
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ── Enforced string formats ─────────────────────────────────────────
|
|
525
|
+
// Each lifts the `format` + ReDoS-safe `pattern` from the corresponding
|
|
526
|
+
// `@bajustone/fetcher/schema` format builder (the single source of truth), so
|
|
527
|
+
// the value is BOTH documented (`format`) and enforced at runtime (`pattern`),
|
|
528
|
+
// while the schema stays a plain fortress JSON Schema object.
|
|
529
|
+
|
|
530
|
+
/** Build a string format builder by lifting `{ format, pattern }` from a fetcher format factory. */
|
|
531
|
+
function makeFormatBuilder(factory: () => unknown): (description?: string) => FortressSchema<string> {
|
|
532
|
+
const f = factory() as { format?: string; pattern?: string };
|
|
533
|
+
return (description?: string): FortressSchema<string> => {
|
|
534
|
+
const s: JSONSchema = { type: 'string' };
|
|
535
|
+
if (f.format)
|
|
536
|
+
s.format = f.format;
|
|
537
|
+
if (f.pattern)
|
|
538
|
+
s.pattern = f.pattern;
|
|
539
|
+
if (description)
|
|
540
|
+
s.description = description;
|
|
541
|
+
return toFortressSchema<string>(s);
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Email string schema (`format: 'email'`) — enforces the WHATWG HTML5 email grammar at runtime. */
|
|
546
|
+
export const email: (description?: string) => FortressSchema<string> = makeFormatBuilder(fEmail);
|
|
547
|
+
|
|
548
|
+
/** UUID string schema (`format: 'uuid'`) — enforces RFC 9562 versions 1–8 plus nil/max at runtime. */
|
|
549
|
+
export const uuid: (description?: string) => FortressSchema<string> = makeFormatBuilder(fUuid);
|
|
550
|
+
|
|
551
|
+
/** URL string schema (`format: 'uri'`) — enforces an explicit `scheme://` authority at runtime. */
|
|
552
|
+
export const url: (description?: string) => FortressSchema<string> = makeFormatBuilder(fUrl);
|
|
553
|
+
|
|
554
|
+
/** RFC 3339 date-time string schema (`format: 'date-time'`) — field ranges enforced at runtime. */
|
|
555
|
+
export const datetime: (description?: string) => FortressSchema<string> = makeFormatBuilder(fDatetime);
|
|
556
|
+
|
|
557
|
+
/** RFC 3339 full-date string schema (`format: 'date'`) — field ranges enforced at runtime. */
|
|
558
|
+
export const date: (description?: string) => FortressSchema<string> = makeFormatBuilder(fDate);
|
|
559
|
+
|
|
560
|
+
/** RFC 3339 time string schema (`format: 'time'`) — field ranges enforced at runtime. */
|
|
561
|
+
export const time: (description?: string) => FortressSchema<string> = makeFormatBuilder(fTime);
|
|
562
|
+
|
|
563
|
+
// ── Schema detection helpers ────────────────────────────────────────
|
|
564
|
+
|
|
565
|
+
/** Type guard — `true` if the value implements Standard Schema V1. */
|
|
566
|
+
export function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
567
|
+
return (
|
|
568
|
+
typeof value === 'object'
|
|
569
|
+
&& value !== null
|
|
570
|
+
&& '~standard' in value
|
|
571
|
+
&& typeof (value as any)['~standard']?.validate === 'function'
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Type guard — `true` if the value is a {@link FortressSchema} (has both JSON Schema fields and Standard Schema wiring). */
|
|
576
|
+
export function isFortressSchema(value: unknown): value is FortressSchema {
|
|
577
|
+
return isStandardSchema(value)
|
|
578
|
+
&& (value as any)['~standard'].vendor === 'fortress'
|
|
579
|
+
&& ('type' in (value as any) || '$ref' in (value as any) || 'oneOf' in (value as any) || 'anyOf' in (value as any));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Valid JSON Schema type values per the spec. */
|
|
583
|
+
const JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* `true` if the value carries structural JSON Schema props directly on the
|
|
587
|
+
* object — a valid `type` value, or `$ref`/`oneOf`/`anyOf`/`allOf`. Libraries
|
|
588
|
+
* that implement Standard Schema over a JSON Schema object (fortress, fetcher)
|
|
589
|
+
* match this; wrapper schemas like Zod do not (their `type` holds internal
|
|
590
|
+
* kind strings like `'ZodObject'`, not JSON Schema types).
|
|
591
|
+
*/
|
|
592
|
+
function hasJsonSchemaShape(value: unknown): boolean {
|
|
593
|
+
if (typeof value !== 'object' || value === null)
|
|
594
|
+
return false;
|
|
595
|
+
const v = value as any;
|
|
596
|
+
if ('$ref' in v || 'oneOf' in v || 'anyOf' in v || 'allOf' in v)
|
|
597
|
+
return true;
|
|
598
|
+
if (typeof v.type === 'string' && JSON_SCHEMA_TYPES.has(v.type))
|
|
599
|
+
return true;
|
|
600
|
+
if (Array.isArray(v.type) && v.type.every((t: unknown) => typeof t === 'string' && JSON_SCHEMA_TYPES.has(t)))
|
|
601
|
+
return true;
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Extract a {@link JSONSchema} from any schema input.
|
|
607
|
+
*
|
|
608
|
+
* - {@link FortressSchema}: returned as-is (it already _is_ JSON Schema).
|
|
609
|
+
* - External Standard Schema with a `~standard.jsonSchema.input()` adapter
|
|
610
|
+
* (Zod, Valibot, ArkType, etc.): extracted via the adapter.
|
|
611
|
+
* - External Standard Schema that already IS a JSON Schema object (fetcher,
|
|
612
|
+
* etc.): returned as-is.
|
|
613
|
+
* - Anything else: empty object fallback.
|
|
614
|
+
*/
|
|
615
|
+
export function extractJsonSchema(schema: FortressSchema<any> | StandardSchemaV1<any>): JSONSchema {
|
|
616
|
+
// Fortress schemas ARE JSON Schema — return directly
|
|
617
|
+
if (isFortressSchema(schema)) {
|
|
618
|
+
return schema;
|
|
619
|
+
}
|
|
620
|
+
if (isStandardSchema(schema)) {
|
|
621
|
+
const std = (schema as any)['~standard'];
|
|
622
|
+
// Wrapper Standard Schemas that expose a JSON Schema adapter (Zod, Valibot, ...)
|
|
623
|
+
if (std?.jsonSchema?.input) {
|
|
624
|
+
return std.jsonSchema.input({ target: 'draft-2020-12' }) as JSONSchema;
|
|
625
|
+
}
|
|
626
|
+
// Standard Schemas that already ARE JSON Schema objects (fetcher, ...)
|
|
627
|
+
if (hasJsonSchemaShape(schema)) {
|
|
628
|
+
return schema as JSONSchema;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Fallback
|
|
632
|
+
return {};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── Schema input type for endpoint builder ──────────────────────────
|
|
636
|
+
|
|
637
|
+
/** Schema input accepted by `EndpointBuilder.body/query/params` — a fortress schema or any Standard Schema V1 implementation. */
|
|
638
|
+
export type SchemaInput = FortressSchema<any> | StandardSchemaV1<any>;
|
|
639
|
+
|
|
640
|
+
// ── Canonical error envelope ───────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Canonical fortress error response body — the exact wire shape emitted by
|
|
644
|
+
* {@link import('./errors').FortressError.toJSON}. Reference this from
|
|
645
|
+
* endpoint `.response(4xx|5xx, ...)` declarations so host APIs document the
|
|
646
|
+
* same error contract Fortress's own routes produce, and so clients can use
|
|
647
|
+
* a single error parser everywhere.
|
|
648
|
+
*
|
|
649
|
+
* Shape:
|
|
650
|
+
* ```ts
|
|
651
|
+
* {
|
|
652
|
+
* code: string, // FortressErrorCode
|
|
653
|
+
* message: string, // human-readable
|
|
654
|
+
* statusCode: number, // HTTP status
|
|
655
|
+
* details?: unknown, // optional structured payload (e.g. validation issues)
|
|
656
|
+
* }
|
|
657
|
+
* ```
|
|
658
|
+
*
|
|
659
|
+
* See {@link EndpointBuilder.errorResponse} for a shorthand that wires this
|
|
660
|
+
* schema into a status declaration in one call.
|
|
661
|
+
*/
|
|
662
|
+
export interface ErrorEnvelopeBody {
|
|
663
|
+
code: string;
|
|
664
|
+
message: string;
|
|
665
|
+
statusCode: number;
|
|
666
|
+
details?: unknown;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export const ErrorEnvelope: FortressSchema<ErrorEnvelopeBody> = obj({
|
|
670
|
+
code: str('Machine-readable error code'),
|
|
671
|
+
message: str('Human-readable error message'),
|
|
672
|
+
statusCode: int('HTTP status code'),
|
|
673
|
+
details: toFortressSchema<unknown>({ description: 'Optional structured error details' }),
|
|
674
|
+
}, 'code', 'message', 'statusCode');
|
|
675
|
+
|
|
676
|
+
// ── Endpoint Builder ────────────────────────────────────────────────
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Fluent builder for {@link EndpointDefinition} objects. Construct via the
|
|
680
|
+
* {@link endpoint} factory and chain `summary`, `body`, `response`, etc.
|
|
681
|
+
* before calling `build()`.
|
|
682
|
+
*
|
|
683
|
+
* The four type parameters accumulate as schemas are declared: each call to
|
|
684
|
+
* `.body()`, `.query()`, `.params()`, and `.response()` returns a new
|
|
685
|
+
* builder type with the inferred schema baked in. By the time `.build()` is
|
|
686
|
+
* called, the returned {@link EndpointDefinition} carries complete phantom
|
|
687
|
+
* type information about its request/response shape, which the
|
|
688
|
+
* `InferEndpoint*` helpers and the `fortress.call.*` proxy read at the call
|
|
689
|
+
* site.
|
|
690
|
+
*/
|
|
691
|
+
export class EndpointBuilder<
|
|
692
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
693
|
+
TBody = {},
|
|
694
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
695
|
+
TQuery = {},
|
|
696
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
697
|
+
TParams = {},
|
|
698
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
699
|
+
TResponses extends Record<number, unknown> = {},
|
|
700
|
+
> {
|
|
701
|
+
private _method: HttpMethod;
|
|
702
|
+
private _path: string;
|
|
703
|
+
private _handler = '';
|
|
704
|
+
private _summary = '';
|
|
705
|
+
private _description?: string;
|
|
706
|
+
private _tags: string[] = [];
|
|
707
|
+
private _security: SecurityRequirement[] = [];
|
|
708
|
+
private _deprecated = false;
|
|
709
|
+
private _permission?: { resource: string; action: string };
|
|
710
|
+
private _body?: SchemaInput;
|
|
711
|
+
private _query?: SchemaInput;
|
|
712
|
+
private _params?: SchemaInput;
|
|
713
|
+
private _responses: Record<number, EndpointResponse> = {};
|
|
714
|
+
|
|
715
|
+
constructor(method: HttpMethod, path: string) {
|
|
716
|
+
this._method = method;
|
|
717
|
+
this._path = path;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
summary(s: string): this {
|
|
721
|
+
this._summary = s;
|
|
722
|
+
return this;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
description(s: string): this {
|
|
726
|
+
this._description = s;
|
|
727
|
+
return this;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
tags(...t: string[]): this {
|
|
731
|
+
this._tags.push(...t);
|
|
732
|
+
return this;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
security(...s: SecurityRequirement[]): this {
|
|
736
|
+
this._security.push(...s);
|
|
737
|
+
return this;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
deprecated(): this {
|
|
741
|
+
this._deprecated = true;
|
|
742
|
+
return this;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
permission(resource: string, action: string): this {
|
|
746
|
+
this._permission = { resource, action };
|
|
747
|
+
return this;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
body<T extends StandardSchemaV1>(
|
|
751
|
+
schema: T,
|
|
752
|
+
): EndpointBuilder<InferSchema<T>, TQuery, TParams, TResponses> {
|
|
753
|
+
this._body = schema;
|
|
754
|
+
return this as unknown as EndpointBuilder<InferSchema<T>, TQuery, TParams, TResponses>;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
query<T extends StandardSchemaV1>(
|
|
758
|
+
schema: T,
|
|
759
|
+
): EndpointBuilder<TBody, InferSchema<T>, TParams, TResponses> {
|
|
760
|
+
this._query = schema;
|
|
761
|
+
return this as unknown as EndpointBuilder<TBody, InferSchema<T>, TParams, TResponses>;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
params<T extends StandardSchemaV1>(
|
|
765
|
+
schema: T,
|
|
766
|
+
): EndpointBuilder<TBody, TQuery, InferSchema<T>, TResponses> {
|
|
767
|
+
this._params = schema;
|
|
768
|
+
return this as unknown as EndpointBuilder<TBody, TQuery, InferSchema<T>, TResponses>;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Declare a response for a given status code. Three call shapes:
|
|
773
|
+
*
|
|
774
|
+
* - `.response(200, 'Ok', schema)` — typed. The inferred output type of
|
|
775
|
+
* `schema` becomes the response body at that status.
|
|
776
|
+
* - `.response(401, 'Unauthorized', schema)` — also typed. Error bodies
|
|
777
|
+
* are inferred the same way as success bodies; the `fortress.call.*`
|
|
778
|
+
* proxy treats non-2xx as throws and only exposes 2xx responses.
|
|
779
|
+
* - `.response(204, 'No content')` — untyped. Use when there's no body.
|
|
780
|
+
*/
|
|
781
|
+
response<S extends number, T extends StandardSchemaV1>(
|
|
782
|
+
status: S,
|
|
783
|
+
description: string,
|
|
784
|
+
schema: T,
|
|
785
|
+
): EndpointBuilder<TBody, TQuery, TParams, TResponses & { [K in S]: InferSchema<T> }>;
|
|
786
|
+
response<S extends number>(
|
|
787
|
+
status: S,
|
|
788
|
+
description: string,
|
|
789
|
+
): EndpointBuilder<TBody, TQuery, TParams, TResponses & { [K in S]: unknown }>;
|
|
790
|
+
response<S extends number>(
|
|
791
|
+
status: S,
|
|
792
|
+
description: string,
|
|
793
|
+
schema?: StandardSchemaV1 | JSONSchema,
|
|
794
|
+
): EndpointBuilder<TBody, TQuery, TParams, any> {
|
|
795
|
+
const stored: EndpointResponse = { description };
|
|
796
|
+
if (schema !== undefined) {
|
|
797
|
+
stored.schema = isStandardSchema(schema) ? extractJsonSchema(schema as FortressSchema<any>) : schema as JSONSchema;
|
|
798
|
+
}
|
|
799
|
+
this._responses[status] = stored;
|
|
800
|
+
return this as unknown as EndpointBuilder<TBody, TQuery, TParams, any>;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Declare an error response that returns the canonical {@link ErrorEnvelope}
|
|
805
|
+
* body. Shorthand for `.response(status, description, ErrorEnvelope)`.
|
|
806
|
+
*
|
|
807
|
+
* Aligns host endpoint error responses with what Fortress's own routes
|
|
808
|
+
* emit, so a single client-side error parser works everywhere.
|
|
809
|
+
*
|
|
810
|
+
* ```ts
|
|
811
|
+
* endpoint('GET', '/schools/:id')
|
|
812
|
+
* .summary('Get a school')
|
|
813
|
+
* .params(obj({ id: str() }, 'id'))
|
|
814
|
+
* .response(200, 'OK', SchoolEnvelope)
|
|
815
|
+
* .errorResponse(404, 'School not found')
|
|
816
|
+
* .errorResponse(403, 'Forbidden')
|
|
817
|
+
* .build();
|
|
818
|
+
* ```
|
|
819
|
+
*/
|
|
820
|
+
errorResponse<S extends number>(
|
|
821
|
+
status: S,
|
|
822
|
+
description: string,
|
|
823
|
+
): EndpointBuilder<TBody, TQuery, TParams, TResponses & { [K in S]: InferSchema<typeof ErrorEnvelope> }> {
|
|
824
|
+
return this.response(status, description, ErrorEnvelope);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
handler(name: string): this {
|
|
828
|
+
this._handler = name;
|
|
829
|
+
return this;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
build(): EndpointDefinition<TBody, TQuery, TParams, TResponses> {
|
|
833
|
+
const def: EndpointDefinition<TBody, TQuery, TParams, TResponses> = {
|
|
834
|
+
method: this._method,
|
|
835
|
+
path: this._path,
|
|
836
|
+
handler: this._handler,
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
if (this._summary || this._tags.length > 0 || this._security.length > 0 || this._description || this._deprecated || this._permission) {
|
|
840
|
+
def.meta = {
|
|
841
|
+
summary: this._summary,
|
|
842
|
+
...(this._description && { description: this._description }),
|
|
843
|
+
...(this._tags.length > 0 && { tags: this._tags }),
|
|
844
|
+
...(this._security.length > 0 && { security: this._security }),
|
|
845
|
+
...(this._deprecated && { deprecated: true }),
|
|
846
|
+
...(this._permission && { permission: this._permission }),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (this._body || this._query || this._params) {
|
|
851
|
+
def.input = {};
|
|
852
|
+
if (this._body) {
|
|
853
|
+
def.input.body = extractJsonSchema(this._body);
|
|
854
|
+
if (isStandardSchema(this._body))
|
|
855
|
+
def.input.bodySchema = this._body;
|
|
856
|
+
}
|
|
857
|
+
if (this._query) {
|
|
858
|
+
def.input.query = extractJsonSchema(this._query);
|
|
859
|
+
if (isStandardSchema(this._query))
|
|
860
|
+
def.input.querySchema = this._query;
|
|
861
|
+
}
|
|
862
|
+
if (this._params) {
|
|
863
|
+
def.input.params = extractJsonSchema(this._params);
|
|
864
|
+
if (isStandardSchema(this._params))
|
|
865
|
+
def.input.paramsSchema = this._params;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (Object.keys(this._responses).length > 0) {
|
|
870
|
+
def.responses = this._responses;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return def;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Start building a new {@link EndpointDefinition} for the given HTTP method and path. */
|
|
878
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
879
|
+
export function endpoint(method: HttpMethod, path: string): EndpointBuilder<{}, {}, {}, {}> {
|
|
880
|
+
return new EndpointBuilder(method, path);
|
|
881
|
+
}
|