@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,753 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var FortressError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
statusCode;
|
|
5
|
+
retryAfter;
|
|
6
|
+
details;
|
|
7
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
8
|
+
oauthError;
|
|
9
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
10
|
+
oauthDescription;
|
|
11
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
12
|
+
oauthErrorUri;
|
|
13
|
+
constructor(code, message, statusCode, options) {
|
|
14
|
+
super(message, { cause: options?.cause });
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.retryAfter = options?.retryAfter;
|
|
18
|
+
this.details = options?.details;
|
|
19
|
+
this.oauthError = options?.oauthError;
|
|
20
|
+
this.oauthDescription = options?.oauthDescription;
|
|
21
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
22
|
+
}
|
|
23
|
+
toJSON() {
|
|
24
|
+
return {
|
|
25
|
+
code: this.code,
|
|
26
|
+
message: this.message,
|
|
27
|
+
statusCode: this.statusCode,
|
|
28
|
+
...this.details !== void 0 && { details: this.details }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var Errors = {
|
|
33
|
+
unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
|
|
34
|
+
tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
|
|
35
|
+
sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
|
|
36
|
+
sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
|
|
37
|
+
forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
|
|
38
|
+
badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
|
|
39
|
+
notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
|
|
40
|
+
conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
|
|
41
|
+
unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
|
|
42
|
+
rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
|
|
43
|
+
database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
|
|
44
|
+
serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
|
|
45
|
+
validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
|
|
46
|
+
/**
|
|
47
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
48
|
+
*
|
|
49
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
50
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
51
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
52
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
53
|
+
* on the machine-readable `error` field.
|
|
54
|
+
*
|
|
55
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
56
|
+
* - `invalid_client` → 401
|
|
57
|
+
* - everything else → 400
|
|
58
|
+
*/
|
|
59
|
+
oauth: (error, description, options) => {
|
|
60
|
+
const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
|
|
61
|
+
const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
|
|
62
|
+
return new FortressError(code, description ?? error, status, {
|
|
63
|
+
oauthError: error,
|
|
64
|
+
oauthDescription: description,
|
|
65
|
+
oauthErrorUri: options?.errorUri,
|
|
66
|
+
cause: options?.cause
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
71
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
72
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
73
|
+
* service-method call would produce.
|
|
74
|
+
*
|
|
75
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
76
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
77
|
+
* failures still become structured `FortressError`s.
|
|
78
|
+
*/
|
|
79
|
+
fromHttpResponse: (status, body) => {
|
|
80
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
81
|
+
const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
|
|
82
|
+
const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
|
|
83
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function isFortressErrorCode(code) {
|
|
87
|
+
return code === "UNAUTHORIZED" || code === "TOKEN_REUSE" || code === "SESSION_IDLE_TIMEOUT" || code === "SESSION_ABSOLUTE_TIMEOUT" || code === "FORBIDDEN" || code === "BAD_REQUEST" || code === "NOT_FOUND" || code === "CONFLICT" || code === "UNPROCESSABLE_ENTITY" || code === "RATE_LIMITED" || code === "DATABASE_ERROR" || code === "VALIDATION_ERROR" || code === "SERVICE_UNAVAILABLE";
|
|
88
|
+
}
|
|
89
|
+
function statusToErrorCode(status) {
|
|
90
|
+
if (status === 401)
|
|
91
|
+
return "UNAUTHORIZED";
|
|
92
|
+
if (status === 403)
|
|
93
|
+
return "FORBIDDEN";
|
|
94
|
+
if (status === 404)
|
|
95
|
+
return "NOT_FOUND";
|
|
96
|
+
if (status === 409)
|
|
97
|
+
return "CONFLICT";
|
|
98
|
+
if (status === 422)
|
|
99
|
+
return "UNPROCESSABLE_ENTITY";
|
|
100
|
+
if (status === 429)
|
|
101
|
+
return "RATE_LIMITED";
|
|
102
|
+
if (status === 503)
|
|
103
|
+
return "SERVICE_UNAVAILABLE";
|
|
104
|
+
if (status >= 500)
|
|
105
|
+
return "DATABASE_ERROR";
|
|
106
|
+
return "BAD_REQUEST";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/core/schema-builder.ts
|
|
110
|
+
import { fromJSONSchema } from "@bajustone/fetcher/openapi";
|
|
111
|
+
import { date as fDate, datetime as fDatetime, email as fEmail, time as fTime, url as fUrl, uuid as fUuid } from "@bajustone/fetcher/schema";
|
|
112
|
+
function collectRefNames(schema, acc) {
|
|
113
|
+
if (!schema || typeof schema !== "object")
|
|
114
|
+
return acc;
|
|
115
|
+
if (typeof schema.$ref === "string") {
|
|
116
|
+
const i = schema.$ref.lastIndexOf("/");
|
|
117
|
+
acc.add(i >= 0 ? schema.$ref.slice(i + 1) : schema.$ref);
|
|
118
|
+
}
|
|
119
|
+
if (schema.properties) {
|
|
120
|
+
for (const key of Object.keys(schema.properties))
|
|
121
|
+
collectRefNames(schema.properties[key], acc);
|
|
122
|
+
}
|
|
123
|
+
collectRefNames(schema.items, acc);
|
|
124
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
125
|
+
const variants = schema[key];
|
|
126
|
+
if (variants) {
|
|
127
|
+
for (const variant of variants)
|
|
128
|
+
collectRefNames(variant, acc);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object")
|
|
132
|
+
collectRefNames(schema.additionalProperties, acc);
|
|
133
|
+
return acc;
|
|
134
|
+
}
|
|
135
|
+
var refDefinitionContexts = /* @__PURE__ */ new WeakMap();
|
|
136
|
+
function refName(refPath) {
|
|
137
|
+
const i = refPath.lastIndexOf("/");
|
|
138
|
+
return i >= 0 ? refPath.slice(i + 1) : refPath;
|
|
139
|
+
}
|
|
140
|
+
var COMPONENT_NAME_RE = /^[\w.-]+$/;
|
|
141
|
+
function assertComponentName(name) {
|
|
142
|
+
if (!COMPONENT_NAME_RE.test(name))
|
|
143
|
+
throw new Error(`Invalid OpenAPI component name '${name}'`);
|
|
144
|
+
}
|
|
145
|
+
function resolveRefDefs(schema) {
|
|
146
|
+
if (collectRefNames(schema, /* @__PURE__ */ new Set()).size === 0)
|
|
147
|
+
return void 0;
|
|
148
|
+
const defs = /* @__PURE__ */ Object.create(null);
|
|
149
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
150
|
+
const hasOwn = (value, key) => Object.hasOwn(value, key);
|
|
151
|
+
const visit = (node, inherited) => {
|
|
152
|
+
if (!node || typeof node !== "object")
|
|
153
|
+
return;
|
|
154
|
+
const context = refDefinitionContexts.get(node) ?? inherited;
|
|
155
|
+
if (typeof node.$ref === "string") {
|
|
156
|
+
const name = refName(node.$ref);
|
|
157
|
+
const component = context && hasOwn(context, name) ? context[name] : void 0;
|
|
158
|
+
if (component) {
|
|
159
|
+
if (hasOwn(defs, name) && !unresolved.has(name) && defs[name] !== component) {
|
|
160
|
+
throw new Error(`Conflicting component definitions for $ref '${name}'`);
|
|
161
|
+
}
|
|
162
|
+
const firstResolution = !hasOwn(defs, name) || unresolved.delete(name);
|
|
163
|
+
defs[name] = component;
|
|
164
|
+
if (firstResolution)
|
|
165
|
+
visit(component, context);
|
|
166
|
+
} else if (!hasOwn(defs, name)) {
|
|
167
|
+
defs[name] = {};
|
|
168
|
+
unresolved.add(name);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (node.properties) {
|
|
172
|
+
for (const child of Object.values(node.properties))
|
|
173
|
+
visit(child, context);
|
|
174
|
+
}
|
|
175
|
+
visit(node.items, context);
|
|
176
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
177
|
+
for (const child of node[key] ?? [])
|
|
178
|
+
visit(child, context);
|
|
179
|
+
}
|
|
180
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object")
|
|
181
|
+
visit(node.additionalProperties, context);
|
|
182
|
+
};
|
|
183
|
+
visit(schema);
|
|
184
|
+
return defs;
|
|
185
|
+
}
|
|
186
|
+
function createStandardProps(schema) {
|
|
187
|
+
let validateFn;
|
|
188
|
+
return {
|
|
189
|
+
version: 1,
|
|
190
|
+
vendor: "fortress",
|
|
191
|
+
validate(value) {
|
|
192
|
+
if (!validateFn) {
|
|
193
|
+
const defs = resolveRefDefs(schema);
|
|
194
|
+
validateFn = fromJSONSchema(schema, defs)["~standard"].validate;
|
|
195
|
+
}
|
|
196
|
+
return validateFn(value);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function toFortressSchema(schema, refDefinitions, standardProps) {
|
|
201
|
+
if (refDefinitions)
|
|
202
|
+
refDefinitionContexts.set(schema, refDefinitions);
|
|
203
|
+
return Object.assign(schema, {
|
|
204
|
+
"~standard": standardProps ?? createStandardProps(schema)
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function str(opts) {
|
|
208
|
+
const o = typeof opts === "string" ? { description: opts } : opts ?? {};
|
|
209
|
+
const s = { type: "string" };
|
|
210
|
+
if (o.description)
|
|
211
|
+
s.description = o.description;
|
|
212
|
+
if (o.min !== void 0)
|
|
213
|
+
s.minLength = o.min;
|
|
214
|
+
if (o.max !== void 0)
|
|
215
|
+
s.maxLength = o.max;
|
|
216
|
+
if (o.pattern !== void 0)
|
|
217
|
+
s.pattern = o.pattern;
|
|
218
|
+
if (o.format !== void 0)
|
|
219
|
+
s.format = o.format;
|
|
220
|
+
return toFortressSchema(s);
|
|
221
|
+
}
|
|
222
|
+
function int(opts) {
|
|
223
|
+
const o = typeof opts === "string" ? { description: opts } : opts ?? {};
|
|
224
|
+
const s = { type: "integer" };
|
|
225
|
+
if (o.description)
|
|
226
|
+
s.description = o.description;
|
|
227
|
+
if (o.min !== void 0)
|
|
228
|
+
s.minimum = o.min;
|
|
229
|
+
if (o.max !== void 0)
|
|
230
|
+
s.maximum = o.max;
|
|
231
|
+
return toFortressSchema(s);
|
|
232
|
+
}
|
|
233
|
+
function id(description) {
|
|
234
|
+
const s = { type: "string", minLength: 1 };
|
|
235
|
+
if (description)
|
|
236
|
+
s.description = description;
|
|
237
|
+
return toFortressSchema(s);
|
|
238
|
+
}
|
|
239
|
+
function bool(description) {
|
|
240
|
+
const s = { type: "boolean" };
|
|
241
|
+
if (description)
|
|
242
|
+
s.description = description;
|
|
243
|
+
return toFortressSchema(s);
|
|
244
|
+
}
|
|
245
|
+
function arr(items, description) {
|
|
246
|
+
const s = { type: "array", items };
|
|
247
|
+
if (description)
|
|
248
|
+
s.description = description;
|
|
249
|
+
return toFortressSchema(s);
|
|
250
|
+
}
|
|
251
|
+
function obj(properties, ...required) {
|
|
252
|
+
const s = { type: "object", properties };
|
|
253
|
+
if (required.length > 0)
|
|
254
|
+
s.required = required;
|
|
255
|
+
return toFortressSchema(s);
|
|
256
|
+
}
|
|
257
|
+
function ref(name, schema) {
|
|
258
|
+
assertComponentName(name);
|
|
259
|
+
const definitions = schema ? { [name]: schema } : void 0;
|
|
260
|
+
return toFortressSchema({ $ref: `#/components/schemas/${name}` }, definitions);
|
|
261
|
+
}
|
|
262
|
+
function makeFormatBuilder(factory) {
|
|
263
|
+
const f = factory();
|
|
264
|
+
return (description) => {
|
|
265
|
+
const s = { type: "string" };
|
|
266
|
+
if (f.format)
|
|
267
|
+
s.format = f.format;
|
|
268
|
+
if (f.pattern)
|
|
269
|
+
s.pattern = f.pattern;
|
|
270
|
+
if (description)
|
|
271
|
+
s.description = description;
|
|
272
|
+
return toFortressSchema(s);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
var email = makeFormatBuilder(fEmail);
|
|
276
|
+
var uuid = makeFormatBuilder(fUuid);
|
|
277
|
+
var url = makeFormatBuilder(fUrl);
|
|
278
|
+
var datetime = makeFormatBuilder(fDatetime);
|
|
279
|
+
var date = makeFormatBuilder(fDate);
|
|
280
|
+
var time = makeFormatBuilder(fTime);
|
|
281
|
+
function isStandardSchema(value) {
|
|
282
|
+
return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
|
|
283
|
+
}
|
|
284
|
+
function isFortressSchema(value) {
|
|
285
|
+
return isStandardSchema(value) && value["~standard"].vendor === "fortress" && ("type" in value || "$ref" in value || "oneOf" in value || "anyOf" in value);
|
|
286
|
+
}
|
|
287
|
+
var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
|
|
288
|
+
function hasJsonSchemaShape(value) {
|
|
289
|
+
if (typeof value !== "object" || value === null)
|
|
290
|
+
return false;
|
|
291
|
+
const v = value;
|
|
292
|
+
if ("$ref" in v || "oneOf" in v || "anyOf" in v || "allOf" in v)
|
|
293
|
+
return true;
|
|
294
|
+
if (typeof v.type === "string" && JSON_SCHEMA_TYPES.has(v.type))
|
|
295
|
+
return true;
|
|
296
|
+
if (Array.isArray(v.type) && v.type.every((t) => typeof t === "string" && JSON_SCHEMA_TYPES.has(t)))
|
|
297
|
+
return true;
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
function extractJsonSchema(schema) {
|
|
301
|
+
if (isFortressSchema(schema)) {
|
|
302
|
+
return schema;
|
|
303
|
+
}
|
|
304
|
+
if (isStandardSchema(schema)) {
|
|
305
|
+
const std = schema["~standard"];
|
|
306
|
+
if (std?.jsonSchema?.input) {
|
|
307
|
+
return std.jsonSchema.input({ target: "draft-2020-12" });
|
|
308
|
+
}
|
|
309
|
+
if (hasJsonSchemaShape(schema)) {
|
|
310
|
+
return schema;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {};
|
|
314
|
+
}
|
|
315
|
+
var ErrorEnvelope = obj({
|
|
316
|
+
code: str("Machine-readable error code"),
|
|
317
|
+
message: str("Human-readable error message"),
|
|
318
|
+
statusCode: int("HTTP status code"),
|
|
319
|
+
details: toFortressSchema({ description: "Optional structured error details" })
|
|
320
|
+
}, "code", "message", "statusCode");
|
|
321
|
+
var EndpointBuilder = class {
|
|
322
|
+
_method;
|
|
323
|
+
_path;
|
|
324
|
+
_handler = "";
|
|
325
|
+
_summary = "";
|
|
326
|
+
_description;
|
|
327
|
+
_tags = [];
|
|
328
|
+
_security = [];
|
|
329
|
+
_deprecated = false;
|
|
330
|
+
_permission;
|
|
331
|
+
_body;
|
|
332
|
+
_query;
|
|
333
|
+
_params;
|
|
334
|
+
_responses = {};
|
|
335
|
+
constructor(method, path) {
|
|
336
|
+
this._method = method;
|
|
337
|
+
this._path = path;
|
|
338
|
+
}
|
|
339
|
+
summary(s) {
|
|
340
|
+
this._summary = s;
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
description(s) {
|
|
344
|
+
this._description = s;
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
tags(...t) {
|
|
348
|
+
this._tags.push(...t);
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
351
|
+
security(...s) {
|
|
352
|
+
this._security.push(...s);
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
deprecated() {
|
|
356
|
+
this._deprecated = true;
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
permission(resource, action) {
|
|
360
|
+
this._permission = { resource, action };
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
body(schema) {
|
|
364
|
+
this._body = schema;
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
query(schema) {
|
|
368
|
+
this._query = schema;
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
params(schema) {
|
|
372
|
+
this._params = schema;
|
|
373
|
+
return this;
|
|
374
|
+
}
|
|
375
|
+
response(status, description, schema) {
|
|
376
|
+
const stored = { description };
|
|
377
|
+
if (schema !== void 0) {
|
|
378
|
+
stored.schema = isStandardSchema(schema) ? extractJsonSchema(schema) : schema;
|
|
379
|
+
}
|
|
380
|
+
this._responses[status] = stored;
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Declare an error response that returns the canonical {@link ErrorEnvelope}
|
|
385
|
+
* body. Shorthand for `.response(status, description, ErrorEnvelope)`.
|
|
386
|
+
*
|
|
387
|
+
* Aligns host endpoint error responses with what Fortress's own routes
|
|
388
|
+
* emit, so a single client-side error parser works everywhere.
|
|
389
|
+
*
|
|
390
|
+
* ```ts
|
|
391
|
+
* endpoint('GET', '/schools/:id')
|
|
392
|
+
* .summary('Get a school')
|
|
393
|
+
* .params(obj({ id: str() }, 'id'))
|
|
394
|
+
* .response(200, 'OK', SchoolEnvelope)
|
|
395
|
+
* .errorResponse(404, 'School not found')
|
|
396
|
+
* .errorResponse(403, 'Forbidden')
|
|
397
|
+
* .build();
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
errorResponse(status, description) {
|
|
401
|
+
return this.response(status, description, ErrorEnvelope);
|
|
402
|
+
}
|
|
403
|
+
handler(name) {
|
|
404
|
+
this._handler = name;
|
|
405
|
+
return this;
|
|
406
|
+
}
|
|
407
|
+
build() {
|
|
408
|
+
const def = {
|
|
409
|
+
method: this._method,
|
|
410
|
+
path: this._path,
|
|
411
|
+
handler: this._handler
|
|
412
|
+
};
|
|
413
|
+
if (this._summary || this._tags.length > 0 || this._security.length > 0 || this._description || this._deprecated || this._permission) {
|
|
414
|
+
def.meta = {
|
|
415
|
+
summary: this._summary,
|
|
416
|
+
...this._description && { description: this._description },
|
|
417
|
+
...this._tags.length > 0 && { tags: this._tags },
|
|
418
|
+
...this._security.length > 0 && { security: this._security },
|
|
419
|
+
...this._deprecated && { deprecated: true },
|
|
420
|
+
...this._permission && { permission: this._permission }
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (this._body || this._query || this._params) {
|
|
424
|
+
def.input = {};
|
|
425
|
+
if (this._body) {
|
|
426
|
+
def.input.body = extractJsonSchema(this._body);
|
|
427
|
+
if (isStandardSchema(this._body))
|
|
428
|
+
def.input.bodySchema = this._body;
|
|
429
|
+
}
|
|
430
|
+
if (this._query) {
|
|
431
|
+
def.input.query = extractJsonSchema(this._query);
|
|
432
|
+
if (isStandardSchema(this._query))
|
|
433
|
+
def.input.querySchema = this._query;
|
|
434
|
+
}
|
|
435
|
+
if (this._params) {
|
|
436
|
+
def.input.params = extractJsonSchema(this._params);
|
|
437
|
+
if (isStandardSchema(this._params))
|
|
438
|
+
def.input.paramsSchema = this._params;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (Object.keys(this._responses).length > 0) {
|
|
442
|
+
def.responses = this._responses;
|
|
443
|
+
}
|
|
444
|
+
return def;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
function endpoint(method, path) {
|
|
448
|
+
return new EndpointBuilder(method, path);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/plugins/tenancy/index.ts
|
|
452
|
+
var SAFE_SCHEMA_PREFIX = /^[a-z_][a-z0-9_]*$/;
|
|
453
|
+
var SAFE_TENANT_ID = /^[\w-]+$/;
|
|
454
|
+
function assertSafeSchemaPrefix(prefix) {
|
|
455
|
+
if (!SAFE_SCHEMA_PREFIX.test(prefix))
|
|
456
|
+
throw Errors.badRequest(`Invalid tenancy schemaPrefix '${prefix}': must match ${SAFE_SCHEMA_PREFIX}`);
|
|
457
|
+
}
|
|
458
|
+
var errorRef = ref("ErrorResponse");
|
|
459
|
+
var tenantResponse = obj({
|
|
460
|
+
id: id("Tenant id"),
|
|
461
|
+
name: str("Tenant name"),
|
|
462
|
+
taxId: str("Unique tenant tax id / external code"),
|
|
463
|
+
description: str("Optional description"),
|
|
464
|
+
createdAt: str("ISO 8601 creation timestamp"),
|
|
465
|
+
updatedAt: str("ISO 8601 update timestamp")
|
|
466
|
+
}, "id", "name", "taxId");
|
|
467
|
+
var tenancyRoutes = {
|
|
468
|
+
createTenant: endpoint("POST", "/tenancy/tenants").summary("Create a tenant").description("Create a new tenant and its PostgreSQL schema. Requires the `fortress:manageTenants` permission.").tags("Tenancy").security("bearer").permission("fortress", "manageTenants").body(obj({
|
|
469
|
+
name: str("Human-readable tenant name"),
|
|
470
|
+
taxId: str("Unique tenant tax id / external code"),
|
|
471
|
+
description: str("Optional description")
|
|
472
|
+
}, "name", "taxId")).response(201, "Tenant created", tenantResponse).response(401, "Not authenticated", errorRef).response(403, "Insufficient permissions", errorRef).response(409, "taxId already exists", errorRef).handler("createTenant").build(),
|
|
473
|
+
deleteTenant: endpoint("DELETE", "/tenancy/tenants/:id").summary("Delete a tenant").description("Remove a tenant, its memberships, and (when `dropSchemaOnDelete` is enabled) its schema. Requires the `fortress:manageTenants` permission.").tags("Tenancy").security("bearer").permission("fortress", "manageTenants").params(obj({ id: str("Tenant id") }, "id")).response(200, "Tenant deleted", obj({ ok: bool("Always true") }, "ok")).response(401, "Not authenticated", errorRef).response(403, "Insufficient permissions", errorRef).response(404, "Tenant not found", errorRef).handler("deleteTenant").build(),
|
|
474
|
+
getMyTenants: endpoint("GET", "/tenancy/tenants/mine").summary("List the caller's tenants").description("Return the tenants the authenticated caller belongs to.").tags("Tenancy").security("bearer").response(200, "Tenants", obj({ tenants: arr(tenantResponse, "Tenants the caller belongs to") }, "tenants")).response(401, "Not authenticated", errorRef).handler("getMyTenants").build(),
|
|
475
|
+
switchTenant: endpoint("POST", "/tenancy/switch").summary("Switch the caller's default tenant").description("Set the authenticated caller's default tenant. The new tenant takes effect on the next token refresh.").tags("Tenancy").security("bearer").body(obj({ taxId: str("Tax id of the tenant to switch to") }, "taxId")).response(200, "Switched", obj({ ok: bool("Always true") }, "ok")).response(401, "Not authenticated", errorRef).response(403, "Caller does not belong to this tenant", errorRef).response(404, "Tenant not found", errorRef).handler("switchTenant").build()
|
|
476
|
+
};
|
|
477
|
+
function tenancy(config = {}) {
|
|
478
|
+
const schemaPrefix = config.schemaPrefix ?? "tenant_";
|
|
479
|
+
assertSafeSchemaPrefix(schemaPrefix);
|
|
480
|
+
const mountRoutes = config.routes === true;
|
|
481
|
+
const tenantSchemaName = (id2) => `${schemaPrefix}${id2}`;
|
|
482
|
+
return {
|
|
483
|
+
name: "tenancy",
|
|
484
|
+
models: [
|
|
485
|
+
{
|
|
486
|
+
name: "tenant",
|
|
487
|
+
fields: {
|
|
488
|
+
id: { type: "number", required: true },
|
|
489
|
+
name: { type: "string", required: true },
|
|
490
|
+
taxId: { type: "string", required: true, unique: true },
|
|
491
|
+
description: { type: "string" },
|
|
492
|
+
createdAt: { type: "date", required: true },
|
|
493
|
+
updatedAt: { type: "date", required: true }
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "tenant_user",
|
|
498
|
+
fields: {
|
|
499
|
+
tenantId: { type: "number", required: true, references: { model: "tenant", field: "id" } },
|
|
500
|
+
userId: { type: "number", required: true, references: { model: "user", field: "id" } },
|
|
501
|
+
isDefault: { type: "boolean", required: true }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
],
|
|
505
|
+
...mountRoutes ? { routes: tenancyRoutes } : {},
|
|
506
|
+
async enrichTokenClaims(userId, ctx) {
|
|
507
|
+
const membership = await ctx.db.findOne({
|
|
508
|
+
model: "tenant_user",
|
|
509
|
+
where: [
|
|
510
|
+
{ field: "userId", operator: "=", value: userId },
|
|
511
|
+
{ field: "isDefault", operator: "=", value: true }
|
|
512
|
+
]
|
|
513
|
+
});
|
|
514
|
+
if (!membership)
|
|
515
|
+
return {};
|
|
516
|
+
const tenant = await ctx.db.findOne({
|
|
517
|
+
model: "tenant",
|
|
518
|
+
where: [{ field: "id", operator: "=", value: membership.tenantId }]
|
|
519
|
+
});
|
|
520
|
+
if (!tenant)
|
|
521
|
+
return {};
|
|
522
|
+
return {
|
|
523
|
+
tenantId: tenant.id,
|
|
524
|
+
tenantCode: tenant.taxId
|
|
525
|
+
};
|
|
526
|
+
},
|
|
527
|
+
wrapAdapter(adapter, requestContext) {
|
|
528
|
+
const tenantId = requestContext.tenantId;
|
|
529
|
+
if (tenantId == null)
|
|
530
|
+
return adapter;
|
|
531
|
+
if (!SAFE_TENANT_ID.test(tenantId))
|
|
532
|
+
throw Errors.forbidden("Invalid tenant claim");
|
|
533
|
+
const isPg = adapter.dialect === "pg" && !!adapter.rawQuery;
|
|
534
|
+
if (!isPg)
|
|
535
|
+
return adapter;
|
|
536
|
+
const schemaName = tenantSchemaName(tenantId);
|
|
537
|
+
const setPath = (tx) => tx.rawQuery(`SELECT set_config('search_path', ?, true)`, [`${schemaName}, public`]);
|
|
538
|
+
return {
|
|
539
|
+
...adapter,
|
|
540
|
+
async create(params) {
|
|
541
|
+
return adapter.transaction(async (tx) => {
|
|
542
|
+
await setPath(tx);
|
|
543
|
+
return tx.create(params);
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
async findOne(params) {
|
|
547
|
+
return adapter.transaction(async (tx) => {
|
|
548
|
+
await setPath(tx);
|
|
549
|
+
return tx.findOne(params);
|
|
550
|
+
});
|
|
551
|
+
},
|
|
552
|
+
async findMany(params) {
|
|
553
|
+
return adapter.transaction(async (tx) => {
|
|
554
|
+
await setPath(tx);
|
|
555
|
+
return tx.findMany(params);
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
async update(params) {
|
|
559
|
+
return adapter.transaction(async (tx) => {
|
|
560
|
+
await setPath(tx);
|
|
561
|
+
return tx.update(params);
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
async delete(params) {
|
|
565
|
+
return adapter.transaction(async (tx) => {
|
|
566
|
+
await setPath(tx);
|
|
567
|
+
return tx.delete(params);
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
async count(params) {
|
|
571
|
+
return adapter.transaction(async (tx) => {
|
|
572
|
+
await setPath(tx);
|
|
573
|
+
return tx.count(params);
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
async transaction(fn) {
|
|
577
|
+
return adapter.transaction(async (tx) => {
|
|
578
|
+
await setPath(tx);
|
|
579
|
+
return fn(tx);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
},
|
|
584
|
+
methods: (ctx) => {
|
|
585
|
+
const findTenantByTaxId = (taxId) => ctx.db.findOne({
|
|
586
|
+
model: "tenant",
|
|
587
|
+
where: [{ field: "taxId", operator: "=", value: taxId }]
|
|
588
|
+
});
|
|
589
|
+
const listUserTenants = async (userId) => {
|
|
590
|
+
const memberships = await ctx.db.findMany({
|
|
591
|
+
model: "tenant_user",
|
|
592
|
+
where: [{ field: "userId", operator: "=", value: userId }]
|
|
593
|
+
});
|
|
594
|
+
if (memberships.length === 0)
|
|
595
|
+
return [];
|
|
596
|
+
const tenantIds = memberships.map((m) => m.tenantId);
|
|
597
|
+
return ctx.db.findMany({
|
|
598
|
+
model: "tenant",
|
|
599
|
+
where: [{ field: "id", operator: "in", value: tenantIds }]
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
const requireUserId = (input, routeCtx) => {
|
|
603
|
+
const userId = routeCtx?.userId ?? input.userId;
|
|
604
|
+
if (userId == null) {
|
|
605
|
+
if (routeCtx)
|
|
606
|
+
throw Errors.unauthorized("Not authenticated");
|
|
607
|
+
throw Errors.badRequest("userId is required for programmatic calls");
|
|
608
|
+
}
|
|
609
|
+
return userId;
|
|
610
|
+
};
|
|
611
|
+
const defaultTenantQueues = /* @__PURE__ */ new Map();
|
|
612
|
+
const withDefaultTenantLock = async (userId, operation) => {
|
|
613
|
+
const previous = defaultTenantQueues.get(userId) ?? Promise.resolve();
|
|
614
|
+
let release;
|
|
615
|
+
const current = new Promise((resolve) => {
|
|
616
|
+
release = resolve;
|
|
617
|
+
});
|
|
618
|
+
defaultTenantQueues.set(userId, current);
|
|
619
|
+
await previous;
|
|
620
|
+
try {
|
|
621
|
+
return await ctx.db.transaction(async (tx) => {
|
|
622
|
+
if (tx.dialect === "pg" && tx.rawQuery) {
|
|
623
|
+
await tx.rawQuery("SELECT pg_advisory_xact_lock(hashtext(?))", [`fortress:tenant-default:${userId}`]);
|
|
624
|
+
}
|
|
625
|
+
return operation(tx);
|
|
626
|
+
});
|
|
627
|
+
} finally {
|
|
628
|
+
release();
|
|
629
|
+
if (defaultTenantQueues.get(userId) === current)
|
|
630
|
+
defaultTenantQueues.delete(userId);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
async createTenant(input) {
|
|
635
|
+
const existing = await findTenantByTaxId(input.taxId);
|
|
636
|
+
if (existing)
|
|
637
|
+
throw Errors.conflict(`Tenant with taxId '${input.taxId}' already exists`);
|
|
638
|
+
return ctx.db.transaction(async (tx) => {
|
|
639
|
+
const tenant = await tx.create({
|
|
640
|
+
model: "tenant",
|
|
641
|
+
data: {
|
|
642
|
+
name: input.name,
|
|
643
|
+
taxId: input.taxId,
|
|
644
|
+
description: input.description ?? null
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
if (tx.dialect === "pg" && tx.rawQuery) {
|
|
648
|
+
const schemaName = tenantSchemaName(tenant.id);
|
|
649
|
+
await tx.rawQuery(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
650
|
+
if (config.onSchemaCreated)
|
|
651
|
+
await config.onSchemaCreated(schemaName, tx.rawQuery.bind(tx));
|
|
652
|
+
}
|
|
653
|
+
return tenant;
|
|
654
|
+
});
|
|
655
|
+
},
|
|
656
|
+
async deleteTenant(input) {
|
|
657
|
+
const id2 = String(input.id ?? "");
|
|
658
|
+
if (!id2)
|
|
659
|
+
throw Errors.badRequest("id is required");
|
|
660
|
+
const tenant = await ctx.db.findOne({
|
|
661
|
+
model: "tenant",
|
|
662
|
+
where: [{ field: "id", operator: "=", value: id2 }]
|
|
663
|
+
});
|
|
664
|
+
if (!tenant)
|
|
665
|
+
throw Errors.notFound(`Tenant '${id2}' not found`);
|
|
666
|
+
await ctx.db.transaction(async (tx) => {
|
|
667
|
+
await tx.delete({
|
|
668
|
+
model: "tenant_user",
|
|
669
|
+
where: [{ field: "tenantId", operator: "=", value: id2 }]
|
|
670
|
+
});
|
|
671
|
+
await tx.delete({
|
|
672
|
+
model: "tenant",
|
|
673
|
+
where: [{ field: "id", operator: "=", value: id2 }]
|
|
674
|
+
});
|
|
675
|
+
if (config.dropSchemaOnDelete && tx.dialect === "pg" && tx.rawQuery) {
|
|
676
|
+
const schemaName = tenantSchemaName(id2);
|
|
677
|
+
await tx.rawQuery(`DROP SCHEMA IF EXISTS ${schemaName} CASCADE`);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return { ok: true };
|
|
681
|
+
},
|
|
682
|
+
async addUserToTenant(userId, tenantId) {
|
|
683
|
+
await withDefaultTenantLock(userId, async (tx) => {
|
|
684
|
+
const existing = await tx.findOne({
|
|
685
|
+
model: "tenant_user",
|
|
686
|
+
where: [
|
|
687
|
+
{ field: "userId", operator: "=", value: userId },
|
|
688
|
+
{ field: "tenantId", operator: "=", value: tenantId }
|
|
689
|
+
]
|
|
690
|
+
});
|
|
691
|
+
if (existing)
|
|
692
|
+
return;
|
|
693
|
+
const memberships = await tx.findMany({
|
|
694
|
+
model: "tenant_user",
|
|
695
|
+
where: [{ field: "userId", operator: "=", value: userId }]
|
|
696
|
+
});
|
|
697
|
+
await tx.create({
|
|
698
|
+
model: "tenant_user",
|
|
699
|
+
data: {
|
|
700
|
+
tenantId,
|
|
701
|
+
userId,
|
|
702
|
+
isDefault: memberships.length === 0
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
async getUserTenants(userId) {
|
|
708
|
+
return listUserTenants(userId);
|
|
709
|
+
},
|
|
710
|
+
async getMyTenants(input, routeCtx) {
|
|
711
|
+
return { tenants: await listUserTenants(requireUserId(input, routeCtx)) };
|
|
712
|
+
},
|
|
713
|
+
async switchTenant(input, routeCtx) {
|
|
714
|
+
const userId = requireUserId(input, routeCtx);
|
|
715
|
+
const tenant = await findTenantByTaxId(input.taxId);
|
|
716
|
+
if (!tenant)
|
|
717
|
+
throw Errors.notFound(`Tenant '${input.taxId}' not found`);
|
|
718
|
+
await withDefaultTenantLock(userId, async (tx) => {
|
|
719
|
+
const membership = await tx.findOne({
|
|
720
|
+
model: "tenant_user",
|
|
721
|
+
where: [
|
|
722
|
+
{ field: "userId", operator: "=", value: userId },
|
|
723
|
+
{ field: "tenantId", operator: "=", value: tenant.id }
|
|
724
|
+
]
|
|
725
|
+
});
|
|
726
|
+
if (!membership)
|
|
727
|
+
throw Errors.forbidden("User does not belong to this tenant");
|
|
728
|
+
await tx.update({
|
|
729
|
+
model: "tenant_user",
|
|
730
|
+
where: [
|
|
731
|
+
{ field: "userId", operator: "=", value: userId },
|
|
732
|
+
{ field: "isDefault", operator: "=", value: true }
|
|
733
|
+
],
|
|
734
|
+
data: { isDefault: false }
|
|
735
|
+
});
|
|
736
|
+
await tx.update({
|
|
737
|
+
model: "tenant_user",
|
|
738
|
+
where: [
|
|
739
|
+
{ field: "userId", operator: "=", value: userId },
|
|
740
|
+
{ field: "tenantId", operator: "=", value: tenant.id }
|
|
741
|
+
],
|
|
742
|
+
data: { isDefault: true }
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
return { ok: true };
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
export {
|
|
752
|
+
tenancy
|
|
753
|
+
};
|