kinetex 1.2.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.
Files changed (86) hide show
  1. package/README.md +919 -445
  2. package/dist/browser/kinetex.esm.js +18 -18
  3. package/dist/browser/kinetex.js +663 -246
  4. package/dist/browser/kinetex.min.js +18 -18
  5. package/dist/cjs/aws-sigv4.js +4 -1
  6. package/dist/cjs/cache.js +52 -14
  7. package/dist/cjs/circuit-breaker.js +24 -4
  8. package/dist/cjs/client.js +495 -131
  9. package/dist/cjs/cookie-parser.js +7 -4
  10. package/dist/cjs/cookie-store.js +16 -8
  11. package/dist/cjs/core.js +46 -7
  12. package/dist/cjs/dedup.js +9 -7
  13. package/dist/cjs/digest.js +26 -0
  14. package/dist/cjs/headers.js +64 -8
  15. package/dist/cjs/interceptors.js +64 -22
  16. package/dist/cjs/logging.js +1 -1
  17. package/dist/cjs/pagination.js +14 -6
  18. package/dist/cjs/progress.js +122 -42
  19. package/dist/cjs/socks5.js +36 -21
  20. package/dist/cjs/sse.js +48 -11
  21. package/dist/cjs/worker.js +6 -6
  22. package/dist/cjs/ws.js +13 -6
  23. package/dist/esm/aws-sigv4.js +4 -1
  24. package/dist/esm/aws-sigv4.js.map +1 -1
  25. package/dist/esm/cache.js +52 -14
  26. package/dist/esm/cache.js.map +1 -1
  27. package/dist/esm/circuit-breaker.js +24 -4
  28. package/dist/esm/circuit-breaker.js.map +1 -1
  29. package/dist/esm/client.js +495 -131
  30. package/dist/esm/client.js.map +1 -1
  31. package/dist/esm/cookie-parser.js +7 -4
  32. package/dist/esm/cookie-parser.js.map +1 -1
  33. package/dist/esm/cookie-store.js +16 -8
  34. package/dist/esm/cookie-store.js.map +1 -1
  35. package/dist/esm/core.js +46 -7
  36. package/dist/esm/core.js.map +1 -1
  37. package/dist/esm/dedup.js +9 -7
  38. package/dist/esm/dedup.js.map +1 -1
  39. package/dist/esm/digest.js +26 -0
  40. package/dist/esm/digest.js.map +1 -1
  41. package/dist/esm/headers.js +64 -8
  42. package/dist/esm/headers.js.map +1 -1
  43. package/dist/esm/interceptors.js +64 -22
  44. package/dist/esm/interceptors.js.map +1 -1
  45. package/dist/esm/logging.js +1 -1
  46. package/dist/esm/pagination.js +14 -6
  47. package/dist/esm/pagination.js.map +1 -1
  48. package/dist/esm/progress.js +122 -42
  49. package/dist/esm/progress.js.map +1 -1
  50. package/dist/esm/socks5.js +36 -21
  51. package/dist/esm/socks5.js.map +1 -1
  52. package/dist/esm/sse.js +48 -11
  53. package/dist/esm/sse.js.map +1 -1
  54. package/dist/esm/types.js.map +1 -1
  55. package/dist/esm/worker.js +6 -6
  56. package/dist/esm/worker.js.map +1 -1
  57. package/dist/esm/ws.js +13 -6
  58. package/dist/esm/ws.js.map +1 -1
  59. package/dist/types/aws-sigv4.d.ts.map +1 -1
  60. package/dist/types/cache.d.ts +8 -1
  61. package/dist/types/cache.d.ts.map +1 -1
  62. package/dist/types/circuit-breaker.d.ts.map +1 -1
  63. package/dist/types/client.d.ts +29 -12
  64. package/dist/types/client.d.ts.map +1 -1
  65. package/dist/types/cookie-parser.d.ts.map +1 -1
  66. package/dist/types/cookie-store.d.ts.map +1 -1
  67. package/dist/types/core.d.ts +6 -0
  68. package/dist/types/core.d.ts.map +1 -1
  69. package/dist/types/dedup.d.ts +0 -7
  70. package/dist/types/dedup.d.ts.map +1 -1
  71. package/dist/types/digest.d.ts +14 -0
  72. package/dist/types/digest.d.ts.map +1 -1
  73. package/dist/types/headers.d.ts +25 -10
  74. package/dist/types/headers.d.ts.map +1 -1
  75. package/dist/types/interceptors.d.ts.map +1 -1
  76. package/dist/types/logging.d.ts +1 -1
  77. package/dist/types/pagination.d.ts.map +1 -1
  78. package/dist/types/progress.d.ts.map +1 -1
  79. package/dist/types/socks5.d.ts.map +1 -1
  80. package/dist/types/sse.d.ts.map +1 -1
  81. package/dist/types/types.d.ts +25 -2
  82. package/dist/types/types.d.ts.map +1 -1
  83. package/dist/types/worker.d.ts +6 -6
  84. package/dist/types/worker.d.ts.map +1 -1
  85. package/dist/types/ws.d.ts.map +1 -1
  86. package/package.json +2 -2
