@zudojs/auth-oauth 1.1.1 → 1.2.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/README.md CHANGED
@@ -5,7 +5,15 @@ default, mandatory anti-CSRF `state` with a timing-safe check, an SSRF guard on
5
5
  every endpoint URL, a redirect-URI allowlist, and size- and time-bounded
6
6
  requests to the provider.
7
7
 
8
- Depends on nothing but Node built-ins (`node:crypto` and the global `fetch`).
8
+ <!-- zudo-docs:start -->
9
+
10
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-auth-oauth](https://zudojs.oyinlola.site/docs/packages-auth-oauth) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-auth-oauth.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
11
+
12
+ <!-- zudo-docs:end -->
13
+
14
+ Built on Node built-ins (`node:crypto` and the global `fetch`), plus
15
+ `@zudojs/errors` (the error base classes) and `@zudojs/security` (the IPv6
16
+ helpers behind the SSRF guard).
9
17
 
10
18
  ```bash
11
19
  pnpm add @zudojs/auth-oauth
@@ -84,7 +92,9 @@ app.get("/auth/callback", async (req, res) => {
84
92
  const profile = await fetchUserInfo(config, tokens.accessToken);
85
93
 
86
94
  if (profile.email === undefined) {
87
- return res.status(400).send("This provider did not release an email address.");
95
+ return res
96
+ .status(400)
97
+ .send("This provider did not release an email address.");
88
98
  }
89
99
 
90
100
  // Now it is your application's turn: find or create the local account keyed
@@ -107,29 +117,29 @@ const fresh = await refreshAccessToken(config, storedRefreshToken);
107
117
 
108
118
  ## API
109
119
 
110
- | Export | What it does |
111
- | --- | --- |
112
- | `createAuthorizationUrl(config, options)` | Builds the authorize URL. Returns `{ url, state, codeVerifier, codeChallenge }`. |
113
- | `exchangeCodeForToken(config, options)` | Authorization-code grant. Returns a validated `OAuthTokenSet`. |
114
- | `refreshAccessToken(config, refreshToken)` | Refresh-token grant, where the provider supports one. |
115
- | `fetchUserInfo(config, accessToken)` | Bearer GET of the user-info endpoint, normalised to `OAuthUserInfo`. |
116
- | `generateState()` / `verifyState(expected, received)` | 256-bit state, timing-safe comparison. |
117
- | `generateCodeVerifier()` / `deriveCodeChallenge(verifier)` | PKCE primitives (`S256`). |
118
- | `parseTokenResponse(payload)` | Validate a token payload you obtained elsewhere. |
119
- | `normalizeUserInfo(provider, payload)` | Normalise a profile payload you obtained elsewhere. |
120
- | `assertSafeUrl(url, label, use)` / `isBlockedFetchHost(host)` | The URL and SSRF guards, exposed for your own checks. |
121
- | `PROVIDER_PRESETS` | Endpoint defaults per provider. |
120
+ | Export | What it does |
121
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------------- |
122
+ | `createAuthorizationUrl(config, options)` | Builds the authorize URL. Returns `{ url, state, codeVerifier, codeChallenge }`. |
123
+ | `exchangeCodeForToken(config, options)` | Authorization-code grant. Returns a validated `OAuthTokenSet`. |
124
+ | `refreshAccessToken(config, refreshToken)` | Refresh-token grant, where the provider supports one. |
125
+ | `fetchUserInfo(config, accessToken)` | Bearer GET of the user-info endpoint, normalised to `OAuthUserInfo`. |
126
+ | `generateState()` / `verifyState(expected, received)` | 256-bit state, timing-safe comparison. |
127
+ | `generateCodeVerifier()` / `deriveCodeChallenge(verifier)` | PKCE primitives (`S256`). |
128
+ | `parseTokenResponse(payload)` | Validate a token payload you obtained elsewhere. |
129
+ | `normalizeUserInfo(provider, payload)` | Normalise a profile payload you obtained elsewhere. |
130
+ | `assertSafeUrl(url, label, use)` / `isBlockedFetchHost(host)` | The URL and SSRF guards, exposed for your own checks. |
131
+ | `PROVIDER_PRESETS` | Endpoint defaults per provider. |
122
132
 
123
133
  ## Providers
124
134
 
125
- | Provider | Endpoints | Client auth | Refresh | Notes |
126
- | --- | --- | --- | --- | --- |
127
- | `google` | preset | body | yes | `access_type=offline` + `prompt=consent` are sent so a refresh token is actually issued. |
128
- | `github` | preset | body | **no** | Classic OAuth App tokens do not expire and no refresh token is issued; `refreshAccessToken` throws rather than making a pointless request. |
129
- | `microsoft` | preset (`common` tenant) | body | yes | Override `authorizeUrl`/`tokenUrl` for a single-tenant app. |
130
- | `apple` | preset | body | yes | `clientSecret` is the ES256 JWT you mint from your private key — this package does not generate it. Apple has **no user-info endpoint**; the profile is in the `id_token`, so `fetchUserInfo` throws for `apple`. |
131
- | `discord` | preset | **basic** | yes | |
132
- | `custom` | you supply all three URLs | body | yes | |
135
+ | Provider | Endpoints | Client auth | Refresh | Notes |
136
+ | ----------- | ------------------------- | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
137
+ | `google` | preset | body | yes | `access_type=offline` + `prompt=consent` are sent so a refresh token is actually issued. |
138
+ | `github` | preset | body | **no** | Classic OAuth App tokens do not expire and no refresh token is issued; `refreshAccessToken` throws rather than making a pointless request. |
139
+ | `microsoft` | preset (`common` tenant) | body | yes | Override `authorizeUrl`/`tokenUrl` for a single-tenant app. |
140
+ | `apple` | preset | body | yes | `clientSecret` is the ES256 JWT you mint from your private key — this package does not generate it. Apple has **no user-info endpoint**; the profile is in the `id_token`, so `fetchUserInfo` throws for `apple`. |
141
+ | `discord` | preset | **basic** | yes | |
142
+ | `custom` | you supply all three URLs | body | yes | |
133
143
 
134
144
  Any preset URL can be overridden on the config; every override goes through the
135
145
  same validation.
@@ -140,7 +150,7 @@ same validation.
140
150
 
141
151
  - **GitHub** omits `email` from `/user` whenever the address is private, which
142
152
  is the default. When the `user:email` scope was granted, this package asks
143
- `/user/emails` and uses the primary *verified* address. Without the scope, or
153
+ `/user/emails` and uses the primary _verified_ address. Without the scope, or
144
154
  without a verified primary, the profile simply comes back with no email.
145
155
  - **Discord** returns no email unless the `email` scope was granted.
146
156
 
@@ -165,11 +175,15 @@ Every one of these is covered by a test in `tests/`.
165
175
  arbitrary redirect target is never reflected.
166
176
  - **URL validation and SSRF guard.** Every URL must be `https` — `http` is
167
177
  tolerated only for `localhost` / `127.0.0.1` / `[::1]` and only on
168
- browser-facing URLs — and must not embed credentials. The *server-fetched*
178
+ browser-facing URLs — and must not embed credentials. The _server-fetched_
169
179
  endpoints (token, user-info) additionally may not point at a loopback,
170
180
  private, CGNAT, link-local, unique-local, multicast or reserved address, at
171
181
  `169.254.169.254` and friends, or at a `localhost` / `*.local` / `*.internal`
172
- / `metadata.google.internal` name. Redirects are not followed
182
+ / `metadata.google.internal` name. IPv6 literals that embed an IPv4
183
+ address (`[::127.0.0.1]`, `[::ffff:0:a9fe:a9fe]`, NAT64
184
+ `[64:ff9b::169.254.169.254]`, 6to4 `2002::/16`) are judged as that IPv4
185
+ address, and the local-use NAT64 prefix `64:ff9b:1::/48` is refused.
186
+ Redirects are not followed
173
187
  (`redirect: "manual"`), so a 3xx cannot walk the request somewhere that never
174
188
  passed the guard.
175
189
  **Limit:** the check is on the literal host; DNS is not resolved, so DNS
@@ -179,7 +193,7 @@ Every one of these is covered by a test in `tests/`.
179
193
  `AbortSignal.timeout(timeoutMs)` (default 10s) and the body is streamed and
180
194
  abandoned the moment it passes `maxResponseBytes` (default 256 KiB); an
181
195
  oversized `Content-Length` is refused before a byte is read.
182
- - **Defensive parsing.** A token response must be a JSON *object* with a
196
+ - **Defensive parsing.** A token response must be a JSON _object_ with a
183
197
  non-blank string `access_token`; `expires_in` must be a non-negative integer
184
198
  (or its decimal string); `refresh_token`, `id_token`, `scope` and `token_type`
185
199
  must be strings when present. `__proto__`, `constructor` and `prototype` are
@@ -198,31 +212,34 @@ Every one of these is covered by a test in `tests/`.
198
212
 
199
213
  Every failure is an `OAuthError` with a machine-readable `code`, a suggested
200
214
  `statusCode`, and `expose` saying whether the message is safe to show a user.
201
-
202
- | Class | Code | Status | Exposed |
203
- | --- | --- | --- | --- |
204
- | `OAuthConfigurationError` | `OAUTH_CONFIGURATION_INVALID` | 500 | no |
205
- | `OAuthEndpointNotAllowedError` | `OAUTH_ENDPOINT_NOT_ALLOWED` | 500 | no |
206
- | `OAuthRedirectUriError` | `OAUTH_REDIRECT_URI_NOT_ALLOWED` | 400 | yes |
207
- | `OAuthStateMismatchError` | `OAUTH_STATE_MISMATCH` | 400 | yes |
208
- | `OAuthProviderError` | `OAUTH_PROVIDER_REJECTED` | 502 | yes |
209
- | `OAuthResponseError` | `OAUTH_PROVIDER_RESPONSE_INVALID` | 502 | yes |
210
- | `OAuthResponseTooLargeError` | `OAUTH_RESPONSE_TOO_LARGE` | 502 | yes |
211
- | `OAuthNetworkError` | `OAUTH_NETWORK` | 504 | yes |
215
+ `OAuthError` extends the shared `OAuthError` from `@zudojs/errors`, so every
216
+ class below is also a `BaseError`, and its codes equal the shared
217
+ `ErrorCode.OAUTH_*` members.
218
+
219
+ | Class | Code | Status | Exposed |
220
+ | ------------------------------ | --------------------------------- | ------ | ------- |
221
+ | `OAuthConfigurationError` | `OAUTH_CONFIGURATION_INVALID` | 500 | no |
222
+ | `OAuthEndpointNotAllowedError` | `OAUTH_ENDPOINT_NOT_ALLOWED` | 500 | no |
223
+ | `OAuthRedirectUriError` | `OAUTH_REDIRECT_URI_NOT_ALLOWED` | 400 | yes |
224
+ | `OAuthStateMismatchError` | `OAUTH_STATE_MISMATCH` | 400 | yes |
225
+ | `OAuthProviderError` | `OAUTH_PROVIDER_REJECTED` | 502 | yes |
226
+ | `OAuthResponseError` | `OAUTH_PROVIDER_RESPONSE_INVALID` | 502 | yes |
227
+ | `OAuthResponseTooLargeError` | `OAUTH_RESPONSE_TOO_LARGE` | 502 | yes |
228
+ | `OAuthNetworkError` | `OAUTH_NETWORK` | 504 | yes |
212
229
 
213
230
  ## Configuration reference
214
231
 
215
- | Field | Required | Default |
216
- | --- | --- | --- |
217
- | `provider` | yes | — |
218
- | `clientId`, `clientSecret` | yes | — |
219
- | `allowedRedirectUris` | yes, non-empty | — |
220
- | `authorizeUrl`, `tokenUrl`, `userInfoUrl` | only for `custom` | the preset's |
221
- | `scopes` | no | the preset's |
222
- | `clientAuthMethod` | no | the preset's |
223
- | `timeoutMs` | no | `10000` (1 - 120000) |
224
- | `maxResponseBytes` | no | `262144` (1024 - 5242880) |
225
- | `fetch` | no | global `fetch` |
232
+ | Field | Required | Default |
233
+ | ----------------------------------------- | ----------------- | ------------------------- |
234
+ | `provider` | yes | — |
235
+ | `clientId`, `clientSecret` | yes | — |
236
+ | `allowedRedirectUris` | yes, non-empty | — |
237
+ | `authorizeUrl`, `tokenUrl`, `userInfoUrl` | only for `custom` | the preset's |
238
+ | `scopes` | no | the preset's |
239
+ | `clientAuthMethod` | no | the preset's |
240
+ | `timeoutMs` | no | `10000` (1 - 120000) |
241
+ | `maxResponseBytes` | no | `262144` (1024 - 5242880) |
242
+ | `fetch` | no | global `fetch` |
226
243
 
227
244
  Out-of-range values are rejected, not clamped.
228
245
 
@@ -28,7 +28,9 @@ function requireString(value, field) {
28
28
  function boundedInt(value, fallback, min, max, field) {
29
29
  if (value === undefined)
30
30
  return fallback;
31
- if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
31
+ if (typeof value !== "number" ||
32
+ !Number.isFinite(value) ||
33
+ !Number.isInteger(value)) {
32
34
  throw new OAuthConfigurationError(`${field} must be an integer.`);
33
35
  }
34
36
  if (value < min || value > max) {
@@ -50,7 +52,8 @@ export function assertScopes(scopes) {
50
52
  throw new OAuthConfigurationError("scopes must be an array of scope-tokens.");
51
53
  }
52
54
  for (const scope of scopes) {
53
- if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
55
+ if (typeof scope !== "string" ||
56
+ !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
54
57
  throw new OAuthConfigurationError("Each scope must be a non-empty RFC 6749 scope-token.");
55
58
  }
56
59
  }
@@ -20,7 +20,9 @@ const SAFE_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
20
20
  /** Read a response body, refusing to buffer more than `maxBytes`. */
21
21
  async function readCappedText(response, maxBytes) {
22
22
  const declared = response.headers.get("content-length");
23
- if (declared !== null && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
23
+ if (declared !== null &&
24
+ /^\d+$/.test(declared) &&
25
+ Number(declared) > maxBytes) {
24
26
  throw new OAuthResponseTooLargeError(maxBytes);
25
27
  }
26
28
  const body = response.body;
@@ -7,7 +7,7 @@ import { OAuthResponseError } from "../oauthErrors/index.js";
7
7
  import { normalizeUserInfo } from "../oauthProviders/index.js";
8
8
  import { assertSafeUrl } from "../oauthSecurity/index.js";
9
9
  import { resolveConfig, resolveUserInfoUrl, } from "./oauthConfig.resolve.js";
10
- import { requestProviderJson, requestProviderValue, } from "./oauthHttp.core.js";
10
+ import { requestProviderJson, requestProviderValue } from "./oauthHttp.core.js";
11
11
  /**
12
12
  * GitHub's `/user` omits `email` whenever the address is private — which is
13
13
  * the default for new accounts. When `user:email` was granted we ask
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * @module oauthErrors/oauthError
5
5
  *
6
- * These are defined locally rather than extending `@zudojs/errors` so the
7
- * package has no `@zudojs/*` dependency at all.
6
+ * `OAuthError` builds on the shared `OAuthError` in `@zudojs/errors`, so
7
+ * every class here is a `BaseError` (structured `toJSON`, metadata
8
+ * redaction, `category`/`severity`). `OAuthErrorCode` values equal the
9
+ * shared `ErrorCode.OAUTH_*` members.
8
10
  *
9
11
  * **Secret hygiene.** No constructor here ever interpolates a client secret,
10
12
  * an access token, a refresh token or a code verifier into `message`. The
@@ -13,6 +15,7 @@
13
15
  * so a provider cannot echo a secret back into your logs. `message` is the
14
16
  * first line of `stack`, so keeping it clean keeps the stack clean.
15
17
  */
18
+ import { OAuthError as SharedOAuthError } from "@zudojs/errors";
16
19
  /** Stable, machine-readable error codes. */
17
20
  export declare const OAuthErrorCode: {
18
21
  /** The config is unusable: missing field, bad URL, unsupported operation. */
@@ -48,16 +51,12 @@ export interface OAuthErrorOptions {
48
51
  *
49
52
  * `expose` says whether the message is safe to hand to an end user; it is
50
53
  * `true` for request-caused failures and `false` for configuration ones
51
- * (which describe your deployment, not the request).
54
+ * (which describe your deployment, not the request). Defaults: 400,
55
+ * exposed, code `OAUTH_PROVIDER_REJECTED`.
52
56
  */
53
- export declare class OAuthError extends Error {
54
- readonly name: string;
57
+ export declare class OAuthError extends SharedOAuthError {
55
58
  /** Machine-readable code. */
56
59
  readonly code: OAuthErrorCode;
57
- /** Suggested HTTP status for a handler that surfaces this. */
58
- readonly statusCode: number;
59
- /** Whether `message` is safe to return to a client verbatim. */
60
- readonly expose: boolean;
61
60
  constructor(message: string, options?: OAuthErrorOptions);
62
61
  }
63
62
  /** The configuration is missing something or is structurally unusable. */
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * @module oauthErrors/oauthError
5
5
  *
6
- * These are defined locally rather than extending `@zudojs/errors` so the
7
- * package has no `@zudojs/*` dependency at all.
6
+ * `OAuthError` builds on the shared `OAuthError` in `@zudojs/errors`, so
7
+ * every class here is a `BaseError` (structured `toJSON`, metadata
8
+ * redaction, `category`/`severity`). `OAuthErrorCode` values equal the
9
+ * shared `ErrorCode.OAUTH_*` members.
8
10
  *
9
11
  * **Secret hygiene.** No constructor here ever interpolates a client secret,
10
12
  * an access token, a refresh token or a code verifier into `message`. The
@@ -13,6 +15,7 @@
13
15
  * so a provider cannot echo a secret back into your logs. `message` is the
14
16
  * first line of `stack`, so keeping it clean keeps the stack clean.
15
17
  */
18
+ import { OAuthError as SharedOAuthError } from "@zudojs/errors";
16
19
  /** Stable, machine-readable error codes. */
17
20
  export const OAuthErrorCode = {
18
21
  /** The config is unusable: missing field, bad URL, unsupported operation. */
@@ -39,21 +42,12 @@ export const OAuthErrorCode = {
39
42
  *
40
43
  * `expose` says whether the message is safe to hand to an end user; it is
41
44
  * `true` for request-caused failures and `false` for configuration ones
42
- * (which describe your deployment, not the request).
45
+ * (which describe your deployment, not the request). Defaults: 400,
46
+ * exposed, code `OAUTH_PROVIDER_REJECTED`.
43
47
  */
44
- export class OAuthError extends Error {
45
- name = "OAuthError";
46
- /** Machine-readable code. */
47
- code;
48
- /** Suggested HTTP status for a handler that surfaces this. */
49
- statusCode;
50
- /** Whether `message` is safe to return to a client verbatim. */
51
- expose;
48
+ export class OAuthError extends SharedOAuthError {
52
49
  constructor(message, options) {
53
- super(message, options?.cause !== undefined ? { cause: options.cause } : {});
54
- this.code = options?.code ?? OAuthErrorCode.PROVIDER_REJECTED;
55
- this.statusCode = options?.statusCode ?? 400;
56
- this.expose = options?.expose ?? true;
50
+ super(message, options);
57
51
  }
58
52
  }
59
53
  /** The configuration is missing something or is structurally unusable. */
@@ -62,7 +62,13 @@ export const PROVIDER_PRESETS = {
62
62
  authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
63
63
  tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
64
64
  userInfoUrl: "https://graph.microsoft.com/v1.0/me",
65
- defaultScopes: ["openid", "email", "profile", "offline_access", "User.Read"],
65
+ defaultScopes: [
66
+ "openid",
67
+ "email",
68
+ "profile",
69
+ "offline_access",
70
+ "User.Read",
71
+ ],
66
72
  clientAuth: "body",
67
73
  supportsRefresh: true,
68
74
  },
@@ -59,7 +59,9 @@ export function sanitizeJsonValue(value) {
59
59
  */
60
60
  export function parseJsonObject(text, label) {
61
61
  const sanitized = parseJsonValue(text, label);
62
- if (sanitized === null || typeof sanitized !== "object" || Array.isArray(sanitized)) {
62
+ if (sanitized === null ||
63
+ typeof sanitized !== "object" ||
64
+ Array.isArray(sanitized)) {
63
65
  throw new OAuthResponseError(`${label} did not return a JSON object.`);
64
66
  }
65
67
  return sanitized;
@@ -26,6 +26,7 @@
26
26
  * rebinding) is not caught here. Pair this with network egress controls if
27
27
  * you accept endpoint URLs from untrusted operators.
28
28
  */
29
+ import { embeddedIpv4, expandIpv6, isNonPublicIpv6Range, } from "@zudojs/security";
29
30
  import { OAuthEndpointNotAllowedError } from "../oauthErrors/index.js";
30
31
  /** Hostnames that are always refused for a server-fetched endpoint. */
31
32
  const BLOCKED_HOST_NAMES = new Set([
@@ -90,40 +91,22 @@ function isNonPublicIpv4(octets) {
90
91
  return true; // multicast + reserved + broadcast
91
92
  return false;
92
93
  }
93
- /** Whether an IPv6 literal (already stripped of brackets) is non-public. */
94
+ /**
95
+ * Whether an IPv6 literal (already stripped of brackets) is non-public.
96
+ *
97
+ * Any form embedding an IPv4 address (compatible `::/96`, mapped, translated,
98
+ * NAT64 `64:ff9b::/96`, 6to4) is judged as that IPv4 address. The WHATWG
99
+ * parser serialises `[::127.0.0.1]` as `[::7f00:1]`, which the old
100
+ * dotted-form regex never matched. An unparseable literal fails closed.
101
+ */
94
102
  function isNonPublicIpv6(raw) {
95
- const host = raw.toLowerCase();
96
- if (host === "::" || host === "::1")
103
+ const groups = expandIpv6(raw);
104
+ if (!groups)
97
105
  return true;
98
- // IPv4-mapped / -compatible: judge the embedded IPv4 address.
99
- const mapped = /^::(?:ffff:)?(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
100
- const embedded = mapped?.[1];
101
- if (embedded !== undefined) {
102
- const octets = parseIpv4(embedded);
103
- return octets === undefined ? true : isNonPublicIpv4(octets);
104
- }
105
- // The WHATWG URL parser rewrites `::ffff:127.0.0.1` as `::ffff:7f00:1`,
106
- // so the hex form has to be decoded back to its embedded IPv4 address.
107
- const hexMapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
108
- const high = hexMapped?.[1];
109
- const low = hexMapped?.[2];
110
- if (high !== undefined && low !== undefined) {
111
- const word1 = Number.parseInt(high, 16);
112
- const word2 = Number.parseInt(low, 16);
113
- return isNonPublicIpv4([
114
- (word1 >> 8) & 0xff,
115
- word1 & 0xff,
116
- (word2 >> 8) & 0xff,
117
- word2 & 0xff,
118
- ]);
119
- }
120
- if (/^f[cd][0-9a-f]{2}:/.test(host))
121
- return true; // fc00::/7 unique local
122
- if (/^fe[89ab][0-9a-f]:/.test(host))
123
- return true; // fe80::/10 link-local
124
- if (/^ff[0-9a-f]{2}:/.test(host))
125
- return true; // ff00::/8 multicast
126
- return false;
106
+ const embedded = embeddedIpv4(groups);
107
+ if (embedded)
108
+ return isNonPublicIpv4(embedded);
109
+ return isNonPublicIpv6Range(groups);
127
110
  }
128
111
  /**
129
112
  * Whether a host literal is one this package refuses to fetch from.
@@ -132,9 +115,7 @@ function isNonPublicIpv6(raw) {
132
115
  */
133
116
  export function isBlockedFetchHost(hostname) {
134
117
  const host = hostname.toLowerCase();
135
- const unbracketed = host.startsWith("[") && host.endsWith("]")
136
- ? host.slice(1, -1)
137
- : host;
118
+ const unbracketed = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
138
119
  // A trailing dot marks a fully-qualified name (`localhost.`,
139
120
  // `metadata.google.internal.`). DNS resolves it to the same address as
140
121
  // the undotted form, but the WHATWG parser keeps the dot on domain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/auth-oauth",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "OAuth2 authorization-code client for the Zudojs framework — PKCE S256, mandatory state, SSRF-guarded endpoints, and provider presets for Google, GitHub, Microsoft, Apple and Discord.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -52,6 +52,10 @@
52
52
  "url": "https://github.com/oyinlola-tech/zudo",
53
53
  "directory": "packages/auth-oauth"
54
54
  },
55
+ "dependencies": {
56
+ "@zudojs/errors": "1.2.0",
57
+ "@zudojs/security": "1.2.0"
58
+ },
55
59
  "scripts": {
56
60
  "build": "tsc -p tsconfig.json",
57
61
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",