@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,148 @@
|
|
|
1
|
+
import type { authEndpoints } from '../auth/auth-endpoints';
|
|
2
|
+
import type { InferEndpointCallInput, InferEndpointSuccessResponse } from '../endpoint';
|
|
3
|
+
import type { Fortress } from '../fortress';
|
|
4
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
5
|
+
import { createTestAdapter } from '../../testing';
|
|
6
|
+
import { FortressError } from '../errors';
|
|
7
|
+
import { createFortress } from '../fortress';
|
|
8
|
+
import { assertSuccess } from '../types';
|
|
9
|
+
|
|
10
|
+
const SECRET = 'call-test-secret-at-least-32-bytes-long!!';
|
|
11
|
+
|
|
12
|
+
function makeFortress(): Fortress {
|
|
13
|
+
return createFortress({
|
|
14
|
+
jwt: { key: SECRET, issuer: 'call-test' },
|
|
15
|
+
database: createTestAdapter(),
|
|
16
|
+
}) as unknown as Fortress;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('fortress.call', () => {
|
|
20
|
+
describe('type inference', () => {
|
|
21
|
+
it('infers login input shape', () => {
|
|
22
|
+
// Compile-time only — verifies the generic flow.
|
|
23
|
+
expectTypeOf<InferEndpointCallInput<typeof authEndpoints.login>>().toEqualTypeOf<{
|
|
24
|
+
identifier: string;
|
|
25
|
+
password: string;
|
|
26
|
+
trustedDeviceToken?: string;
|
|
27
|
+
}>();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('infers revokeSession params shape', () => {
|
|
31
|
+
expectTypeOf<InferEndpointCallInput<typeof authEndpoints.revokeSession>>().toEqualTypeOf<{
|
|
32
|
+
id: string;
|
|
33
|
+
}>();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('infers login success response carries a tagged union', () => {
|
|
37
|
+
type R = InferEndpointSuccessResponse<typeof authEndpoints.login>;
|
|
38
|
+
// The AuthResult oneOf collapses to a discriminated union — exercise
|
|
39
|
+
// that at least one variant has an accessToken string.
|
|
40
|
+
expectTypeOf<R>().not.toBeNever();
|
|
41
|
+
expectTypeOf<R>().not.toBeUnknown();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('infers me endpoint takes no input', () => {
|
|
45
|
+
// `me` has no body/query/params, so the inferred call input collapses
|
|
46
|
+
// to the empty intersection — we assert it's not `never` and not `unknown`.
|
|
47
|
+
expectTypeOf<InferEndpointCallInput<typeof authEndpoints.me>>().not.toBeNever();
|
|
48
|
+
expectTypeOf<InferEndpointCallInput<typeof authEndpoints.me>>().not.toBeUnknown();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('runtime: happy path', () => {
|
|
53
|
+
it('creates a user via call.createUser and logs in via call.login', async () => {
|
|
54
|
+
const fortress = makeFortress();
|
|
55
|
+
|
|
56
|
+
// Direct service call to create the user — createUser over HTTP uses
|
|
57
|
+
// a different password-policy path than we want to exercise here.
|
|
58
|
+
await fortress.auth.createUser({
|
|
59
|
+
email: 'call@example.com',
|
|
60
|
+
name: 'Call Test',
|
|
61
|
+
password: 'password-123456',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = await (fortress.call as any).login({
|
|
65
|
+
identifier: 'call@example.com',
|
|
66
|
+
password: 'password-123456',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result).toBeDefined();
|
|
70
|
+
expect(typeof result.accessToken).toBe('string');
|
|
71
|
+
expect(typeof result.refreshToken).toBe('string');
|
|
72
|
+
expect(result.user.email).toBe('call@example.com');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('substitutes :id from a params input', async () => {
|
|
76
|
+
const fortress = makeFortress();
|
|
77
|
+
await fortress.auth.createUser({
|
|
78
|
+
email: 'sessions@example.com',
|
|
79
|
+
name: 'Sessions',
|
|
80
|
+
password: 'password-123456',
|
|
81
|
+
});
|
|
82
|
+
const loginResult = await fortress.auth.login(
|
|
83
|
+
'sessions@example.com',
|
|
84
|
+
'password-123456',
|
|
85
|
+
);
|
|
86
|
+
assertSuccess(loginResult);
|
|
87
|
+
const accessToken = loginResult.accessToken;
|
|
88
|
+
expect(typeof accessToken).toBe('string');
|
|
89
|
+
if (accessToken === null)
|
|
90
|
+
throw new Error('expected accessToken');
|
|
91
|
+
|
|
92
|
+
// Rotate so we have a sessions row to revoke. Then call.revokeSession
|
|
93
|
+
// using params substitution.
|
|
94
|
+
const sessions = await fortress.auth.listSessions(
|
|
95
|
+
(await fortress.auth.verifyToken(accessToken)).sub,
|
|
96
|
+
);
|
|
97
|
+
expect(sessions.length).toBeGreaterThan(0);
|
|
98
|
+
const sessionId = sessions[0].id;
|
|
99
|
+
|
|
100
|
+
// revokeSession requires bearer auth. Pass it via options.headers.
|
|
101
|
+
await expect(
|
|
102
|
+
(fortress.call as any).revokeSession(
|
|
103
|
+
{ id: sessionId },
|
|
104
|
+
{ headers: { authorization: `Bearer ${accessToken}` } },
|
|
105
|
+
),
|
|
106
|
+
).resolves.toEqual({ ok: true });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('runtime: error path', () => {
|
|
111
|
+
it('throws FortressError on non-2xx', async () => {
|
|
112
|
+
const fortress = makeFortress();
|
|
113
|
+
await fortress.auth.createUser({
|
|
114
|
+
email: 'bad@example.com',
|
|
115
|
+
name: 'Bad',
|
|
116
|
+
password: 'password-123456',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await expect((fortress.call as any).login({
|
|
120
|
+
identifier: 'bad@example.com',
|
|
121
|
+
password: 'wrong-password',
|
|
122
|
+
})).rejects.toBeInstanceOf(FortressError);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('preserves the error code from the JSON body', async () => {
|
|
126
|
+
const fortress = makeFortress();
|
|
127
|
+
await fortress.auth.createUser({
|
|
128
|
+
email: 'coded@example.com',
|
|
129
|
+
name: 'Coded',
|
|
130
|
+
password: 'password-123456',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await (fortress.call as any).login({
|
|
135
|
+
identifier: 'coded@example.com',
|
|
136
|
+
password: 'wrong',
|
|
137
|
+
});
|
|
138
|
+
expect.fail('expected call to throw');
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
expect(err).toBeInstanceOf(FortressError);
|
|
142
|
+
const fe = err as FortressError;
|
|
143
|
+
expect(fe.code).toBe('UNAUTHORIZED');
|
|
144
|
+
expect(fe.statusCode).toBe(401);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed in-process client for fortress endpoints.
|
|
3
|
+
*
|
|
4
|
+
* `buildCall` takes a flat map of `EndpointDefinition`s (keyed by handler
|
|
5
|
+
* name) and returns an object with one callable per entry. Each callable:
|
|
6
|
+
*
|
|
7
|
+
* 1. Splits the flat input object into `body`, `query`, and `params` based
|
|
8
|
+
* on the endpoint's declared schemas.
|
|
9
|
+
* 2. Substitutes `:param` placeholders in the path.
|
|
10
|
+
* 3. Serializes the query string.
|
|
11
|
+
* 4. Builds a web-standard `Request` with a JSON body (when applicable).
|
|
12
|
+
* 5. Delegates to `fortress.handleRequest`, exercising the full pipeline
|
|
13
|
+
* (plugin middleware → token verification → RBAC → validation →
|
|
14
|
+
* dispatch → cookie attachment).
|
|
15
|
+
* 6. Parses the JSON response body. On non-2xx, throws a structured
|
|
16
|
+
* `FortressError` reconstructed via `Errors.fromHttpResponse`.
|
|
17
|
+
*
|
|
18
|
+
* This is the foundation of the typed client SDK: the same code works
|
|
19
|
+
* in-process today and will work over the network tomorrow by swapping
|
|
20
|
+
* `handleRequest` for `fetch`. All of the type machinery lives on the
|
|
21
|
+
* `EndpointDefinition` generic parameters and the `InferEndpoint*` helpers
|
|
22
|
+
* in `src/core/endpoint.ts`.
|
|
23
|
+
*
|
|
24
|
+
* **Authentication.** Protected endpoints expect a bearer token. Callers
|
|
25
|
+
* pass it via the optional second argument: `call.me({}, { headers:
|
|
26
|
+
* { authorization: 'Bearer …' } })`. The in-process client does not share
|
|
27
|
+
* state with any active session; it is purely a typed wire formatter.
|
|
28
|
+
*
|
|
29
|
+
* **Runs the full pipeline.** Because each call lands in
|
|
30
|
+
* `fortress.handleRequest`, the following all fire: plugin middleware (incl.
|
|
31
|
+
* rate limits), principal resolution, RBAC, JSON Schema / Standard Schema
|
|
32
|
+
* validation, auth observers, IAM observers, OpenTelemetry spans, and any
|
|
33
|
+
* `wrapAdapter` hooks. This is intentional — it's the same code path a
|
|
34
|
+
* network client would hit, so tests that exercise `fortress.call.*` give
|
|
35
|
+
* the same behavioral guarantees as production traffic.
|
|
36
|
+
*
|
|
37
|
+
* **Bypassing hooks in tests.** If a test fixture needs to create users or
|
|
38
|
+
* log in *without* tripping rate limits, emitting audit entries, or running
|
|
39
|
+
* observers, call the service layer directly — `fortress.auth.createUser`,
|
|
40
|
+
* `fortress.auth.login`, `fortress.iam.createRole` — instead of the typed
|
|
41
|
+
* call surface. The service layer still validates inputs but skips the
|
|
42
|
+
* middleware chain.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import type { EndpointDefinition } from '../endpoint';
|
|
46
|
+
import type { Fortress } from '../fortress';
|
|
47
|
+
import { Errors } from '../errors';
|
|
48
|
+
|
|
49
|
+
/** Optional per-call options. */
|
|
50
|
+
export interface CallOptions {
|
|
51
|
+
/** Extra headers to attach to the synthesized request (e.g. bearer auth). */
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Extract the top-level keys of an input JSON Schema so input can be split into body/query/params. */
|
|
56
|
+
function schemaKeys(schema: unknown): Set<string> {
|
|
57
|
+
if (!schema || typeof schema !== 'object')
|
|
58
|
+
return new Set();
|
|
59
|
+
const props = (schema as { properties?: Record<string, unknown> }).properties;
|
|
60
|
+
if (!props)
|
|
61
|
+
return new Set();
|
|
62
|
+
return new Set(Object.keys(props));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build an object of typed callables from a keyed {@link EndpointDefinition}
|
|
67
|
+
* map. Each callable invokes `fortress.handleRequest` with a synthesized
|
|
68
|
+
* `Request` and returns the parsed JSON body on success.
|
|
69
|
+
*
|
|
70
|
+
* The returned type is intentionally loose (`Record<string, ...>`). The
|
|
71
|
+
* strong typing happens at the call site via the `TypedCall` helper in
|
|
72
|
+
* `src/core/fortress.ts`, which layers a mapped type over the runtime
|
|
73
|
+
* callable map to expose per-handler inferred I/O.
|
|
74
|
+
*/
|
|
75
|
+
export function buildCall(
|
|
76
|
+
fortress: Fortress,
|
|
77
|
+
endpoints: Record<string, EndpointDefinition>,
|
|
78
|
+
): Record<string, (input?: Record<string, unknown>, options?: CallOptions) => Promise<unknown>> {
|
|
79
|
+
const out: Record<string, (input?: Record<string, unknown>, options?: CallOptions) => Promise<unknown>> = {};
|
|
80
|
+
|
|
81
|
+
for (const [key, ep] of Object.entries(endpoints)) {
|
|
82
|
+
const bodyKeys = schemaKeys(ep.input?.body);
|
|
83
|
+
const queryKeys = schemaKeys(ep.input?.query);
|
|
84
|
+
const paramsKeys = schemaKeys(ep.input?.params);
|
|
85
|
+
|
|
86
|
+
out[key] = async (input: Record<string, unknown> = {}, options: CallOptions = {}): Promise<unknown> => {
|
|
87
|
+
// Split the flat input into body/query/params by schema membership.
|
|
88
|
+
// Params take priority (path substitution), then query, then body.
|
|
89
|
+
const body: Record<string, unknown> = {};
|
|
90
|
+
const query: Record<string, unknown> = {};
|
|
91
|
+
const params: Record<string, unknown> = {};
|
|
92
|
+
for (const [k, v] of Object.entries(input)) {
|
|
93
|
+
if (paramsKeys.has(k))
|
|
94
|
+
params[k] = v;
|
|
95
|
+
else if (queryKeys.has(k))
|
|
96
|
+
query[k] = v;
|
|
97
|
+
else if (bodyKeys.has(k))
|
|
98
|
+
body[k] = v;
|
|
99
|
+
else
|
|
100
|
+
// Unknown key — default to body so callers can pass extra fields
|
|
101
|
+
// without tripping the splitter. Server-side validation is the
|
|
102
|
+
// authority on accepted shapes.
|
|
103
|
+
body[k] = v;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Substitute `:param` placeholders in the path.
|
|
107
|
+
let path = ep.path;
|
|
108
|
+
for (const [k, v] of Object.entries(params)) {
|
|
109
|
+
path = path.replace(`:${k}`, encodeURIComponent(String(v)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Build URL with query string. `http://fortress.local` is a dummy
|
|
113
|
+
// origin — `handleRequest` only reads `url.pathname` + `searchParams`.
|
|
114
|
+
const url = new URL(`http://fortress.local${path}`);
|
|
115
|
+
for (const [k, v] of Object.entries(query)) {
|
|
116
|
+
if (v !== undefined && v !== null)
|
|
117
|
+
url.searchParams.set(k, String(v));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build request. GET/HEAD never carry a body; other methods get a
|
|
121
|
+
// JSON body when at least one body field was provided.
|
|
122
|
+
const headers: Record<string, string> = {
|
|
123
|
+
'content-type': 'application/json',
|
|
124
|
+
...options.headers,
|
|
125
|
+
};
|
|
126
|
+
const init: RequestInit = {
|
|
127
|
+
method: ep.method,
|
|
128
|
+
headers,
|
|
129
|
+
};
|
|
130
|
+
const methodHasBody = ep.method !== 'GET';
|
|
131
|
+
if (methodHasBody && Object.keys(body).length > 0) {
|
|
132
|
+
init.body = JSON.stringify(body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const res = await fortress.handleRequest(new Request(url, init));
|
|
136
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
137
|
+
const payload: unknown = contentType.includes('json')
|
|
138
|
+
? await res.clone().json().catch(() => null)
|
|
139
|
+
: await res.clone().text().catch(() => null);
|
|
140
|
+
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
throw Errors.fromHttpResponse(res.status, payload);
|
|
143
|
+
}
|
|
144
|
+
return payload;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveCookieConfig } from '../config';
|
|
3
|
+
import {
|
|
4
|
+
clearAuthCookies,
|
|
5
|
+
parseCookieHeader,
|
|
6
|
+
serializeAuthCookies,
|
|
7
|
+
serializeCookie,
|
|
8
|
+
} from './cookie-serialize';
|
|
9
|
+
|
|
10
|
+
describe('resolveCookieConfig', () => {
|
|
11
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
12
|
+
const setEnv = (val: string | undefined): void => {
|
|
13
|
+
if (val === undefined)
|
|
14
|
+
delete process.env.NODE_ENV;
|
|
15
|
+
else process.env.NODE_ENV = val;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
it('uses __Host- prefix and Secure when NODE_ENV=production', () => {
|
|
19
|
+
setEnv('production');
|
|
20
|
+
const c = resolveCookieConfig();
|
|
21
|
+
expect(c.accessName).toBe('__Host-fortress_access');
|
|
22
|
+
expect(c.refreshName).toBe('__Host-fortress_refresh');
|
|
23
|
+
expect(c.secure).toBe(true);
|
|
24
|
+
expect(c.sameSite).toBe('lax');
|
|
25
|
+
expect(c.path).toBe('/');
|
|
26
|
+
setEnv(originalNodeEnv);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('defaults Secure on outside production too (P3.7 — NODE_ENV not relied on)', () => {
|
|
30
|
+
setEnv('development');
|
|
31
|
+
const c = resolveCookieConfig();
|
|
32
|
+
expect(c.accessName).toBe('__Host-fortress_access');
|
|
33
|
+
expect(c.refreshName).toBe('__Host-fortress_refresh');
|
|
34
|
+
expect(c.secure).toBe(true);
|
|
35
|
+
setEnv(originalNodeEnv);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('opting out of Secure drops __Host- prefix (local HTTP dev escape hatch)', () => {
|
|
39
|
+
setEnv('development');
|
|
40
|
+
const c = resolveCookieConfig({ secure: false });
|
|
41
|
+
expect(c.accessName).toBe('fortress_access');
|
|
42
|
+
expect(c.refreshName).toBe('fortress_refresh');
|
|
43
|
+
expect(c.secure).toBe(false);
|
|
44
|
+
setEnv(originalNodeEnv);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('drops __Host- prefix when a Domain is set even in production', () => {
|
|
48
|
+
setEnv('production');
|
|
49
|
+
const c = resolveCookieConfig({ domain: 'example.com' });
|
|
50
|
+
expect(c.accessName).toBe('fortress_access');
|
|
51
|
+
expect(c.refreshName).toBe('fortress_refresh');
|
|
52
|
+
expect(c.secure).toBe(true);
|
|
53
|
+
expect(c.domain).toBe('example.com');
|
|
54
|
+
setEnv(originalNodeEnv);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('normalizes an empty domain to host-only cookies', () => {
|
|
58
|
+
const c = resolveCookieConfig({ domain: '' });
|
|
59
|
+
expect(c.domain).toBeUndefined();
|
|
60
|
+
expect(c.accessName).toBe('__Host-fortress_access');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('drops __Host- prefix when path is not /', () => {
|
|
64
|
+
setEnv('production');
|
|
65
|
+
const c = resolveCookieConfig({ path: '/api' });
|
|
66
|
+
expect(c.accessName).toBe('fortress_access');
|
|
67
|
+
setEnv(originalNodeEnv);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('honors caller-provided overrides', () => {
|
|
71
|
+
setEnv('production');
|
|
72
|
+
const c = resolveCookieConfig({
|
|
73
|
+
accessName: 'my_access',
|
|
74
|
+
refreshName: 'my_refresh',
|
|
75
|
+
sameSite: 'strict',
|
|
76
|
+
secure: false,
|
|
77
|
+
});
|
|
78
|
+
expect(c.accessName).toBe('my_access');
|
|
79
|
+
expect(c.refreshName).toBe('my_refresh');
|
|
80
|
+
expect(c.sameSite).toBe('strict');
|
|
81
|
+
expect(c.secure).toBe(false);
|
|
82
|
+
setEnv(originalNodeEnv);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects SameSite=None without Secure', () => {
|
|
86
|
+
expect(() => resolveCookieConfig({ sameSite: 'none', secure: false }))
|
|
87
|
+
.toThrow('SameSite=None requires Secure');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects invalid caller-supplied cookie prefixes', () => {
|
|
91
|
+
expect(() => resolveCookieConfig({ accessName: '__Secure-access', secure: false }))
|
|
92
|
+
.toThrow('__Secure- prefix');
|
|
93
|
+
expect(() => resolveCookieConfig({ refreshName: '__Host-refresh', domain: 'example.com' }))
|
|
94
|
+
.toThrow('__Host- prefix');
|
|
95
|
+
expect(() => resolveCookieConfig({ refreshName: '__Host-refresh', path: '/auth' }))
|
|
96
|
+
.toThrow('__Host- prefix');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('serializeCookie', () => {
|
|
101
|
+
it('produces a Set-Cookie value with all attributes', () => {
|
|
102
|
+
const cookies = resolveCookieConfig({ secure: true });
|
|
103
|
+
const out = serializeCookie('test', 'value', cookies, 60);
|
|
104
|
+
expect(out).toContain('test=value');
|
|
105
|
+
expect(out).toContain('Path=/');
|
|
106
|
+
expect(out).toContain('HttpOnly');
|
|
107
|
+
expect(out).toContain('Secure');
|
|
108
|
+
expect(out).toContain('SameSite=Lax');
|
|
109
|
+
expect(out).toContain('Max-Age=60');
|
|
110
|
+
expect(out).toContain('Expires=');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('omits Secure when not set', () => {
|
|
114
|
+
const cookies = resolveCookieConfig({ secure: false });
|
|
115
|
+
const out = serializeCookie('test', 'value', cookies);
|
|
116
|
+
expect(out).not.toContain('Secure');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('url-encodes the value', () => {
|
|
120
|
+
const cookies = resolveCookieConfig({ secure: false });
|
|
121
|
+
const out = serializeCookie('test', 'a b/c', cookies);
|
|
122
|
+
expect(out).toContain('test=a%20b%2Fc');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('omits Max-Age/Expires when no expiry given', () => {
|
|
126
|
+
const cookies = resolveCookieConfig({ secure: false });
|
|
127
|
+
const out = serializeCookie('test', 'value', cookies);
|
|
128
|
+
expect(out).not.toContain('Max-Age');
|
|
129
|
+
expect(out).not.toContain('Expires');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('serializeAuthCookies', () => {
|
|
134
|
+
it('emits one Set-Cookie value when only accessToken is present', () => {
|
|
135
|
+
const cookies = resolveCookieConfig({ secure: false });
|
|
136
|
+
const out = serializeAuthCookies({ accessToken: 'a' }, cookies);
|
|
137
|
+
expect(out).toHaveLength(1);
|
|
138
|
+
expect(out[0]).toContain('fortress_access=a');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('emits two values when refreshToken is also present', () => {
|
|
142
|
+
const cookies = resolveCookieConfig({ secure: false });
|
|
143
|
+
const out = serializeAuthCookies(
|
|
144
|
+
{ accessToken: 'a', refreshToken: 'r' },
|
|
145
|
+
cookies,
|
|
146
|
+
{ access: 60, refresh: 120 },
|
|
147
|
+
);
|
|
148
|
+
expect(out).toHaveLength(2);
|
|
149
|
+
expect(out[0]).toContain('fortress_access=a');
|
|
150
|
+
expect(out[0]).toContain('Max-Age=60');
|
|
151
|
+
expect(out[1]).toContain('fortress_refresh=r');
|
|
152
|
+
expect(out[1]).toContain('Max-Age=120');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('omits the refresh cookie when refreshToken is null', () => {
|
|
156
|
+
const cookies = resolveCookieConfig({ secure: false });
|
|
157
|
+
const out = serializeAuthCookies(
|
|
158
|
+
{ accessToken: 'a', refreshToken: null },
|
|
159
|
+
cookies,
|
|
160
|
+
);
|
|
161
|
+
expect(out).toHaveLength(1);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('clearAuthCookies', () => {
|
|
166
|
+
it('produces immediately-expiring Set-Cookie values for both names', () => {
|
|
167
|
+
const cookies = resolveCookieConfig({ secure: false });
|
|
168
|
+
const out = clearAuthCookies(cookies);
|
|
169
|
+
expect(out).toHaveLength(2);
|
|
170
|
+
expect(out[0]).toContain('fortress_access=');
|
|
171
|
+
expect(out[0]).toContain('Max-Age=0');
|
|
172
|
+
expect(out[1]).toContain('fortress_refresh=');
|
|
173
|
+
expect(out[1]).toContain('Max-Age=0');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('parseCookieHeader', () => {
|
|
178
|
+
it('returns {} for null/empty', () => {
|
|
179
|
+
expect(parseCookieHeader(null)).toEqual({});
|
|
180
|
+
expect(parseCookieHeader('')).toEqual({});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('parses a single cookie', () => {
|
|
184
|
+
expect(parseCookieHeader('a=b')).toEqual({ a: 'b' });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('parses multiple cookies', () => {
|
|
188
|
+
expect(parseCookieHeader('a=b; c=d')).toEqual({ a: 'b', c: 'd' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('decodes URL-encoded values', () => {
|
|
192
|
+
expect(parseCookieHeader('a=b%20c')).toEqual({ a: 'b c' });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('skips malformed segments', () => {
|
|
196
|
+
expect(parseCookieHeader('a=b; nokey; c=d')).toEqual({ a: 'b', c: 'd' });
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie serialization for fortress auth tokens.
|
|
3
|
+
*
|
|
4
|
+
* Produces and parses cookie strings for the access/refresh token pair using
|
|
5
|
+
* the resolved {@link ResolvedCookieConfig} (which already accounts for
|
|
6
|
+
* `__Host-` prefix rules and dev-vs-prod relaxation).
|
|
7
|
+
*
|
|
8
|
+
* Returns raw `Set-Cookie` header values so any framework adapter can append
|
|
9
|
+
* them to a `Response` directly without depending on a framework cookie API.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ResolvedCookieConfig } from '../config';
|
|
13
|
+
|
|
14
|
+
/** Result of an auth flow that may set cookies on a Response. */
|
|
15
|
+
export interface AuthCookiePayload {
|
|
16
|
+
accessToken: string;
|
|
17
|
+
refreshToken?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the `Set-Cookie` header values for an access + refresh token pair.
|
|
22
|
+
* Returns one or two strings depending on whether `refreshToken` is present.
|
|
23
|
+
*/
|
|
24
|
+
export function serializeAuthCookies(
|
|
25
|
+
payload: AuthCookiePayload,
|
|
26
|
+
cookies: ResolvedCookieConfig,
|
|
27
|
+
expirySeconds: { access?: number; refresh?: number } = {},
|
|
28
|
+
): string[] {
|
|
29
|
+
const out: string[] = [];
|
|
30
|
+
out.push(serializeCookie(cookies.accessName, payload.accessToken, cookies, expirySeconds.access));
|
|
31
|
+
if (payload.refreshToken) {
|
|
32
|
+
out.push(serializeCookie(cookies.refreshName, payload.refreshToken, cookies, expirySeconds.refresh));
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build expired `Set-Cookie` header values that clear both auth cookies.
|
|
39
|
+
* Used by logout flows.
|
|
40
|
+
*/
|
|
41
|
+
export function clearAuthCookies(cookies: ResolvedCookieConfig): string[] {
|
|
42
|
+
return [
|
|
43
|
+
serializeCookie(cookies.accessName, '', cookies, 0),
|
|
44
|
+
serializeCookie(cookies.refreshName, '', cookies, 0),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Serialize a single cookie attribute set into a `Set-Cookie` header value.
|
|
50
|
+
*
|
|
51
|
+
* `maxAgeSeconds` of 0 produces an immediately-expiring cookie (used for
|
|
52
|
+
* `clearAuthCookies`). Omit it to set a session cookie. Otherwise the cookie
|
|
53
|
+
* gets both `Max-Age` and `Expires` for broad client compatibility.
|
|
54
|
+
*/
|
|
55
|
+
export function serializeCookie(
|
|
56
|
+
name: string,
|
|
57
|
+
value: string,
|
|
58
|
+
cookies: ResolvedCookieConfig,
|
|
59
|
+
maxAgeSeconds?: number,
|
|
60
|
+
): string {
|
|
61
|
+
const parts: string[] = [`${name}=${encodeURIComponent(value)}`];
|
|
62
|
+
parts.push(`Path=${cookies.path}`);
|
|
63
|
+
if (cookies.domain) {
|
|
64
|
+
parts.push(`Domain=${cookies.domain}`);
|
|
65
|
+
}
|
|
66
|
+
parts.push('HttpOnly');
|
|
67
|
+
if (cookies.secure) {
|
|
68
|
+
parts.push('Secure');
|
|
69
|
+
}
|
|
70
|
+
parts.push(`SameSite=${capitalize(cookies.sameSite)}`);
|
|
71
|
+
if (maxAgeSeconds !== undefined) {
|
|
72
|
+
parts.push(`Max-Age=${maxAgeSeconds}`);
|
|
73
|
+
const expires = new Date(Date.now() + maxAgeSeconds * 1000);
|
|
74
|
+
parts.push(`Expires=${expires.toUTCString()}`);
|
|
75
|
+
}
|
|
76
|
+
return parts.join('; ');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse a `Cookie` request header into a name → value map. Returns an empty
|
|
81
|
+
* object if the header is missing or malformed. Values are URL-decoded.
|
|
82
|
+
*/
|
|
83
|
+
export function parseCookieHeader(header: string | null | undefined): Record<string, string> {
|
|
84
|
+
if (!header)
|
|
85
|
+
return {};
|
|
86
|
+
const out: Record<string, string> = {};
|
|
87
|
+
for (const part of header.split(';')) {
|
|
88
|
+
const eq = part.indexOf('=');
|
|
89
|
+
if (eq === -1)
|
|
90
|
+
continue;
|
|
91
|
+
const name = part.slice(0, eq).trim();
|
|
92
|
+
if (!name)
|
|
93
|
+
continue;
|
|
94
|
+
const raw = part.slice(eq + 1).trim();
|
|
95
|
+
try {
|
|
96
|
+
out[name] = decodeURIComponent(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
out[name] = raw;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function capitalize(s: string): string {
|
|
106
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
107
|
+
}
|