@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
package/dist/jwt.cjs
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/core/auth/jwt.ts
|
|
21
|
+
var jwt_exports = {};
|
|
22
|
+
__export(jwt_exports, {
|
|
23
|
+
RESERVED_JWT_CLAIMS: () => RESERVED_JWT_CLAIMS,
|
|
24
|
+
signAccessToken: () => signAccessToken,
|
|
25
|
+
stripReservedClaims: () => stripReservedClaims,
|
|
26
|
+
verifyAccessToken: () => verifyAccessToken
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(jwt_exports);
|
|
29
|
+
var import_jose = require("jose");
|
|
30
|
+
|
|
31
|
+
// src/core/errors.ts
|
|
32
|
+
var FortressError = class extends Error {
|
|
33
|
+
code;
|
|
34
|
+
statusCode;
|
|
35
|
+
retryAfter;
|
|
36
|
+
details;
|
|
37
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
38
|
+
oauthError;
|
|
39
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
40
|
+
oauthDescription;
|
|
41
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
42
|
+
oauthErrorUri;
|
|
43
|
+
constructor(code, message, statusCode, options) {
|
|
44
|
+
super(message, { cause: options?.cause });
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.statusCode = statusCode;
|
|
47
|
+
this.retryAfter = options?.retryAfter;
|
|
48
|
+
this.details = options?.details;
|
|
49
|
+
this.oauthError = options?.oauthError;
|
|
50
|
+
this.oauthDescription = options?.oauthDescription;
|
|
51
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
52
|
+
}
|
|
53
|
+
toJSON() {
|
|
54
|
+
return {
|
|
55
|
+
code: this.code,
|
|
56
|
+
message: this.message,
|
|
57
|
+
statusCode: this.statusCode,
|
|
58
|
+
...this.details !== void 0 && { details: this.details }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var Errors = {
|
|
63
|
+
unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
|
|
64
|
+
tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
|
|
65
|
+
sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
|
|
66
|
+
sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
|
|
67
|
+
forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
|
|
68
|
+
badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
|
|
69
|
+
notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
|
|
70
|
+
conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
|
|
71
|
+
unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
|
|
72
|
+
rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
|
|
73
|
+
database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
|
|
74
|
+
serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
|
|
75
|
+
validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
|
|
76
|
+
/**
|
|
77
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
78
|
+
*
|
|
79
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
80
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
81
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
82
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
83
|
+
* on the machine-readable `error` field.
|
|
84
|
+
*
|
|
85
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
86
|
+
* - `invalid_client` → 401
|
|
87
|
+
* - everything else → 400
|
|
88
|
+
*/
|
|
89
|
+
oauth: (error, description, options) => {
|
|
90
|
+
const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
|
|
91
|
+
const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
|
|
92
|
+
return new FortressError(code, description ?? error, status, {
|
|
93
|
+
oauthError: error,
|
|
94
|
+
oauthDescription: description,
|
|
95
|
+
oauthErrorUri: options?.errorUri,
|
|
96
|
+
cause: options?.cause
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
101
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
102
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
103
|
+
* service-method call would produce.
|
|
104
|
+
*
|
|
105
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
106
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
107
|
+
* failures still become structured `FortressError`s.
|
|
108
|
+
*/
|
|
109
|
+
fromHttpResponse: (status, body) => {
|
|
110
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
111
|
+
const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
|
|
112
|
+
const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
|
|
113
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function isFortressErrorCode(code) {
|
|
117
|
+
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";
|
|
118
|
+
}
|
|
119
|
+
function statusToErrorCode(status) {
|
|
120
|
+
if (status === 401)
|
|
121
|
+
return "UNAUTHORIZED";
|
|
122
|
+
if (status === 403)
|
|
123
|
+
return "FORBIDDEN";
|
|
124
|
+
if (status === 404)
|
|
125
|
+
return "NOT_FOUND";
|
|
126
|
+
if (status === 409)
|
|
127
|
+
return "CONFLICT";
|
|
128
|
+
if (status === 422)
|
|
129
|
+
return "UNPROCESSABLE_ENTITY";
|
|
130
|
+
if (status === 429)
|
|
131
|
+
return "RATE_LIMITED";
|
|
132
|
+
if (status === 503)
|
|
133
|
+
return "SERVICE_UNAVAILABLE";
|
|
134
|
+
if (status >= 500)
|
|
135
|
+
return "DATABASE_ERROR";
|
|
136
|
+
return "BAD_REQUEST";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/core/auth/jwt.ts
|
|
140
|
+
var RESERVED_JWT_CLAIMS = /* @__PURE__ */ new Set([
|
|
141
|
+
"sub",
|
|
142
|
+
"iss",
|
|
143
|
+
"iat",
|
|
144
|
+
"exp",
|
|
145
|
+
"nbf",
|
|
146
|
+
"aud",
|
|
147
|
+
"jti",
|
|
148
|
+
"act",
|
|
149
|
+
"groups",
|
|
150
|
+
"subjectType",
|
|
151
|
+
"name"
|
|
152
|
+
]);
|
|
153
|
+
var REQUIRED_ACCESS_TOKEN_CLAIMS = ["sub", "subjectType", "name", "groups", "iss", "iat", "exp"];
|
|
154
|
+
var SUBJECT_TYPES = ["USER", "GROUP", "SERVICE_ACCOUNT"];
|
|
155
|
+
function stripReservedClaims(claims, context) {
|
|
156
|
+
if (!claims)
|
|
157
|
+
return {};
|
|
158
|
+
const out = {};
|
|
159
|
+
let stripped;
|
|
160
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
161
|
+
if (RESERVED_JWT_CLAIMS.has(key)) {
|
|
162
|
+
(stripped ??= []).push(key);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
out[key] = value;
|
|
166
|
+
}
|
|
167
|
+
if (stripped && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
168
|
+
console.warn(`[fortress/jwt] dropping reserved claim(s) from ${context}: ${stripped.join(", ")}`);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function encodeSecret(secret) {
|
|
173
|
+
return new TextEncoder().encode(secret);
|
|
174
|
+
}
|
|
175
|
+
function normalizeKeys(key) {
|
|
176
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
177
|
+
return keys.map(encodeSecret);
|
|
178
|
+
}
|
|
179
|
+
async function signAccessToken(claims, key, expiresInSeconds, options) {
|
|
180
|
+
const [signingKey] = normalizeKeys(key);
|
|
181
|
+
const safeCustom = stripReservedClaims(claims.customClaims, "signAccessToken.customClaims");
|
|
182
|
+
let jwt = new import_jose.SignJWT({
|
|
183
|
+
name: claims.name,
|
|
184
|
+
subjectType: claims.subjectType,
|
|
185
|
+
groups: claims.groups,
|
|
186
|
+
...claims.act ? { act: claims.act } : {},
|
|
187
|
+
...safeCustom
|
|
188
|
+
}).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${expiresInSeconds}s`).setIssuer(claims.iss);
|
|
189
|
+
if (options?.audience !== void 0)
|
|
190
|
+
jwt = jwt.setAudience(options.audience);
|
|
191
|
+
return jwt.setSubject(claims.sub).sign(signingKey);
|
|
192
|
+
}
|
|
193
|
+
async function verifyAccessToken(token, key, options) {
|
|
194
|
+
const keys = normalizeKeys(key);
|
|
195
|
+
const requiredClaims = [.../* @__PURE__ */ new Set([...REQUIRED_ACCESS_TOKEN_CLAIMS, ...options?.requiredClaims ?? []])];
|
|
196
|
+
const verifyOptions = { algorithms: ["HS256"], requiredClaims };
|
|
197
|
+
if (options?.issuer)
|
|
198
|
+
verifyOptions.issuer = options.issuer;
|
|
199
|
+
if (options?.audience)
|
|
200
|
+
verifyOptions.audience = options.audience;
|
|
201
|
+
if (options?.clockTolerance !== void 0)
|
|
202
|
+
verifyOptions.clockTolerance = options.clockTolerance;
|
|
203
|
+
for (const candidate of keys) {
|
|
204
|
+
try {
|
|
205
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, candidate, verifyOptions);
|
|
206
|
+
const subjectType = payload.subjectType;
|
|
207
|
+
const groups = payload.groups;
|
|
208
|
+
const name = payload.name;
|
|
209
|
+
const issuer = payload.iss;
|
|
210
|
+
const audience = payload.aud;
|
|
211
|
+
const issuedAt = payload.iat;
|
|
212
|
+
const expiresAt = payload.exp;
|
|
213
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0)
|
|
214
|
+
throw new Error("Invalid sub claim");
|
|
215
|
+
if (typeof subjectType !== "string" || !SUBJECT_TYPES.includes(subjectType))
|
|
216
|
+
throw new Error("Invalid subjectType claim");
|
|
217
|
+
if (typeof name !== "string")
|
|
218
|
+
throw new Error("Invalid name claim");
|
|
219
|
+
if (!Array.isArray(groups) || !groups.every((group) => typeof group === "string"))
|
|
220
|
+
throw new Error("Invalid groups claim");
|
|
221
|
+
if (typeof issuer !== "string" || issuer.length === 0)
|
|
222
|
+
throw new Error("Invalid iss claim");
|
|
223
|
+
if (audience !== void 0 && (!Array.isArray(audience) || !audience.every((value) => typeof value === "string")) && typeof audience !== "string")
|
|
224
|
+
throw new Error("Invalid aud claim");
|
|
225
|
+
if (typeof issuedAt !== "number")
|
|
226
|
+
throw new Error("Invalid iat claim");
|
|
227
|
+
if (typeof expiresAt !== "number")
|
|
228
|
+
throw new Error("Invalid exp claim");
|
|
229
|
+
return {
|
|
230
|
+
sub: payload.sub,
|
|
231
|
+
subjectType,
|
|
232
|
+
name,
|
|
233
|
+
groups,
|
|
234
|
+
iss: issuer,
|
|
235
|
+
...audience !== void 0 ? { aud: audience } : {},
|
|
236
|
+
iat: issuedAt,
|
|
237
|
+
exp: expiresAt,
|
|
238
|
+
act: payload.act,
|
|
239
|
+
customClaims: Object.fromEntries(
|
|
240
|
+
Object.entries(payload).filter(([k]) => !RESERVED_JWT_CLAIMS.has(k))
|
|
241
|
+
)
|
|
242
|
+
};
|
|
243
|
+
} catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw Errors.unauthorized("Invalid or expired token");
|
|
248
|
+
}
|
|
249
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
250
|
+
0 && (module.exports = {
|
|
251
|
+
RESERVED_JWT_CLAIMS,
|
|
252
|
+
signAccessToken,
|
|
253
|
+
stripReservedClaims,
|
|
254
|
+
verifyAccessToken
|
|
255
|
+
});
|
package/dist/jwt.d.cts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { T as TokenClaims } from './types-et0R8tsC.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JWT signing and verification helpers built on `jose` (Web Crypto API).
|
|
5
|
+
*
|
|
6
|
+
* Provides {@link signAccessToken} and {@link verifyAccessToken} for short-lived
|
|
7
|
+
* access tokens. Refresh tokens are handled separately in
|
|
8
|
+
* `core/auth/refresh-token.ts`. Supports key rotation by accepting an array
|
|
9
|
+
* of keys — the first is used to sign, all are accepted on verify.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Key material accepted by {@link signAccessToken} / {@link verifyAccessToken}.
|
|
16
|
+
*
|
|
17
|
+
* Today this is just an HS256 shared secret (string) or a rotation array of
|
|
18
|
+
* shared secrets. The alias exists so the public signature is stable when we
|
|
19
|
+
* expand to asymmetric algorithms (RS256 / EdDSA) and JWKS-backed verification.
|
|
20
|
+
*
|
|
21
|
+
* Expected expansion:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* import type { CryptoKey, JWK, JWTVerifyGetKey, KeyObject } from 'jose';
|
|
25
|
+
*
|
|
26
|
+
* export type JwtStaticKey =
|
|
27
|
+
* | string // HS* shared secret (utf-8)
|
|
28
|
+
* | Uint8Array // HS* shared secret (raw bytes)
|
|
29
|
+
* | CryptoKey // Web Crypto key (any alg)
|
|
30
|
+
* | KeyObject // Node KeyObject (any alg)
|
|
31
|
+
* | JWK; // JSON Web Key
|
|
32
|
+
*
|
|
33
|
+
* export type JwtKeyMaterial = JwtStaticKey | readonly JwtStaticKey[];
|
|
34
|
+
*
|
|
35
|
+
* // Verification additionally accepts a resolver (e.g. createRemoteJWKSet):
|
|
36
|
+
* export type JwtVerifyKeyMaterial = JwtKeyMaterial | JWTVerifyGetKey;
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Until then, the runtime helpers ({@link normalizeKeys}) only know how to
|
|
40
|
+
* encode strings — widening the type without widening the helpers would be a
|
|
41
|
+
* lie, so do both together.
|
|
42
|
+
*/
|
|
43
|
+
type JwtKeyMaterial = string | string[];
|
|
44
|
+
/**
|
|
45
|
+
* JWT claim names fortress controls and that must never be overridable by
|
|
46
|
+
* caller-supplied `customClaims` or by plugin `enrichTokenClaims` output.
|
|
47
|
+
*
|
|
48
|
+
* Letting a caller set `act` would forge an impersonation marker; letting a
|
|
49
|
+
* caller set `sub`/`iss`/`exp` would let them mint a token for any subject
|
|
50
|
+
* or extend its lifetime. The denylist is enforced in {@link signAccessToken}
|
|
51
|
+
* — extra claims are dropped (with a `warn` log in dev) before the safe
|
|
52
|
+
* claims are spread.
|
|
53
|
+
*/
|
|
54
|
+
declare const RESERVED_JWT_CLAIMS: ReadonlySet<string>;
|
|
55
|
+
interface SignAccessTokenOptions {
|
|
56
|
+
/** Audience to include in the JWT `aud` claim. Omitted when unset. */
|
|
57
|
+
audience?: string | string[];
|
|
58
|
+
}
|
|
59
|
+
interface VerifyAccessTokenOptions {
|
|
60
|
+
/** Expected issuer. When set, jose also requires the `iss` claim to be present. */
|
|
61
|
+
issuer?: string | string[];
|
|
62
|
+
/** Expected audience. When set, jose also requires the `aud` claim to be present. */
|
|
63
|
+
audience?: string | string[];
|
|
64
|
+
/** Clock skew tolerance for `nbf`, `exp`, and max-token-age checks. */
|
|
65
|
+
clockTolerance?: string | number;
|
|
66
|
+
/** Additional claim names that must be present alongside Fortress's required access-token claims. */
|
|
67
|
+
requiredClaims?: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Strip any reserved keys from a custom-claims object. In development we
|
|
71
|
+
* log a warning so library users notice the collision; in production we
|
|
72
|
+
* silently drop (the safe default — a misbehaving plugin shouldn't be able
|
|
73
|
+
* to break token issuance).
|
|
74
|
+
*/
|
|
75
|
+
declare function stripReservedClaims(claims: Record<string, unknown> | undefined, context: string): Record<string, unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* Sign a JWT access token.
|
|
78
|
+
*/
|
|
79
|
+
declare function signAccessToken(claims: Omit<TokenClaims, 'iat' | 'exp' | 'aud'>, key: JwtKeyMaterial, expiresInSeconds: number, options?: SignAccessTokenOptions): Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Verify a JWT access token. Tries each key in order for rotation support.
|
|
82
|
+
*
|
|
83
|
+
* Pins `alg: HS256` and (when supplied) the expected `issuer`. jose already
|
|
84
|
+
* rejects `alg: none` by default; the explicit allowlist is defense in
|
|
85
|
+
* depth against future jose behavioural drift and against confused-deputy
|
|
86
|
+
* attacks if anyone later adds asymmetric key support.
|
|
87
|
+
*/
|
|
88
|
+
declare function verifyAccessToken(token: string, key: JwtKeyMaterial, options?: VerifyAccessTokenOptions): Promise<TokenClaims>;
|
|
89
|
+
|
|
90
|
+
export { type JwtKeyMaterial, RESERVED_JWT_CLAIMS, type SignAccessTokenOptions, type VerifyAccessTokenOptions, signAccessToken, stripReservedClaims, verifyAccessToken };
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { T as TokenClaims } from './types-et0R8tsC.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JWT signing and verification helpers built on `jose` (Web Crypto API).
|
|
5
|
+
*
|
|
6
|
+
* Provides {@link signAccessToken} and {@link verifyAccessToken} for short-lived
|
|
7
|
+
* access tokens. Refresh tokens are handled separately in
|
|
8
|
+
* `core/auth/refresh-token.ts`. Supports key rotation by accepting an array
|
|
9
|
+
* of keys — the first is used to sign, all are accepted on verify.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Key material accepted by {@link signAccessToken} / {@link verifyAccessToken}.
|
|
16
|
+
*
|
|
17
|
+
* Today this is just an HS256 shared secret (string) or a rotation array of
|
|
18
|
+
* shared secrets. The alias exists so the public signature is stable when we
|
|
19
|
+
* expand to asymmetric algorithms (RS256 / EdDSA) and JWKS-backed verification.
|
|
20
|
+
*
|
|
21
|
+
* Expected expansion:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* import type { CryptoKey, JWK, JWTVerifyGetKey, KeyObject } from 'jose';
|
|
25
|
+
*
|
|
26
|
+
* export type JwtStaticKey =
|
|
27
|
+
* | string // HS* shared secret (utf-8)
|
|
28
|
+
* | Uint8Array // HS* shared secret (raw bytes)
|
|
29
|
+
* | CryptoKey // Web Crypto key (any alg)
|
|
30
|
+
* | KeyObject // Node KeyObject (any alg)
|
|
31
|
+
* | JWK; // JSON Web Key
|
|
32
|
+
*
|
|
33
|
+
* export type JwtKeyMaterial = JwtStaticKey | readonly JwtStaticKey[];
|
|
34
|
+
*
|
|
35
|
+
* // Verification additionally accepts a resolver (e.g. createRemoteJWKSet):
|
|
36
|
+
* export type JwtVerifyKeyMaterial = JwtKeyMaterial | JWTVerifyGetKey;
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Until then, the runtime helpers ({@link normalizeKeys}) only know how to
|
|
40
|
+
* encode strings — widening the type without widening the helpers would be a
|
|
41
|
+
* lie, so do both together.
|
|
42
|
+
*/
|
|
43
|
+
type JwtKeyMaterial = string | string[];
|
|
44
|
+
/**
|
|
45
|
+
* JWT claim names fortress controls and that must never be overridable by
|
|
46
|
+
* caller-supplied `customClaims` or by plugin `enrichTokenClaims` output.
|
|
47
|
+
*
|
|
48
|
+
* Letting a caller set `act` would forge an impersonation marker; letting a
|
|
49
|
+
* caller set `sub`/`iss`/`exp` would let them mint a token for any subject
|
|
50
|
+
* or extend its lifetime. The denylist is enforced in {@link signAccessToken}
|
|
51
|
+
* — extra claims are dropped (with a `warn` log in dev) before the safe
|
|
52
|
+
* claims are spread.
|
|
53
|
+
*/
|
|
54
|
+
declare const RESERVED_JWT_CLAIMS: ReadonlySet<string>;
|
|
55
|
+
interface SignAccessTokenOptions {
|
|
56
|
+
/** Audience to include in the JWT `aud` claim. Omitted when unset. */
|
|
57
|
+
audience?: string | string[];
|
|
58
|
+
}
|
|
59
|
+
interface VerifyAccessTokenOptions {
|
|
60
|
+
/** Expected issuer. When set, jose also requires the `iss` claim to be present. */
|
|
61
|
+
issuer?: string | string[];
|
|
62
|
+
/** Expected audience. When set, jose also requires the `aud` claim to be present. */
|
|
63
|
+
audience?: string | string[];
|
|
64
|
+
/** Clock skew tolerance for `nbf`, `exp`, and max-token-age checks. */
|
|
65
|
+
clockTolerance?: string | number;
|
|
66
|
+
/** Additional claim names that must be present alongside Fortress's required access-token claims. */
|
|
67
|
+
requiredClaims?: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Strip any reserved keys from a custom-claims object. In development we
|
|
71
|
+
* log a warning so library users notice the collision; in production we
|
|
72
|
+
* silently drop (the safe default — a misbehaving plugin shouldn't be able
|
|
73
|
+
* to break token issuance).
|
|
74
|
+
*/
|
|
75
|
+
declare function stripReservedClaims(claims: Record<string, unknown> | undefined, context: string): Record<string, unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* Sign a JWT access token.
|
|
78
|
+
*/
|
|
79
|
+
declare function signAccessToken(claims: Omit<TokenClaims, 'iat' | 'exp' | 'aud'>, key: JwtKeyMaterial, expiresInSeconds: number, options?: SignAccessTokenOptions): Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Verify a JWT access token. Tries each key in order for rotation support.
|
|
82
|
+
*
|
|
83
|
+
* Pins `alg: HS256` and (when supplied) the expected `issuer`. jose already
|
|
84
|
+
* rejects `alg: none` by default; the explicit allowlist is defense in
|
|
85
|
+
* depth against future jose behavioural drift and against confused-deputy
|
|
86
|
+
* attacks if anyone later adds asymmetric key support.
|
|
87
|
+
*/
|
|
88
|
+
declare function verifyAccessToken(token: string, key: JwtKeyMaterial, options?: VerifyAccessTokenOptions): Promise<TokenClaims>;
|
|
89
|
+
|
|
90
|
+
export { type JwtKeyMaterial, RESERVED_JWT_CLAIMS, type SignAccessTokenOptions, type VerifyAccessTokenOptions, signAccessToken, stripReservedClaims, verifyAccessToken };
|
package/dist/jwt.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/core/auth/jwt.ts
|
|
2
|
+
import { jwtVerify, SignJWT } from "jose";
|
|
3
|
+
|
|
4
|
+
// src/core/errors.ts
|
|
5
|
+
var FortressError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
statusCode;
|
|
8
|
+
retryAfter;
|
|
9
|
+
details;
|
|
10
|
+
/** RFC 6749 §5.2 / §4.1.2.1 machine code. Set by `Errors.oauth()`. */
|
|
11
|
+
oauthError;
|
|
12
|
+
/** Human-readable description for the OAuth `error_description` field. */
|
|
13
|
+
oauthDescription;
|
|
14
|
+
/** Optional URI for the OAuth `error_uri` field (§5.2). */
|
|
15
|
+
oauthErrorUri;
|
|
16
|
+
constructor(code, message, statusCode, options) {
|
|
17
|
+
super(message, { cause: options?.cause });
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.retryAfter = options?.retryAfter;
|
|
21
|
+
this.details = options?.details;
|
|
22
|
+
this.oauthError = options?.oauthError;
|
|
23
|
+
this.oauthDescription = options?.oauthDescription;
|
|
24
|
+
this.oauthErrorUri = options?.oauthErrorUri;
|
|
25
|
+
}
|
|
26
|
+
toJSON() {
|
|
27
|
+
return {
|
|
28
|
+
code: this.code,
|
|
29
|
+
message: this.message,
|
|
30
|
+
statusCode: this.statusCode,
|
|
31
|
+
...this.details !== void 0 && { details: this.details }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var Errors = {
|
|
36
|
+
unauthorized: (message = "Unauthorized") => new FortressError("UNAUTHORIZED", message, 401),
|
|
37
|
+
tokenReuse: () => new FortressError("TOKEN_REUSE", "Token reuse detected", 401),
|
|
38
|
+
sessionIdleTimeout: () => new FortressError("SESSION_IDLE_TIMEOUT", "Session idle timeout exceeded", 401),
|
|
39
|
+
sessionAbsoluteTimeout: () => new FortressError("SESSION_ABSOLUTE_TIMEOUT", "Session absolute timeout exceeded", 401),
|
|
40
|
+
forbidden: (message = "Forbidden") => new FortressError("FORBIDDEN", message, 403),
|
|
41
|
+
badRequest: (message = "Bad request") => new FortressError("BAD_REQUEST", message, 400),
|
|
42
|
+
notFound: (message = "Not found") => new FortressError("NOT_FOUND", message, 404),
|
|
43
|
+
conflict: (message = "Conflict", options) => new FortressError("CONFLICT", message, 409, options),
|
|
44
|
+
unprocessable: (message = "Unprocessable entity", options) => new FortressError("UNPROCESSABLE_ENTITY", message, 422, options),
|
|
45
|
+
rateLimited: (retryAfter) => new FortressError("RATE_LIMITED", "Too many requests", 429, { retryAfter }),
|
|
46
|
+
database: (message = "Database error", cause) => new FortressError("DATABASE_ERROR", message, 500, { cause }),
|
|
47
|
+
serviceUnavailable: (message = "Service unavailable", options) => new FortressError("SERVICE_UNAVAILABLE", message, 503, options),
|
|
48
|
+
validationError: (issues) => new FortressError("VALIDATION_ERROR", "Validation failed", 422, { details: issues }),
|
|
49
|
+
/**
|
|
50
|
+
* RFC 6749 §5.2 / §4.1.2.1 OAuth error.
|
|
51
|
+
*
|
|
52
|
+
* The HTTP error mapper detects `oauthError` and emits the OAuth-spec
|
|
53
|
+
* JSON body `{ error, error_description, error_uri? }` instead of the
|
|
54
|
+
* default fortress `{ code, message }` shape, so strict OAuth clients
|
|
55
|
+
* (Moodle, openid-client, Spring Security, etc.) can switch behaviour
|
|
56
|
+
* on the machine-readable `error` field.
|
|
57
|
+
*
|
|
58
|
+
* Status is inferred from the OAuth code per the spec table:
|
|
59
|
+
* - `invalid_client` → 401
|
|
60
|
+
* - everything else → 400
|
|
61
|
+
*/
|
|
62
|
+
oauth: (error, description, options) => {
|
|
63
|
+
const status = options?.status ?? (error === "invalid_client" ? 401 : 400);
|
|
64
|
+
const code = status === 401 ? "UNAUTHORIZED" : "BAD_REQUEST";
|
|
65
|
+
return new FortressError(code, description ?? error, status, {
|
|
66
|
+
oauthError: error,
|
|
67
|
+
oauthDescription: description,
|
|
68
|
+
oauthErrorUri: options?.errorUri,
|
|
69
|
+
cause: options?.cause
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* Reconstruct a {@link FortressError} from a JSON error body emitted by
|
|
74
|
+
* `errorToResponse`. Used by the in-process `fortress.call.*` client to
|
|
75
|
+
* convert non-2xx responses into typed throws that mirror what a direct
|
|
76
|
+
* service-method call would produce.
|
|
77
|
+
*
|
|
78
|
+
* Maps by HTTP status when the body doesn't carry a recognizable
|
|
79
|
+
* `{ code, message }` payload, so network errors and opaque upstream
|
|
80
|
+
* failures still become structured `FortressError`s.
|
|
81
|
+
*/
|
|
82
|
+
fromHttpResponse: (status, body) => {
|
|
83
|
+
const payload = body && typeof body === "object" ? body : {};
|
|
84
|
+
const message = typeof payload.message === "string" ? payload.message : `HTTP ${status}`;
|
|
85
|
+
const code = typeof payload.code === "string" && isFortressErrorCode(payload.code) ? payload.code : statusToErrorCode(status);
|
|
86
|
+
return new FortressError(code, message, status, { details: payload.details });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
function isFortressErrorCode(code) {
|
|
90
|
+
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";
|
|
91
|
+
}
|
|
92
|
+
function statusToErrorCode(status) {
|
|
93
|
+
if (status === 401)
|
|
94
|
+
return "UNAUTHORIZED";
|
|
95
|
+
if (status === 403)
|
|
96
|
+
return "FORBIDDEN";
|
|
97
|
+
if (status === 404)
|
|
98
|
+
return "NOT_FOUND";
|
|
99
|
+
if (status === 409)
|
|
100
|
+
return "CONFLICT";
|
|
101
|
+
if (status === 422)
|
|
102
|
+
return "UNPROCESSABLE_ENTITY";
|
|
103
|
+
if (status === 429)
|
|
104
|
+
return "RATE_LIMITED";
|
|
105
|
+
if (status === 503)
|
|
106
|
+
return "SERVICE_UNAVAILABLE";
|
|
107
|
+
if (status >= 500)
|
|
108
|
+
return "DATABASE_ERROR";
|
|
109
|
+
return "BAD_REQUEST";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/core/auth/jwt.ts
|
|
113
|
+
var RESERVED_JWT_CLAIMS = /* @__PURE__ */ new Set([
|
|
114
|
+
"sub",
|
|
115
|
+
"iss",
|
|
116
|
+
"iat",
|
|
117
|
+
"exp",
|
|
118
|
+
"nbf",
|
|
119
|
+
"aud",
|
|
120
|
+
"jti",
|
|
121
|
+
"act",
|
|
122
|
+
"groups",
|
|
123
|
+
"subjectType",
|
|
124
|
+
"name"
|
|
125
|
+
]);
|
|
126
|
+
var REQUIRED_ACCESS_TOKEN_CLAIMS = ["sub", "subjectType", "name", "groups", "iss", "iat", "exp"];
|
|
127
|
+
var SUBJECT_TYPES = ["USER", "GROUP", "SERVICE_ACCOUNT"];
|
|
128
|
+
function stripReservedClaims(claims, context) {
|
|
129
|
+
if (!claims)
|
|
130
|
+
return {};
|
|
131
|
+
const out = {};
|
|
132
|
+
let stripped;
|
|
133
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
134
|
+
if (RESERVED_JWT_CLAIMS.has(key)) {
|
|
135
|
+
(stripped ??= []).push(key);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
out[key] = value;
|
|
139
|
+
}
|
|
140
|
+
if (stripped && typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
141
|
+
console.warn(`[fortress/jwt] dropping reserved claim(s) from ${context}: ${stripped.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
function encodeSecret(secret) {
|
|
146
|
+
return new TextEncoder().encode(secret);
|
|
147
|
+
}
|
|
148
|
+
function normalizeKeys(key) {
|
|
149
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
150
|
+
return keys.map(encodeSecret);
|
|
151
|
+
}
|
|
152
|
+
async function signAccessToken(claims, key, expiresInSeconds, options) {
|
|
153
|
+
const [signingKey] = normalizeKeys(key);
|
|
154
|
+
const safeCustom = stripReservedClaims(claims.customClaims, "signAccessToken.customClaims");
|
|
155
|
+
let jwt = new SignJWT({
|
|
156
|
+
name: claims.name,
|
|
157
|
+
subjectType: claims.subjectType,
|
|
158
|
+
groups: claims.groups,
|
|
159
|
+
...claims.act ? { act: claims.act } : {},
|
|
160
|
+
...safeCustom
|
|
161
|
+
}).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${expiresInSeconds}s`).setIssuer(claims.iss);
|
|
162
|
+
if (options?.audience !== void 0)
|
|
163
|
+
jwt = jwt.setAudience(options.audience);
|
|
164
|
+
return jwt.setSubject(claims.sub).sign(signingKey);
|
|
165
|
+
}
|
|
166
|
+
async function verifyAccessToken(token, key, options) {
|
|
167
|
+
const keys = normalizeKeys(key);
|
|
168
|
+
const requiredClaims = [.../* @__PURE__ */ new Set([...REQUIRED_ACCESS_TOKEN_CLAIMS, ...options?.requiredClaims ?? []])];
|
|
169
|
+
const verifyOptions = { algorithms: ["HS256"], requiredClaims };
|
|
170
|
+
if (options?.issuer)
|
|
171
|
+
verifyOptions.issuer = options.issuer;
|
|
172
|
+
if (options?.audience)
|
|
173
|
+
verifyOptions.audience = options.audience;
|
|
174
|
+
if (options?.clockTolerance !== void 0)
|
|
175
|
+
verifyOptions.clockTolerance = options.clockTolerance;
|
|
176
|
+
for (const candidate of keys) {
|
|
177
|
+
try {
|
|
178
|
+
const { payload } = await jwtVerify(token, candidate, verifyOptions);
|
|
179
|
+
const subjectType = payload.subjectType;
|
|
180
|
+
const groups = payload.groups;
|
|
181
|
+
const name = payload.name;
|
|
182
|
+
const issuer = payload.iss;
|
|
183
|
+
const audience = payload.aud;
|
|
184
|
+
const issuedAt = payload.iat;
|
|
185
|
+
const expiresAt = payload.exp;
|
|
186
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0)
|
|
187
|
+
throw new Error("Invalid sub claim");
|
|
188
|
+
if (typeof subjectType !== "string" || !SUBJECT_TYPES.includes(subjectType))
|
|
189
|
+
throw new Error("Invalid subjectType claim");
|
|
190
|
+
if (typeof name !== "string")
|
|
191
|
+
throw new Error("Invalid name claim");
|
|
192
|
+
if (!Array.isArray(groups) || !groups.every((group) => typeof group === "string"))
|
|
193
|
+
throw new Error("Invalid groups claim");
|
|
194
|
+
if (typeof issuer !== "string" || issuer.length === 0)
|
|
195
|
+
throw new Error("Invalid iss claim");
|
|
196
|
+
if (audience !== void 0 && (!Array.isArray(audience) || !audience.every((value) => typeof value === "string")) && typeof audience !== "string")
|
|
197
|
+
throw new Error("Invalid aud claim");
|
|
198
|
+
if (typeof issuedAt !== "number")
|
|
199
|
+
throw new Error("Invalid iat claim");
|
|
200
|
+
if (typeof expiresAt !== "number")
|
|
201
|
+
throw new Error("Invalid exp claim");
|
|
202
|
+
return {
|
|
203
|
+
sub: payload.sub,
|
|
204
|
+
subjectType,
|
|
205
|
+
name,
|
|
206
|
+
groups,
|
|
207
|
+
iss: issuer,
|
|
208
|
+
...audience !== void 0 ? { aud: audience } : {},
|
|
209
|
+
iat: issuedAt,
|
|
210
|
+
exp: expiresAt,
|
|
211
|
+
act: payload.act,
|
|
212
|
+
customClaims: Object.fromEntries(
|
|
213
|
+
Object.entries(payload).filter(([k]) => !RESERVED_JWT_CLAIMS.has(k))
|
|
214
|
+
)
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw Errors.unauthorized("Invalid or expired token");
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
RESERVED_JWT_CLAIMS,
|
|
224
|
+
signAccessToken,
|
|
225
|
+
stripReservedClaims,
|
|
226
|
+
verifyAccessToken
|
|
227
|
+
};
|