@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,86 @@
|
|
|
1
|
+
import type { ExpressRequestLike, InferOutput } from './validated';
|
|
2
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
3
|
+
import { obj, str } from '../core/schema-builder';
|
|
4
|
+
import { vBody, vParam, vQuery } from './validated';
|
|
5
|
+
|
|
6
|
+
const CreateUserBody = obj({ name: str(), email: str() }, 'name', 'email');
|
|
7
|
+
const IdParam = obj({ id: str('User ID') }, 'id');
|
|
8
|
+
const SearchQuery = obj({ q: str('Search query'), page: str() }, 'q');
|
|
9
|
+
|
|
10
|
+
describe('vBody', () => {
|
|
11
|
+
it('returns parsed body when valid', async () => {
|
|
12
|
+
const req: ExpressRequestLike = { body: { name: 'Alice', email: 'alice@test.com' } };
|
|
13
|
+
const data = await vBody(req, CreateUserBody);
|
|
14
|
+
expect(data).toEqual({ name: 'Alice', email: 'alice@test.com' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('throws VALIDATION_ERROR when body is missing required fields', async () => {
|
|
18
|
+
const req: ExpressRequestLike = { body: { name: 'Alice' } };
|
|
19
|
+
await expect(vBody(req, CreateUserBody)).rejects.toMatchObject({
|
|
20
|
+
code: 'VALIDATION_ERROR',
|
|
21
|
+
statusCode: 422,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('throws VALIDATION_ERROR when body is undefined', async () => {
|
|
26
|
+
const req: ExpressRequestLike = {};
|
|
27
|
+
await expect(vBody(req, CreateUserBody)).rejects.toMatchObject({
|
|
28
|
+
code: 'VALIDATION_ERROR',
|
|
29
|
+
statusCode: 422,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('vParam', () => {
|
|
35
|
+
it('returns route params when valid', async () => {
|
|
36
|
+
const req: ExpressRequestLike = { params: { id: '42' } };
|
|
37
|
+
const data = await vParam(req, IdParam);
|
|
38
|
+
expect(data.id).toBe('42');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('throws VALIDATION_ERROR when required param missing', async () => {
|
|
42
|
+
const req: ExpressRequestLike = { params: {} };
|
|
43
|
+
await expect(vParam(req, IdParam)).rejects.toMatchObject({
|
|
44
|
+
code: 'VALIDATION_ERROR',
|
|
45
|
+
statusCode: 422,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('vQuery', () => {
|
|
51
|
+
it('returns query parameters when valid', async () => {
|
|
52
|
+
const req: ExpressRequestLike = { query: { q: 'hello' } };
|
|
53
|
+
const data = await vQuery(req, SearchQuery);
|
|
54
|
+
expect(data.q).toBe('hello');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('throws VALIDATION_ERROR when required query field missing', async () => {
|
|
58
|
+
const req: ExpressRequestLike = { query: {} };
|
|
59
|
+
await expect(vQuery(req, SearchQuery)).rejects.toMatchObject({
|
|
60
|
+
code: 'VALIDATION_ERROR',
|
|
61
|
+
statusCode: 422,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('type inference', () => {
|
|
67
|
+
it('infers vBody return type from schema', () => {
|
|
68
|
+
const fn = (r: ExpressRequestLike) => vBody(r, CreateUserBody);
|
|
69
|
+
expectTypeOf(fn).returns.resolves.toEqualTypeOf<{ name: string; email: string }>();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('infers vParam return type from schema', () => {
|
|
73
|
+
const fn = (r: ExpressRequestLike) => vParam(r, IdParam);
|
|
74
|
+
expectTypeOf(fn).returns.resolves.toEqualTypeOf<{ id: string }>();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('infers vQuery return type from schema', () => {
|
|
78
|
+
const fn = (r: ExpressRequestLike) => vQuery(r, SearchQuery);
|
|
79
|
+
expectTypeOf(fn).returns.resolves.toEqualTypeOf<{ q: string; page?: string }>();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('exports InferOutput utility type', () => {
|
|
83
|
+
type Expected = InferOutput<typeof CreateUserBody>;
|
|
84
|
+
expectTypeOf<Expected>().toEqualTypeOf<{ name: string; email: string }>();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed extraction-and-validation helpers for Express handlers.
|
|
3
|
+
*
|
|
4
|
+
* These helpers extract request data (body, params, query) **and validate
|
|
5
|
+
* it at runtime** against the supplied Standard Schema. On success they
|
|
6
|
+
* return the parsed value with full TypeScript inference; on failure they
|
|
7
|
+
* throw `FortressError('VALIDATION_ERROR', 422)` — the same shape every
|
|
8
|
+
* fortress-managed endpoint produces — so the existing Express error
|
|
9
|
+
* handler (`createErrorHandler`) formats it identically.
|
|
10
|
+
*
|
|
11
|
+
* Works with any Standard Schema V1 library: Zod, Valibot, ArkType, or
|
|
12
|
+
* fortress's built-in schema builders.
|
|
13
|
+
*
|
|
14
|
+
* Assumes body-parsing middleware (e.g. `app.use(express.json())`) has
|
|
15
|
+
* already populated `req.body` for `vBody` to read.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import express from 'express';
|
|
20
|
+
* import { vBody, vParam, vQuery, createErrorHandler } from '@bajustone/fortress/express';
|
|
21
|
+
* import { obj, str } from '@bajustone/fortress';
|
|
22
|
+
*
|
|
23
|
+
* const app = express();
|
|
24
|
+
* app.use(express.json());
|
|
25
|
+
*
|
|
26
|
+
* const Body = obj({ name: str() }, 'name');
|
|
27
|
+
* const Param = obj({ id: str() }, 'id');
|
|
28
|
+
*
|
|
29
|
+
* app.patch('/users/:id', async (req, res, next) => {
|
|
30
|
+
* try {
|
|
31
|
+
* const { id } = await vParam(req, Param);
|
|
32
|
+
* const { name } = await vBody(req, Body);
|
|
33
|
+
* res.json({ id, name });
|
|
34
|
+
* }
|
|
35
|
+
* catch (err) { next(err); }
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* app.use(createErrorHandler());
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Validation runs per call, so the first failing helper throws and any
|
|
42
|
+
* downstream helpers in the same handler do not run. If you need to
|
|
43
|
+
* aggregate body+query+params issues into a single response, use the
|
|
44
|
+
* framework-agnostic `validateRequest` from `@bajustone/fortress` instead.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { StandardSchemaV1 } from '../core/standard-schema';
|
|
48
|
+
import { validateValue } from '../core/validation';
|
|
49
|
+
|
|
50
|
+
/** Infer the output type of a Standard Schema V1 schema. */
|
|
51
|
+
export type InferOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Structural shape of an Express `Request` accepted by these helpers.
|
|
55
|
+
* Defined locally instead of importing from `express` so fortress can ship
|
|
56
|
+
* without a hard `express` dependency. Compatible by structural typing with
|
|
57
|
+
* the real `express.Request` — consumers pass their `req` directly.
|
|
58
|
+
*/
|
|
59
|
+
export interface ExpressRequestLike {
|
|
60
|
+
body?: unknown;
|
|
61
|
+
params?: unknown;
|
|
62
|
+
query?: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract and validate the request body. Assumes body-parsing middleware
|
|
67
|
+
* (e.g. `express.json()`) populated `req.body`. Returns the parsed value
|
|
68
|
+
* typed as `InferOutput<T>`, or throws `FortressError('VALIDATION_ERROR', 422)`.
|
|
69
|
+
*/
|
|
70
|
+
export async function vBody<T extends StandardSchemaV1>(
|
|
71
|
+
req: ExpressRequestLike,
|
|
72
|
+
schema: T,
|
|
73
|
+
): Promise<InferOutput<T>> {
|
|
74
|
+
return validateValue(schema, req.body, 'body');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract and validate route parameters from `req.params`. Returns the
|
|
79
|
+
* parsed value typed as `InferOutput<T>`, or throws
|
|
80
|
+
* `FortressError('VALIDATION_ERROR', 422)`.
|
|
81
|
+
*/
|
|
82
|
+
export async function vParam<T extends StandardSchemaV1>(
|
|
83
|
+
req: ExpressRequestLike,
|
|
84
|
+
schema: T,
|
|
85
|
+
): Promise<InferOutput<T>> {
|
|
86
|
+
return validateValue(schema, req.params, 'params');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract and validate query parameters from `req.query`. Returns the
|
|
91
|
+
* parsed value typed as `InferOutput<T>`, or throws
|
|
92
|
+
* `FortressError('VALIDATION_ERROR', 422)`.
|
|
93
|
+
*/
|
|
94
|
+
export async function vQuery<T extends StandardSchemaV1>(
|
|
95
|
+
req: ExpressRequestLike,
|
|
96
|
+
schema: T,
|
|
97
|
+
): Promise<InferOutput<T>> {
|
|
98
|
+
return validateValue(schema, req.query, 'query');
|
|
99
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export of `@bajustone/fetcher` — the schema-validated fetch client and
|
|
3
|
+
* schema builder that power fortress's own request validation (and, in later
|
|
4
|
+
* phases, its outbound HTTP).
|
|
5
|
+
*
|
|
6
|
+
* Surfaced here so consumers can use the exact same toolkit fortress uses
|
|
7
|
+
* internally — authoring endpoint schemas with the same builder, building
|
|
8
|
+
* their own validated outbound clients — without installing
|
|
9
|
+
* `@bajustone/fetcher` as a separate dependency.
|
|
10
|
+
*
|
|
11
|
+
* - Flat root re-export: `createFetch`, middleware (`retry`, `timeout`,
|
|
12
|
+
* `authBearer`, …), error classes, and types.
|
|
13
|
+
* - `schema` namespace: the schema builder (`object`, `string`, `integer`,
|
|
14
|
+
* `optional`, `email`, `uuid`, `discriminatedUnion`, `ref`, `compile`, …).
|
|
15
|
+
* - `openapi` namespace: `fromOpenAPI`, `fromJSONSchema`, `extractRouteSchemas`, …
|
|
16
|
+
* - `specTools` namespace: `coverage`, `lintSpec`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createFetch, schema } from '@bajustone/fortress/fetcher';
|
|
21
|
+
*
|
|
22
|
+
* const Login = schema.object({ email: schema.email(), password: schema.string() });
|
|
23
|
+
* const api = createFetch({ baseUrl: 'https://api.example.com' });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
authBearer,
|
|
31
|
+
bearerWithRefresh,
|
|
32
|
+
cookieAuth,
|
|
33
|
+
createFetch,
|
|
34
|
+
extractErrorMessage,
|
|
35
|
+
FetcherAbortError,
|
|
36
|
+
FetcherHTTPError,
|
|
37
|
+
FetcherNetworkError,
|
|
38
|
+
FetcherRequestError,
|
|
39
|
+
FetcherTimeoutError,
|
|
40
|
+
FetcherValidationError,
|
|
41
|
+
parseSetCookie,
|
|
42
|
+
retry,
|
|
43
|
+
timeout,
|
|
44
|
+
withInputType,
|
|
45
|
+
} from '@bajustone/fetcher';
|
|
46
|
+
export type {
|
|
47
|
+
BearerWithRefreshOptions,
|
|
48
|
+
CookieAuthOptions,
|
|
49
|
+
ExcludeMatcher,
|
|
50
|
+
ExtractPathParams,
|
|
51
|
+
FetchConfig,
|
|
52
|
+
FetcherError,
|
|
53
|
+
FetcherErrorLocation,
|
|
54
|
+
FetchFn,
|
|
55
|
+
HttpMethod,
|
|
56
|
+
InferOutput,
|
|
57
|
+
InferRoutesFromSpec,
|
|
58
|
+
InferSchema,
|
|
59
|
+
JSONSchemaToType,
|
|
60
|
+
MethodShortcutFn,
|
|
61
|
+
Middleware,
|
|
62
|
+
PathsToRoutes,
|
|
63
|
+
QueryDescriptor,
|
|
64
|
+
QuerySerializer,
|
|
65
|
+
ResultData,
|
|
66
|
+
RetryOptions,
|
|
67
|
+
RouteDefinition,
|
|
68
|
+
Routes,
|
|
69
|
+
Schema,
|
|
70
|
+
SchemaOf,
|
|
71
|
+
StandardSchemaV1,
|
|
72
|
+
StandardSchemaV1Issue,
|
|
73
|
+
StandardSchemaV1PathSegment,
|
|
74
|
+
StandardSchemaV1Result,
|
|
75
|
+
TypedFetchFn,
|
|
76
|
+
TypedFetchPromise,
|
|
77
|
+
TypedResponse,
|
|
78
|
+
} from '@bajustone/fetcher';
|
|
79
|
+
export * as openapi from '@bajustone/fetcher/openapi';
|
|
80
|
+
export * as schema from '@bajustone/fetcher/schema';
|
|
81
|
+
export * as specTools from '@bajustone/fetcher/spec-tools';
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { JSONSchema } from '../core/json-schema';
|
|
2
|
+
import type { ExternalRoute } from './convert-routes';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { convertRoutes } from './convert-routes';
|
|
5
|
+
|
|
6
|
+
/** Passthrough converter — returns whatever it receives as JSONSchema. */
|
|
7
|
+
const identity = (schema: unknown): JSONSchema => schema as JSONSchema;
|
|
8
|
+
|
|
9
|
+
/** Tracking converter — records calls and returns the input. */
|
|
10
|
+
function trackingConverter(): { converter: (s: unknown) => JSONSchema; calls: unknown[] } {
|
|
11
|
+
const calls: unknown[] = [];
|
|
12
|
+
return {
|
|
13
|
+
converter: (s: unknown) => {
|
|
14
|
+
calls.push(s);
|
|
15
|
+
return s as JSONSchema;
|
|
16
|
+
},
|
|
17
|
+
calls,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const GET_HEALTH: ExternalRoute = {
|
|
22
|
+
method: 'get',
|
|
23
|
+
path: '/health',
|
|
24
|
+
tags: ['Health'],
|
|
25
|
+
summary: 'Health check',
|
|
26
|
+
responses: {
|
|
27
|
+
200: {
|
|
28
|
+
description: 'OK',
|
|
29
|
+
content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string' } } } } },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const POST_LOGIN: ExternalRoute = {
|
|
35
|
+
method: 'post',
|
|
36
|
+
path: '/login',
|
|
37
|
+
tags: ['Auth'],
|
|
38
|
+
summary: 'Login',
|
|
39
|
+
security: [{ Bearer: [] }],
|
|
40
|
+
request: {
|
|
41
|
+
body: {
|
|
42
|
+
content: {
|
|
43
|
+
'application/json': {
|
|
44
|
+
schema: { type: 'object', properties: { email: { type: 'string' }, password: { type: 'string' } } },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
responses: {
|
|
50
|
+
200: { description: 'Login successful', content: { 'application/json': { schema: { type: 'object' } } } },
|
|
51
|
+
401: { description: 'Invalid credentials' },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const GET_USER: ExternalRoute = {
|
|
56
|
+
method: 'get',
|
|
57
|
+
path: '/users/{id}',
|
|
58
|
+
tags: ['Users'],
|
|
59
|
+
summary: 'Get user by ID',
|
|
60
|
+
security: [{ Bearer: [] }],
|
|
61
|
+
request: {
|
|
62
|
+
params: { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
|
|
63
|
+
},
|
|
64
|
+
responses: {
|
|
65
|
+
200: { description: 'User found', content: { 'application/json': { schema: { type: 'object' } } } },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const LIST_ITEMS: ExternalRoute = {
|
|
70
|
+
method: 'get',
|
|
71
|
+
path: '/items',
|
|
72
|
+
summary: 'List items',
|
|
73
|
+
request: {
|
|
74
|
+
query: { type: 'object', properties: { page: { type: 'integer' }, limit: { type: 'integer' } } },
|
|
75
|
+
},
|
|
76
|
+
responses: {
|
|
77
|
+
200: { description: 'Items list' },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
describe('convertRoutes', () => {
|
|
82
|
+
it('converts a simple GET route', () => {
|
|
83
|
+
const [ep] = convertRoutes([GET_HEALTH], { schemaConverter: identity });
|
|
84
|
+
|
|
85
|
+
expect(ep.method).toBe('GET');
|
|
86
|
+
expect(ep.path).toBe('/health');
|
|
87
|
+
expect(ep.handler).toBe('get_health');
|
|
88
|
+
expect(ep.meta?.tags).toEqual(['Health']);
|
|
89
|
+
expect(ep.meta?.summary).toBe('Health check');
|
|
90
|
+
expect(ep.responses?.[200]?.description).toBe('OK');
|
|
91
|
+
expect(ep.responses?.[200]?.schema).toEqual({ type: 'object', properties: { status: { type: 'string' } } });
|
|
92
|
+
expect(ep.input).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('converts a POST route with body and security', () => {
|
|
96
|
+
const [ep] = convertRoutes([POST_LOGIN], { schemaConverter: identity });
|
|
97
|
+
|
|
98
|
+
expect(ep.method).toBe('POST');
|
|
99
|
+
expect(ep.path).toBe('/login');
|
|
100
|
+
expect(ep.meta?.security).toEqual(['bearer']);
|
|
101
|
+
expect(ep.input?.body).toEqual({
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: { email: { type: 'string' }, password: { type: 'string' } },
|
|
104
|
+
});
|
|
105
|
+
expect(ep.responses?.[200]?.description).toBe('Login successful');
|
|
106
|
+
expect(ep.responses?.[401]?.description).toBe('Invalid credentials');
|
|
107
|
+
expect(ep.responses?.[401]?.schema).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('converts {param} to :param in paths', () => {
|
|
111
|
+
const [ep] = convertRoutes([GET_USER], { schemaConverter: identity });
|
|
112
|
+
|
|
113
|
+
expect(ep.path).toBe('/users/:id');
|
|
114
|
+
expect(ep.input?.params).toEqual({
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: { id: { type: 'integer' } },
|
|
117
|
+
required: ['id'],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('converts query parameters', () => {
|
|
122
|
+
const [ep] = convertRoutes([LIST_ITEMS], { schemaConverter: identity });
|
|
123
|
+
|
|
124
|
+
expect(ep.input?.query).toEqual({
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: { page: { type: 'integer' }, limit: { type: 'integer' } },
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('applies prefix to paths', () => {
|
|
131
|
+
const endpoints = convertRoutes([GET_HEALTH, GET_USER], {
|
|
132
|
+
schemaConverter: identity,
|
|
133
|
+
prefix: '/api/v1',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(endpoints[0].path).toBe('/api/v1/health');
|
|
137
|
+
expect(endpoints[0].handler).toBe('get_api_v1_health');
|
|
138
|
+
expect(endpoints[1].path).toBe('/api/v1/users/:id');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('strips trailing slash from prefix', () => {
|
|
142
|
+
const [ep] = convertRoutes([GET_HEALTH], {
|
|
143
|
+
schemaConverter: identity,
|
|
144
|
+
prefix: '/api/v1/',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(ep.path).toBe('/api/v1/health');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('calls schemaConverter for every schema', () => {
|
|
151
|
+
const { converter, calls } = trackingConverter();
|
|
152
|
+
|
|
153
|
+
convertRoutes([POST_LOGIN], { schemaConverter: converter });
|
|
154
|
+
|
|
155
|
+
// body schema + 200 response schema (401 has no schema)
|
|
156
|
+
expect(calls).toHaveLength(2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('calls schemaConverter for params and query', () => {
|
|
160
|
+
const route: ExternalRoute = {
|
|
161
|
+
method: 'get',
|
|
162
|
+
path: '/items/{id}',
|
|
163
|
+
request: {
|
|
164
|
+
params: { type: 'object', properties: { id: { type: 'string' } } },
|
|
165
|
+
query: { type: 'object', properties: { expand: { type: 'boolean' } } },
|
|
166
|
+
},
|
|
167
|
+
responses: { 200: { description: 'OK' } },
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const { converter, calls } = trackingConverter();
|
|
171
|
+
convertRoutes([route], { schemaConverter: converter });
|
|
172
|
+
|
|
173
|
+
// params + query (no body, no response schema)
|
|
174
|
+
expect(calls).toHaveLength(2);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('maps security schemes correctly', () => {
|
|
178
|
+
const routes: ExternalRoute[] = [
|
|
179
|
+
{ ...GET_HEALTH, security: [{ Bearer: [] }] },
|
|
180
|
+
{ ...GET_HEALTH, path: '/basic', security: [{ BasicAuth: [] }] },
|
|
181
|
+
{ ...GET_HEALTH, path: '/apikey', security: [{ ApiKeyAuth: [] }] },
|
|
182
|
+
{ ...GET_HEALTH, path: '/public', security: [{}] },
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const endpoints = convertRoutes(routes, { schemaConverter: identity });
|
|
186
|
+
|
|
187
|
+
expect(endpoints[0].meta?.security).toEqual(['bearer']);
|
|
188
|
+
expect(endpoints[1].meta?.security).toEqual(['basic']);
|
|
189
|
+
expect(endpoints[2].meta?.security).toEqual(['apiKey']);
|
|
190
|
+
expect(endpoints[3].meta?.security).toEqual(['none']);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('handles deprecated routes', () => {
|
|
194
|
+
const route: ExternalRoute = { ...GET_HEALTH, deprecated: true };
|
|
195
|
+
const [ep] = convertRoutes([route], { schemaConverter: identity });
|
|
196
|
+
|
|
197
|
+
expect(ep.meta?.deprecated).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('converts multiple routes at once', () => {
|
|
201
|
+
const endpoints = convertRoutes(
|
|
202
|
+
[GET_HEALTH, POST_LOGIN, GET_USER, LIST_ITEMS],
|
|
203
|
+
{ schemaConverter: identity },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(endpoints).toHaveLength(4);
|
|
207
|
+
expect(endpoints.map(e => e.method)).toEqual(['GET', 'POST', 'GET', 'GET']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('skips invalid HTTP methods', () => {
|
|
211
|
+
const route: ExternalRoute = { ...GET_HEALTH, method: 'OPTIONS' };
|
|
212
|
+
const endpoints = convertRoutes([route], { schemaConverter: identity });
|
|
213
|
+
|
|
214
|
+
expect(endpoints).toHaveLength(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('returns empty array for empty input', () => {
|
|
218
|
+
expect(convertRoutes([], { schemaConverter: identity })).toEqual([]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert external route definitions (e.g., from @hono/zod-openapi createRoute)
|
|
3
|
+
* into Fortress EndpointDefinitions.
|
|
4
|
+
*
|
|
5
|
+
* Schema-library agnostic — the user provides a converter function
|
|
6
|
+
* that turns their schema objects into JSON Schema.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { z } from 'zod';
|
|
11
|
+
* import { convertRoutes } from '@bajustone/fortress/hono';
|
|
12
|
+
* import { loginRoute, listUsersRoute } from './modules/auth/routes';
|
|
13
|
+
*
|
|
14
|
+
* const endpoints = convertRoutes(
|
|
15
|
+
* [loginRoute, listUsersRoute],
|
|
16
|
+
* { prefix: '/api/v1/auth', schemaConverter: z.toJSONSchema },
|
|
17
|
+
* );
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { EndpointDefinition, EndpointInput, EndpointResponse, HttpMethod, SecurityRequirement } from '../core/endpoint';
|
|
22
|
+
import type { JSONSchema } from '../core/json-schema';
|
|
23
|
+
|
|
24
|
+
const TRAILING_SLASH_REGEX = /\/$/;
|
|
25
|
+
|
|
26
|
+
/** A function that converts a schema object (Zod, Valibot, etc.) to JSON Schema. */
|
|
27
|
+
export type ToJSONSchemaConverter = (schema: unknown) => JSONSchema;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generic route shape matching what `createRoute` from `@hono/zod-openapi` returns.
|
|
31
|
+
* No dependency on any specific framework or schema library.
|
|
32
|
+
*/
|
|
33
|
+
export interface ExternalRoute {
|
|
34
|
+
method: string;
|
|
35
|
+
path: string;
|
|
36
|
+
tags?: string[];
|
|
37
|
+
summary?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
deprecated?: boolean;
|
|
40
|
+
security?: Array<Record<string, string[]>>;
|
|
41
|
+
request?: {
|
|
42
|
+
body?: { content: { 'application/json': { schema: unknown } } };
|
|
43
|
+
params?: unknown;
|
|
44
|
+
query?: unknown;
|
|
45
|
+
};
|
|
46
|
+
responses: Record<number | string, {
|
|
47
|
+
description: string;
|
|
48
|
+
content?: { 'application/json': { schema: unknown } };
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Options accepted by {@link convertRoutes}. */
|
|
53
|
+
export interface ConvertRoutesOptions {
|
|
54
|
+
/** Convert your schema objects to JSON Schema (e.g., `z.toJSONSchema` for Zod v4). */
|
|
55
|
+
schemaConverter: ToJSONSchemaConverter;
|
|
56
|
+
/** Path prefix to prepend (e.g., '/api/v1/auth'). */
|
|
57
|
+
prefix?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const RE_LEADING_SLASH = /^\//;
|
|
61
|
+
const RE_PARAM_BRACE = /\{(\w+)\}/g;
|
|
62
|
+
const RE_SLASH = /\//g;
|
|
63
|
+
const RE_NON_WORD = /\W/g;
|
|
64
|
+
|
|
65
|
+
/** Convert OpenAPI `{param}` path to Express-style `:param`. */
|
|
66
|
+
function fromOpenAPIPath(path: string): string {
|
|
67
|
+
return path.replace(RE_PARAM_BRACE, ':$1');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Generate a handler/operationId from method + path. */
|
|
71
|
+
function toOperationId(method: string, path: string): string {
|
|
72
|
+
const normalized = path
|
|
73
|
+
.replace(RE_LEADING_SLASH, '')
|
|
74
|
+
.replace(RE_PARAM_BRACE, '$1')
|
|
75
|
+
.replace(RE_SLASH, '_')
|
|
76
|
+
.replace(RE_NON_WORD, '_');
|
|
77
|
+
return `${method.toLowerCase()}_${normalized}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const VALID_METHODS = new Set<HttpMethod>(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
|
|
81
|
+
|
|
82
|
+
/** Reverse map from security scheme names to SecurityRequirement. */
|
|
83
|
+
const SECURITY_REVERSE: Record<string, SecurityRequirement> = {
|
|
84
|
+
bearer: 'bearer',
|
|
85
|
+
bearerauth: 'bearer',
|
|
86
|
+
basic: 'basic',
|
|
87
|
+
basicauth: 'basic',
|
|
88
|
+
apikey: 'apiKey',
|
|
89
|
+
apikeyauth: 'apiKey',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function mapSecurity(security: Array<Record<string, string[]>>): SecurityRequirement[] {
|
|
93
|
+
const result: SecurityRequirement[] = [];
|
|
94
|
+
for (const entry of security) {
|
|
95
|
+
const keys = Object.keys(entry);
|
|
96
|
+
if (keys.length === 0) {
|
|
97
|
+
result.push('none');
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const mapped = SECURITY_REVERSE[key.toLowerCase()];
|
|
102
|
+
if (mapped && !result.includes(mapped)) {
|
|
103
|
+
result.push(mapped);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert an array of external route definitions into Fortress EndpointDefinitions.
|
|
112
|
+
*
|
|
113
|
+
* The `schemaConverter` is called for every schema object encountered in request/response
|
|
114
|
+
* definitions. Fortress never imports or depends on your schema library.
|
|
115
|
+
*/
|
|
116
|
+
export function convertRoutes(
|
|
117
|
+
routes: ExternalRoute[],
|
|
118
|
+
options: ConvertRoutesOptions,
|
|
119
|
+
): EndpointDefinition[] {
|
|
120
|
+
const { schemaConverter, prefix } = options;
|
|
121
|
+
const result: EndpointDefinition[] = [];
|
|
122
|
+
|
|
123
|
+
for (const route of routes) {
|
|
124
|
+
const method = route.method.toUpperCase() as HttpMethod;
|
|
125
|
+
if (!VALID_METHODS.has(method))
|
|
126
|
+
continue;
|
|
127
|
+
|
|
128
|
+
const rawPath = prefix ? `${prefix.replace(TRAILING_SLASH_REGEX, '')}${route.path}` : route.path;
|
|
129
|
+
const path = fromOpenAPIPath(rawPath);
|
|
130
|
+
const handler = toOperationId(method, rawPath);
|
|
131
|
+
|
|
132
|
+
// Meta
|
|
133
|
+
const meta: EndpointDefinition['meta'] = {
|
|
134
|
+
summary: route.summary ?? '',
|
|
135
|
+
};
|
|
136
|
+
if (route.tags)
|
|
137
|
+
meta.tags = route.tags;
|
|
138
|
+
if (route.description)
|
|
139
|
+
meta.description = route.description;
|
|
140
|
+
if (route.deprecated)
|
|
141
|
+
meta.deprecated = true;
|
|
142
|
+
if (route.security) {
|
|
143
|
+
const mapped = mapSecurity(route.security);
|
|
144
|
+
if (mapped.length > 0)
|
|
145
|
+
meta.security = mapped;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Input
|
|
149
|
+
const input: EndpointInput = {};
|
|
150
|
+
let hasInput = false;
|
|
151
|
+
|
|
152
|
+
if (route.request?.body?.content?.['application/json']?.schema) {
|
|
153
|
+
input.body = schemaConverter(route.request.body.content['application/json'].schema);
|
|
154
|
+
hasInput = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (route.request?.params) {
|
|
158
|
+
input.params = schemaConverter(route.request.params);
|
|
159
|
+
hasInput = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (route.request?.query) {
|
|
163
|
+
input.query = schemaConverter(route.request.query);
|
|
164
|
+
hasInput = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Responses
|
|
168
|
+
const responses: Record<number, EndpointResponse> = {};
|
|
169
|
+
let hasResponses = false;
|
|
170
|
+
|
|
171
|
+
for (const [status, resp] of Object.entries(route.responses)) {
|
|
172
|
+
const entry: EndpointResponse = { description: resp.description };
|
|
173
|
+
if (resp.content?.['application/json']?.schema) {
|
|
174
|
+
entry.schema = schemaConverter(resp.content['application/json'].schema);
|
|
175
|
+
}
|
|
176
|
+
responses[Number(status)] = entry;
|
|
177
|
+
hasResponses = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const endpoint: EndpointDefinition = {
|
|
181
|
+
method,
|
|
182
|
+
path,
|
|
183
|
+
handler,
|
|
184
|
+
meta,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (hasInput)
|
|
188
|
+
endpoint.input = input;
|
|
189
|
+
if (hasResponses)
|
|
190
|
+
endpoint.responses = responses;
|
|
191
|
+
|
|
192
|
+
result.push(endpoint);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|