@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,149 @@
|
|
|
1
|
+
import type { JSONSchema, Simplify } from './json-schema';
|
|
2
|
+
import type { StandardSchemaV1 } from './standard-schema';
|
|
3
|
+
|
|
4
|
+
/** HTTP method an {@link EndpointDefinition} can declare. */
|
|
5
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
6
|
+
|
|
7
|
+
/** Authentication scheme required to call an endpoint. */
|
|
8
|
+
export type SecurityRequirement = 'bearer' | 'basic' | 'apiKey' | 'none';
|
|
9
|
+
|
|
10
|
+
/** A required IAM permission, expressed as a `(resource, action)` pair. */
|
|
11
|
+
export interface EndpointPermission {
|
|
12
|
+
resource: string;
|
|
13
|
+
action: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** OpenAPI / IAM metadata attached to an {@link EndpointDefinition}. */
|
|
17
|
+
export interface EndpointMeta {
|
|
18
|
+
summary: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
tags?: string[];
|
|
21
|
+
security?: SecurityRequirement[];
|
|
22
|
+
deprecated?: boolean;
|
|
23
|
+
/** IAM permission required to access this endpoint. Enforced by RBAC middleware. */
|
|
24
|
+
permission?: EndpointPermission;
|
|
25
|
+
/**
|
|
26
|
+
* What kind of bearer token this route accepts.
|
|
27
|
+
*
|
|
28
|
+
* - `'jwt'` (default) — the route expects a Fortress session JWT in
|
|
29
|
+
* `Authorization: Bearer <jwt>` (or the cookie). The dispatcher runs
|
|
30
|
+
* the plugin principal chain, JWT verification, and RBAC enforcement
|
|
31
|
+
* automatically before invoking the handler.
|
|
32
|
+
* - `'oauth'` — the route's bearer is an OAuth 2.0 access token (or no
|
|
33
|
+
* token at all, e.g. `/oauth/authorize`). The dispatcher skips its
|
|
34
|
+
* auth pipeline entirely and the handler self-parses the bearer
|
|
35
|
+
* (typical for `/oauth/userinfo`, `/oauth/token`, etc.). Body parsing
|
|
36
|
+
* and validation are also skipped, since OAuth bodies are
|
|
37
|
+
* `application/x-www-form-urlencoded` per RFC 6749.
|
|
38
|
+
*
|
|
39
|
+
* Routes that don't set this field default to `'jwt'`. Without this
|
|
40
|
+
* marker, the OAuth plugin's consent-flow endpoints
|
|
41
|
+
* (`/oauth/flows/:flowId{,/approve,/deny}`) — which are SPA-driven and
|
|
42
|
+
* require a Fortress JWT — used to be silently unauthenticated due to
|
|
43
|
+
* a path-based `startsWith('/oauth/')` short-circuit in the dispatcher,
|
|
44
|
+
* forcing every host app to ship a workaround shim. Setting
|
|
45
|
+
* `bearerKind: 'oauth'` only on the actual protocol routes lets the
|
|
46
|
+
* dispatcher honour `security: ['bearer']` everywhere else.
|
|
47
|
+
*/
|
|
48
|
+
bearerKind?: 'jwt' | 'oauth';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Request input declarations for an endpoint — JSON Schemas for OpenAPI plus Standard Schemas for runtime validation. */
|
|
52
|
+
export interface EndpointInput {
|
|
53
|
+
/** JSON Schema for OpenAPI spec generation. */
|
|
54
|
+
body?: JSONSchema;
|
|
55
|
+
query?: JSONSchema;
|
|
56
|
+
params?: JSONSchema;
|
|
57
|
+
/** Standard Schema references for runtime validation (set by endpoint builder). */
|
|
58
|
+
bodySchema?: StandardSchemaV1;
|
|
59
|
+
querySchema?: StandardSchemaV1;
|
|
60
|
+
paramsSchema?: StandardSchemaV1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A single OpenAPI response definition (description plus optional JSON Schema). */
|
|
64
|
+
export interface EndpointResponse {
|
|
65
|
+
description: string;
|
|
66
|
+
schema?: JSONSchema;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Declarative description of one HTTP endpoint.
|
|
71
|
+
*
|
|
72
|
+
* The four generic parameters are **phantom types** — they're populated by
|
|
73
|
+
* the fluent {@link EndpointBuilder} as schemas are declared via `.body()`,
|
|
74
|
+
* `.query()`, `.params()`, and `.response()`, and then extracted at the call
|
|
75
|
+
* site by the `InferEndpoint*` helpers (and by the typed `fortress.call.*`
|
|
76
|
+
* proxy). None of them exist at runtime.
|
|
77
|
+
*
|
|
78
|
+
* The defaults are `{}` (empty object), not `unknown`, so that the
|
|
79
|
+
* intersection-based `InferEndpointCallInput` collapses cleanly for
|
|
80
|
+
* endpoints that only declare one of the three input slots.
|
|
81
|
+
*/
|
|
82
|
+
export interface EndpointDefinition<
|
|
83
|
+
// eslint-disable-next-line ts/no-empty-object-type -- default must be {} so the input intersection collapses
|
|
84
|
+
TBody = {},
|
|
85
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
86
|
+
TQuery = {},
|
|
87
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
88
|
+
TParams = {},
|
|
89
|
+
// eslint-disable-next-line ts/no-empty-object-type
|
|
90
|
+
TResponses extends Record<number, unknown> = {},
|
|
91
|
+
> {
|
|
92
|
+
method: HttpMethod;
|
|
93
|
+
path: string;
|
|
94
|
+
handler: string;
|
|
95
|
+
meta?: EndpointMeta;
|
|
96
|
+
input?: EndpointInput;
|
|
97
|
+
responses?: Record<number, EndpointResponse>;
|
|
98
|
+
/** Phantom — never present at runtime. Consumed by `InferEndpoint*` helpers. */
|
|
99
|
+
__types?: {
|
|
100
|
+
body: TBody;
|
|
101
|
+
query: TQuery;
|
|
102
|
+
params: TParams;
|
|
103
|
+
responses: TResponses;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Endpoint type inference helpers ────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/** Extract the request-body type from an {@link EndpointDefinition}. */
|
|
110
|
+
export type InferEndpointBody<E> = E extends EndpointDefinition<infer B, any, any, any> ? B : never;
|
|
111
|
+
/** Extract the query-string type from an {@link EndpointDefinition}. */
|
|
112
|
+
export type InferEndpointQuery<E> = E extends EndpointDefinition<any, infer Q, any, any> ? Q : never;
|
|
113
|
+
/** Extract the path-params type from an {@link EndpointDefinition}. */
|
|
114
|
+
export type InferEndpointParams<E> = E extends EndpointDefinition<any, any, infer P, any> ? P : never;
|
|
115
|
+
/** Extract the full `Record<status, body>` response map from an {@link EndpointDefinition}. */
|
|
116
|
+
export type InferEndpointResponses<E> = E extends EndpointDefinition<any, any, any, infer R> ? R : never;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract the success-response body from an {@link EndpointDefinition}.
|
|
120
|
+
*
|
|
121
|
+
* Tries `200`, then `201`, then `204`, then falls back to `unknown`.
|
|
122
|
+
* This matches the behavior of the `fortress.call.*` proxy, which always
|
|
123
|
+
* resolves to the first successful status declared.
|
|
124
|
+
*/
|
|
125
|
+
export type InferEndpointSuccessResponse<E>
|
|
126
|
+
= InferEndpointResponses<E> extends infer R
|
|
127
|
+
? R extends { 200: infer T } ? T
|
|
128
|
+
: R extends { 201: infer T } ? T
|
|
129
|
+
: R extends { 204: infer T } ? T
|
|
130
|
+
: unknown
|
|
131
|
+
: never;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The flat input shape accepted by the typed `fortress.call.<handler>(input)`
|
|
135
|
+
* proxy — the intersection of body, query, and params. For endpoints that
|
|
136
|
+
* only declare one of the three, the other slots are `{}` and disappear from
|
|
137
|
+
* the intersection.
|
|
138
|
+
*/
|
|
139
|
+
export type InferEndpointCallInput<E> = Simplify<
|
|
140
|
+
InferEndpointBody<E> & InferEndpointQuery<E> & InferEndpointParams<E>
|
|
141
|
+
>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Component schemas are reusable JSON Schema definitions
|
|
145
|
+
* that endpoints reference via $ref (e.g., '#/components/schemas/User').
|
|
146
|
+
*/
|
|
147
|
+
export interface ComponentSchemas {
|
|
148
|
+
[name: string]: JSONSchema;
|
|
149
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/** Discriminated string union of every error code fortress can throw. Maps 1:1 to an HTTP status. */
|
|
2
|
+
export type FortressErrorCode
|
|
3
|
+
= | 'UNAUTHORIZED'
|
|
4
|
+
| 'TOKEN_REUSE'
|
|
5
|
+
| 'SESSION_IDLE_TIMEOUT'
|
|
6
|
+
| 'SESSION_ABSOLUTE_TIMEOUT'
|
|
7
|
+
| 'FORBIDDEN'
|
|
8
|
+
| 'BAD_REQUEST'
|
|
9
|
+
| 'NOT_FOUND'
|
|
10
|
+
| 'CONFLICT'
|
|
11
|
+
| 'UNPROCESSABLE_ENTITY'
|
|
12
|
+
| 'RATE_LIMITED'
|
|
13
|
+
| 'DATABASE_ERROR'
|
|
14
|
+
| 'VALIDATION_ERROR'
|
|
15
|
+
| 'SERVICE_UNAVAILABLE';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Machine-readable error codes defined by RFC 6749 §5.2 (token endpoint) and
|
|
19
|
+
* §4.1.2.1 (authorization endpoint). The OAuth plugin uses these as the
|
|
20
|
+
* `error` field in token-endpoint failure responses; the HTTP error mapper
|
|
21
|
+
* detects {@link FortressError.oauthError} and emits the
|
|
22
|
+
* `{ error, error_description }` shape required by the spec.
|
|
23
|
+
*/
|
|
24
|
+
export type OAuthErrorCode
|
|
25
|
+
= | 'invalid_request'
|
|
26
|
+
| 'invalid_client'
|
|
27
|
+
| 'invalid_grant'
|
|
28
|
+
| 'unauthorized_client'
|
|
29
|
+
| 'unsupported_grant_type'
|
|
30
|
+
| 'invalid_scope'
|
|
31
|
+
| 'access_denied'
|
|
32
|
+
| 'unsupported_response_type'
|
|
33
|
+
| 'server_error'
|
|
34
|
+
| 'temporarily_unavailable';
|
|
35
|
+
|
|
36
|
+
/** The single error class fortress throws. Carries an error code, HTTP status, and structured details. */
|
|
37
|
+
export class FortressError extends Error {
|
|
38
|
+
readonly code: FortressErrorCode;
|
|
39
|
+
readonly statusCode: number;
|
|
40
|
+
readonly retryAfter?: number;
|
|
41
|
+
readonly details?: unknown;
|
|
42
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
43
|
+
readonly oauthError?: OAuthErrorCode;
|
|
44
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
45
|
+
readonly oauthDescription?: string;
|
|
46
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
47
|
+
readonly oauthErrorUri?: string;
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
code: FortressErrorCode,
|
|
51
|
+
message: string,
|
|
52
|
+
statusCode: number,
|
|
53
|
+
options?: {
|
|
54
|
+
cause?: unknown;
|
|
55
|
+
retryAfter?: number;
|
|
56
|
+
details?: unknown;
|
|
57
|
+
oauthError?: OAuthErrorCode;
|
|
58
|
+
oauthDescription?: string;
|
|
59
|
+
oauthErrorUri?: string;
|
|
60
|
+
},
|
|
61
|
+
) {
|
|
62
|
+
super(message, { cause: options?.cause });
|
|
63
|
+
this.code = code;
|
|
64
|
+
this.statusCode = statusCode;
|
|
65
|
+
this.retryAfter = options?.retryAfter;
|
|
66
|
+
this.details = options?.details;
|
|
67
|
+
this.oauthError = options?.oauthError;
|
|
68
|
+
this.oauthDescription = options?.oauthDescription;
|
|
69
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toJSON(): { code: FortressErrorCode; message: string; statusCode: number; details?: unknown } {
|
|
73
|
+
return {
|
|
74
|
+
code: this.code,
|
|
75
|
+
message: this.message,
|
|
76
|
+
statusCode: this.statusCode,
|
|
77
|
+
...(this.details !== undefined && { details: this.details }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Typed factory of {@link FortressError} constructors — one helper per error code. */
|
|
83
|
+
export const Errors = {
|
|
84
|
+
unauthorized: (message = 'Unauthorized'): FortressError =>
|
|
85
|
+
new FortressError('UNAUTHORIZED', message, 401),
|
|
86
|
+
tokenReuse: (): FortressError =>
|
|
87
|
+
new FortressError('TOKEN_REUSE', 'Token reuse detected', 401),
|
|
88
|
+
sessionIdleTimeout: (): FortressError =>
|
|
89
|
+
new FortressError('SESSION_IDLE_TIMEOUT', 'Session idle timeout exceeded', 401),
|
|
90
|
+
sessionAbsoluteTimeout: (): FortressError =>
|
|
91
|
+
new FortressError('SESSION_ABSOLUTE_TIMEOUT', 'Session absolute timeout exceeded', 401),
|
|
92
|
+
forbidden: (message = 'Forbidden'): FortressError =>
|
|
93
|
+
new FortressError('FORBIDDEN', message, 403),
|
|
94
|
+
badRequest: (message = 'Bad request'): FortressError =>
|
|
95
|
+
new FortressError('BAD_REQUEST', message, 400),
|
|
96
|
+
notFound: (message = 'Not found'): FortressError =>
|
|
97
|
+
new FortressError('NOT_FOUND', message, 404),
|
|
98
|
+
conflict: (message = 'Conflict', options?: { cause?: unknown; details?: unknown }): FortressError =>
|
|
99
|
+
new FortressError('CONFLICT', message, 409, options),
|
|
100
|
+
unprocessable: (message = 'Unprocessable entity', options?: { cause?: unknown; details?: unknown }): FortressError =>
|
|
101
|
+
new FortressError('UNPROCESSABLE_ENTITY', message, 422, options),
|
|
102
|
+
rateLimited: (retryAfter: number): FortressError =>
|
|
103
|
+
new FortressError('RATE_LIMITED', 'Too many requests', 429, { retryAfter }),
|
|
104
|
+
database: (message = 'Database error', cause?: unknown): FortressError =>
|
|
105
|
+
new FortressError('DATABASE_ERROR', message, 500, { cause }),
|
|
106
|
+
serviceUnavailable: (message = 'Service unavailable', options?: { cause?: unknown; retryAfter?: number }): FortressError =>
|
|
107
|
+
new FortressError('SERVICE_UNAVAILABLE', message, 503, options),
|
|
108
|
+
validationError: (issues: Array<{ path?: unknown; message: string }>): FortressError =>
|
|
109
|
+
new FortressError('VALIDATION_ERROR', 'Validation failed', 422, { details: issues }),
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
113
|
+
*
|
|
114
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
115
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
116
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
117
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
118
|
+
* on the machine-readable `error` field.
|
|
119
|
+
*
|
|
120
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
121
|
+
* - `invalid_client` → 401
|
|
122
|
+
* - everything else → 400
|
|
123
|
+
*/
|
|
124
|
+
oauth: (
|
|
125
|
+
error: OAuthErrorCode,
|
|
126
|
+
description?: string,
|
|
127
|
+
options?: { status?: number; errorUri?: string; cause?: unknown },
|
|
128
|
+
): FortressError => {
|
|
129
|
+
const status = options?.status ?? (error === 'invalid_client' ? 401 : 400);
|
|
130
|
+
const code: FortressErrorCode = status === 401 ? 'UNAUTHORIZED' : 'BAD_REQUEST';
|
|
131
|
+
return new FortressError(code, description ?? error, status, {
|
|
132
|
+
oauthError: error,
|
|
133
|
+
oauthDescription: description,
|
|
134
|
+
oauthErrorUri: options?.errorUri,
|
|
135
|
+
cause: options?.cause,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
141
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
142
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
143
|
+
* service-method call would produce.
|
|
144
|
+
*
|
|
145
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
146
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
147
|
+
* failures still become structured `FortressError`s.
|
|
148
|
+
*/
|
|
149
|
+
fromHttpResponse: (status: number, body: unknown): FortressError => {
|
|
150
|
+
const payload = (body && typeof body === 'object' ? body : {}) as {
|
|
151
|
+
code?: string;
|
|
152
|
+
message?: string;
|
|
153
|
+
details?: unknown;
|
|
154
|
+
};
|
|
155
|
+
const message = typeof payload.message === 'string' ? payload.message : `HTTP ${status}`;
|
|
156
|
+
const code = typeof payload.code === 'string' && isFortressErrorCode(payload.code)
|
|
157
|
+
? payload.code
|
|
158
|
+
: statusToErrorCode(status);
|
|
159
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
160
|
+
},
|
|
161
|
+
} as const;
|
|
162
|
+
|
|
163
|
+
function isFortressErrorCode(code: string): code is FortressErrorCode {
|
|
164
|
+
return (
|
|
165
|
+
code === 'UNAUTHORIZED'
|
|
166
|
+
|| code === 'TOKEN_REUSE'
|
|
167
|
+
|| code === 'SESSION_IDLE_TIMEOUT'
|
|
168
|
+
|| code === 'SESSION_ABSOLUTE_TIMEOUT'
|
|
169
|
+
|| code === 'FORBIDDEN'
|
|
170
|
+
|| code === 'BAD_REQUEST'
|
|
171
|
+
|| code === 'NOT_FOUND'
|
|
172
|
+
|| code === 'CONFLICT'
|
|
173
|
+
|| code === 'UNPROCESSABLE_ENTITY'
|
|
174
|
+
|| code === 'RATE_LIMITED'
|
|
175
|
+
|| code === 'DATABASE_ERROR'
|
|
176
|
+
|| code === 'VALIDATION_ERROR'
|
|
177
|
+
|| code === 'SERVICE_UNAVAILABLE'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function statusToErrorCode(status: number): FortressErrorCode {
|
|
182
|
+
if (status === 401)
|
|
183
|
+
return 'UNAUTHORIZED';
|
|
184
|
+
if (status === 403)
|
|
185
|
+
return 'FORBIDDEN';
|
|
186
|
+
if (status === 404)
|
|
187
|
+
return 'NOT_FOUND';
|
|
188
|
+
if (status === 409)
|
|
189
|
+
return 'CONFLICT';
|
|
190
|
+
if (status === 422)
|
|
191
|
+
return 'UNPROCESSABLE_ENTITY';
|
|
192
|
+
if (status === 429)
|
|
193
|
+
return 'RATE_LIMITED';
|
|
194
|
+
if (status === 503)
|
|
195
|
+
return 'SERVICE_UNAVAILABLE';
|
|
196
|
+
if (status >= 500)
|
|
197
|
+
return 'DATABASE_ERROR';
|
|
198
|
+
return 'BAD_REQUEST';
|
|
199
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interop: authoring fortress endpoint schemas with `@bajustone/fetcher`'s own
|
|
3
|
+
* builder (re-exported at `@bajustone/fortress/fetcher`). Fetcher schemas are
|
|
4
|
+
* JSON Schema objects that implement Standard Schema V1, so they drop straight
|
|
5
|
+
* into `endpoint().body()/.query()/.params()/.response()`:
|
|
6
|
+
* - runtime validation uses the fetcher schema's own compiled validator
|
|
7
|
+
* - OpenAPI emission strips fetcher's internal `~`-prefixed keys, leaving
|
|
8
|
+
* clean JSON Schema
|
|
9
|
+
*
|
|
10
|
+
* This locks that contract in so neither `extractJsonSchema` (detection) nor
|
|
11
|
+
* `cleanSchema` (OpenAPI serialization) can regress it.
|
|
12
|
+
*/
|
|
13
|
+
import type { StandardSchemaV1 } from './standard-schema';
|
|
14
|
+
import { discriminatedUnion, email as femail, integer, literal, nullable, object, optional, string } from '@bajustone/fetcher/schema';
|
|
15
|
+
import { describe, expect, it } from 'vitest';
|
|
16
|
+
import { toOpenAPI } from './openapi';
|
|
17
|
+
import { endpoint, extractJsonSchema, isStandardSchema } from './schema-builder';
|
|
18
|
+
import { validateValue } from './validation';
|
|
19
|
+
|
|
20
|
+
/** Recursively collect any object key beginning with `~` (fetcher internals). */
|
|
21
|
+
function tildeKeys(value: unknown, acc: string[] = []): string[] {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const v of value)
|
|
24
|
+
tildeKeys(v, acc);
|
|
25
|
+
}
|
|
26
|
+
else if (value && typeof value === 'object') {
|
|
27
|
+
for (const [k, v] of Object.entries(value)) {
|
|
28
|
+
if (k.startsWith('~'))
|
|
29
|
+
acc.push(k);
|
|
30
|
+
tildeKeys(v, acc);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return acc;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const Body = object({
|
|
37
|
+
email: femail(),
|
|
38
|
+
name: string({ minLength: 1 }),
|
|
39
|
+
age: optional(integer()),
|
|
40
|
+
nick: nullable(string()),
|
|
41
|
+
kind: discriminatedUnion('t', {
|
|
42
|
+
a: object({ t: literal('a'), x: integer() }),
|
|
43
|
+
b: object({ t: literal('b'), y: string() }),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('fetcher builder interop — endpoint authoring', () => {
|
|
48
|
+
it('a fetcher schema is a valid Standard Schema and JSON Schema', () => {
|
|
49
|
+
expect(isStandardSchema(Body)).toBe(true);
|
|
50
|
+
expect((Body as any).type).toBe('object');
|
|
51
|
+
// extractJsonSchema accepts the fetcher-shaped schema (no Zod-style adapter)
|
|
52
|
+
expect((extractJsonSchema(Body as any) as any).type).toBe('object');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('validates request data via the fetcher schema validator', async () => {
|
|
56
|
+
const okValue = await validateValue(
|
|
57
|
+
Body as unknown as StandardSchemaV1,
|
|
58
|
+
{ email: 'user@example.com', name: 'x', nick: null, kind: { t: 'a', x: 1 } },
|
|
59
|
+
'body',
|
|
60
|
+
);
|
|
61
|
+
expect(okValue).toMatchObject({ email: 'user@example.com', name: 'x' });
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
validateValue(
|
|
65
|
+
Body as unknown as StandardSchemaV1,
|
|
66
|
+
{ email: 'not-an-email', name: '', nick: 1, kind: { t: 'a', x: 'no' } },
|
|
67
|
+
'body',
|
|
68
|
+
),
|
|
69
|
+
).rejects.toMatchObject({ code: 'VALIDATION_ERROR', statusCode: 422 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('stores both JSON Schema (OpenAPI) and the validator on the endpoint', () => {
|
|
73
|
+
const ep = endpoint('POST', '/users')
|
|
74
|
+
.summary('Create user')
|
|
75
|
+
.body(Body as any)
|
|
76
|
+
.response(200, 'Created', object({ id: string() }) as any)
|
|
77
|
+
.handler('createUser')
|
|
78
|
+
.build();
|
|
79
|
+
|
|
80
|
+
expect(ep.input?.body?.type).toBe('object');
|
|
81
|
+
expect(typeof (ep.input?.bodySchema as any)?.['~standard'].validate).toBe('function');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('emits clean OpenAPI — no fetcher ~ keys, correct shape', () => {
|
|
85
|
+
const ep = endpoint('POST', '/users')
|
|
86
|
+
.summary('Create user')
|
|
87
|
+
.body(Body as any)
|
|
88
|
+
.handler('createUser')
|
|
89
|
+
.build();
|
|
90
|
+
|
|
91
|
+
const spec = toOpenAPI([ep], { title: 't', version: '1' });
|
|
92
|
+
expect(tildeKeys(spec)).toEqual([]);
|
|
93
|
+
|
|
94
|
+
const schema = (spec as any).paths['/users'].post.requestBody.content['application/json'].schema;
|
|
95
|
+
// optional `age` is excluded from required; required keys present
|
|
96
|
+
expect(schema.required).toEqual(['email', 'name', 'nick', 'kind']);
|
|
97
|
+
// enforced email format surfaces as format + pattern
|
|
98
|
+
expect(schema.properties.email.format).toBe('email');
|
|
99
|
+
expect(typeof schema.properties.email.pattern).toBe('string');
|
|
100
|
+
// nullable → anyOf with a null branch (valid OpenAPI 3.1)
|
|
101
|
+
expect(schema.properties.nick.anyOf).toEqual([{ type: 'string' }, { type: 'null' }]);
|
|
102
|
+
// discriminatedUnion → oneOf + discriminator
|
|
103
|
+
expect(schema.properties.kind.oneOf).toHaveLength(2);
|
|
104
|
+
expect(schema.properties.kind.discriminator.propertyName).toBe('t');
|
|
105
|
+
});
|
|
106
|
+
});
|