@udibo/oauth2 0.1.0
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/LICENSE +20 -0
- package/README.md +174 -0
- package/SECURITY.md +77 -0
- package/esm/_dnt.polyfills.d.ts +17 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/adapters/hono/_common.d.ts +66 -0
- package/esm/adapters/hono/_common.js +108 -0
- package/esm/adapters/hono/authorization-server.d.ts +211 -0
- package/esm/adapters/hono/authorization-server.js +214 -0
- package/esm/adapters/hono/bff/auth-request-store.d.ts +164 -0
- package/esm/adapters/hono/bff/auth-request-store.js +154 -0
- package/esm/adapters/hono/bff/bff.d.ts +852 -0
- package/esm/adapters/hono/bff/bff.js +1194 -0
- package/esm/adapters/hono/bff/cookie-prefix.d.ts +72 -0
- package/esm/adapters/hono/bff/cookie-prefix.js +85 -0
- package/esm/adapters/hono/bff/mod.d.ts +19 -0
- package/esm/adapters/hono/bff/mod.js +19 -0
- package/esm/adapters/hono/bff/proxy.d.ts +108 -0
- package/esm/adapters/hono/bff/proxy.js +314 -0
- package/esm/adapters/hono/bff/session-store.d.ts +250 -0
- package/esm/adapters/hono/bff/session-store.js +216 -0
- package/esm/adapters/hono/bff/testing.d.ts +165 -0
- package/esm/adapters/hono/bff/testing.js +254 -0
- package/esm/adapters/hono/identity.d.ts +117 -0
- package/esm/adapters/hono/identity.js +215 -0
- package/esm/adapters/hono/log.d.ts +49 -0
- package/esm/adapters/hono/log.js +66 -0
- package/esm/adapters/hono/resource-server.d.ts +101 -0
- package/esm/adapters/hono/resource-server.js +108 -0
- package/esm/client/_http.d.ts +91 -0
- package/esm/client/_http.js +303 -0
- package/esm/client/base.d.ts +184 -0
- package/esm/client/base.js +93 -0
- package/esm/client/bff-client.d.ts +235 -0
- package/esm/client/bff-client.js +280 -0
- package/esm/client/browser-storage.d.ts +152 -0
- package/esm/client/browser-storage.js +246 -0
- package/esm/client/check.d.ts +114 -0
- package/esm/client/check.js +97 -0
- package/esm/client/direct-client.d.ts +707 -0
- package/esm/client/direct-client.js +1237 -0
- package/esm/client/discovery-cache.d.ts +138 -0
- package/esm/client/discovery-cache.js +124 -0
- package/esm/client/events.d.ts +54 -0
- package/esm/client/events.js +37 -0
- package/esm/client/mod.d.ts +35 -0
- package/esm/client/mod.js +33 -0
- package/esm/client/storage.d.ts +127 -0
- package/esm/client/storage.js +81 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.js +47 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.js +65 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts +16 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assert.js +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts +25 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.js +30 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts +24 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equal.js +193 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts +36 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/equals.js +61 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/exists.js +27 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/fail.js +20 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts +18 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/false.js +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts +22 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.js +52 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts +24 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.js +53 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts +18 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less.js +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/match.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts +43 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/mod.js +45 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.js +39 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts +19 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.js +29 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts +22 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.js +32 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts +28 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.js +177 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts +41 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.js +53 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts +23 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.js +58 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts +17 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.js +26 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts +44 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/throws.js +44 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.js +20 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts +14 -0
- package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.js +20 -0
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.d.ts +44 -0
- package/esm/deps/jsr.io/@std/async/1.5.0/delay.js +84 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.d.ts +39 -0
- package/esm/deps/jsr.io/@std/crypto/1.1.0/timing_safe_equal.js +61 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.d.ts +22 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common16.js +51 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.d.ts +34 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common32.js +192 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.d.ts +34 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common64.js +113 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.d.ts +3 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_common_detach.js +13 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.d.ts +8 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_types.js +2 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.d.ts +1 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/_validate_binary_like.js +26 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.d.ts +60 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/ascii85.js +152 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base32.js +87 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base58.js +131 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64.js +82 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.d.ts +39 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/base64url.js +72 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.d.ts +38 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/hex.js +87 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.d.ts +97 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/mod.js +99 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.d.ts +119 -0
- package/esm/deps/jsr.io/@std/encoding/1.0.11/varint.js +209 -0
- package/esm/deps/jsr.io/@std/http/1.1.3/status.d.ts +343 -0
- package/esm/deps/jsr.io/@std/http/1.1.3/status.js +343 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.d.ts +159 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/assertion_state.js +223 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.d.ts +79 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/build_message.js +116 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.d.ts +139 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff.js +277 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.d.ts +98 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/diff_str.js +181 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.d.ts +29 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/format.js +68 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.d.ts +158 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/styles.js +206 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.d.ts +24 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/types.js +3 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.d.ts +91 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/_test_suite.js +347 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.d.ts +382 -0
- package/esm/deps/jsr.io/@std/testing/1.0.20/bdd.js +1110 -0
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.d.ts +363 -0
- package/esm/deps/jsr.io/@udibo/http-error/0.11.1/mod.js +616 -0
- package/esm/errors.d.ts +203 -0
- package/esm/errors.js +287 -0
- package/esm/identity/captcha.d.ts +147 -0
- package/esm/identity/captcha.js +64 -0
- package/esm/identity/delivery.d.ts +96 -0
- package/esm/identity/delivery.js +41 -0
- package/esm/identity/errors.d.ts +39 -0
- package/esm/identity/errors.js +46 -0
- package/esm/identity/events.d.ts +242 -0
- package/esm/identity/events.js +39 -0
- package/esm/identity/external/_shared.d.ts +86 -0
- package/esm/identity/external/_shared.js +107 -0
- package/esm/identity/external/_token-exchange.d.ts +61 -0
- package/esm/identity/external/_token-exchange.js +179 -0
- package/esm/identity/external/apple-client-secret.d.ts +82 -0
- package/esm/identity/external/apple-client-secret.js +133 -0
- package/esm/identity/external/apple.d.ts +64 -0
- package/esm/identity/external/apple.js +248 -0
- package/esm/identity/external/discord.d.ts +38 -0
- package/esm/identity/external/discord.js +62 -0
- package/esm/identity/external/errors.d.ts +48 -0
- package/esm/identity/external/errors.js +36 -0
- package/esm/identity/external/flow.d.ts +107 -0
- package/esm/identity/external/flow.js +158 -0
- package/esm/identity/external/github.d.ts +42 -0
- package/esm/identity/external/github.js +164 -0
- package/esm/identity/external/google.d.ts +52 -0
- package/esm/identity/external/google.js +38 -0
- package/esm/identity/external/mod.d.ts +63 -0
- package/esm/identity/external/mod.js +61 -0
- package/esm/identity/external/oauth2.d.ts +110 -0
- package/esm/identity/external/oauth2.js +158 -0
- package/esm/identity/external/oidc.d.ts +81 -0
- package/esm/identity/external/oidc.js +250 -0
- package/esm/identity/external/provider.d.ts +132 -0
- package/esm/identity/external/provider.js +12 -0
- package/esm/identity/hibp.d.ts +67 -0
- package/esm/identity/hibp.js +87 -0
- package/esm/identity/identifier.d.ts +44 -0
- package/esm/identity/identifier.js +47 -0
- package/esm/identity/lockout.d.ts +155 -0
- package/esm/identity/lockout.js +117 -0
- package/esm/identity/mfa/mod.d.ts +42 -0
- package/esm/identity/mfa/mod.js +42 -0
- package/esm/identity/mfa/recovery-codes.d.ts +96 -0
- package/esm/identity/mfa/recovery-codes.js +80 -0
- package/esm/identity/mfa/service.d.ts +338 -0
- package/esm/identity/mfa/service.js +351 -0
- package/esm/identity/mfa/totp.d.ts +186 -0
- package/esm/identity/mfa/totp.js +194 -0
- package/esm/identity/migration.d.ts +155 -0
- package/esm/identity/migration.js +243 -0
- package/esm/identity/mod.d.ts +65 -0
- package/esm/identity/mod.js +64 -0
- package/esm/identity/otp.d.ts +255 -0
- package/esm/identity/otp.js +245 -0
- package/esm/identity/password-policy.d.ts +45 -0
- package/esm/identity/password-policy.js +60 -0
- package/esm/identity/password.d.ts +171 -0
- package/esm/identity/password.js +160 -0
- package/esm/identity/rate-limit.d.ts +192 -0
- package/esm/identity/rate-limit.js +168 -0
- package/esm/identity/service.d.ts +579 -0
- package/esm/identity/service.js +835 -0
- package/esm/identity/session.d.ts +122 -0
- package/esm/identity/session.js +48 -0
- package/esm/identity/token-flow.d.ts +214 -0
- package/esm/identity/token-flow.js +173 -0
- package/esm/models/authentication.d.ts +24 -0
- package/esm/models/authentication.js +1 -0
- package/esm/models/authorization-code.d.ts +63 -0
- package/esm/models/authorization-code.js +1 -0
- package/esm/models/authorization.d.ts +165 -0
- package/esm/models/authorization.js +160 -0
- package/esm/models/client.d.ts +78 -0
- package/esm/models/client.js +1 -0
- package/esm/models/device-authorization.d.ts +30 -0
- package/esm/models/device-authorization.js +1 -0
- package/esm/models/responses.d.ts +93 -0
- package/esm/models/responses.js +1 -0
- package/esm/models/scope.d.ts +81 -0
- package/esm/models/scope.js +125 -0
- package/esm/models/token.d.ts +135 -0
- package/esm/models/token.js +1 -0
- package/esm/package.json +3 -0
- package/esm/react/callback.d.ts +52 -0
- package/esm/react/callback.js +71 -0
- package/esm/react/components/class-names.d.ts +91 -0
- package/esm/react/components/class-names.js +118 -0
- package/esm/react/components/internal.d.ts +62 -0
- package/esm/react/components/internal.js +102 -0
- package/esm/react/components/mfa-challenge-form.d.ts +66 -0
- package/esm/react/components/mfa-challenge-form.js +46 -0
- package/esm/react/components/mfa-enrollment-form.d.ts +83 -0
- package/esm/react/components/mfa-enrollment-form.js +41 -0
- package/esm/react/components/mod.d.ts +103 -0
- package/esm/react/components/mod.js +93 -0
- package/esm/react/components/request-password-reset-form.d.ts +61 -0
- package/esm/react/components/request-password-reset-form.js +41 -0
- package/esm/react/components/reset-password-form.d.ts +66 -0
- package/esm/react/components/reset-password-form.js +56 -0
- package/esm/react/components/sign-in-form.d.ts +74 -0
- package/esm/react/components/sign-in-form.js +43 -0
- package/esm/react/components/sign-up-form.d.ts +85 -0
- package/esm/react/components/sign-up-form.js +57 -0
- package/esm/react/components/types.d.ts +208 -0
- package/esm/react/components/types.js +14 -0
- package/esm/react/components/use-auth-form.d.ts +118 -0
- package/esm/react/components/use-auth-form.js +172 -0
- package/esm/react/components/user-menu.d.ts +79 -0
- package/esm/react/components/user-menu.js +73 -0
- package/esm/react/context.d.ts +51 -0
- package/esm/react/context.js +16 -0
- package/esm/react/mod.d.ts +26 -0
- package/esm/react/mod.js +26 -0
- package/esm/react/provider.d.ts +40 -0
- package/esm/react/provider.js +189 -0
- package/esm/react/require-auth.d.ts +31 -0
- package/esm/react/require-auth.js +37 -0
- package/esm/react/testing.d.ts +322 -0
- package/esm/react/testing.js +461 -0
- package/esm/react/use-authorization.d.ts +31 -0
- package/esm/react/use-authorization.js +36 -0
- package/esm/react/use-oauth2.d.ts +112 -0
- package/esm/react/use-oauth2.js +127 -0
- package/esm/server/authorization-server.d.ts +736 -0
- package/esm/server/authorization-server.js +1304 -0
- package/esm/server/client-authentication.d.ts +28 -0
- package/esm/server/client-authentication.js +51 -0
- package/esm/server/grants/authorization-code.d.ts +179 -0
- package/esm/server/grants/authorization-code.js +232 -0
- package/esm/server/grants/client-credentials.d.ts +52 -0
- package/esm/server/grants/client-credentials.js +53 -0
- package/esm/server/grants/device-authorization.d.ts +103 -0
- package/esm/server/grants/device-authorization.js +156 -0
- package/esm/server/grants/grant.d.ts +165 -0
- package/esm/server/grants/grant.js +173 -0
- package/esm/server/grants/password.d.ts +80 -0
- package/esm/server/grants/password.js +92 -0
- package/esm/server/grants/refresh-token.d.ts +80 -0
- package/esm/server/grants/refresh-token.js +136 -0
- package/esm/server/introspection-token-reader.d.ts +153 -0
- package/esm/server/introspection-token-reader.js +164 -0
- package/esm/server/jwks-token-reader.d.ts +263 -0
- package/esm/server/jwks-token-reader.js +508 -0
- package/esm/server/mod.d.ts +23 -0
- package/esm/server/mod.js +15 -0
- package/esm/server/public-suffix/mod.d.ts +62 -0
- package/esm/server/public-suffix/mod.js +103 -0
- package/esm/server/public-suffix/rules.d.ts +23 -0
- package/esm/server/public-suffix/rules.js +23 -0
- package/esm/server/redirect-uri.d.ts +187 -0
- package/esm/server/redirect-uri.js +331 -0
- package/esm/server/resource-server.d.ts +286 -0
- package/esm/server/resource-server.js +365 -0
- package/esm/server/services/authorization-code.d.ts +84 -0
- package/esm/server/services/authorization-code.js +35 -0
- package/esm/server/services/client.d.ts +35 -0
- package/esm/server/services/client.js +1 -0
- package/esm/server/services/device-authorization.d.ts +134 -0
- package/esm/server/services/device-authorization.js +7 -0
- package/esm/server/services/token.d.ts +242 -0
- package/esm/server/services/token.js +78 -0
- package/esm/server/services/user.d.ts +29 -0
- package/esm/server/services/user.js +7 -0
- package/esm/server/signing-keys.d.ts +201 -0
- package/esm/server/signing-keys.js +225 -0
- package/esm/server/utils/hash.d.ts +6 -0
- package/esm/server/utils/hash.js +11 -0
- package/esm/testing/contract/authorization-code.d.ts +37 -0
- package/esm/testing/contract/authorization-code.js +93 -0
- package/esm/testing/contract/client.d.ts +85 -0
- package/esm/testing/contract/client.js +126 -0
- package/esm/testing/contract/device-authorization.d.ts +37 -0
- package/esm/testing/contract/device-authorization.js +193 -0
- package/esm/testing/contract/lockout-store.d.ts +47 -0
- package/esm/testing/contract/lockout-store.js +122 -0
- package/esm/testing/contract/mfa-store.d.ts +50 -0
- package/esm/testing/contract/mfa-store.js +226 -0
- package/esm/testing/contract/mod.d.ts +39 -0
- package/esm/testing/contract/mod.js +39 -0
- package/esm/testing/contract/otp-store.d.ts +48 -0
- package/esm/testing/contract/otp-store.js +197 -0
- package/esm/testing/contract/rate-limit-store.d.ts +52 -0
- package/esm/testing/contract/rate-limit-store.js +105 -0
- package/esm/testing/contract/token-flow-store.d.ts +54 -0
- package/esm/testing/contract/token-flow-store.js +179 -0
- package/esm/testing/contract/token-reader.d.ts +77 -0
- package/esm/testing/contract/token-reader.js +78 -0
- package/esm/testing/contract/token.d.ts +61 -0
- package/esm/testing/contract/token.js +287 -0
- package/esm/testing/contract/user.d.ts +63 -0
- package/esm/testing/contract/user.js +75 -0
- package/esm/testing/mod.d.ts +34 -0
- package/esm/testing/mod.js +34 -0
- package/esm/testing/server.d.ts +197 -0
- package/esm/testing/server.js +193 -0
- package/esm/testing/services.d.ts +373 -0
- package/esm/testing/services.js +703 -0
- package/esm/utils/_buffer.d.ts +10 -0
- package/esm/utils/_buffer.js +12 -0
- package/esm/utils/_timing-safe.d.ts +13 -0
- package/esm/utils/_timing-safe.js +22 -0
- package/esm/utils/authentication-context.d.ts +3 -0
- package/esm/utils/authentication-context.js +14 -0
- package/esm/utils/basic-auth.d.ts +50 -0
- package/esm/utils/basic-auth.js +106 -0
- package/esm/utils/crypto.d.ts +97 -0
- package/esm/utils/crypto.js +162 -0
- package/esm/utils/pkce.d.ts +151 -0
- package/esm/utils/pkce.js +179 -0
- package/esm/utils/text.d.ts +24 -0
- package/esm/utils/text.js +28 -0
- package/esm/utils/url.d.ts +66 -0
- package/esm/utils/url.js +90 -0
- package/package.json +92 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `__Host-` / `__Secure-` cookie-name resolution shared by the BFF's cookies,
|
|
3
|
+
* plus the attribute combinations a browser refuses outright.
|
|
4
|
+
*
|
|
5
|
+
* A browser honors those prefixes only when the cookie's own attributes satisfy
|
|
6
|
+
* them, and silently drops the cookie otherwise — as it does for
|
|
7
|
+
* `SameSite=None` without `Secure`. So the default name and the validation of
|
|
8
|
+
* an explicit name both have to be derived from the same attributes — kept here
|
|
9
|
+
* so the session cookie and the pending-authorization cookie cannot drift
|
|
10
|
+
* apart, and so a dropped-cookie configuration fails loudly at construction
|
|
11
|
+
* instead of as an inexplicably broken session.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/** Cookie attributes a browser validates a cookie's name and delivery against. */
|
|
16
|
+
export interface PrefixConstrainedAttributes {
|
|
17
|
+
/** Whether the cookie is marked `Secure`. */
|
|
18
|
+
secure: boolean;
|
|
19
|
+
/** The cookie's `Path`. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** The cookie's `Domain`, or `undefined` when the cookie is host-bound. */
|
|
22
|
+
domain: string | undefined;
|
|
23
|
+
/** The cookie's `SameSite`, or `undefined` when the caller left it to the emitter. */
|
|
24
|
+
sameSite: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** The subset of a cookie-options object the prefixes constrain. */
|
|
27
|
+
export interface PrefixConstrainedOptions {
|
|
28
|
+
/** Marks the cookie `Secure`. Defaults to `true`. */
|
|
29
|
+
secure?: boolean;
|
|
30
|
+
/** Cookie path. Defaults to `"/"`. */
|
|
31
|
+
path?: string;
|
|
32
|
+
/** Cookie domain. Omitted by default (host-bound). */
|
|
33
|
+
domain?: string;
|
|
34
|
+
/** Cookie `SameSite`. Left to the emitting cookie when unset. */
|
|
35
|
+
sameSite?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Apply the attribute defaults every BFF cookie shares — `Secure`, `Path=/`,
|
|
39
|
+
* and no `Domain` — so name resolution and the emitted cookie agree on what
|
|
40
|
+
* the cookie will actually carry.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolvePrefixConstrainedAttributes(opts: PrefixConstrainedOptions | undefined): PrefixConstrainedAttributes;
|
|
43
|
+
/** Inputs for {@link resolveCookieName}. */
|
|
44
|
+
export interface CookieNameResolution {
|
|
45
|
+
/** The explicit `cookie.name`, when the caller set one. */
|
|
46
|
+
name: string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Unprefixed default name. `__Host-` is prepended to it when
|
|
49
|
+
* {@link attributes} satisfy that prefix.
|
|
50
|
+
*/
|
|
51
|
+
base: string;
|
|
52
|
+
/** Attributes the emitted cookie carries. */
|
|
53
|
+
attributes: PrefixConstrainedAttributes;
|
|
54
|
+
/**
|
|
55
|
+
* What is lost when a browser drops the cookie, as a clause completing
|
|
56
|
+
* "Every browser rejects such a cookie, so …" in the thrown error.
|
|
57
|
+
*/
|
|
58
|
+
consequence: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a cookie's name: the caller's explicit name when set, else
|
|
62
|
+
* `__Host-<base>` when the attributes satisfy that prefix, else the bare
|
|
63
|
+
* `base`.
|
|
64
|
+
*
|
|
65
|
+
* @throws {Error} When the attributes describe a cookie no browser will keep —
|
|
66
|
+
* an explicit name whose `__Host-` / `__Secure-` prefix they contradict, or
|
|
67
|
+
* `SameSite=None` without `Secure`. Either way the cookie is silently dropped
|
|
68
|
+
* and the failure surfaces as a broken flow rather than an error, so this
|
|
69
|
+
* refuses the configuration up front. `None` and the prefixes are matched
|
|
70
|
+
* case-insensitively, as browsers match them.
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveCookieName(resolution: CookieNameResolution): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `__Host-` / `__Secure-` cookie-name resolution shared by the BFF's cookies,
|
|
3
|
+
* plus the attribute combinations a browser refuses outright.
|
|
4
|
+
*
|
|
5
|
+
* A browser honors those prefixes only when the cookie's own attributes satisfy
|
|
6
|
+
* them, and silently drops the cookie otherwise — as it does for
|
|
7
|
+
* `SameSite=None` without `Secure`. So the default name and the validation of
|
|
8
|
+
* an explicit name both have to be derived from the same attributes — kept here
|
|
9
|
+
* so the session cookie and the pending-authorization cookie cannot drift
|
|
10
|
+
* apart, and so a dropped-cookie configuration fails loudly at construction
|
|
11
|
+
* instead of as an inexplicably broken session.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Apply the attribute defaults every BFF cookie shares — `Secure`, `Path=/`,
|
|
17
|
+
* and no `Domain` — so name resolution and the emitted cookie agree on what
|
|
18
|
+
* the cookie will actually carry.
|
|
19
|
+
*/
|
|
20
|
+
export function resolvePrefixConstrainedAttributes(opts) {
|
|
21
|
+
return {
|
|
22
|
+
secure: opts?.secure ?? true,
|
|
23
|
+
path: opts?.path ?? "/",
|
|
24
|
+
domain: opts?.domain || undefined,
|
|
25
|
+
sameSite: opts?.sameSite,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function assertSameSiteSatisfiable(attrs, consequence) {
|
|
29
|
+
if (attrs.sameSite?.toLowerCase() !== "none" || attrs.secure)
|
|
30
|
+
return;
|
|
31
|
+
throw new Error('cookie.sameSite is "None", which requires Secure, but cookie.secure is ' +
|
|
32
|
+
`false. Every browser rejects such a cookie, so ${consequence}. Leave ` +
|
|
33
|
+
'cookie.secure on, or use "Lax" for local http development.');
|
|
34
|
+
}
|
|
35
|
+
function assertCookiePrefixSatisfiable(name, attrs, consequence) {
|
|
36
|
+
const lower = name.toLowerCase();
|
|
37
|
+
const quoted = JSON.stringify(name);
|
|
38
|
+
if (lower.startsWith("__host-")) {
|
|
39
|
+
const conflicts = [];
|
|
40
|
+
if (!attrs.secure)
|
|
41
|
+
conflicts.push("cookie.secure is false");
|
|
42
|
+
if (attrs.domain) {
|
|
43
|
+
conflicts.push(`cookie.domain is ${JSON.stringify(attrs.domain)}`);
|
|
44
|
+
}
|
|
45
|
+
if (attrs.path !== "/") {
|
|
46
|
+
conflicts.push(`cookie.path is ${JSON.stringify(attrs.path)}`);
|
|
47
|
+
}
|
|
48
|
+
if (conflicts.length > 0) {
|
|
49
|
+
throw new Error(`cookie.name ${quoted} uses the "__Host-" prefix, which requires ` +
|
|
50
|
+
`Secure, Path=/, and no Domain, but ${conflicts.join(" and ")}. ` +
|
|
51
|
+
`Every browser rejects such a cookie, so ${consequence}. Remove the ` +
|
|
52
|
+
"conflicting attribute or drop the prefix from cookie.name.");
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (lower.startsWith("__secure-") && !attrs.secure) {
|
|
57
|
+
throw new Error(`cookie.name ${quoted} uses the "__Secure-" prefix, which requires ` +
|
|
58
|
+
`Secure, but cookie.secure is false. Every browser rejects such a ` +
|
|
59
|
+
`cookie, so ${consequence}. Leave cookie.secure on or drop the prefix ` +
|
|
60
|
+
"from cookie.name.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a cookie's name: the caller's explicit name when set, else
|
|
65
|
+
* `__Host-<base>` when the attributes satisfy that prefix, else the bare
|
|
66
|
+
* `base`.
|
|
67
|
+
*
|
|
68
|
+
* @throws {Error} When the attributes describe a cookie no browser will keep —
|
|
69
|
+
* an explicit name whose `__Host-` / `__Secure-` prefix they contradict, or
|
|
70
|
+
* `SameSite=None` without `Secure`. Either way the cookie is silently dropped
|
|
71
|
+
* and the failure surfaces as a broken flow rather than an error, so this
|
|
72
|
+
* refuses the configuration up front. `None` and the prefixes are matched
|
|
73
|
+
* case-insensitively, as browsers match them.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveCookieName(resolution) {
|
|
76
|
+
const { name, base, attributes, consequence } = resolution;
|
|
77
|
+
assertSameSiteSatisfiable(attributes, consequence);
|
|
78
|
+
if (name) {
|
|
79
|
+
assertCookiePrefixSatisfiable(name, attributes, consequence);
|
|
80
|
+
return name;
|
|
81
|
+
}
|
|
82
|
+
const hostPrefixOk = attributes.secure && attributes.path === "/" &&
|
|
83
|
+
!attributes.domain;
|
|
84
|
+
return hostPrefixOk ? `__Host-${base}` : base;
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-for-Frontend adapter for Hono.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a {@link HonoBff} class that wraps an
|
|
5
|
+
* `DirectClient` constructed with a client secret, plus the
|
|
6
|
+
* {@link SessionStore} contract and two ready-to-use implementations
|
|
7
|
+
* ({@link MemorySessionStore}, {@link EncryptedCookieSessionStore}).
|
|
8
|
+
*
|
|
9
|
+
* Need a server-side store (Postgres, Redis, Deno KV, …)? Implement the
|
|
10
|
+
* {@link SessionStore} interface and verify it with
|
|
11
|
+
* `runSessionStoreContractTests` from `@udibo/oauth2/hono/bff/testing`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import "../../../_dnt.polyfills.js";
|
|
16
|
+
export { type BackchannelLogoutSubject, type BffCallbackError, HonoBff, type HonoBffBackchannelOptions, type HonoBffCookieOptions, type HonoBffCsrfOptions, type HonoBffOptions, type HonoBffPaths, type HonoBffResourceServer, } from "./bff.js";
|
|
17
|
+
export { type AuthRequestCookieOptions, type AuthRequestStorageFactory, EncryptedCookieAuthRequestStorage, encryptedCookieAuthRequestStorage, type EncryptedCookieAuthRequestStorageOptions, } from "./auth-request-store.js";
|
|
18
|
+
export { DEFAULT_PROXY_FORWARD_HEADERS, type HonoBffProxyOptions, } from "./proxy.js";
|
|
19
|
+
export { type BackchannelLogoutStore, type BoundedSessionStore, DEFAULT_SESSION_MAX_AGE_MS, EncryptedCookieSessionStore, type EncryptedCookieSessionStoreOptions, MemorySessionStore, type SessionData, type SessionStore, sessionStoreMaxAgeMs, supportsBackchannelLogout, } from "./session-store.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-for-Frontend adapter for Hono.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a {@link HonoBff} class that wraps an
|
|
5
|
+
* `DirectClient` constructed with a client secret, plus the
|
|
6
|
+
* {@link SessionStore} contract and two ready-to-use implementations
|
|
7
|
+
* ({@link MemorySessionStore}, {@link EncryptedCookieSessionStore}).
|
|
8
|
+
*
|
|
9
|
+
* Need a server-side store (Postgres, Redis, Deno KV, …)? Implement the
|
|
10
|
+
* {@link SessionStore} interface and verify it with
|
|
11
|
+
* `runSessionStoreContractTests` from `@udibo/oauth2/hono/bff/testing`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import "../../../_dnt.polyfills.js";
|
|
16
|
+
export { HonoBff, } from "./bff.js";
|
|
17
|
+
export { EncryptedCookieAuthRequestStorage, encryptedCookieAuthRequestStorage, } from "./auth-request-store.js";
|
|
18
|
+
export { DEFAULT_PROXY_FORWARD_HEADERS, } from "./proxy.js";
|
|
19
|
+
export { DEFAULT_SESSION_MAX_AGE_MS, EncryptedCookieSessionStore, MemorySessionStore, sessionStoreMaxAgeMs, supportsBackchannelLogout, } from "./session-store.js";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header hygiene and URL resolution for {@link HonoBff.proxy}.
|
|
3
|
+
*
|
|
4
|
+
* The proxy is the "full BFF" shape from the IETF "OAuth 2.0 for Browser-Based
|
|
5
|
+
* Apps" BCP §6.1.1: the browser calls the BFF, the BFF forwards to a **separate**
|
|
6
|
+
* resource server with the session's access token attached, and the response is
|
|
7
|
+
* streamed back. This module owns the parts that are pure functions of the
|
|
8
|
+
* request — which headers cross the boundary in each direction, and which
|
|
9
|
+
* upstream URL an inbound path maps to.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Options for {@link HonoBff.proxy}. The upstream base URL is a separate
|
|
15
|
+
* argument because it is fixed at configuration time: nothing about the target
|
|
16
|
+
* is ever derived from the request.
|
|
17
|
+
*/
|
|
18
|
+
export interface HonoBffProxyOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Mount prefix removed from the request path before it is appended to the
|
|
21
|
+
* upstream base. Mounting at `/api/*` against `https://api.example.com/v1`
|
|
22
|
+
* with `stripPrefix: "/api"` maps `GET /api/things?page=2` to
|
|
23
|
+
* `GET https://api.example.com/v1/things?page=2`. Omit to append the full
|
|
24
|
+
* request path.
|
|
25
|
+
*/
|
|
26
|
+
stripPrefix?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Request headers forwarded upstream, replacing
|
|
29
|
+
* {@link DEFAULT_PROXY_FORWARD_HEADERS}. Spread the default to extend it:
|
|
30
|
+
* `[...DEFAULT_PROXY_FORWARD_HEADERS, "x-tenant"]`. Matched
|
|
31
|
+
* case-insensitively. `Cookie`, `Authorization`, `Host`, the CSRF header, and
|
|
32
|
+
* the hop-by-hop headers are dropped even when listed here.
|
|
33
|
+
*/
|
|
34
|
+
forwardHeaders?: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Retry once with a freshly refreshed access token when the upstream answers
|
|
37
|
+
* `401` with an `error="invalid_token"` challenge. Defaults to `true`.
|
|
38
|
+
* Requests that carry a body are never retried — the body has already been
|
|
39
|
+
* streamed upstream and buffering it would defeat the streaming — so those
|
|
40
|
+
* `401`s pass through as-is.
|
|
41
|
+
*/
|
|
42
|
+
retryOn401?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* `fetch` used for the upstream call. Defaults to the global. Inject a stub
|
|
45
|
+
* in tests to assert what crosses the boundary without running a server.
|
|
46
|
+
*/
|
|
47
|
+
fetch?: (input: URL, init: RequestInit) => Promise<Response>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Request headers forwarded upstream unless {@link HonoBffProxyOptions.forwardHeaders}
|
|
51
|
+
* replaces the list. Content negotiation, conditional requests, ranges, and the
|
|
52
|
+
* common tracing headers — everything a resource server needs to answer
|
|
53
|
+
* correctly, and nothing that identifies the BFF's own session.
|
|
54
|
+
*/
|
|
55
|
+
export declare const DEFAULT_PROXY_FORWARD_HEADERS: readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the effective request-forward allowlist: the caller's list (or the
|
|
58
|
+
* default) minus everything that must never leave the BFF, including the CSRF
|
|
59
|
+
* header, which is a BFF-internal signal.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveForwardHeaders(forwardHeaders: readonly string[] | undefined, csrfHeaderName: string | undefined): Set<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Builds the upstream request headers: the allowlisted inbound headers plus the
|
|
64
|
+
* session's bearer token. The session cookie, any inbound `Authorization`, and
|
|
65
|
+
* every hop-by-hop header are left behind by construction — this starts from an
|
|
66
|
+
* empty `Headers` and only copies what `allowed` names.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildUpstreamHeaders(inbound: Headers, allowed: Set<string>, accessToken: string): Headers;
|
|
69
|
+
/**
|
|
70
|
+
* Builds the browser-facing response headers: everything except hop-by-hop
|
|
71
|
+
* headers (the fixed set and whatever `Connection` names), upstream
|
|
72
|
+
* `Set-Cookie`, and — when a body is actually streamed back — the content
|
|
73
|
+
* coding and length that `fetch` already resolved.
|
|
74
|
+
*
|
|
75
|
+
* URL-valued headers are rewritten into the mount's namespace, and the result
|
|
76
|
+
* is marked `private` + `Vary: Cookie` (it is cookie-authenticated) and
|
|
77
|
+
* `nosniff` (an upstream body is served from the BFF's own origin).
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildDownstreamHeaders(upstream: Response, context: {
|
|
80
|
+
upstreamUrl: URL;
|
|
81
|
+
base: URL;
|
|
82
|
+
mountPrefix: string;
|
|
83
|
+
}): Headers;
|
|
84
|
+
/**
|
|
85
|
+
* Maps an inbound request URL onto the configured upstream base.
|
|
86
|
+
*
|
|
87
|
+
* The base is fixed at configuration time and only the path and query travel
|
|
88
|
+
* with the request, so the target can never be steered by the caller. `URL`
|
|
89
|
+
* resolves dot segments (percent-encoded ones included) while parsing, so what
|
|
90
|
+
* remains to defend against is a segment that only becomes `../` once an
|
|
91
|
+
* upstream server decodes it again — see {@link assertSafeSegment}.
|
|
92
|
+
*
|
|
93
|
+
* Query strings are merged rather than replaced: a parameter pinned on the
|
|
94
|
+
* target survives every request and **wins** over an inbound parameter of the
|
|
95
|
+
* same name, so a browser cannot override what the mount configured.
|
|
96
|
+
*
|
|
97
|
+
* @param stripPrefix Mount prefix removed from the path before joining.
|
|
98
|
+
* @throws {TypeError} If a path segment has a malformed escape or hides `..`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function resolveProxyUrl(base: URL, requestUrl: string, stripPrefix?: string): URL;
|
|
101
|
+
/**
|
|
102
|
+
* Whether an upstream `401` is the kind a token refresh could fix. Mirrors the
|
|
103
|
+
* `WWW-Authenticate: ... error="invalid_token"` gate `DirectClient.fetch`
|
|
104
|
+
* applies (quotes optional here, since not every server emits them), so a `401`
|
|
105
|
+
* meaning "wrong user" or "no such thing" passes straight through instead of
|
|
106
|
+
* burning a refresh.
|
|
107
|
+
*/
|
|
108
|
+
export declare function isInvalidTokenChallenge(response: Response): boolean;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header hygiene and URL resolution for {@link HonoBff.proxy}.
|
|
3
|
+
*
|
|
4
|
+
* The proxy is the "full BFF" shape from the IETF "OAuth 2.0 for Browser-Based
|
|
5
|
+
* Apps" BCP §6.1.1: the browser calls the BFF, the BFF forwards to a **separate**
|
|
6
|
+
* resource server with the session's access token attached, and the response is
|
|
7
|
+
* streamed back. This module owns the parts that are pure functions of the
|
|
8
|
+
* request — which headers cross the boundary in each direction, and which
|
|
9
|
+
* upstream URL an inbound path maps to.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Hop-by-hop headers (RFC 9110 §7.6.1). They describe a single transport
|
|
15
|
+
* connection, so a proxy must not pass them through in either direction.
|
|
16
|
+
*/
|
|
17
|
+
const HOP_BY_HOP_HEADERS = [
|
|
18
|
+
"connection",
|
|
19
|
+
"keep-alive",
|
|
20
|
+
"proxy-authenticate",
|
|
21
|
+
"proxy-authorization",
|
|
22
|
+
"te",
|
|
23
|
+
"trailer",
|
|
24
|
+
"transfer-encoding",
|
|
25
|
+
"upgrade",
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Request headers forwarded upstream unless {@link HonoBffProxyOptions.forwardHeaders}
|
|
29
|
+
* replaces the list. Content negotiation, conditional requests, ranges, and the
|
|
30
|
+
* common tracing headers — everything a resource server needs to answer
|
|
31
|
+
* correctly, and nothing that identifies the BFF's own session.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_PROXY_FORWARD_HEADERS = [
|
|
34
|
+
"accept",
|
|
35
|
+
"accept-language",
|
|
36
|
+
"content-language",
|
|
37
|
+
"content-type",
|
|
38
|
+
"if-match",
|
|
39
|
+
"if-modified-since",
|
|
40
|
+
"if-none-match",
|
|
41
|
+
"if-unmodified-since",
|
|
42
|
+
"range",
|
|
43
|
+
"user-agent",
|
|
44
|
+
"baggage",
|
|
45
|
+
"traceparent",
|
|
46
|
+
"tracestate",
|
|
47
|
+
"x-request-id",
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Never forwarded upstream, even when named in
|
|
51
|
+
* {@link HonoBffProxyOptions.forwardHeaders}. `cookie` and `authorization` are
|
|
52
|
+
* the security-critical entries: the browser's session cookie is the BFF's
|
|
53
|
+
* credential (not the API's), and an inbound `Authorization` must never be able
|
|
54
|
+
* to override the token the BFF attaches. `accept-encoding` and `content-length`
|
|
55
|
+
* are dropped because `fetch` owns content coding and body framing.
|
|
56
|
+
*/
|
|
57
|
+
const NEVER_FORWARDED_REQUEST_HEADERS = [
|
|
58
|
+
...HOP_BY_HOP_HEADERS,
|
|
59
|
+
"accept-encoding",
|
|
60
|
+
"authorization",
|
|
61
|
+
"content-length",
|
|
62
|
+
"cookie",
|
|
63
|
+
"cookie2",
|
|
64
|
+
"host",
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Never returned to the browser. `set-cookie` is dropped so an upstream API
|
|
68
|
+
* cannot plant cookies on the BFF's origin, where they would ride along with
|
|
69
|
+
* the session cookie.
|
|
70
|
+
*/
|
|
71
|
+
const NEVER_RETURNED_RESPONSE_HEADERS = [
|
|
72
|
+
...HOP_BY_HOP_HEADERS,
|
|
73
|
+
"set-cookie",
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Dropped only when a body is actually streamed back: `fetch` has already
|
|
77
|
+
* decoded it, so the upstream's content coding and length no longer describe
|
|
78
|
+
* what the browser receives. A body-less response (`HEAD`, `204`, `304`) was
|
|
79
|
+
* never decoded, and there `Content-Length` *is* the answer.
|
|
80
|
+
*/
|
|
81
|
+
const DECODED_BODY_RESPONSE_HEADERS = [
|
|
82
|
+
"content-encoding",
|
|
83
|
+
"content-length",
|
|
84
|
+
];
|
|
85
|
+
/** Case-insensitive header name set. */
|
|
86
|
+
function headerSet(names) {
|
|
87
|
+
return new Set(names.map((name) => name.toLowerCase()));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the effective request-forward allowlist: the caller's list (or the
|
|
91
|
+
* default) minus everything that must never leave the BFF, including the CSRF
|
|
92
|
+
* header, which is a BFF-internal signal.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveForwardHeaders(forwardHeaders, csrfHeaderName) {
|
|
95
|
+
const allowed = headerSet(forwardHeaders ?? DEFAULT_PROXY_FORWARD_HEADERS);
|
|
96
|
+
const denied = headerSet(NEVER_FORWARDED_REQUEST_HEADERS);
|
|
97
|
+
if (csrfHeaderName)
|
|
98
|
+
denied.add(csrfHeaderName.toLowerCase());
|
|
99
|
+
for (const name of denied)
|
|
100
|
+
allowed.delete(name);
|
|
101
|
+
return allowed;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Builds the upstream request headers: the allowlisted inbound headers plus the
|
|
105
|
+
* session's bearer token. The session cookie, any inbound `Authorization`, and
|
|
106
|
+
* every hop-by-hop header are left behind by construction — this starts from an
|
|
107
|
+
* empty `Headers` and only copies what `allowed` names.
|
|
108
|
+
*/
|
|
109
|
+
export function buildUpstreamHeaders(inbound, allowed, accessToken) {
|
|
110
|
+
const headers = new Headers();
|
|
111
|
+
for (const [name, value] of inbound) {
|
|
112
|
+
if (allowed.has(name.toLowerCase()))
|
|
113
|
+
headers.set(name, value);
|
|
114
|
+
}
|
|
115
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
116
|
+
return headers;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The header names an upstream `Connection` field lists. RFC 9110 §7.6.1 makes
|
|
120
|
+
* those connection-specific too, so a proxy must drop them alongside the
|
|
121
|
+
* well-known hop-by-hop set.
|
|
122
|
+
*/
|
|
123
|
+
function connectionNamedHeaders(headers) {
|
|
124
|
+
return (headers.get("connection") ?? "")
|
|
125
|
+
.split(",")
|
|
126
|
+
.map((name) => name.trim().toLowerCase())
|
|
127
|
+
.filter((name) => name.length > 0);
|
|
128
|
+
}
|
|
129
|
+
/** Headers whose value is a URL into the upstream's namespace. */
|
|
130
|
+
const URL_VALUED_RESPONSE_HEADERS = [
|
|
131
|
+
"location",
|
|
132
|
+
"content-location",
|
|
133
|
+
];
|
|
134
|
+
/**
|
|
135
|
+
* Rewrites an upstream URL into the mount's namespace, so a `Location` naming
|
|
136
|
+
* the resource server neither leaks internal topology to the browser nor points
|
|
137
|
+
* it at a host it cannot reach. A relative value resolves against the upstream
|
|
138
|
+
* request first. Anything outside the configured base is left alone — it is a
|
|
139
|
+
* genuinely external destination, not part of what this mount proxies.
|
|
140
|
+
*/
|
|
141
|
+
function rewriteUpstreamUrl(value, upstreamUrl, base, mountPrefix) {
|
|
142
|
+
let resolved;
|
|
143
|
+
try {
|
|
144
|
+
resolved = new URL(value, upstreamUrl);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
if (resolved.origin !== base.origin)
|
|
150
|
+
return value;
|
|
151
|
+
const basePath = base.pathname.endsWith("/")
|
|
152
|
+
? base.pathname.slice(0, -1)
|
|
153
|
+
: base.pathname;
|
|
154
|
+
const underBase = basePath === "" ||
|
|
155
|
+
resolved.pathname === basePath ||
|
|
156
|
+
resolved.pathname.startsWith(`${basePath}/`);
|
|
157
|
+
if (!underBase)
|
|
158
|
+
return value;
|
|
159
|
+
const suffix = resolved.pathname.slice(basePath.length) || "/";
|
|
160
|
+
return `${mountPrefix}${suffix}${resolved.search}${resolved.hash}`;
|
|
161
|
+
}
|
|
162
|
+
/** Adds `name` to `Vary` without disturbing what the upstream already varies on. */
|
|
163
|
+
function appendVary(headers, name) {
|
|
164
|
+
const existing = headers.get("vary");
|
|
165
|
+
if (!existing) {
|
|
166
|
+
headers.set("vary", name);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (existing.trim() === "*")
|
|
170
|
+
return;
|
|
171
|
+
const present = existing
|
|
172
|
+
.split(",")
|
|
173
|
+
.some((token) => token.trim().toLowerCase() === name.toLowerCase());
|
|
174
|
+
if (!present)
|
|
175
|
+
headers.set("vary", `${existing}, ${name}`);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Forces the response private. Upstream sees an `Authorization`-protected
|
|
179
|
+
* request, which RFC 9111 §3.5 already keeps out of shared caches; downstream
|
|
180
|
+
* the same response is authenticated by a cookie, which carries no such rule.
|
|
181
|
+
*/
|
|
182
|
+
function forcePrivateCache(headers) {
|
|
183
|
+
const existing = headers.get("cache-control");
|
|
184
|
+
if (!existing) {
|
|
185
|
+
headers.set("cache-control", "private");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const directives = existing
|
|
189
|
+
.split(",")
|
|
190
|
+
.map((directive) => directive.trim().toLowerCase());
|
|
191
|
+
if (directives.some((directive) => directive === "private" || directive === "no-store" ||
|
|
192
|
+
directive.startsWith("private="))) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
headers.set("cache-control", `private, ${existing}`);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Builds the browser-facing response headers: everything except hop-by-hop
|
|
199
|
+
* headers (the fixed set and whatever `Connection` names), upstream
|
|
200
|
+
* `Set-Cookie`, and — when a body is actually streamed back — the content
|
|
201
|
+
* coding and length that `fetch` already resolved.
|
|
202
|
+
*
|
|
203
|
+
* URL-valued headers are rewritten into the mount's namespace, and the result
|
|
204
|
+
* is marked `private` + `Vary: Cookie` (it is cookie-authenticated) and
|
|
205
|
+
* `nosniff` (an upstream body is served from the BFF's own origin).
|
|
206
|
+
*/
|
|
207
|
+
export function buildDownstreamHeaders(upstream, context) {
|
|
208
|
+
const denied = headerSet(NEVER_RETURNED_RESPONSE_HEADERS);
|
|
209
|
+
for (const name of connectionNamedHeaders(upstream.headers)) {
|
|
210
|
+
denied.add(name);
|
|
211
|
+
}
|
|
212
|
+
if (upstream.body !== null) {
|
|
213
|
+
for (const name of DECODED_BODY_RESPONSE_HEADERS)
|
|
214
|
+
denied.add(name);
|
|
215
|
+
}
|
|
216
|
+
const urlValued = headerSet(URL_VALUED_RESPONSE_HEADERS);
|
|
217
|
+
const headers = new Headers();
|
|
218
|
+
for (const [name, value] of upstream.headers) {
|
|
219
|
+
if (denied.has(name.toLowerCase()))
|
|
220
|
+
continue;
|
|
221
|
+
headers.append(name, urlValued.has(name.toLowerCase())
|
|
222
|
+
? rewriteUpstreamUrl(value, context.upstreamUrl, context.base, context.mountPrefix)
|
|
223
|
+
: value);
|
|
224
|
+
}
|
|
225
|
+
appendVary(headers, "Cookie");
|
|
226
|
+
forcePrivateCache(headers);
|
|
227
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
228
|
+
return headers;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Rejects a path segment that would become a dot segment once decoded.
|
|
232
|
+
*
|
|
233
|
+
* Fails closed on a malformed escape: `decodeURIComponent` throws on overlong
|
|
234
|
+
* or invalid UTF-8 (`%C0%AF`, the classic `/` smuggle), and a lenient upstream
|
|
235
|
+
* decoder may still reconstruct the separator, so the segment cannot be
|
|
236
|
+
* forwarded unexamined. Encoded separators themselves are allowed through —
|
|
237
|
+
* `a%2Fb` is a legitimate resource id — it is only `..` between them that is
|
|
238
|
+
* refused.
|
|
239
|
+
*
|
|
240
|
+
* @throws {TypeError} If the segment has a malformed escape or hides `..`.
|
|
241
|
+
*/
|
|
242
|
+
function assertSafeSegment(segment) {
|
|
243
|
+
let decoded;
|
|
244
|
+
try {
|
|
245
|
+
decoded = decodeURIComponent(segment);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw new TypeError("proxy path segment has a malformed escape sequence");
|
|
249
|
+
}
|
|
250
|
+
if (decoded.split(/[/\\]/).some((part) => part === "..")) {
|
|
251
|
+
throw new TypeError("proxy path segment hides a dot segment");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Maps an inbound request URL onto the configured upstream base.
|
|
256
|
+
*
|
|
257
|
+
* The base is fixed at configuration time and only the path and query travel
|
|
258
|
+
* with the request, so the target can never be steered by the caller. `URL`
|
|
259
|
+
* resolves dot segments (percent-encoded ones included) while parsing, so what
|
|
260
|
+
* remains to defend against is a segment that only becomes `../` once an
|
|
261
|
+
* upstream server decodes it again — see {@link assertSafeSegment}.
|
|
262
|
+
*
|
|
263
|
+
* Query strings are merged rather than replaced: a parameter pinned on the
|
|
264
|
+
* target survives every request and **wins** over an inbound parameter of the
|
|
265
|
+
* same name, so a browser cannot override what the mount configured.
|
|
266
|
+
*
|
|
267
|
+
* @param stripPrefix Mount prefix removed from the path before joining.
|
|
268
|
+
* @throws {TypeError} If a path segment has a malformed escape or hides `..`.
|
|
269
|
+
*/
|
|
270
|
+
export function resolveProxyUrl(base, requestUrl, stripPrefix) {
|
|
271
|
+
const inbound = new URL(requestUrl);
|
|
272
|
+
let path = inbound.pathname;
|
|
273
|
+
if (stripPrefix) {
|
|
274
|
+
const prefix = stripPrefix.endsWith("/")
|
|
275
|
+
? stripPrefix.slice(0, -1)
|
|
276
|
+
: stripPrefix;
|
|
277
|
+
if (path === prefix) {
|
|
278
|
+
path = "/";
|
|
279
|
+
}
|
|
280
|
+
else if (path.startsWith(`${prefix}/`)) {
|
|
281
|
+
path = path.slice(prefix.length);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const segment of path.split("/"))
|
|
285
|
+
assertSafeSegment(segment);
|
|
286
|
+
const basePath = base.pathname.endsWith("/")
|
|
287
|
+
? base.pathname.slice(0, -1)
|
|
288
|
+
: base.pathname;
|
|
289
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
290
|
+
const url = new URL(base);
|
|
291
|
+
url.pathname = `${basePath}${suffix}`;
|
|
292
|
+
url.search = inbound.search;
|
|
293
|
+
for (const name of new Set(base.searchParams.keys())) {
|
|
294
|
+
url.searchParams.delete(name);
|
|
295
|
+
}
|
|
296
|
+
for (const [name, value] of base.searchParams) {
|
|
297
|
+
url.searchParams.append(name, value);
|
|
298
|
+
}
|
|
299
|
+
url.hash = "";
|
|
300
|
+
return url;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Whether an upstream `401` is the kind a token refresh could fix. Mirrors the
|
|
304
|
+
* `WWW-Authenticate: ... error="invalid_token"` gate `DirectClient.fetch`
|
|
305
|
+
* applies (quotes optional here, since not every server emits them), so a `401`
|
|
306
|
+
* meaning "wrong user" or "no such thing" passes straight through instead of
|
|
307
|
+
* burning a refresh.
|
|
308
|
+
*/
|
|
309
|
+
export function isInvalidTokenChallenge(response) {
|
|
310
|
+
if (response.status !== 401)
|
|
311
|
+
return false;
|
|
312
|
+
const challenge = response.headers.get("WWW-Authenticate") ?? "";
|
|
313
|
+
return /error\s*=\s*"?invalid_token"?/i.test(challenge);
|
|
314
|
+
}
|