kinetex 1.3.0 → 1.4.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 +246 -9
- package/dist/browser/kinetex.esm.js +38 -22
- package/dist/browser/kinetex.js +2545 -550
- package/dist/browser/kinetex.min.js +38 -22
- package/dist/cjs/aws-sigv4.js +133 -19
- package/dist/cjs/cache.js +49 -7
- package/dist/cjs/circuit-breaker.js +45 -3
- package/dist/cjs/client.js +387 -104
- package/dist/cjs/cookie-parser.js +103 -5
- package/dist/cjs/cookie-store.js +125 -28
- package/dist/cjs/core.js +465 -66
- package/dist/cjs/dedup.js +49 -11
- package/dist/cjs/digest.js +160 -24
- package/dist/cjs/graphql.js +164 -24
- package/dist/cjs/headers.js +303 -45
- package/dist/cjs/interceptors.js +221 -7
- package/dist/cjs/lifecycle.js +89 -40
- package/dist/cjs/logging.js +168 -15
- package/dist/cjs/mod.js +3 -2
- package/dist/cjs/pagination.js +247 -22
- package/dist/cjs/progress.js +177 -27
- package/dist/cjs/proxy.js +412 -0
- package/dist/cjs/response.js +316 -47
- package/dist/cjs/socks5.js +131 -15
- package/dist/cjs/sse.js +173 -43
- package/dist/cjs/url.js +191 -45
- package/dist/cjs/utils.js +222 -48
- package/dist/cjs/ws.js +19 -10
- package/dist/esm/aws-sigv4.js +133 -19
- package/dist/esm/aws-sigv4.js.map +1 -1
- package/dist/esm/cache.js +49 -7
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/circuit-breaker.js +45 -3
- package/dist/esm/circuit-breaker.js.map +1 -1
- package/dist/esm/client.js +387 -104
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/cookie-parser.js +103 -5
- package/dist/esm/cookie-parser.js.map +1 -1
- package/dist/esm/cookie-store.js +125 -28
- package/dist/esm/cookie-store.js.map +1 -1
- package/dist/esm/core.js +465 -66
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/dedup.js +49 -11
- package/dist/esm/dedup.js.map +1 -1
- package/dist/esm/digest.js +160 -24
- package/dist/esm/digest.js.map +1 -1
- package/dist/esm/graphql.js +164 -24
- package/dist/esm/graphql.js.map +1 -1
- package/dist/esm/headers.js +303 -45
- package/dist/esm/headers.js.map +1 -1
- package/dist/esm/interceptors.js +221 -7
- package/dist/esm/interceptors.js.map +1 -1
- package/dist/esm/lifecycle.js +89 -40
- package/dist/esm/lifecycle.js.map +1 -1
- package/dist/esm/logging.js +168 -15
- package/dist/esm/logging.js.map +1 -1
- package/dist/esm/mod.js +3 -2
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/pagination.js +247 -22
- package/dist/esm/pagination.js.map +1 -1
- package/dist/esm/progress.js +177 -27
- package/dist/esm/progress.js.map +1 -1
- package/dist/esm/proxy.js +413 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/response.js +316 -47
- package/dist/esm/response.js.map +1 -1
- package/dist/esm/socks5.js +131 -15
- package/dist/esm/socks5.js.map +1 -1
- package/dist/esm/sse.js +173 -43
- package/dist/esm/sse.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/url.js +191 -45
- package/dist/esm/url.js.map +1 -1
- package/dist/esm/utils.js +222 -48
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/ws.js +19 -10
- package/dist/esm/ws.js.map +1 -1
- package/dist/types/aws-sigv4.d.ts.map +1 -1
- package/dist/types/cache.d.ts +19 -1
- package/dist/types/cache.d.ts.map +1 -1
- package/dist/types/circuit-breaker.d.ts +14 -1
- package/dist/types/circuit-breaker.d.ts.map +1 -1
- package/dist/types/client.d.ts +69 -11
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/cookie-parser.d.ts +0 -17
- 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 +103 -25
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/dedup.d.ts.map +1 -1
- package/dist/types/digest.d.ts +17 -37
- package/dist/types/digest.d.ts.map +1 -1
- package/dist/types/graphql.d.ts.map +1 -1
- package/dist/types/headers.d.ts +45 -27
- package/dist/types/headers.d.ts.map +1 -1
- package/dist/types/interceptors.d.ts +102 -0
- package/dist/types/interceptors.d.ts.map +1 -1
- package/dist/types/lifecycle.d.ts +19 -2
- package/dist/types/lifecycle.d.ts.map +1 -1
- package/dist/types/logging.d.ts +22 -3
- package/dist/types/logging.d.ts.map +1 -1
- package/dist/types/mod.d.ts +5 -3
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/pagination.d.ts +0 -25
- package/dist/types/pagination.d.ts.map +1 -1
- package/dist/types/progress.d.ts +1 -1
- package/dist/types/progress.d.ts.map +1 -1
- package/dist/types/proxy.d.ts +50 -0
- package/dist/types/proxy.d.ts.map +1 -0
- package/dist/types/response.d.ts +7 -1
- package/dist/types/response.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 +114 -3
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/url.d.ts +0 -14
- package/dist/types/url.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/types/ws.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cjs/utils.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-runtime utilities for type safety, validation, and security.
|
|
3
3
|
*/
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
4
|
let _process;
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
5
|
let _Buffer;
|
|
8
6
|
// Check globalThis.process for Node.js runtime detection (no dynamic import needed)
|
|
9
7
|
function getProcess() {
|
|
@@ -47,7 +45,17 @@ const DEFAULT_LIMITS = {
|
|
|
47
45
|
* @returns Parse result with success/failure information
|
|
48
46
|
*/
|
|
49
47
|
export function safeJSONParse(text, options = {}) {
|
|
50
|
-
|
|
48
|
+
// `{ ...DEFAULT_LIMITS, ...options }` copies an *explicitly present*
|
|
49
|
+
// `undefined` straight over a real limit, and every comparison this file
|
|
50
|
+
// makes is `<=` / `>` against a number — so `{ maxDepth: undefined }` did not
|
|
51
|
+
// mean "use the default", it meant "there is no depth limit", silently. That
|
|
52
|
+
// is the shape config plumbing produces (`{ maxDepth: env.MAX_DEPTH }`), and
|
|
53
|
+
// the `Required<>` on `limits` says the opposite. Only defined values merge.
|
|
54
|
+
const limits = { ...DEFAULT_LIMITS };
|
|
55
|
+
for (const [key, value] of Object.entries(options)) {
|
|
56
|
+
if (value !== undefined)
|
|
57
|
+
limits[key] = value;
|
|
58
|
+
}
|
|
51
59
|
// Check string length first
|
|
52
60
|
if (text.length > limits.maxStringLength) {
|
|
53
61
|
return {
|
|
@@ -241,6 +249,44 @@ export function parseUntrustedJSON(text) {
|
|
|
241
249
|
// ============================================================================
|
|
242
250
|
// §2 TYPE GUARDS
|
|
243
251
|
// ============================================================================
|
|
252
|
+
/**
|
|
253
|
+
* Exact brand test for a platform type.
|
|
254
|
+
*
|
|
255
|
+
* `instanceof` is authoritative in the current realm and — unlike a
|
|
256
|
+
* constructor-name check — survives subclasses, so `new File()` is still a
|
|
257
|
+
* `Blob`. It fails across realms (an `iframe`'s `Headers` is a different
|
|
258
|
+
* constructor), so the platform's own `Symbol.toStringTag` is consulted as a
|
|
259
|
+
* fallback: that is the tag the runtime sets on the real object, and the one
|
|
260
|
+
* thing a `Map` or a `Set` does not carry.
|
|
261
|
+
*
|
|
262
|
+
* The previous guards duck-typed on a *single* method name, which made the
|
|
263
|
+
* most common built-ins in the language pass for types they are not:
|
|
264
|
+
* `Map` and `Set` both have `has`, `FormData` has `forEach`, so
|
|
265
|
+
* `isURLSearchParams(new Map())`, `isURLSearchParams(new Set())` and
|
|
266
|
+
* `isHeaders(new FormData())` were all `true`. Meanwhile `isBlob` compared
|
|
267
|
+
* `constructor.name === "Blob"` and rejected every `File`, which is a `Blob`.
|
|
268
|
+
*/
|
|
269
|
+
function hasBrand(value, ctor, tag, required) {
|
|
270
|
+
if (value === null || typeof value !== "object")
|
|
271
|
+
return false;
|
|
272
|
+
if (typeof ctor === "function") {
|
|
273
|
+
try {
|
|
274
|
+
if (value instanceof ctor)
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// A Proxy with a hostile getPrototypeOf — fall through to the brand.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Cross-realm: the runtime's own brand, plus the methods that make the
|
|
282
|
+
// value usable as the type. The brand alone is one getter away from
|
|
283
|
+
// anything, and a guard whose whole job is to route a value to the right
|
|
284
|
+
// code path is not one that should hand it out on a say-so.
|
|
285
|
+
if (Object.prototype.toString.call(value) !== `[object ${tag}]`)
|
|
286
|
+
return false;
|
|
287
|
+
const obj = value;
|
|
288
|
+
return required.every((m) => typeof obj[m] === "function");
|
|
289
|
+
}
|
|
244
290
|
/**
|
|
245
291
|
* Type guard for Uint8Array.
|
|
246
292
|
*
|
|
@@ -266,10 +312,7 @@ export function isArrayBuffer(value) {
|
|
|
266
312
|
* @returns True if the value is a ReadableStream.
|
|
267
313
|
*/
|
|
268
314
|
export function isReadableStream(value) {
|
|
269
|
-
return (value
|
|
270
|
-
typeof value === "object" &&
|
|
271
|
-
"getReader" in value &&
|
|
272
|
-
typeof value.getReader === "function");
|
|
315
|
+
return hasBrand(value, globalThis.ReadableStream, "ReadableStream", ["getReader", "cancel"]);
|
|
273
316
|
}
|
|
274
317
|
/**
|
|
275
318
|
* Type guard for Headers.
|
|
@@ -278,10 +321,7 @@ export function isReadableStream(value) {
|
|
|
278
321
|
* @returns True if the value is a Headers instance.
|
|
279
322
|
*/
|
|
280
323
|
export function isHeaders(value) {
|
|
281
|
-
return (value
|
|
282
|
-
typeof value === "object" &&
|
|
283
|
-
"forEach" in value &&
|
|
284
|
-
typeof value.forEach === "function");
|
|
324
|
+
return hasBrand(value, globalThis.Headers, "Headers", ["get", "set", "append", "forEach"]);
|
|
285
325
|
}
|
|
286
326
|
/**
|
|
287
327
|
* Type guard for AbortSignal.
|
|
@@ -290,10 +330,10 @@ export function isHeaders(value) {
|
|
|
290
330
|
* @returns True if the value is an AbortSignal.
|
|
291
331
|
*/
|
|
292
332
|
export function isAbortSignal(value) {
|
|
293
|
-
return (value
|
|
294
|
-
|
|
295
|
-
"
|
|
296
|
-
|
|
333
|
+
return hasBrand(value, globalThis.AbortSignal, "AbortSignal", [
|
|
334
|
+
"addEventListener",
|
|
335
|
+
"removeEventListener",
|
|
336
|
+
]);
|
|
297
337
|
}
|
|
298
338
|
/**
|
|
299
339
|
* Type guard for plain objects (\[object Object\]).
|
|
@@ -302,9 +342,17 @@ export function isAbortSignal(value) {
|
|
|
302
342
|
* @returns True if the value is a plain Object.
|
|
303
343
|
*/
|
|
304
344
|
export function isPlainObject(value) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
345
|
+
if (value === null || typeof value !== "object")
|
|
346
|
+
return false;
|
|
347
|
+
// `Object.prototype.toString.call(x) === "[object Object]"` is the classic
|
|
348
|
+
// wrong way to write this: it is true for *every* object whose class does
|
|
349
|
+
// not override Symbol.toStringTag, so `new (class { constructor() { this.a = 1 } })()`
|
|
350
|
+
// passed — which is the opposite of what "plain" means, and the reason the
|
|
351
|
+
// guard cannot be trusted for the copy/merge decisions its name invites.
|
|
352
|
+
// What makes an object plain is its prototype: Object.prototype, or none at
|
|
353
|
+
// all (a null-prototype object is a dictionary, and still plain).
|
|
354
|
+
const proto = Object.getPrototypeOf(value);
|
|
355
|
+
return proto === Object.prototype || proto === null;
|
|
308
356
|
}
|
|
309
357
|
/**
|
|
310
358
|
* Type guard for FormData.
|
|
@@ -313,9 +361,7 @@ export function isPlainObject(value) {
|
|
|
313
361
|
* @returns True if the value is a FormData instance.
|
|
314
362
|
*/
|
|
315
363
|
export function isFormData(value) {
|
|
316
|
-
return (value
|
|
317
|
-
typeof value === "object" &&
|
|
318
|
-
value.constructor?.name === "FormData");
|
|
364
|
+
return hasBrand(value, globalThis.FormData, "FormData", ["append", "get", "getAll", "set"]);
|
|
319
365
|
}
|
|
320
366
|
/**
|
|
321
367
|
* Type guard for Blob.
|
|
@@ -324,7 +370,10 @@ export function isFormData(value) {
|
|
|
324
370
|
* @returns True if the value is a Blob instance.
|
|
325
371
|
*/
|
|
326
372
|
export function isBlob(value) {
|
|
327
|
-
|
|
373
|
+
// File extends Blob, and its own brand is "[object File]", so this is
|
|
374
|
+
// `instanceof`-first on purpose: a constructor-name check rejected every
|
|
375
|
+
// File, which is the single most common Blob-shaped value in a browser.
|
|
376
|
+
return hasBrand(value, globalThis.Blob, "Blob", ["arrayBuffer", "slice", "text", "stream"]);
|
|
328
377
|
}
|
|
329
378
|
/**
|
|
330
379
|
* Type guard for URLSearchParams.
|
|
@@ -333,10 +382,12 @@ export function isBlob(value) {
|
|
|
333
382
|
* @returns True if the value is a URLSearchParams instance.
|
|
334
383
|
*/
|
|
335
384
|
export function isURLSearchParams(value) {
|
|
336
|
-
return (value
|
|
337
|
-
|
|
338
|
-
"
|
|
339
|
-
|
|
385
|
+
return hasBrand(value, globalThis.URLSearchParams, "URLSearchParams", [
|
|
386
|
+
"append",
|
|
387
|
+
"get",
|
|
388
|
+
"getAll",
|
|
389
|
+
"sort",
|
|
390
|
+
]);
|
|
340
391
|
}
|
|
341
392
|
// ============================================================================
|
|
342
393
|
// §3 VALIDATION UTILITIES
|
|
@@ -366,13 +417,21 @@ export function isValidHeaderValue(value) {
|
|
|
366
417
|
return false;
|
|
367
418
|
if (value.length > 8192)
|
|
368
419
|
return false; // Reasonable length limit
|
|
369
|
-
//
|
|
370
|
-
//
|
|
420
|
+
// RFC 9110 §5.5: field-value is VCHAR / SP / HTAB / obs-text, where obs-text
|
|
421
|
+
// is %x80-FF. So a value may hold the Latin-1 range and nothing above it —
|
|
422
|
+
// there is no upper bound in the loop, which accepted an emoji and then let
|
|
423
|
+
// the very next `new Headers({ "X-A": "\u{1F600}" })` throw
|
|
424
|
+
// "Cannot convert argument to a ByteString because the character at index 0
|
|
425
|
+
// has a value of 55357". This guard is what a caller checks first, so a
|
|
426
|
+
// "valid" verdict that the runtime then refuses is worse than no verdict.
|
|
371
427
|
for (let i = 0; i < value.length; i++) {
|
|
372
428
|
const code = value.charCodeAt(i);
|
|
373
|
-
// No control characters (0-31 except 9=HT)
|
|
374
|
-
//
|
|
375
|
-
if ((code < 32 && code !== 9) || code === 127
|
|
429
|
+
// No control characters (0-31 except 9=HT) and no DEL — CR and LF are
|
|
430
|
+
// inside that range, so this is also the header-injection guard.
|
|
431
|
+
if ((code < 32 && code !== 9) || code === 127)
|
|
432
|
+
return false;
|
|
433
|
+
// Above obs-text: a code point no ByteString header value can carry.
|
|
434
|
+
if (code > 0xff)
|
|
376
435
|
return false;
|
|
377
436
|
}
|
|
378
437
|
return true;
|
|
@@ -581,9 +640,7 @@ function isBlockedIPv6(b) {
|
|
|
581
640
|
if (b[0] === 0x20 && b[1] === 0x01 && b[2] === 0x0d && b[3] === 0xb8)
|
|
582
641
|
return true;
|
|
583
642
|
// ::ffff:0:0/96 — IPv4-mapped → apply the IPv4 checks to the tail
|
|
584
|
-
if (b.slice(0, 10).every((x) => x === 0) &&
|
|
585
|
-
b[10] === 0xff &&
|
|
586
|
-
b[11] === 0xff) {
|
|
643
|
+
if (b.slice(0, 10).every((x) => x === 0) && b[10] === 0xff && b[11] === 0xff) {
|
|
587
644
|
return isBlockedIPv4(read32(12));
|
|
588
645
|
}
|
|
589
646
|
// ::/96 — deprecated IPv4-compatible → apply the IPv4 checks to the tail
|
|
@@ -712,7 +769,24 @@ export function deepClone(value) {
|
|
|
712
769
|
value.forEach((v) => cloned.add(deepClone(v)));
|
|
713
770
|
return cloned;
|
|
714
771
|
}
|
|
715
|
-
|
|
772
|
+
// A class instance is rebuilt on *its own* prototype. The walk below
|
|
773
|
+
// produced a bare `{}` for every object that was not a plain one, so
|
|
774
|
+
// `deepClone(new (class { m() { return 1 } })())` came back as an `Object`
|
|
775
|
+
// with `m` gone — the clone was no longer the thing it was cloned from, and
|
|
776
|
+
// the failure was silent until something called a method. `new (proto)()` is
|
|
777
|
+
// not generally callable, so the instance is created with its prototype
|
|
778
|
+
// attached and its own properties copied across.
|
|
779
|
+
//
|
|
780
|
+
// A *host* object — RegExp, URL, a typed array, an Error — is not: those
|
|
781
|
+
// carry internal slots that `Object.create` cannot produce, and
|
|
782
|
+
// `Object.create(RegExp.prototype)` satisfies `instanceof` while throwing
|
|
783
|
+
// "called on non-RegExp object" the moment a getter is touched. The brand is
|
|
784
|
+
// the discriminator, and it is exact: a class instance reports the plain
|
|
785
|
+
// `[object Object]`, because the brand comes from the runtime, not the
|
|
786
|
+
// constructor. Those keep the dictionary form they always had.
|
|
787
|
+
const proto = Object.getPrototypeOf(value);
|
|
788
|
+
const isClassInstance = Object.prototype.toString.call(value) === "[object Object]";
|
|
789
|
+
const cloned = proto === Object.prototype || proto === null || !isClassInstance ? {} : Object.create(proto);
|
|
716
790
|
for (const key in value) {
|
|
717
791
|
// Prototype-pollution guard: never copy __proto__ / constructor / prototype
|
|
718
792
|
if (key === "__proto__" || key === "constructor" || key === "prototype")
|
|
@@ -742,7 +816,27 @@ export function isPromise(value) {
|
|
|
742
816
|
*/
|
|
743
817
|
export function createStructuredError(message, context) {
|
|
744
818
|
const error = new Error(message);
|
|
745
|
-
Object.assign
|
|
819
|
+
// `Object.assign` writes through [[Set]], so a context carrying an *own*
|
|
820
|
+
// `__proto__` key — which is exactly what `JSON.parse('{"__proto\u003a…}')`
|
|
821
|
+
// produces, and what a caller forwarding a remote error payload hands over —
|
|
822
|
+
// did not add a field. It reached the inherited `__proto__` setter and
|
|
823
|
+
// replaced the error's prototype, so the attacker-supplied object sat in the
|
|
824
|
+
// prototype chain of every error the client then went on to format, log or
|
|
825
|
+
// inspect. The one key that has to be expressible is written as a data
|
|
826
|
+
// property, which is what was meant.
|
|
827
|
+
for (const key of Object.keys(context)) {
|
|
828
|
+
const value = context[key];
|
|
829
|
+
if (key === "__proto__") {
|
|
830
|
+
Object.defineProperty(error, key, {
|
|
831
|
+
value,
|
|
832
|
+
writable: true,
|
|
833
|
+
enumerable: true,
|
|
834
|
+
configurable: true,
|
|
835
|
+
});
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
error[key] = value;
|
|
839
|
+
}
|
|
746
840
|
return error;
|
|
747
841
|
}
|
|
748
842
|
/**
|
|
@@ -758,11 +852,40 @@ export function formatError(error) {
|
|
|
758
852
|
context[key] = value;
|
|
759
853
|
}
|
|
760
854
|
}
|
|
761
|
-
const contextStr = Object.keys(context).length > 0 ? ` | ${
|
|
855
|
+
const contextStr = Object.keys(context).length > 0 ? ` | ${safeStringify(context)}` : "";
|
|
762
856
|
return `${error.name}: ${error.message}${contextStr}`;
|
|
763
857
|
}
|
|
764
858
|
return String(error);
|
|
765
859
|
}
|
|
860
|
+
/**
|
|
861
|
+
* `JSON.stringify` for a log line, on a value that is very likely to be
|
|
862
|
+
* hostile.
|
|
863
|
+
*
|
|
864
|
+
* The context attached by {@link createStructuredError} is whatever the failing
|
|
865
|
+
* call had in hand — a `request`, a `response`, a `cause` — and all three
|
|
866
|
+
* routinely point back at each other. `JSON.stringify` answers that with a
|
|
867
|
+
* thrown `TypeError: Converting circular structure to JSON`, and a BigInt with
|
|
868
|
+
* another, so the function whose entire job is to turn an error into a string
|
|
869
|
+
* threw instead, taking the `catch` that was logging it down with it.
|
|
870
|
+
*/
|
|
871
|
+
function safeStringify(value) {
|
|
872
|
+
const seen = new Set();
|
|
873
|
+
try {
|
|
874
|
+
return (JSON.stringify(value, (_key, v) => {
|
|
875
|
+
if (typeof v === "bigint")
|
|
876
|
+
return v.toString();
|
|
877
|
+
if (typeof v === "object" && v !== null) {
|
|
878
|
+
if (seen.has(v))
|
|
879
|
+
return "[Circular]";
|
|
880
|
+
seen.add(v);
|
|
881
|
+
}
|
|
882
|
+
return v;
|
|
883
|
+
}) ?? String(value));
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
return String(value);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
766
889
|
// ============================================================================
|
|
767
890
|
// §5 TIME UTILITIES
|
|
768
891
|
// ============================================================================
|
|
@@ -865,10 +988,16 @@ export function concatUint8Arrays(chunks) {
|
|
|
865
988
|
* @returns Uint8Array or null if unsupported type
|
|
866
989
|
*/
|
|
867
990
|
export function toUint8Array(data) {
|
|
991
|
+
// Both branches copy. The Uint8Array branch sliced but the ArrayBuffer
|
|
992
|
+
// branch wrapped, so the same call handed back a copy for one input type and
|
|
993
|
+
// a live view for the other: writing to the result of
|
|
994
|
+
// `toUint8Array(buffer)` wrote through to the caller's buffer, and two calls
|
|
995
|
+
// given the same buffer shared it. A function whose output is "the bytes" has
|
|
996
|
+
// to be the same kind of thing whichever type it was handed.
|
|
868
997
|
if (data instanceof Uint8Array)
|
|
869
998
|
return data.slice();
|
|
870
999
|
if (data instanceof ArrayBuffer)
|
|
871
|
-
return new Uint8Array(data);
|
|
1000
|
+
return new Uint8Array(data.slice(0));
|
|
872
1001
|
const b = getBuffer();
|
|
873
1002
|
if (b && b.isBuffer(data)) {
|
|
874
1003
|
return new Uint8Array(data);
|
|
@@ -929,12 +1058,37 @@ export function mergeSignals(...signals) {
|
|
|
929
1058
|
}
|
|
930
1059
|
if (validSignals.length === 1)
|
|
931
1060
|
return validSignals[0];
|
|
932
|
-
// Check if any signal is already aborted
|
|
933
|
-
|
|
1061
|
+
// Check if any signal is already aborted.
|
|
1062
|
+
//
|
|
1063
|
+
// `controller.abort()` with no argument installs the platform's generic
|
|
1064
|
+
// `AbortError: This operation was aborted`, which threw away *why* the call
|
|
1065
|
+
// was aborted. The two-live-signal branch below preserves the reason, and so
|
|
1066
|
+
// does the `AbortSignal.any` path this function prefers on every current
|
|
1067
|
+
// runtime — so the one case a caller is most likely to hit (a signal that
|
|
1068
|
+
// already fired, e.g. `AbortSignal.timeout()`) was the only one that lost
|
|
1069
|
+
// it. `interceptors.ts` re-aborts with `existing.reason` when merging, and
|
|
1070
|
+
// anything reading the merged signal's reason saw a bare AbortError where it
|
|
1071
|
+
// should have seen the caller's TimeoutError or their own Error.
|
|
1072
|
+
const preAborted = validSignals.find((s) => s.aborted);
|
|
1073
|
+
if (preAborted) {
|
|
934
1074
|
const controller = new AbortController();
|
|
935
|
-
controller.abort();
|
|
1075
|
+
controller.abort(preAborted.reason);
|
|
936
1076
|
return controller.signal;
|
|
937
1077
|
}
|
|
1078
|
+
// The platform primitive, where it exists. The hand-rolled version below
|
|
1079
|
+
// attached an `abort` listener to every input and only ever removed it when
|
|
1080
|
+
// one of those inputs fired — so merging a long-lived caller signal (a
|
|
1081
|
+
// request-scoped controller is the usual one) accumulated one listener per
|
|
1082
|
+
// merge, forever, and tripped Node's MaxListenersExceededWarning at 11.
|
|
1083
|
+
// The "9.11" self-cleanup that was supposed to release them could not run:
|
|
1084
|
+
// the merged controller is never handed out, so nothing but the inputs can
|
|
1085
|
+
// ever abort it. `AbortSignal.any` holds its inputs weakly and adds nothing
|
|
1086
|
+
// observable to them. Node 20.3+, Deno, Bun and current browsers have it;
|
|
1087
|
+
// the manual path remains for Node 18 and older.
|
|
1088
|
+
const nativeAny = AbortSignal.any;
|
|
1089
|
+
if (typeof nativeAny === "function") {
|
|
1090
|
+
return nativeAny.call(AbortSignal, validSignals);
|
|
1091
|
+
}
|
|
938
1092
|
const controller = new AbortController();
|
|
939
1093
|
// abort: fire the controller and clean up ALL listeners immediately.
|
|
940
1094
|
const abort = () => {
|
|
@@ -942,12 +1096,6 @@ export function mergeSignals(...signals) {
|
|
|
942
1096
|
s.removeEventListener("abort", abort);
|
|
943
1097
|
controller.abort(_abortError());
|
|
944
1098
|
};
|
|
945
|
-
// 9.11: when the merged signal itself aborts (e.g. from another path), also clean up.
|
|
946
|
-
// This prevents listener accumulation when callers abort the controller externally.
|
|
947
|
-
controller.signal.addEventListener("abort", () => {
|
|
948
|
-
for (const s of validSignals)
|
|
949
|
-
s.removeEventListener("abort", abort);
|
|
950
|
-
}, { once: true });
|
|
951
1099
|
for (const s of validSignals)
|
|
952
1100
|
s.addEventListener("abort", abort, { once: true });
|
|
953
1101
|
return controller.signal;
|
|
@@ -1088,7 +1236,33 @@ export function hasNativeFetch() {
|
|
|
1088
1236
|
export function normalizeHeaders(headers) {
|
|
1089
1237
|
const result = {};
|
|
1090
1238
|
headers.forEach((value, key) => {
|
|
1091
|
-
|
|
1239
|
+
const name = key.toLowerCase();
|
|
1240
|
+
// `__proto__` is a legal header name — it is made of token characters — and
|
|
1241
|
+
// `result[name] = value` is a [[Set]], so a response carrying it went
|
|
1242
|
+
// through the inherited setter, which ignores a primitive. The header did
|
|
1243
|
+
// not overwrite anything; it simply disappeared, and the caller reading the
|
|
1244
|
+
// normalized record never learned the response had sent it.
|
|
1245
|
+
if (name === "__proto__") {
|
|
1246
|
+
Object.defineProperty(result, name, {
|
|
1247
|
+
value,
|
|
1248
|
+
writable: true,
|
|
1249
|
+
enumerable: true,
|
|
1250
|
+
configurable: true,
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
// `Set-Cookie` is the one header the Fetch spec does NOT combine: it
|
|
1255
|
+
// yields each cookie as a separate `forEach` entry, while every other
|
|
1256
|
+
// repeated header arrives already joined with ", ". Assigning therefore
|
|
1257
|
+
// overwrote: a response setting two cookies normalized to the last one
|
|
1258
|
+
// only, and the first vanished with no error anywhere. `Headers.get()`
|
|
1259
|
+
// reports them combined as "a=1, b=2", so accumulate to match it — the
|
|
1260
|
+
// cookie jar splits this form back apart with `splitSetCookieHeaders`.
|
|
1261
|
+
if (name === "set-cookie" && Object.hasOwn(result, name)) {
|
|
1262
|
+
result[name] = `${result[name]}, ${value}`;
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
result[name] = value;
|
|
1092
1266
|
});
|
|
1093
1267
|
return result;
|
|
1094
1268
|
}
|
package/dist/cjs/ws.js
CHANGED
|
@@ -273,15 +273,15 @@ export class WSClient {
|
|
|
273
273
|
if (this._state === "OPEN")
|
|
274
274
|
return Promise.resolve();
|
|
275
275
|
return new Promise((resolve, reject) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
276
|
+
// The waiter is built first so the timeout can remove *this* entry by
|
|
277
|
+
// identity. It used to search for `w.resolve === resolve`, but the
|
|
278
|
+
// resolver stored on the queue is the wrapper below, not the promise's
|
|
279
|
+
// own `resolve` — so the search never matched, the timed-out waiter was
|
|
280
|
+
// never spliced out, and it stayed in `_openWaiters` for the lifetime
|
|
281
|
+
// of the client. A client whose `waitForOpen` timed out repeatedly grew
|
|
282
|
+
// that array without bound, and every later `open()`/`close()` walked
|
|
283
|
+
// the dead entries.
|
|
284
|
+
const waiter = {
|
|
285
285
|
resolve: () => {
|
|
286
286
|
if (tid)
|
|
287
287
|
clearTimeout(tid);
|
|
@@ -292,7 +292,16 @@ export class WSClient {
|
|
|
292
292
|
clearTimeout(tid);
|
|
293
293
|
reject(e);
|
|
294
294
|
},
|
|
295
|
-
}
|
|
295
|
+
};
|
|
296
|
+
const tid = timeoutMs > 0
|
|
297
|
+
? setTimeout(() => {
|
|
298
|
+
const i = this._openWaiters.indexOf(waiter);
|
|
299
|
+
if (i !== -1)
|
|
300
|
+
this._openWaiters.splice(i, 1);
|
|
301
|
+
reject(new WSConnectTimeoutError(this._url, timeoutMs));
|
|
302
|
+
}, timeoutMs)
|
|
303
|
+
: null;
|
|
304
|
+
this._openWaiters.push(waiter);
|
|
296
305
|
});
|
|
297
306
|
}
|
|
298
307
|
/**
|
package/dist/esm/aws-sigv4.js
CHANGED
|
@@ -162,12 +162,61 @@ export function imdsCredentials(options = {}) {
|
|
|
162
162
|
const credsRes = await fetchWithTimeout(`${endpoint}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`, { headers: { "x-aws-ec2-metadata-token": token } }, timeout);
|
|
163
163
|
if (!credsRes.ok)
|
|
164
164
|
throw new NetworkError(`IMDS credentials fetch failed: ${credsRes.status}`);
|
|
165
|
-
|
|
165
|
+
// Two failures used to escape this function unlabelled, and both of them
|
|
166
|
+
// turned into a silently broken signature rather than an error:
|
|
167
|
+
//
|
|
168
|
+
// - `Response.json()` throws a bare `SyntaxError` on a non-JSON body, so
|
|
169
|
+
// a proxy's HTML 502 page arrived with no `code` — a caller branching
|
|
170
|
+
// on `err.code === "ENETWORK"` never saw it, and the message said
|
|
171
|
+
// nothing about IMDS. Every other failure here is a `NetworkError`.
|
|
172
|
+
// - A well-formed but shapeless body parsed fine and produced a
|
|
173
|
+
// "successful" result: `{}` yielded `accessKeyId: undefined`, and
|
|
174
|
+
// `{"AccessKeyId": null, …}` yielded nulls. Both then went on to
|
|
175
|
+
// produce a SigV4 signature AWS rejects with an opaque
|
|
176
|
+
// `SignatureDoesNotMatch`, pointing at the caller rather than at the
|
|
177
|
+
// metadata endpoint that had actually answered with nonsense.
|
|
178
|
+
let data;
|
|
179
|
+
try {
|
|
180
|
+
data = (await credsRes.json());
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
throw new NetworkError(`IMDS credentials response was not JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
}
|
|
185
|
+
if (data === null || typeof data !== "object") {
|
|
186
|
+
throw new NetworkError(`IMDS credentials response was not an object: ${typeof data}`);
|
|
187
|
+
}
|
|
188
|
+
const { AccessKeyId, SecretAccessKey, Token, Expiration } = data;
|
|
189
|
+
const missing = [];
|
|
190
|
+
if (typeof AccessKeyId !== "string" || AccessKeyId.length === 0)
|
|
191
|
+
missing.push("AccessKeyId");
|
|
192
|
+
if (typeof SecretAccessKey !== "string" || SecretAccessKey.length === 0)
|
|
193
|
+
missing.push("SecretAccessKey");
|
|
194
|
+
if (typeof Token !== "string" || Token.length === 0)
|
|
195
|
+
missing.push("Token");
|
|
196
|
+
if (missing.length > 0) {
|
|
197
|
+
throw new NetworkError(`IMDS credentials response is missing ${missing.join(", ")} — ` +
|
|
198
|
+
"the metadata endpoint returned a body this client cannot sign with");
|
|
199
|
+
}
|
|
200
|
+
// Re-read through a narrowing helper: the checks above report every missing
|
|
201
|
+
// field at once, which is the right message but does not let the compiler
|
|
202
|
+
// carry the narrowing into this scope.
|
|
203
|
+
const requireString = (value, name) => {
|
|
204
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
205
|
+
throw new NetworkError(`IMDS credentials response is missing ${name}`);
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
};
|
|
209
|
+
const accessKeyId = requireString(AccessKeyId, "AccessKeyId");
|
|
210
|
+
const secretAccessKey = requireString(SecretAccessKey, "SecretAccessKey");
|
|
211
|
+
const sessionToken = requireString(Token, "Token");
|
|
166
212
|
return {
|
|
167
|
-
accessKeyId
|
|
168
|
-
secretAccessKey
|
|
169
|
-
sessionToken
|
|
170
|
-
|
|
213
|
+
accessKeyId,
|
|
214
|
+
secretAccessKey,
|
|
215
|
+
sessionToken,
|
|
216
|
+
// Optional: a role without an expiry is legal, and the signer treats a
|
|
217
|
+
// missing expiration as "do not cache". Omitted rather than set to
|
|
218
|
+
// `undefined` — the project uses exactOptionalPropertyTypes.
|
|
219
|
+
...(typeof Expiration === "string" ? { expiration: Expiration } : {}),
|
|
171
220
|
};
|
|
172
221
|
});
|
|
173
222
|
}
|
|
@@ -339,6 +388,12 @@ function buildCanonicalHeaders(headers, unsignedExtra) {
|
|
|
339
388
|
const entries = [];
|
|
340
389
|
for (const [name, value] of Object.entries(headers)) {
|
|
341
390
|
const lower = name.toLowerCase();
|
|
391
|
+
// An empty header name is not a valid HTTP field-name (RFC 9110 §5.1) and
|
|
392
|
+
// used to reach the canonical request verbatim, producing a `:value` line
|
|
393
|
+
// and a leading `;` in SignedHeaders. AWS answers that with an opaque
|
|
394
|
+
// SignatureDoesNotMatch rather than saying the header name was empty.
|
|
395
|
+
if (lower === "")
|
|
396
|
+
continue;
|
|
342
397
|
if (unsigned.has(lower) && !ALWAYS_SIGNED_HEADERS.has(lower))
|
|
343
398
|
continue;
|
|
344
399
|
// Trim + collapse internal whitespace
|
|
@@ -446,10 +501,19 @@ export async function signRequest(request, config) {
|
|
|
446
501
|
const amzDate = formatAmzDate(signingDate);
|
|
447
502
|
const dateStamp = formatDateStamp(signingDate);
|
|
448
503
|
const parsedUrl = new URL(request.url);
|
|
449
|
-
// Build the headers to sign — start from request headers
|
|
504
|
+
// Build the headers to sign — start from request headers.
|
|
505
|
+
//
|
|
506
|
+
// An explicitly-supplied `host` (in any casing) must WIN. Setting it is how
|
|
507
|
+
// you sign for a virtual-hosted-style bucket, a custom endpoint or a proxy.
|
|
508
|
+
// The URL host used to be injected unconditionally, which left both keys in
|
|
509
|
+
// the map whenever the caller used a different casing; `buildCanonicalHeaders`
|
|
510
|
+
// lowercased them and its dedupe step joined them, so the request was signed
|
|
511
|
+
// as `host:override.example,s3.amazonaws.com` — a host that can never
|
|
512
|
+
// validate, and one the library reported no error about.
|
|
513
|
+
const hasExplicitHost = Object.keys(request.headers).some((k) => k.toLowerCase() === "host");
|
|
450
514
|
const headers = {
|
|
451
515
|
...request.headers,
|
|
452
|
-
host: parsedUrl.host,
|
|
516
|
+
...(hasExplicitHost ? {} : { host: parsedUrl.host }),
|
|
453
517
|
"x-amz-date": amzDate,
|
|
454
518
|
};
|
|
455
519
|
if (config.unsignedPayload) {
|
|
@@ -498,11 +562,22 @@ export async function presignRequest(request, config, options = {}) {
|
|
|
498
562
|
const signingDate = resolveSigningDate(config);
|
|
499
563
|
const amzDate = formatAmzDate(signingDate);
|
|
500
564
|
const dateStamp = formatDateStamp(signingDate);
|
|
501
|
-
|
|
502
|
-
//
|
|
565
|
+
// Validate and CLAMP expiresIn. AWS enforces a hard maximum per service and
|
|
566
|
+
// rejects an out-of-range or fractional value with an opaque
|
|
567
|
+
// AuthorizationQueryParametersError at use time, long after the URL was
|
|
568
|
+
// handed out. The old code logged a warning and then wrote the caller's
|
|
569
|
+
// number straight into the query string, so `expiresIn: 0` produced
|
|
570
|
+
// `X-Amz-Expires=0`, `expiresIn: -1` produced `=-1`, and `expiresIn: 1e9`
|
|
571
|
+
// produced a link valid for ~31 years — every one of them permanently
|
|
572
|
+
// unusable, with a console warning as the only signal. Clamping can only
|
|
573
|
+
// shorten a link, never lengthen one, so it is the safe direction.
|
|
503
574
|
const maxExpires = config.service === "s3" ? 604800 : 3600;
|
|
504
|
-
|
|
505
|
-
|
|
575
|
+
const requested = options.expiresIn ?? 3600;
|
|
576
|
+
const expiresIn = Number.isFinite(requested)
|
|
577
|
+
? Math.min(Math.max(Math.floor(requested), 1), maxExpires)
|
|
578
|
+
: maxExpires;
|
|
579
|
+
if (expiresIn !== requested) {
|
|
580
|
+
console.warn(`[aws-sigv4] presignRequest: expiresIn must be an integer in 1-${maxExpires} for ${config.service}, got ${requested} — clamped to ${expiresIn}`);
|
|
506
581
|
}
|
|
507
582
|
const parsedUrl = new URL(request.url);
|
|
508
583
|
const credentialScope = `${dateStamp}/${config.region}/${config.service}/aws4_request`;
|
|
@@ -520,10 +595,14 @@ export async function presignRequest(request, config, options = {}) {
|
|
|
520
595
|
parsedUrl.searchParams.set(k, v);
|
|
521
596
|
}
|
|
522
597
|
}
|
|
523
|
-
// Determine signed headers (only "host" for presigned URLs typically)
|
|
598
|
+
// Determine signed headers (only "host" for presigned URLs typically).
|
|
599
|
+
// An explicit `host` wins here for the same reason it does in sign() — a
|
|
600
|
+
// differently-cased caller header used to be merged with the URL host into
|
|
601
|
+
// `host:override.example,s3.amazonaws.com`, which can never validate.
|
|
602
|
+
const presignHasExplicitHost = Object.keys(request.headers).some((k) => k.toLowerCase() === "host");
|
|
524
603
|
const headers = {
|
|
525
604
|
...request.headers,
|
|
526
|
-
host: parsedUrl.host,
|
|
605
|
+
...(presignHasExplicitHost ? {} : { host: parsedUrl.host }),
|
|
527
606
|
};
|
|
528
607
|
const unsignedHdrs = config.unsignedHeaders ?? [];
|
|
529
608
|
// For presigned URLs, payload hash is always UNSIGNED-PAYLOAD
|
|
@@ -666,14 +745,49 @@ export async function signS3PostPolicy(policy, config) {
|
|
|
666
745
|
* Returns 0 if the header is absent or unparseable.
|
|
667
746
|
*/
|
|
668
747
|
export function detectClockSkew(responseHeaders) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
748
|
+
// `x-amz-date` is AWS's own signed timestamp and is the one present on the
|
|
749
|
+
// clock-skew error itself. `Date` is generated by whatever proxy fronts the
|
|
750
|
+
// endpoint and is frequently absent. Reading only `Date` meant a skew was
|
|
751
|
+
// silently undetectable on exactly the responses that carry it.
|
|
752
|
+
const amzDate = responseHeaders["x-amz-date"] ?? responseHeaders["X-Amz-Date"];
|
|
753
|
+
let serverTime;
|
|
754
|
+
if (amzDate !== undefined) {
|
|
755
|
+
serverTime = parseAmzDate(amzDate);
|
|
756
|
+
if (isNaN(serverTime))
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
const dateHeader = responseHeaders["date"] ?? responseHeaders["Date"];
|
|
761
|
+
if (!dateHeader)
|
|
762
|
+
return 0;
|
|
763
|
+
serverTime = new Date(dateHeader).getTime();
|
|
764
|
+
if (isNaN(serverTime))
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
675
767
|
return Math.round((serverTime - Date.now()) / 1000);
|
|
676
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* Parse AWS's basic ISO 8601 timestamp (`20300101T120000Z`), which is not a
|
|
771
|
+
* form `Date` can parse.
|
|
772
|
+
*
|
|
773
|
+
* @param value - Candidate `x-amz-date` value
|
|
774
|
+
* @returns Epoch milliseconds, or `NaN` when the value is not in that form
|
|
775
|
+
*/
|
|
776
|
+
function parseAmzDate(value) {
|
|
777
|
+
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(value.trim());
|
|
778
|
+
if (!m)
|
|
779
|
+
return NaN;
|
|
780
|
+
const [, y, mo, d, h, mi, sec] = m;
|
|
781
|
+
// Reject values that roll over silently (e.g. month 13) instead of
|
|
782
|
+
// producing a date in the following month.
|
|
783
|
+
const t = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(sec));
|
|
784
|
+
const dt = new Date(t);
|
|
785
|
+
if (dt.getUTCFullYear() !== Number(y) || dt.getUTCMonth() + 1 !== Number(mo))
|
|
786
|
+
return NaN;
|
|
787
|
+
if (dt.getUTCDate() !== Number(d))
|
|
788
|
+
return NaN;
|
|
789
|
+
return t;
|
|
790
|
+
}
|
|
677
791
|
/**
|
|
678
792
|
* Determine if an error response is a clock skew error.
|
|
679
793
|
*/
|