@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,765 @@
|
|
|
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/core/auth/refresh-token.ts
|
|
452
|
+
async function hashToken(raw) {
|
|
453
|
+
const data = new TextEncoder().encode(raw);
|
|
454
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
455
|
+
return hexEncode(new Uint8Array(hashBuffer));
|
|
456
|
+
}
|
|
457
|
+
function hexEncode(bytes) {
|
|
458
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/plugins/api-key/core.ts
|
|
462
|
+
async function generateApiKey(prefix) {
|
|
463
|
+
const bytes = new Uint8Array(32);
|
|
464
|
+
crypto.getRandomValues(bytes);
|
|
465
|
+
const random = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
466
|
+
const raw = `${prefix}_sk_${random}`;
|
|
467
|
+
const hash = await hashToken(raw);
|
|
468
|
+
const keyPrefix = `${prefix}_sk_${random.slice(0, 8)}`;
|
|
469
|
+
return { raw, hash, keyPrefix };
|
|
470
|
+
}
|
|
471
|
+
function toInfo(r) {
|
|
472
|
+
return {
|
|
473
|
+
id: r.id,
|
|
474
|
+
name: r.name,
|
|
475
|
+
keyPrefix: r.keyPrefix,
|
|
476
|
+
scopes: r.scopes ? JSON.parse(r.scopes) : null,
|
|
477
|
+
expiresAt: r.expiresAt,
|
|
478
|
+
lastUsedAt: r.lastUsedAt,
|
|
479
|
+
createdAt: r.createdAt
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function subjectWhere(subject) {
|
|
483
|
+
return [
|
|
484
|
+
{ field: "subjectType", operator: "=", value: subject.type },
|
|
485
|
+
{ field: "subjectId", operator: "=", value: subject.id }
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
async function createKeyForSubject(db, subject, options, knobs) {
|
|
489
|
+
return db.transaction(async (tx) => {
|
|
490
|
+
if (tx.dialect === "pg" && tx.rawQuery) {
|
|
491
|
+
await tx.rawQuery("SELECT pg_advisory_xact_lock(hashtext(?), hashtext(?))", [subject.type, subject.id]);
|
|
492
|
+
}
|
|
493
|
+
const activeCount = await tx.count({
|
|
494
|
+
model: "api_key",
|
|
495
|
+
where: [
|
|
496
|
+
...subjectWhere(subject),
|
|
497
|
+
{ field: "isRevoked", operator: "=", value: false }
|
|
498
|
+
]
|
|
499
|
+
});
|
|
500
|
+
if (activeCount >= knobs.maxKeysPerSubject) {
|
|
501
|
+
throw Errors.badRequest(`Maximum of ${knobs.maxKeysPerSubject} active API keys per subject`);
|
|
502
|
+
}
|
|
503
|
+
const { raw, hash, keyPrefix } = await generateApiKey(knobs.prefix);
|
|
504
|
+
let expiresAt = null;
|
|
505
|
+
if (options.expiresAt) {
|
|
506
|
+
expiresAt = options.expiresAt;
|
|
507
|
+
} else if (knobs.defaultExpirySeconds) {
|
|
508
|
+
expiresAt = new Date(Date.now() + knobs.defaultExpirySeconds * 1e3);
|
|
509
|
+
}
|
|
510
|
+
const record = await tx.create({
|
|
511
|
+
model: "api_key",
|
|
512
|
+
data: {
|
|
513
|
+
subjectType: subject.type,
|
|
514
|
+
subjectId: subject.id,
|
|
515
|
+
name: options.name,
|
|
516
|
+
keyHash: hash,
|
|
517
|
+
keyPrefix,
|
|
518
|
+
scopes: options.scopes ? JSON.stringify(options.scopes) : null,
|
|
519
|
+
expiresAt,
|
|
520
|
+
lastUsedAt: null,
|
|
521
|
+
isRevoked: false
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
return { key: raw, id: record.id };
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function listKeysForSubject(db, subject) {
|
|
528
|
+
const records = await db.findMany({
|
|
529
|
+
model: "api_key",
|
|
530
|
+
where: [
|
|
531
|
+
...subjectWhere(subject),
|
|
532
|
+
{ field: "isRevoked", operator: "=", value: false }
|
|
533
|
+
]
|
|
534
|
+
});
|
|
535
|
+
return records.map(toInfo);
|
|
536
|
+
}
|
|
537
|
+
async function revokeKeyForSubject(db, subject, keyId) {
|
|
538
|
+
const record = await db.findOne({
|
|
539
|
+
model: "api_key",
|
|
540
|
+
where: [{ field: "id", operator: "=", value: keyId }]
|
|
541
|
+
});
|
|
542
|
+
if (!record || record.subjectType !== subject.type || record.subjectId !== subject.id) {
|
|
543
|
+
throw Errors.notFound("API key not found");
|
|
544
|
+
}
|
|
545
|
+
await db.update({
|
|
546
|
+
model: "api_key",
|
|
547
|
+
where: [{ field: "id", operator: "=", value: keyId }],
|
|
548
|
+
data: { isRevoked: true }
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
async function rotateKeyForSubject(db, subject, keyId, knobs) {
|
|
552
|
+
const record = await db.findOne({
|
|
553
|
+
model: "api_key",
|
|
554
|
+
where: [{ field: "id", operator: "=", value: keyId }]
|
|
555
|
+
});
|
|
556
|
+
if (!record || record.subjectType !== subject.type || record.subjectId !== subject.id) {
|
|
557
|
+
throw Errors.notFound("API key not found");
|
|
558
|
+
}
|
|
559
|
+
await db.update({
|
|
560
|
+
model: "api_key",
|
|
561
|
+
where: [{ field: "id", operator: "=", value: keyId }],
|
|
562
|
+
data: { isRevoked: true }
|
|
563
|
+
});
|
|
564
|
+
const { raw, hash, keyPrefix } = await generateApiKey(knobs.prefix);
|
|
565
|
+
const newRecord = await db.create({
|
|
566
|
+
model: "api_key",
|
|
567
|
+
data: {
|
|
568
|
+
subjectType: record.subjectType,
|
|
569
|
+
subjectId: record.subjectId,
|
|
570
|
+
name: record.name,
|
|
571
|
+
keyHash: hash,
|
|
572
|
+
keyPrefix,
|
|
573
|
+
scopes: record.scopes,
|
|
574
|
+
expiresAt: record.expiresAt,
|
|
575
|
+
lastUsedAt: null,
|
|
576
|
+
isRevoked: false
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
return { key: raw, id: newRecord.id };
|
|
580
|
+
}
|
|
581
|
+
async function resolveApiKey(db, rawKey) {
|
|
582
|
+
const hash = await hashToken(rawKey);
|
|
583
|
+
const record = await db.findOne({
|
|
584
|
+
model: "api_key",
|
|
585
|
+
where: [{ field: "keyHash", operator: "=", value: hash }]
|
|
586
|
+
});
|
|
587
|
+
if (!record || record.isRevoked)
|
|
588
|
+
return null;
|
|
589
|
+
if (record.expiresAt && record.expiresAt < /* @__PURE__ */ new Date())
|
|
590
|
+
return null;
|
|
591
|
+
if (record.subjectType === "USER") {
|
|
592
|
+
const user = await db.findOne({
|
|
593
|
+
model: "user",
|
|
594
|
+
where: [{ field: "id", operator: "=", value: record.subjectId }]
|
|
595
|
+
});
|
|
596
|
+
if (!user || !user.isActive)
|
|
597
|
+
return null;
|
|
598
|
+
} else if (record.subjectType === "SERVICE_ACCOUNT") {
|
|
599
|
+
const sa = await db.findOne({
|
|
600
|
+
model: "service_account",
|
|
601
|
+
where: [{ field: "id", operator: "=", value: record.subjectId }]
|
|
602
|
+
});
|
|
603
|
+
if (!sa || !sa.isActive)
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
await db.update({
|
|
607
|
+
model: "api_key",
|
|
608
|
+
where: [{ field: "id", operator: "=", value: record.id }],
|
|
609
|
+
data: { lastUsedAt: /* @__PURE__ */ new Date() }
|
|
610
|
+
});
|
|
611
|
+
return {
|
|
612
|
+
subject: { type: record.subjectType, id: record.subjectId },
|
|
613
|
+
scopes: record.scopes ? JSON.parse(record.scopes) : null
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async function deleteAllKeysForSubject(db, subject) {
|
|
617
|
+
await db.delete({
|
|
618
|
+
model: "api_key",
|
|
619
|
+
where: subjectWhere(subject)
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/plugins/api-key/index.ts
|
|
624
|
+
var errorRef = ref("ErrorResponse");
|
|
625
|
+
var apiKeySelfServiceRoutes = {
|
|
626
|
+
createKey: endpoint("POST", "/api-key/keys").summary("Create an API key").description("Create a new API key for the authenticated caller. The raw key is returned exactly once \u2014 it cannot be retrieved later.").tags("API Keys").security("bearer").body(obj({
|
|
627
|
+
name: str("Human-readable key label"),
|
|
628
|
+
scopes: arr(str(), "Optional permission scopes attached to the key"),
|
|
629
|
+
expiresAt: str("Optional expiry (ISO 8601 string)")
|
|
630
|
+
}, "name")).response(201, "Key created", obj({
|
|
631
|
+
key: str("Raw API key \u2014 shown exactly once, store it immediately"),
|
|
632
|
+
id: int("Database id of the key")
|
|
633
|
+
}, "key", "id")).response(400, "Bad request", errorRef).response(401, "Not authenticated", errorRef).handler("createKey").build(),
|
|
634
|
+
listKeys: endpoint("GET", "/api-key/keys").summary("List the caller's API keys").description("Return the active (non-revoked) API keys belonging to the authenticated caller. Raw keys and hashes are never returned.").tags("API Keys").security("bearer").response(200, "Keys", obj({
|
|
635
|
+
keys: arr(obj({
|
|
636
|
+
id: id("Database id"),
|
|
637
|
+
name: str("Key label"),
|
|
638
|
+
keyPrefix: str("First 12 characters of the key, for identification"),
|
|
639
|
+
scopes: arr(str()),
|
|
640
|
+
expiresAt: str("ISO 8601 expiry, if any"),
|
|
641
|
+
lastUsedAt: str("ISO 8601 timestamp of last successful resolve"),
|
|
642
|
+
createdAt: str("ISO 8601 creation timestamp")
|
|
643
|
+
}, "id", "name", "keyPrefix", "createdAt"))
|
|
644
|
+
}, "keys")).response(401, "Not authenticated", errorRef).handler("listKeys").build(),
|
|
645
|
+
revokeKey: endpoint("DELETE", "/api-key/keys/:id").summary("Revoke an API key").description("Revoke one of the authenticated caller's own API keys. Returns 404 if the id does not belong to the caller.").tags("API Keys").security("bearer").params(obj({ id: str("Key id") }, "id")).response(200, "Revoked", obj({ ok: bool() }, "ok")).response(401, "Not authenticated", errorRef).response(404, "Not found", errorRef).handler("revokeKey").build(),
|
|
646
|
+
rotateKey: endpoint("POST", "/api-key/keys/:id/rotate").summary("Rotate an API key").description("Revoke an existing key and issue a new one with the same name, scopes, and expiry. The new raw key is returned exactly once.").tags("API Keys").security("bearer").params(obj({ id: str("Key id to rotate") }, "id")).response(200, "Rotated", obj({
|
|
647
|
+
key: str("New raw API key \u2014 shown exactly once"),
|
|
648
|
+
id: int("Database id of the new key")
|
|
649
|
+
}, "key", "id")).response(401, "Not authenticated", errorRef).response(404, "Not found", errorRef).handler("rotateKey").build()
|
|
650
|
+
};
|
|
651
|
+
function apiKey(config = {}) {
|
|
652
|
+
const knobs = {
|
|
653
|
+
prefix: config.prefix ?? "fortress",
|
|
654
|
+
defaultExpirySeconds: config.defaultExpirySeconds ?? null,
|
|
655
|
+
maxKeysPerSubject: config.maxKeysPerSubject ?? 10
|
|
656
|
+
};
|
|
657
|
+
const mountRoutes = config.routes === true;
|
|
658
|
+
let observerRegistered = false;
|
|
659
|
+
return {
|
|
660
|
+
name: "api-key",
|
|
661
|
+
models: [{
|
|
662
|
+
name: "api_key",
|
|
663
|
+
fields: {
|
|
664
|
+
id: { type: "number", required: true },
|
|
665
|
+
subjectType: { type: "string", required: true },
|
|
666
|
+
subjectId: { type: "number", required: true },
|
|
667
|
+
name: { type: "string", required: true },
|
|
668
|
+
keyHash: { type: "string", required: true, unique: true },
|
|
669
|
+
keyPrefix: { type: "string", required: true },
|
|
670
|
+
scopes: { type: "string" },
|
|
671
|
+
expiresAt: { type: "date" },
|
|
672
|
+
lastUsedAt: { type: "date" },
|
|
673
|
+
isRevoked: { type: "boolean", required: true },
|
|
674
|
+
createdAt: { type: "date", required: true }
|
|
675
|
+
}
|
|
676
|
+
}],
|
|
677
|
+
...mountRoutes ? { routes: apiKeySelfServiceRoutes } : {},
|
|
678
|
+
async resolvePrincipal(request, ctx) {
|
|
679
|
+
const auth = request.headers.get("authorization") ?? "";
|
|
680
|
+
const fromAuth = auth.startsWith("ApiKey ") ? auth.slice(7).trim() : null;
|
|
681
|
+
const fromHeader = !fromAuth ? request.headers.get("x-api-key") : null;
|
|
682
|
+
const key = fromAuth ?? fromHeader;
|
|
683
|
+
if (!key)
|
|
684
|
+
return null;
|
|
685
|
+
const resolved = await resolveApiKey(ctx.db, key);
|
|
686
|
+
if (!resolved)
|
|
687
|
+
return null;
|
|
688
|
+
return { subject: resolved.subject, scopes: resolved.scopes };
|
|
689
|
+
},
|
|
690
|
+
methods: (ctx) => {
|
|
691
|
+
if (!observerRegistered && ctx.iam) {
|
|
692
|
+
observerRegistered = true;
|
|
693
|
+
const cascade = async (event) => {
|
|
694
|
+
if (event.eventType === "SERVICE_ACCOUNT_DELETED" && event.targetId != null) {
|
|
695
|
+
await deleteAllKeysForSubject(ctx.db, {
|
|
696
|
+
type: "SERVICE_ACCOUNT",
|
|
697
|
+
id: event.targetId
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
ctx.iam.addIamObserver(cascade);
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
async createKey(input, routeCtx) {
|
|
705
|
+
const subject = resolveCallerSubject(input, routeCtx);
|
|
706
|
+
const options = {
|
|
707
|
+
name: String(input.name ?? ""),
|
|
708
|
+
scopes: Array.isArray(input.scopes) ? input.scopes.map(String) : void 0,
|
|
709
|
+
expiresAt: coerceDate(input.expiresAt)
|
|
710
|
+
};
|
|
711
|
+
if (!options.name)
|
|
712
|
+
throw Errors.badRequest("name is required");
|
|
713
|
+
return createKeyForSubject(ctx.db, subject, options, knobs);
|
|
714
|
+
},
|
|
715
|
+
async listKeys(input, routeCtx) {
|
|
716
|
+
const subject = resolveCallerSubject(input, routeCtx);
|
|
717
|
+
return listKeysForSubject(ctx.db, subject);
|
|
718
|
+
},
|
|
719
|
+
async revokeKey(input, routeCtx) {
|
|
720
|
+
const subject = resolveCallerSubject(input, routeCtx);
|
|
721
|
+
const keyId = String(input.id ?? "");
|
|
722
|
+
if (!keyId)
|
|
723
|
+
throw Errors.badRequest("id is required");
|
|
724
|
+
await revokeKeyForSubject(ctx.db, subject, keyId);
|
|
725
|
+
return { ok: true };
|
|
726
|
+
},
|
|
727
|
+
async rotateKey(input, routeCtx) {
|
|
728
|
+
const subject = resolveCallerSubject(input, routeCtx);
|
|
729
|
+
const keyId = String(input.id ?? "");
|
|
730
|
+
if (!keyId)
|
|
731
|
+
throw Errors.badRequest("id is required");
|
|
732
|
+
return rotateKeyForSubject(ctx.db, subject, keyId, { prefix: knobs.prefix });
|
|
733
|
+
},
|
|
734
|
+
async resolveKey(rawKey) {
|
|
735
|
+
return resolveApiKey(ctx.db, rawKey);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function resolveCallerSubject(input, routeCtx) {
|
|
742
|
+
if (routeCtx) {
|
|
743
|
+
if (!routeCtx.subject)
|
|
744
|
+
throw Errors.unauthorized("Not authenticated");
|
|
745
|
+
if (routeCtx.scopes !== void 0)
|
|
746
|
+
throw Errors.forbidden("API keys cannot manage API keys");
|
|
747
|
+
return routeCtx.subject;
|
|
748
|
+
}
|
|
749
|
+
if (!input.subject)
|
|
750
|
+
throw Errors.badRequest("subject is required for programmatic calls");
|
|
751
|
+
return input.subject;
|
|
752
|
+
}
|
|
753
|
+
function coerceDate(value) {
|
|
754
|
+
if (value == null)
|
|
755
|
+
return void 0;
|
|
756
|
+
if (value instanceof Date)
|
|
757
|
+
return value;
|
|
758
|
+
const parsed = new Date(value);
|
|
759
|
+
if (Number.isNaN(parsed.getTime()))
|
|
760
|
+
throw Errors.badRequest("expiresAt must be a valid date");
|
|
761
|
+
return parsed;
|
|
762
|
+
}
|
|
763
|
+
export {
|
|
764
|
+
apiKey
|
|
765
|
+
};
|