kinetex 1.1.0 → 1.3.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/README.md +919 -445
- package/dist/browser/kinetex.esm.js +18 -18
- package/dist/browser/kinetex.js +749 -295
- package/dist/browser/kinetex.min.js +18 -18
- package/dist/cjs/aws-sigv4.js +4 -1
- package/dist/cjs/cache.js +52 -14
- package/dist/cjs/circuit-breaker.js +24 -4
- package/dist/cjs/client.js +495 -131
- package/dist/cjs/cookie-parser.js +7 -4
- package/dist/cjs/cookie-store.js +16 -8
- package/dist/cjs/core.js +83 -35
- package/dist/cjs/dedup.js +9 -7
- package/dist/cjs/digest.js +26 -0
- package/dist/cjs/graphql.js +19 -4
- package/dist/cjs/headers.js +64 -8
- package/dist/cjs/interceptors.js +86 -33
- package/dist/cjs/logging.js +1 -1
- package/dist/cjs/pagination.js +14 -6
- package/dist/cjs/progress.js +129 -42
- package/dist/cjs/socks5.js +36 -21
- package/dist/cjs/sse.js +48 -11
- package/dist/cjs/utils.js +24 -12
- package/dist/cjs/worker.js +6 -6
- package/dist/cjs/ws.js +23 -7
- package/dist/esm/aws-sigv4.js +4 -1
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +52 -14
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +24 -4
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +495 -131
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +7 -4
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +16 -8
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +83 -35
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +9 -7
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +26 -0
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +19 -4
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +64 -8
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +86 -33
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/logging.js +1 -1
- package/dist/esm/pagination.js +14 -6
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +129 -42
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/socks5.js +36 -21
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +48 -11
- package/dist/esm/sse.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils.js +24 -12
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/worker.js +6 -6
- package/dist/esm/worker.js.map +1 -1
- package/dist/esm/ws.js +23 -7
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +8 -1
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts +29 -12
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/cookie-parser.d.ts.map +1 -1
- package/dist/types/cookie-store.d.ts.map +1 -1
- package/dist/types/core.d.ts +10 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts +0 -7
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +14 -0
- package/dist/types/digest.d.ts.map +1 -1
- package/dist/types/graphql.d.ts.map +1 -1
- package/dist/types/headers.d.ts +25 -10
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts.map +1 -1
- package/dist/types/logging.d.ts +1 -1
- package/dist/types/pagination.d.ts.map +1 -1
- package/dist/types/progress.d.ts.map +1 -1
- package/dist/types/socks5.d.ts.map +1 -1
- package/dist/types/sse.d.ts.map +1 -1
- package/dist/types/types.d.ts +25 -2
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/worker.d.ts +6 -6
- package/dist/types/worker.d.ts.map +1 -1
- package/dist/types/ws.d.ts +4 -0
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -1735,10 +1735,13 @@ export function domainMatch(requestHost, cookieDomain) {
|
|
|
1735
1735
|
if (isIPAddress(cd))
|
|
1736
1736
|
return false;
|
|
1737
1737
|
// RFC 6265 §5.3: cookie domain must not be a public suffix
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1738
|
+
// The COOKIE DOMAIN must not be a public suffix: "com" or "co.uk" must
|
|
1739
|
+
// never be accepted as a cookie domain. This check used to test the request
|
|
1740
|
+
// host instead, so it could never fire and a Domain=com cookie was reported
|
|
1741
|
+
// as matching every .com host. (HTTPCookieJar.setCookie guards it separately
|
|
1742
|
+
// with isPublicSuffix(cd); this exported helper must be correct on its own.)
|
|
1743
|
+
if (isPublicSuffix(cd))
|
|
1744
|
+
return false;
|
|
1742
1745
|
return true;
|
|
1743
1746
|
}
|
|
1744
1747
|
// ============================================================================
|
package/dist/cjs/cookie-store.js
CHANGED
|
@@ -180,7 +180,11 @@ export class CookieJar {
|
|
|
180
180
|
let cookieDomain;
|
|
181
181
|
let hostOnly;
|
|
182
182
|
if (parsed.domain !== null && parsed.domain !== "") {
|
|
183
|
-
|
|
183
|
+
// Normalize the case: the Domain attribute is case-insensitive, but the
|
|
184
|
+
// value was stored verbatim while every lookup compares against a
|
|
185
|
+
// lower-cased request host. `Domain=Example.COM` therefore stored a cookie
|
|
186
|
+
// that could never be matched again — silently lost for the jar's lifetime.
|
|
187
|
+
const cd = parsed.domain.toLowerCase();
|
|
184
188
|
// Must domain-match the request host (use custom matcher if provided)
|
|
185
189
|
const matcher = this.domainMatcherFn ?? domainMatch;
|
|
186
190
|
if (!matcher(reqHost, cd))
|
|
@@ -269,10 +273,9 @@ export class CookieJar {
|
|
|
269
273
|
* @returns Array of matching Cookie objects (direct references into storage)
|
|
270
274
|
*/
|
|
271
275
|
getCookies(options) {
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
+
// Expired cookies are removed by the periodic cleanup timer (and by
|
|
277
|
+
// clearExpired()); the old 1%-per-access coin flip walked the whole store
|
|
278
|
+
// at random and made eviction timing non-deterministic.
|
|
276
279
|
const url = safeParseUrl(options.url);
|
|
277
280
|
if (!url)
|
|
278
281
|
return [];
|
|
@@ -747,11 +750,16 @@ export class CookieJar {
|
|
|
747
750
|
// For domain cookies: domain-match (subdomains allowed).
|
|
748
751
|
// We can't know per-domain whether it's host-only without checking cookies,
|
|
749
752
|
// so we do the broader domain-match here and filter host-only per cookie.
|
|
750
|
-
|
|
753
|
+
// Lower-case both sides: cookie domains are stored from the (already
|
|
754
|
+
// normalized) Domain attribute, but a caller can also insert cookies
|
|
755
|
+
// directly via putCookie() with mixed-case input.
|
|
756
|
+
const host = reqHost.toLowerCase();
|
|
757
|
+
const domain = cookieDomain.toLowerCase();
|
|
758
|
+
if (host === domain)
|
|
751
759
|
return true;
|
|
752
|
-
if (isIPAddress(
|
|
760
|
+
if (isIPAddress(host))
|
|
753
761
|
return false;
|
|
754
|
-
return
|
|
762
|
+
return host.endsWith("." + domain);
|
|
755
763
|
}
|
|
756
764
|
}
|
|
757
765
|
// ============================================================================
|
package/dist/cjs/core.js
CHANGED
|
@@ -103,6 +103,12 @@ function isProductionEnvironment() {
|
|
|
103
103
|
return false;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The accept-encoding value client.ts injects when the caller did not set
|
|
108
|
+
* one. FetchTransport removes exactly this string so fetch() negotiates its
|
|
109
|
+
* own encodings; any other value is treated as caller intent.
|
|
110
|
+
*/
|
|
111
|
+
export const DEFAULT_ACCEPT_ENCODING = "gzip, deflate, br";
|
|
106
112
|
/**
|
|
107
113
|
* Universal fetch-based transport.
|
|
108
114
|
* Suitable for all runtimes where `fetch` is available.
|
|
@@ -160,10 +166,12 @@ export class FetchTransport {
|
|
|
160
166
|
}
|
|
161
167
|
sanitizedHeaders[name] = value;
|
|
162
168
|
}
|
|
163
|
-
// Strip
|
|
164
|
-
//
|
|
169
|
+
// Strip the accept-encoding value INJECTED by client.ts so fetch() can add
|
|
170
|
+
// its own. The old check removed ANY value containing gzip+deflate+br,
|
|
171
|
+
// including one the caller set deliberately; matching the exact injected
|
|
172
|
+
// default keeps caller intent intact.
|
|
165
173
|
const ae = sanitizedHeaders["accept-encoding"];
|
|
166
|
-
if (ae && ae.toLowerCase().
|
|
174
|
+
if (ae && ae.toLowerCase().replace(/\s+/g, " ") === DEFAULT_ACCEPT_ENCODING) {
|
|
167
175
|
delete sanitizedHeaders["accept-encoding"];
|
|
168
176
|
}
|
|
169
177
|
// Build fetch init
|
|
@@ -248,6 +256,8 @@ export class NodeHTTP2Transport {
|
|
|
248
256
|
sessionUsage = new Map();
|
|
249
257
|
_strict;
|
|
250
258
|
_onDroppedHeader;
|
|
259
|
+
/** Optional CA bundle for origins with self-signed / private-PKI certificates. */
|
|
260
|
+
_ca;
|
|
251
261
|
/** FIX 11: Configurable connect timeout (replaces hardcoded 30 000 ms) */
|
|
252
262
|
_connectTimeoutMs;
|
|
253
263
|
/** FIX 11: Configurable per-request stream timeout (replaces hardcoded 30 000 ms) */
|
|
@@ -263,6 +273,7 @@ export class NodeHTTP2Transport {
|
|
|
263
273
|
this.maxSessions = options.maxSessions ?? 100;
|
|
264
274
|
this._strict = options.strict ?? false;
|
|
265
275
|
this._onDroppedHeader = options.onDroppedHeader;
|
|
276
|
+
this._ca = options.ca;
|
|
266
277
|
this._connectTimeoutMs = options.connectTimeoutMs ?? 30_000;
|
|
267
278
|
this._requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
268
279
|
}
|
|
@@ -323,7 +334,10 @@ export class NodeHTTP2Transport {
|
|
|
323
334
|
}
|
|
324
335
|
}
|
|
325
336
|
const session = await new Promise((resolve, reject) => {
|
|
326
|
-
const s = http2.connect(origin, {
|
|
337
|
+
const s = http2.connect(origin, {
|
|
338
|
+
rejectUnauthorized: true,
|
|
339
|
+
...(this._ca !== undefined ? { ca: this._ca } : {}),
|
|
340
|
+
});
|
|
327
341
|
// FIX 11: use configurable connect timeout instead of hardcoded 30 000 ms
|
|
328
342
|
// FIX 9: unref() the timer so it does not prevent process exit
|
|
329
343
|
const connectTimeout = setTimeout(() => {
|
|
@@ -451,35 +465,38 @@ export class NodeHTTP2Transport {
|
|
|
451
465
|
":authority": currentUrl.host,
|
|
452
466
|
...currentReq.headers,
|
|
453
467
|
};
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
468
|
+
// Header validation (HTTP/2 control-character check). Runs in BOTH modes:
|
|
469
|
+
// strict throws, non-strict drops with callback/warn — matching the
|
|
470
|
+
// FetchTransport contract. (Previously the whole loop was gated on
|
|
471
|
+
// strict mode, so non-strict requests never validated and a header with
|
|
472
|
+
// forbidden control characters crashed session.request() with a raw
|
|
473
|
+
// ERR_INVALID_HEADER_VALUE instead of being dropped.)
|
|
474
|
+
for (const [hName, hValue] of Object.entries(h2ReqHeaders)) {
|
|
475
|
+
if (hName.startsWith(":"))
|
|
476
|
+
continue;
|
|
477
|
+
const hStr = Array.isArray(hValue) ? hValue.join(", ") : String(hValue);
|
|
478
|
+
let hasForbidden = false;
|
|
479
|
+
for (let ci = 0; ci < hStr.length; ci++) {
|
|
480
|
+
const code = hStr.charCodeAt(ci);
|
|
481
|
+
if ((code >= 0x00 && code <= 0x08) || (code >= 0x0a && code <= 0x1f) || code === 0x7f) {
|
|
482
|
+
hasForbidden = true;
|
|
483
|
+
break;
|
|
467
484
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
else if (typeof console !== "undefined") {
|
|
478
|
-
console.warn(`[kinetex] Invalid header dropped (HTTP/2): "${hName}" — value contains illegal control characters. ` +
|
|
479
|
-
`Pass strictHeaders: true to throw instead.`);
|
|
480
|
-
}
|
|
481
|
-
delete h2ReqHeaders[hName];
|
|
485
|
+
}
|
|
486
|
+
if (hasForbidden) {
|
|
487
|
+
if (this._strict) {
|
|
488
|
+
throw new KinetexError(`Strict mode: header "${hName}" contains forbidden control characters`, "EVALIDATION", { request: currentReq });
|
|
489
|
+
}
|
|
490
|
+
// FIX (H3): non-strict mode must match FetchTransport behavior —
|
|
491
|
+
// notify the callback (if any) and warn, never drop silently.
|
|
492
|
+
if (this._onDroppedHeader) {
|
|
493
|
+
this._onDroppedHeader(hName, hStr);
|
|
482
494
|
}
|
|
495
|
+
else if (typeof console !== "undefined") {
|
|
496
|
+
console.warn(`[kinetex] Invalid header dropped (HTTP/2): "${hName}" — value contains illegal control characters. ` +
|
|
497
|
+
`Pass strictHeaders: true to throw instead.`);
|
|
498
|
+
}
|
|
499
|
+
delete h2ReqHeaders[hName];
|
|
483
500
|
}
|
|
484
501
|
}
|
|
485
502
|
const endStream = !currentReq.body || currentReq.method === "GET" || currentReq.method === "HEAD";
|
|
@@ -726,6 +743,13 @@ export function createTransport(fetchFn, preferHTTP2 = true, sessionOptions, tra
|
|
|
726
743
|
// "Custom fetch implementation" behavior holds on every runtime.
|
|
727
744
|
// Use NodeHTTP2Transport for Node.js when HTTP/2 is preferred and no custom
|
|
728
745
|
// fetch is given. Falls back to FetchTransport otherwise.
|
|
746
|
+
if (IS_NODE && preferHTTP2 && fetchFn) {
|
|
747
|
+
if (!isProductionEnvironment()) {
|
|
748
|
+
console.warn('[kinetex] httpVersion: "HTTP/2" is ignored when a custom `fetch` is configured — ' +
|
|
749
|
+
"NodeHTTP2Transport cannot use a custom fetch, so the request goes through " +
|
|
750
|
+
"FetchTransport (HTTP/1.1 semantics). Drop the `fetch` option to use HTTP/2.");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
729
753
|
if (IS_NODE && preferHTTP2 && !fetchFn) {
|
|
730
754
|
return new NodeHTTP2Transport({
|
|
731
755
|
...(sessionOptions?.sessionTTLMs !== undefined
|
|
@@ -1102,7 +1126,7 @@ async function writeChunkWithBackpressure(stream, chunk) {
|
|
|
1102
1126
|
}
|
|
1103
1127
|
/**
|
|
1104
1128
|
* Write a request body to an HTTP/2 stream, respecting backpressure.
|
|
1105
|
-
* Handles ReadableStream, Uint8Array, ArrayBuffer, and
|
|
1129
|
+
* Handles ReadableStream, Uint8Array, ArrayBuffer, string, URLSearchParams and Blob bodies.
|
|
1106
1130
|
*
|
|
1107
1131
|
* @param stream - HTTP/2 stream to write to
|
|
1108
1132
|
* @param body - Request body
|
|
@@ -1127,12 +1151,16 @@ async function attachBodyToH2Stream(stream, body) {
|
|
|
1127
1151
|
stream.end(body);
|
|
1128
1152
|
}
|
|
1129
1153
|
else {
|
|
1130
|
-
|
|
1154
|
+
// The raw Node transports bypass fetch, so bodies fetch would normally
|
|
1155
|
+
// serialize (URLSearchParams, Blob) must be encoded here. Skipping them
|
|
1156
|
+
// silently sent an empty body to the server.
|
|
1157
|
+
const bytes = await serializeRawBody(body);
|
|
1158
|
+
stream.end(bytes);
|
|
1131
1159
|
}
|
|
1132
1160
|
}
|
|
1133
1161
|
/**
|
|
1134
1162
|
* Write a request body to a Node.js http.ClientRequest, respecting backpressure.
|
|
1135
|
-
* Handles ReadableStream, Uint8Array, ArrayBuffer, and
|
|
1163
|
+
* Handles ReadableStream, Uint8Array, ArrayBuffer, string, URLSearchParams and Blob bodies.
|
|
1136
1164
|
*
|
|
1137
1165
|
* @param req - Node.js ClientRequest
|
|
1138
1166
|
* @param body - Request body
|
|
@@ -1157,8 +1185,28 @@ async function pipeBodyToNodeReq(req, body) {
|
|
|
1157
1185
|
req.end(body);
|
|
1158
1186
|
}
|
|
1159
1187
|
else {
|
|
1160
|
-
|
|
1188
|
+
// The raw Node transports bypass fetch, so bodies fetch would normally
|
|
1189
|
+
// serialize (URLSearchParams, Blob) must be encoded here. Skipping them
|
|
1190
|
+
// silently sent an empty body to the server.
|
|
1191
|
+
const bytes = await serializeRawBody(body);
|
|
1192
|
+
req.end(bytes);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Serialize body types that `fetch` would normally encode for us, so the raw
|
|
1197
|
+
* Node HTTP/1.1 and HTTP/2 transports do not silently send an empty payload.
|
|
1198
|
+
*
|
|
1199
|
+
* @param body - Request body that is not a stream, byte array, or string
|
|
1200
|
+
* @returns The encoded bytes to write (empty for unsupported types)
|
|
1201
|
+
*/
|
|
1202
|
+
async function serializeRawBody(body) {
|
|
1203
|
+
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
|
|
1204
|
+
return new TextEncoder().encode(body.toString());
|
|
1205
|
+
}
|
|
1206
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
1207
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
1161
1208
|
}
|
|
1209
|
+
return new Uint8Array(0);
|
|
1162
1210
|
}
|
|
1163
1211
|
// ============================================================================
|
|
1164
1212
|
// §10 DECOMPRESSION
|
package/dist/cjs/dedup.js
CHANGED
|
@@ -136,6 +136,15 @@ export class DedupMap {
|
|
|
136
136
|
this.inflight.delete(key);
|
|
137
137
|
}
|
|
138
138
|
else {
|
|
139
|
+
// Clear any previous timer for this key first. Registering a new
|
|
140
|
+
// window without clearing the old one let the stale timer fire later
|
|
141
|
+
// and `inflight.delete(key)` — wiping the *new*, still-in-flight entry
|
|
142
|
+
// out from under callers that were sharing it.
|
|
143
|
+
const stale = this.timeouts.get(key);
|
|
144
|
+
if (stale !== undefined) {
|
|
145
|
+
clearTimeout(stale);
|
|
146
|
+
this.timeouts.delete(key);
|
|
147
|
+
}
|
|
139
148
|
const timeoutId = setTimeout(() => {
|
|
140
149
|
this.inflight.delete(key);
|
|
141
150
|
this.timeouts.delete(key);
|
|
@@ -177,13 +186,6 @@ export class DedupMap {
|
|
|
177
186
|
get keys() {
|
|
178
187
|
return [...this.inflight.keys()];
|
|
179
188
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Get comprehensive deduplication statistics.
|
|
182
|
-
* NOTE: inFlightCount and trackedKeys reflect snapshot time; entries may
|
|
183
|
-
* resolve asynchronously between sampling and return (non-blocking, acceptable).
|
|
184
|
-
*
|
|
185
|
-
* @returns Snapshot of hits, misses, hit rate, and tracked entries
|
|
186
|
-
*/
|
|
187
189
|
/**
|
|
188
190
|
* Get comprehensive deduplication statistics.
|
|
189
191
|
*
|
package/dist/cjs/digest.js
CHANGED
|
@@ -550,3 +550,29 @@ export async function createDigestAuthorization(wwwAuth, username, password, met
|
|
|
550
550
|
const response = await computeDigestResponse(challenge, username, password, method, uri, cnonce, nc);
|
|
551
551
|
return formatDigestAuth(challenge, username, response, uri, cnonce, nc);
|
|
552
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Stateful Digest-auth authorizer.
|
|
555
|
+
*
|
|
556
|
+
* Keeps a per-nonce request counter and increments it on every call, as
|
|
557
|
+
* RFC 7616 §3.4.1 requires: `nc` is the hex request count for the current
|
|
558
|
+
* nonce and MUST increase for each request. The stateless
|
|
559
|
+
* {@link createDigestAuthorization} always sends 00000001, which replay-
|
|
560
|
+
* protecting servers (nginx, Apache with `AuthDigestNonceLifetime`) reject on
|
|
561
|
+
* the second request. A new nonce (challenge change) resets the counter to 1.
|
|
562
|
+
*
|
|
563
|
+
* Not safe to share across clients that authenticate as different users —
|
|
564
|
+
* create one per client.
|
|
565
|
+
*/
|
|
566
|
+
export function createDigestAuthorizer() {
|
|
567
|
+
const counters = new Map();
|
|
568
|
+
return async (wwwAuth, username, password, method, uri) => {
|
|
569
|
+
const challenge = parseDigestChallenge(wwwAuth);
|
|
570
|
+
const cnonce = randomBytes(5);
|
|
571
|
+
const scope = `${challenge.realm}\u0000${challenge.nonce}`;
|
|
572
|
+
const next = (counters.get(scope) ?? 0) + 1;
|
|
573
|
+
counters.set(scope, next);
|
|
574
|
+
const nc = next.toString(16).padStart(8, "0");
|
|
575
|
+
const response = await computeDigestResponse(challenge, username, password, method, uri, cnonce, nc);
|
|
576
|
+
return formatDigestAuth(challenge, username, response, uri, cnonce, nc);
|
|
577
|
+
};
|
|
578
|
+
}
|
package/dist/cjs/graphql.js
CHANGED
|
@@ -383,8 +383,10 @@ async function executeHTTP(req, config, signal, apqMode = "none", getAPQHashFn)
|
|
|
383
383
|
fetchHeaders["content-type"] = "application/json";
|
|
384
384
|
const controller = new AbortController();
|
|
385
385
|
const timer = config.timeoutMs > 0 ? setTimeout(() => controller.abort(), config.timeoutMs) : null;
|
|
386
|
-
// Merge external signal
|
|
387
|
-
|
|
386
|
+
// Merge external signal. The listener is removed once the fetch settles so
|
|
387
|
+
// it does not accumulate on long-lived caller-provided signals (leak fix).
|
|
388
|
+
const onExternalAbort = () => controller.abort();
|
|
389
|
+
signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
388
390
|
let response;
|
|
389
391
|
try {
|
|
390
392
|
response = await config.fetch(fetchUrl, {
|
|
@@ -405,6 +407,7 @@ async function executeHTTP(req, config, signal, apqMode = "none", getAPQHashFn)
|
|
|
405
407
|
finally {
|
|
406
408
|
if (timer)
|
|
407
409
|
clearTimeout(timer);
|
|
410
|
+
signal?.removeEventListener("abort", onExternalAbort);
|
|
408
411
|
}
|
|
409
412
|
return parseGraphQLResponse(response, req);
|
|
410
413
|
}
|
|
@@ -634,7 +637,10 @@ export class GraphQLClient {
|
|
|
634
637
|
delete headers["content-type"]; // FormData sets it with boundary
|
|
635
638
|
const form = buildMultipartBody(req, uploads);
|
|
636
639
|
const controller = new AbortController();
|
|
637
|
-
|
|
640
|
+
// Listener removed when the fetch settles — avoids accumulating on the
|
|
641
|
+
// caller's signal across many uploads (leak fix).
|
|
642
|
+
const onExternalAbort = () => controller.abort();
|
|
643
|
+
options.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
638
644
|
this.config.onRequest(req);
|
|
639
645
|
let response;
|
|
640
646
|
try {
|
|
@@ -650,6 +656,9 @@ export class GraphQLClient {
|
|
|
650
656
|
this.config.onError(clientErr, req);
|
|
651
657
|
throw clientErr;
|
|
652
658
|
}
|
|
659
|
+
finally {
|
|
660
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
661
|
+
}
|
|
653
662
|
const gqlRes = await parseGraphQLResponse(response, req);
|
|
654
663
|
this.config.onResponse(gqlRes, req);
|
|
655
664
|
if (gqlRes.errors?.length) {
|
|
@@ -679,7 +688,10 @@ export class GraphQLClient {
|
|
|
679
688
|
async batch(requests, options = {}) {
|
|
680
689
|
const headers = await buildHeaders(this.config);
|
|
681
690
|
const controller = new AbortController();
|
|
682
|
-
|
|
691
|
+
// Listener removed when the fetch settles — avoids accumulating on the
|
|
692
|
+
// caller's signal across many batches (leak fix).
|
|
693
|
+
const onExternalAbort = () => controller.abort();
|
|
694
|
+
options.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
683
695
|
let response;
|
|
684
696
|
try {
|
|
685
697
|
response = await this.config.fetch(this.config.url, {
|
|
@@ -692,6 +704,9 @@ export class GraphQLClient {
|
|
|
692
704
|
catch (err) {
|
|
693
705
|
throw new GraphQLClientError(err instanceof Error ? err.message : "Batch network error", "ENETWORK", undefined, requests[0], undefined, err);
|
|
694
706
|
}
|
|
707
|
+
finally {
|
|
708
|
+
options.signal?.removeEventListener("abort", onExternalAbort);
|
|
709
|
+
}
|
|
695
710
|
// FIX (H6): sanitize untrusted batch response JSON before validation.
|
|
696
711
|
const rawResults = sanitizeParsedJSON(await response.json());
|
|
697
712
|
// Validate response is array (B-5 fix)
|
package/dist/cjs/headers.js
CHANGED
|
@@ -897,6 +897,31 @@ export function formatContentDisposition(cd) {
|
|
|
897
897
|
* @param value - Raw `Cache-Control` header value
|
|
898
898
|
* @returns Structured directives object with boolean flags and numeric values
|
|
899
899
|
*/
|
|
900
|
+
/**
|
|
901
|
+
* Split a `Cache-Control` value on commas, ignoring commas inside a quoted
|
|
902
|
+
* string. RFC 7234 §5.2 allows quoted-string values that contain commas, e.g.
|
|
903
|
+
* `private="field1, field2"`, so a plain `split(",")` corrupts them.
|
|
904
|
+
*
|
|
905
|
+
* @param value - Raw `Cache-Control` header value
|
|
906
|
+
* @returns Each directive as its own (untrimmed) string
|
|
907
|
+
*/
|
|
908
|
+
function splitCacheControlDirectives(value) {
|
|
909
|
+
const parts = [];
|
|
910
|
+
let current = "";
|
|
911
|
+
let inQuotes = false;
|
|
912
|
+
for (const ch of value) {
|
|
913
|
+
if (ch === '"')
|
|
914
|
+
inQuotes = !inQuotes;
|
|
915
|
+
if (ch === "," && !inQuotes) {
|
|
916
|
+
parts.push(current);
|
|
917
|
+
current = "";
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
current += ch;
|
|
921
|
+
}
|
|
922
|
+
parts.push(current);
|
|
923
|
+
return parts;
|
|
924
|
+
}
|
|
900
925
|
export function parseCacheControl(value) {
|
|
901
926
|
const d = {
|
|
902
927
|
noCache: false,
|
|
@@ -917,7 +942,7 @@ export function parseCacheControl(value) {
|
|
|
917
942
|
staleWhileRevalidate: null,
|
|
918
943
|
unknown: new Map(),
|
|
919
944
|
};
|
|
920
|
-
for (const part of value
|
|
945
|
+
for (const part of splitCacheControlDirectives(value)) {
|
|
921
946
|
const t = part.trim();
|
|
922
947
|
const eq = t.indexOf("=");
|
|
923
948
|
const k = (eq === -1 ? t : t.slice(0, eq)).trim().toLowerCase();
|
|
@@ -1379,17 +1404,48 @@ export function normalizeForwardedHeaders(headers) {
|
|
|
1379
1404
|
};
|
|
1380
1405
|
}
|
|
1381
1406
|
/**
|
|
1382
|
-
* Extract the
|
|
1383
|
-
*
|
|
1407
|
+
* Extract the client IP from Forwarded, X-Forwarded-For, or X-Real-IP.
|
|
1408
|
+
*
|
|
1409
|
+
* ⚠️ SECURITY: the value is derived from client-supplied headers and must NOT be
|
|
1410
|
+
* used for access control, rate limiting or audit trails unless `trustedHops` is
|
|
1411
|
+
* set to the real number of proxies in front of the server. The returned string
|
|
1412
|
+
* is also not validated as an IP address.
|
|
1384
1413
|
*
|
|
1385
1414
|
* @param headers - Source headers object
|
|
1386
|
-
* @
|
|
1415
|
+
* @param options - Trust configuration
|
|
1416
|
+
* @returns The selected client IP, or `null` if none present
|
|
1387
1417
|
*/
|
|
1388
|
-
export function getClientIP(headers) {
|
|
1418
|
+
export function getClientIP(headers, options = {}) {
|
|
1419
|
+
const trustedHops = Math.max(0, Math.trunc(options.trustedHops ?? 0));
|
|
1389
1420
|
const fwd = normalizeForwardedHeaders(headers);
|
|
1390
|
-
if (fwd.for.length > 0)
|
|
1391
|
-
|
|
1392
|
-
|
|
1421
|
+
if (fwd.for.length > 0) {
|
|
1422
|
+
const list = fwd.for;
|
|
1423
|
+
// Right-most entry is the closest hop. trustedHops = 1 → the entry the
|
|
1424
|
+
// nearest trusted proxy appended; index = length - trustedHops.
|
|
1425
|
+
const index = trustedHops === 0 ? 0 : list.length - trustedHops;
|
|
1426
|
+
const value = list[index] ?? list[0];
|
|
1427
|
+
return stripIPPortAndBrackets(value);
|
|
1428
|
+
}
|
|
1429
|
+
return stripIPPortAndBrackets(headers.get(HeaderName.XRealIP) ?? "") || null;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Normalize a Forwarded/XFF entry to a bare host: strip `for="…"` quoting,
|
|
1433
|
+
* `[ipv6]:port`, and a bare `:port` suffix.
|
|
1434
|
+
*/
|
|
1435
|
+
function stripIPPortAndBrackets(value) {
|
|
1436
|
+
let v = value.trim();
|
|
1437
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2)
|
|
1438
|
+
v = v.slice(1, -1);
|
|
1439
|
+
if (v.startsWith("[")) {
|
|
1440
|
+
const end = v.indexOf("]");
|
|
1441
|
+
if (end !== -1)
|
|
1442
|
+
return v.slice(1, end);
|
|
1443
|
+
}
|
|
1444
|
+
// Only strip a trailing :digits (never the colons inside an IPv6 literal).
|
|
1445
|
+
const m = /^(.*):\d+$/.exec(v);
|
|
1446
|
+
if (m && (m[1] ?? "").includes("."))
|
|
1447
|
+
return m[1];
|
|
1448
|
+
return v;
|
|
1393
1449
|
}
|
|
1394
1450
|
/**
|
|
1395
1451
|
* Parse a Retry-After header which may be either delta-seconds or an
|