@@ -156,7 +156,10 @@ export function imdsCredentials(options = {}) {
156
156
  if (!role)
157
157
  throw new NetworkError("No IAM role found in IMDS response");
158
158
  // Step 3: Get credentials for role
159
- const credsRes = await fetchWithTimeout(`${endpoint}/latest/meta-data/iam/security-credentials/${role}`, { headers: { "x-aws-ec2-metadata-token": token } }, timeout);
159
+ // encodeURIComponent: the role name comes from the metadata response and
160
+ // was interpolated raw, so a `../` or `?` in it could redirect the
161
+ // credentials request to another path on the metadata endpoint.
162
+ const credsRes = await fetchWithTimeout(`${endpoint}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`, { headers: { "x-aws-ec2-metadata-token": token } }, timeout);
160
163
  if (!credsRes.ok)
161
164
  throw new NetworkError(`IMDS credentials fetch failed: ${credsRes.status}`);
162
165
  const data = (await credsRes.json());
package/dist/cjs/cache.js CHANGED
@@ -326,14 +326,17 @@ function buildVaryKey(varyHeader, requestHeaders) {
326
326
  /**
327
327
  * Normalize a string for use in a cache key to prevent injection attacks.
328
328
  * Removes control characters and normalizes whitespace.
329
+ *
330
+ * The control characters are the entire point of the match: they are what has
331
+ * to be stripped out of a URL before it is used as a cache-key component, so
332
+ * `no-control-regex` is suppressed here deliberately.
329
333
  */
334
+ // deno-lint-ignore no-control-regex
335
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
330
336
  function normalizeCacheKeyPart(part) {
331
- const controlRanges = [
332
- String.fromCharCode(0x00) + "-" + String.fromCharCode(0x1f),
333
- String.fromCharCode(0x7f) + "-" + String.fromCharCode(0x9f),
334
- ].join("");
335
- const controlRegex = new RegExp(`[${controlRanges}]`, "g");
336
- return part.replaceAll(controlRegex, "").replaceAll(/\s+/g, " ").trim();
337
+ // The regex is module-level: rebuilding it from String.fromCharCode on every
338
+ // call (for every URL component, for every cache key) was pure overhead.
339
+ return part.replaceAll(CONTROL_CHARS, "").replaceAll(/\s+/g, " ").trim();
337
340
  }
338
341
  /**
339
342
  * Generate a cryptographic fingerprint of auth-sensitive headers using SHA-256.
@@ -341,10 +344,17 @@ function normalizeCacheKeyPart(part) {
341
344
  * Returns "" when no auth headers are present (shared/anonymous cache).
342
345
  */
343
346
  export async function getAuthFingerprint(headers) {
344
- const authHeaders = ["authorization", "cookie", "x-api-key", "x-auth-token"];
347
+ // HTTP header names are case-insensitive, so a caller passing
348
+ // `Authorization` must fingerprint identically to `authorization` —
349
+ // otherwise a cache key or dedup key silently changes meaning with the
350
+ // casing of the caller's object literal.
351
+ const lowered = new Map();
352
+ for (const [name, value] of Object.entries(headers)) {
353
+ lowered.set(name.toLowerCase(), value);
354
+ }
345
355
  const parts = [];
346
- for (const header of authHeaders) {
347
- const value = headers[header.toLowerCase()];
356
+ for (const header of CREDENTIAL_HEADERS) {
357
+ const value = lowered.get(header);
348
358
  if (value)
349
359
  parts.push(`${header}=${value}`);
350
360
  }
@@ -358,6 +368,29 @@ export async function getAuthFingerprint(headers) {
358
368
  .slice(0, 32); // 128-bit prefix — ample for auth isolation
359
369
  return `auth:${hex}`;
360
370
  }
371
+ /**
372
+ * Every header kinetex treats as credential-bearing. Shared by the cache-key
373
+ * fingerprint, the dedup key and the cross-origin redirect strip list so the
374
+ * three can never drift apart again: a header missing from the fingerprint list
375
+ * lets two different users share one cache entry or one coalesced response.
376
+ */
377
+ export const CREDENTIAL_HEADERS = [
378
+ "authorization",
379
+ "proxy-authorization",
380
+ "cookie",
381
+ "x-api-key",
382
+ "apikey",
383
+ "api-key",
384
+ "x-auth-token",
385
+ "x-access-token",
386
+ "x-refresh-token",
387
+ "x-session-id",
388
+ "x-session-token",
389
+ "x-secret",
390
+ "x-secret-key",
391
+ "x-private-key",
392
+ "x-csrf-token",
393
+ ];
361
394
  /**
362
395
  * Default cache key function.
363
396
  * Normalizes: method, URL (sorted query params, stripped hash), auth fingerprint.
@@ -583,8 +616,10 @@ export class HTTPCache {
583
616
  maxAbsoluteAgeMs: config.maxAbsoluteAgeMs ?? 7 * 24 * 60 * 60 * 1000, // 7 days
584
617
  honorCacheControl: config.honorCacheControl ?? true,
585
618
  cacheMethods: config.cacheMethods ?? ["GET", "HEAD"],
619
+ // 304 is intentionally absent: the client revalidates rather than
620
+ // storing a 304, so listing it only implied a guarantee that never held.
586
621
  cacheStatuses: config.cacheStatuses ?? [
587
- 200, 203, 204, 206, 300, 301, 304, 404, 405, 410, 414, 501,
622
+ 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501,
588
623
  ],
589
624
  cacheKey: config.cacheKey ?? defaultCacheKey,
590
625
  storage: config.storage ?? new MemoryStorageAdapter(),
@@ -999,10 +1034,13 @@ export class HTTPCache {
999
1034
  this.tagIndex.get(tag)?.delete(key);
1000
1035
  }
1001
1036
  }
1002
- /* NOTE: If entry is undefined (e.g., not found during LRU eviction),
1003
- the tag index entry for that key will not be cleaned up.
1004
- This is a minor leak on disk-based storage miss, acceptable trade-off
1005
- since the key is gone from LRU and storage anyway. */
1037
+ else {
1038
+ // Storage miss (already evicted / gone): the tag sets still referenced
1039
+ // this key and those references used to be left behind forever, growing
1040
+ // tagIndex on every eviction until invalidateByTag() walked stale keys.
1041
+ for (const set of this.tagIndex.values())
1042
+ set.delete(key);
1043
+ }
1006
1044
  }
1007
1045
  async _ensureCapacity(newEntrySize) {
1008
1046
  // Entry count cap
@@ -115,9 +115,18 @@ export class CircuitBreaker {
115
115
  }
116
116
  /** Snapshot of all counters — suitable for logging or dashboards. */
117
117
  get snapshot() {
118
+ // Counted with a loop: filter() allocated a new array on every snapshot,
119
+ // and snapshot is taken on every rejection and state transition.
120
+ let failureCount = this._consecutiveFails;
121
+ if (this.windowSize > 0) {
122
+ failureCount = 0;
123
+ for (const ok of this._window)
124
+ if (!ok)
125
+ failureCount++;
126
+ }
118
127
  return {
119
128
  state: this._state,
120
- failureCount: this.windowSize > 0 ? this._window.filter((v) => !v).length : this._consecutiveFails,
129
+ failureCount,
121
130
  successCount: this._consecutiveSucc,
122
131
  lastFailureAt: this._lastFailureAt,
123
132
  lastSuccessAt: this._lastSuccessAt,
@@ -143,6 +152,11 @@ export class CircuitBreaker {
143
152
  this.cfg.onRejected?.(snap);
144
153
  return Promise.reject(new CircuitOpenError(this.key, snap));
145
154
  }
155
+ // Remember whether THIS call holds a half-open probe. Decrementing based
156
+ // on the state at settle time was wrong: if a sibling probe failed first
157
+ // the state was already OPEN, the decrement was skipped, and the probe slot
158
+ // stayed occupied for the rest of the half-open window.
159
+ let heldProbe = false;
146
160
  if (this._state === "HALF_OPEN") {
147
161
  if (this._inFlightProbes >= this.halfOpenConcurrency) {
148
162
  this._totalRejected++;
@@ -151,15 +165,21 @@ export class CircuitBreaker {
151
165
  return Promise.reject(new CircuitOpenError(this.key, snap));
152
166
  }
153
167
  this._inFlightProbes++;
168
+ heldProbe = true;
154
169
  }
155
170
  this._totalRequests++;
156
- return fn().then((result) => {
157
- if (this._state === "HALF_OPEN")
171
+ // Promise.resolve().then(fn) so a synchronous throw inside `fn` becomes a
172
+ // rejection. execute() is an async API — callers destructure .catch() off
173
+ // the return value, and a sync throw would escape before they could.
174
+ return Promise.resolve()
175
+ .then(fn)
176
+ .then((result) => {
177
+ if (heldProbe)
158
178
  this._inFlightProbes = Math.max(0, this._inFlightProbes - 1);
159
179
  this._recordSuccess();
160
180
  return result;
161
181
  }, (err) => {
162
- if (this._state === "HALF_OPEN")
182
+ if (heldProbe)
163
183
  this._inFlightProbes = Math.max(0, this._inFlightProbes - 1);
164
184
  if (this._isCountableFailure(err)) {
165
185
  this._recordFailure();