kinetex 1.3.0 → 1.5.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 (123) hide show
  1. package/README.md +413 -52
  2. package/dist/browser/kinetex.esm.js +38 -22
  3. package/dist/browser/kinetex.js +3134 -787
  4. package/dist/browser/kinetex.min.js +38 -22
  5. package/dist/cjs/aws-sigv4.js +140 -33
  6. package/dist/cjs/cache.js +58 -7
  7. package/dist/cjs/circuit-breaker.js +53 -4
  8. package/dist/cjs/client.js +726 -188
  9. package/dist/cjs/cookie-parser.js +131 -6
  10. package/dist/cjs/cookie-store.js +125 -28
  11. package/dist/cjs/core.js +691 -70
  12. package/dist/cjs/dedup.js +142 -21
  13. package/dist/cjs/digest.js +195 -38
  14. package/dist/cjs/graphql.js +166 -35
  15. package/dist/cjs/headers.js +327 -46
  16. package/dist/cjs/interceptors.js +227 -24
  17. package/dist/cjs/lifecycle.js +92 -59
  18. package/dist/cjs/logging.js +160 -24
  19. package/dist/cjs/mod.js +3 -2
  20. package/dist/cjs/pagination.js +247 -22
  21. package/dist/cjs/progress.js +178 -30
  22. package/dist/cjs/proxy.js +429 -0
  23. package/dist/cjs/response.js +506 -110
  24. package/dist/cjs/socks5.js +136 -15
  25. package/dist/cjs/sse.js +173 -43
  26. package/dist/cjs/url.js +191 -45
  27. package/dist/cjs/utils.js +335 -73
  28. package/dist/cjs/ws.js +62 -12
  29. package/dist/esm/aws-sigv4.js +140 -33
  30. package/dist/esm/aws-sigv4.js.map +1 -1
  31. package/dist/esm/cache.js +58 -7
  32. package/dist/esm/cache.js.map +1 -1
  33. package/dist/esm/circuit-breaker.js +53 -4
  34. package/dist/esm/circuit-breaker.js.map +1 -1
  35. package/dist/esm/client.js +726 -188
  36. package/dist/esm/client.js.map +1 -1
  37. package/dist/esm/cookie-parser.js +131 -6
  38. package/dist/esm/cookie-parser.js.map +1 -1
  39. package/dist/esm/cookie-store.js +125 -28
  40. package/dist/esm/cookie-store.js.map +1 -1
  41. package/dist/esm/core.js +691 -70
  42. package/dist/esm/core.js.map +1 -1
  43. package/dist/esm/dedup.js +142 -21
  44. package/dist/esm/dedup.js.map +1 -1
  45. package/dist/esm/digest.js +195 -38
  46. package/dist/esm/digest.js.map +1 -1
  47. package/dist/esm/graphql.js +166 -35
  48. package/dist/esm/graphql.js.map +1 -1
  49. package/dist/esm/headers.js +327 -46
  50. package/dist/esm/headers.js.map +1 -1
  51. package/dist/esm/interceptors.js +227 -24
  52. package/dist/esm/interceptors.js.map +1 -1
  53. package/dist/esm/lifecycle.js +92 -59
  54. package/dist/esm/lifecycle.js.map +1 -1
  55. package/dist/esm/logging.js +160 -24
  56. package/dist/esm/logging.js.map +1 -1
  57. package/dist/esm/mod.js +3 -2
  58. package/dist/esm/mod.js.map +1 -1
  59. package/dist/esm/pagination.js +247 -22
  60. package/dist/esm/pagination.js.map +1 -1
  61. package/dist/esm/progress.js +178 -30
  62. package/dist/esm/progress.js.map +1 -1
  63. package/dist/esm/proxy.js +430 -0
  64. package/dist/esm/proxy.js.map +1 -0
  65. package/dist/esm/response.js +506 -110
  66. package/dist/esm/response.js.map +1 -1
  67. package/dist/esm/socks5.js +136 -15
  68. package/dist/esm/socks5.js.map +1 -1
  69. package/dist/esm/sse.js +173 -43
  70. package/dist/esm/sse.js.map +1 -1
  71. package/dist/esm/types.js.map +1 -1
  72. package/dist/esm/url.js +191 -45
  73. package/dist/esm/url.js.map +1 -1
  74. package/dist/esm/utils.js +335 -73
  75. package/dist/esm/utils.js.map +1 -1
  76. package/dist/esm/ws.js +62 -12
  77. package/dist/esm/ws.js.map +1 -1
  78. package/dist/types/aws-sigv4.d.ts.map +1 -1
  79. package/dist/types/cache.d.ts +19 -1
  80. package/dist/types/cache.d.ts.map +1 -1
  81. package/dist/types/circuit-breaker.d.ts +15 -2
  82. package/dist/types/circuit-breaker.d.ts.map +1 -1
  83. package/dist/types/client.d.ts +69 -11
  84. package/dist/types/client.d.ts.map +1 -1
  85. package/dist/types/cookie-parser.d.ts +0 -17
  86. package/dist/types/cookie-parser.d.ts.map +1 -1
  87. package/dist/types/cookie-store.d.ts.map +1 -1
  88. package/dist/types/core.d.ts +216 -25
  89. package/dist/types/core.d.ts.map +1 -1
  90. package/dist/types/dedup.d.ts +38 -3
  91. package/dist/types/dedup.d.ts.map +1 -1
  92. package/dist/types/digest.d.ts +17 -37
  93. package/dist/types/digest.d.ts.map +1 -1
  94. package/dist/types/graphql.d.ts.map +1 -1
  95. package/dist/types/headers.d.ts +68 -31
  96. package/dist/types/headers.d.ts.map +1 -1
  97. package/dist/types/interceptors.d.ts +126 -38
  98. package/dist/types/interceptors.d.ts.map +1 -1
  99. package/dist/types/lifecycle.d.ts +19 -2
  100. package/dist/types/lifecycle.d.ts.map +1 -1
  101. package/dist/types/logging.d.ts +22 -3
  102. package/dist/types/logging.d.ts.map +1 -1
  103. package/dist/types/mod.d.ts +5 -3
  104. package/dist/types/mod.d.ts.map +1 -1
  105. package/dist/types/pagination.d.ts +0 -25
  106. package/dist/types/pagination.d.ts.map +1 -1
  107. package/dist/types/progress.d.ts +1 -1
  108. package/dist/types/progress.d.ts.map +1 -1
  109. package/dist/types/proxy.d.ts +50 -0
  110. package/dist/types/proxy.d.ts.map +1 -0
  111. package/dist/types/response.d.ts +49 -2
  112. package/dist/types/response.d.ts.map +1 -1
  113. package/dist/types/socks5.d.ts.map +1 -1
  114. package/dist/types/sse.d.ts.map +1 -1
  115. package/dist/types/types.d.ts +219 -26
  116. package/dist/types/types.d.ts.map +1 -1
  117. package/dist/types/url.d.ts +0 -25
  118. package/dist/types/url.d.ts.map +1 -1
  119. package/dist/types/utils.d.ts +69 -1
  120. package/dist/types/utils.d.ts.map +1 -1
  121. package/dist/types/ws.d.ts +0 -1
  122. package/dist/types/ws.d.ts.map +1 -1
  123. package/package.json +10 -4
@@ -27,7 +27,7 @@
27
27
  */
28
28
  // FIX (H6): logged bodies are untrusted — parsed JSON is stripped of
29
29
  // prototype-pollution keys before redaction writes (see sanitizeParsedJSON).
30
- import { sanitizeParsedJSON } from "./utils.js";
30
+ import { sanitizeParsedJSON, perfNow, redactHeaders, safeStringify as stringifySafely, } from "./utils.js";
31
31
  // Cross-runtime: safely detect Node.js process.env
32
32
  const getNodeEnv = () => {
33
33
  try {
@@ -120,6 +120,17 @@ const DEFAULT_REDACT_PARAMS = [
120
120
  * Requests/responses with other content-types will have their body replaced
121
121
  * with a short indicator (e.g. "[binary]") instead of being logged verbatim.
122
122
  */
123
+ /**
124
+ * Is this a JSON media type?
125
+ *
126
+ * `application/json` and every `+json` structured suffix, case-insensitively
127
+ * and ignoring parameters, because RFC 9110 makes media types
128
+ * case-insensitive and `application/JSON; charset=utf-8` is the same type.
129
+ */
130
+ function isJSONContentType(contentType) {
131
+ const type = contentType.split(";", 1)[0].trim().toLowerCase();
132
+ return type === "application/json" || type === "text/json" || type.endsWith("+json");
133
+ }
123
134
  const DEFAULT_ALLOWED_BODY_TYPES = [
124
135
  "application/json",
125
136
  "application/x-www-form-urlencoded",
@@ -137,12 +148,14 @@ export class ConsoleTransport {
137
148
  /**
138
149
  * @param opts - Console transport options
139
150
  */
140
- constructor(opts = {
141
- pretty: false,
142
- onWrite: (output) => console.log(output),
143
- }) {
151
+ constructor(opts = {}) {
152
+ // `pretty` resolves the same way whether the caller passed nothing, `{}`, or
153
+ // a full options object. The old default parameter hard-coded `pretty: false`
154
+ // for the no-argument form only, so `new ConsoleTransport()` emitted JSON
155
+ // while `new ConsoleTransport({ onWrite })` emitted pretty output outside
156
+ // production — the opposite of the documented default.
144
157
  this.pretty = opts.pretty ?? getNodeEnv() !== "production";
145
- this.onWrite = opts.onWrite;
158
+ this.onWrite = opts.onWrite ?? ((output) => console.log(output));
146
159
  // useColors reserved for future ANSI colorization
147
160
  void opts.useColors;
148
161
  }
@@ -254,6 +267,16 @@ export class BatchingTransport {
254
267
  }
255
268
  /** Internal flush — sends all buffered entries to inner transport. */
256
269
  _flush() {
270
+ // A size-triggered flush has to disarm the pending timer as well. Leaving
271
+ // it armed meant `write()`'s `if (!this.timer)` guard stayed false for the
272
+ // next entry, so nothing scheduled a fresh interval: entries written after
273
+ // a full batch waited out the remainder of the previous batch's timer, and a
274
+ // process that hit `maxBatch` and then went quiet held the interval for the
275
+ // whole `flushMs` with an empty buffer.
276
+ if (this.timer !== null) {
277
+ clearTimeout(this.timer);
278
+ this.timer = null;
279
+ }
257
280
  if (this.buffer.length === 0)
258
281
  return;
259
282
  const batch = this.buffer.splice(0);
@@ -302,6 +325,12 @@ export class RemoteTransport {
302
325
  }
303
326
  /** Fire-and-forget flush with error handling. */
304
327
  _flush() {
328
+ // Same disarming as BatchingTransport: a size-triggered flush must not
329
+ // leave the next entry without a timer of its own.
330
+ if (this.timer !== null) {
331
+ clearTimeout(this.timer);
332
+ this.timer = null;
333
+ }
305
334
  this._flushAsync().catch(this.options.onError ?? ((err) => console.error("[logging] Flush error:", err)));
306
335
  }
307
336
  /** Async flush implementation — sends batch via HTTP POST. */
@@ -433,13 +462,18 @@ export class Redactor {
433
462
  }
434
463
  /** Redact sensitive header values, replacing them with "***". */
435
464
  redactHeaders(headers) {
436
- const out = {};
437
- for (const [k, v] of Object.entries(headers)) {
438
- out[k] = this.headerSet.has(k.toLowerCase()) ? "***" : v;
439
- }
440
- return out;
465
+ return redactHeaders(headers, this.headerSet, "***");
441
466
  }
442
- /** Redact sensitive query parameters from a URL. */
467
+ /**
468
+ * Redact sensitive query parameters from a URL.
469
+ *
470
+ * Absolute URLs go through `URL`, which also normalises the result. A URL the
471
+ * `URL` constructor rejects — every relative form, and relative forms are what
472
+ * `HTTPLogger.child`'s own example logs — used to be returned untouched, so
473
+ * `logRequest(id, "GET", "/users?token=abc", ...)` wrote the token in clear
474
+ * while the absolute spelling of the same request redacted it. Those fall back
475
+ * to a textual scan of the query string below.
476
+ */
443
477
  redactURL(url) {
444
478
  try {
445
479
  const u = new URL(url);
@@ -451,9 +485,41 @@ export class Redactor {
451
485
  return u.toString();
452
486
  }
453
487
  catch {
454
- return url;
488
+ return this._redactRelativeQuery(url);
455
489
  }
456
490
  }
491
+ /** Redact the query string of a URL the `URL` constructor cannot parse. */
492
+ _redactRelativeQuery(url) {
493
+ const q = url.indexOf("?");
494
+ if (q === -1)
495
+ return url;
496
+ const base = url.slice(0, q);
497
+ const rest = url.slice(q + 1);
498
+ const hash = rest.indexOf("#");
499
+ const fragment = hash === -1 ? "" : rest.slice(hash);
500
+ const query = hash === -1 ? rest : rest.slice(0, hash);
501
+ const params = query.split("&").map((pair) => {
502
+ if (pair === "")
503
+ return pair;
504
+ const eq = pair.indexOf("=");
505
+ const name = eq === -1 ? pair : pair.slice(0, eq);
506
+ let decoded = name;
507
+ try {
508
+ decoded = decodeURIComponent(name);
509
+ }
510
+ catch {
511
+ /* keep the raw name if it is not valid percent-encoding */
512
+ }
513
+ if (!this.paramSet.has(decoded.toLowerCase()))
514
+ return pair;
515
+ // A valueless parameter (`?token`) is redacted to `token=***` rather than
516
+ // left as the bare name, so the redaction stays visible. The value, when
517
+ // there is one, is discarded either way — which is why this used to be
518
+ // written as a ternary whose two arms were the same string.
519
+ return `${name}=***`;
520
+ });
521
+ return `${base}?${params.join("&")}${fragment}`;
522
+ }
457
523
  /**
458
524
  * Redact and optionally truncate a request or response body.
459
525
  *
@@ -470,6 +536,12 @@ export class Redactor {
470
536
  if (!allowed) {
471
537
  return { body: `[${ct || "binary"}]`, size: body ? bodySize(body) : null };
472
538
  }
539
+ // The size of the body *as received*, measured before any lossy decoding.
540
+ // Two invalid UTF-8 bytes decode to two U+FFFD characters that re-encode to
541
+ // 6 bytes, and reporting 6 for a 2-byte body is a size of nothing the caller
542
+ // sent. `maxBodyLength` is applied to the re-encoded form below, which is
543
+ // what actually gets written out.
544
+ const size = bodySize(body);
473
545
  let str;
474
546
  if (body instanceof Uint8Array) {
475
547
  try {
@@ -482,9 +554,14 @@ export class Redactor {
482
554
  else {
483
555
  str = body;
484
556
  }
485
- const size = str.length;
486
- // Redact body fields (JSON)
487
- if (ct.includes("application/json") && this.bodyFields.length > 0) {
557
+ // Redact body fields (JSON). The test has to be at least as wide as the
558
+ // `allowedBodyTypes` gate above: that one uses `startsWith` on a
559
+ // caller-configured list, so a config of `["application/"]` lets a
560
+ // `application/vnd.api+json` body through — and this narrower
561
+ // `includes("application/json")` then logged its `password` field in the
562
+ // clear. `+json` is the registered suffix form (RFC 6839) and this library
563
+ // already parses it as JSON elsewhere, so the redactor has to agree.
564
+ if (isJSONContentType(ct) && this.bodyFields.length > 0) {
488
565
  try {
489
566
  // FIX (H6): log bodies are untrusted — sanitize before mutation so a
490
567
  // crafted payload cannot smuggle __proto__ paths into redaction writes.
@@ -501,9 +578,12 @@ export class Redactor {
501
578
  for (const pattern of this.patterns) {
502
579
  str = str.replace(pattern, "***");
503
580
  }
504
- // Truncate
505
- if (str.length > this.maxBody) {
506
- str = str.slice(0, this.maxBody) + `... [truncated ${str.length - this.maxBody} bytes]`;
581
+ // Truncate by UTF-8 bytes, which is the unit `maxBodyLength` is documented
582
+ // in, and never split a multi-byte character in half.
583
+ const afterRedaction = bodySize(str);
584
+ if (afterRedaction > this.maxBody) {
585
+ const cut = truncateToBytes(str, this.maxBody);
586
+ str = `${cut.text}... [truncated ${cut.removed} bytes]`;
507
587
  }
508
588
  return { body: str, size };
509
589
  }
@@ -512,6 +592,15 @@ export class Redactor {
512
592
  function redactObjectPath(obj, path) {
513
593
  if (!obj || typeof obj !== "object" || path.length === 0)
514
594
  return;
595
+ // A JSON array was skipped outright: `bodyFields: ["password"]` only ever
596
+ // matched a top-level key, so a bulk payload — `[{"password":"…"}]`, the
597
+ // shape of most collection endpoints — logged its secrets in clear while
598
+ // the identical object form was redacted. Apply the path to every element.
599
+ if (Array.isArray(obj)) {
600
+ for (const item of obj)
601
+ redactObjectPath(item, path);
602
+ return;
603
+ }
515
604
  const [head, ...rest] = path;
516
605
  if (head === undefined)
517
606
  return;
@@ -642,6 +731,13 @@ export class HTTPLogger {
642
731
  const dur = active ? perfNow() - active.startMs : 0;
643
732
  const method = active?.method ?? "GET";
644
733
  const url = active?.url ?? "";
734
+ // The active ID is released before the filter runs, not after. A response
735
+ // dropped by `statuses` / `level` / `excludeURLs` / sampling returned early
736
+ // and left its entry in `activeIds` forever: the only cleanup is a
737
+ // size-triggered sweep inside `logRequest`, so a service logging with a
738
+ // narrow status filter accumulated one dead entry per request until it
739
+ // reached 10000 and started evicting live ones.
740
+ this.activeIds.delete(requestId);
645
741
  if (!this._shouldLog("INFO", method, url, status))
646
742
  return;
647
743
  const ct = headers["content-type"] ?? headers["Content-Type"] ?? null;
@@ -665,7 +761,6 @@ export class HTTPLogger {
665
761
  cached,
666
762
  meta: { ...this.cfg.context, ...meta },
667
763
  };
668
- this.activeIds.delete(requestId);
669
764
  this._write(entry);
670
765
  }
671
766
  /**
@@ -682,6 +777,8 @@ export class HTTPLogger {
682
777
  const dur = active ? perfNow() - active.startMs : 0;
683
778
  const method = active?.method ?? "GET";
684
779
  const url = active?.url ?? "";
780
+ // Released before the filter, for the same reason as `logResponse`.
781
+ this.activeIds.delete(requestId);
685
782
  if (!this._shouldLog("ERROR", method, url))
686
783
  return;
687
784
  const entry = {
@@ -698,7 +795,6 @@ export class HTTPLogger {
698
795
  attempt,
699
796
  meta: { ...this.cfg.context, ...meta },
700
797
  };
701
- this.activeIds.delete(requestId);
702
798
  this._write(entry);
703
799
  }
704
800
  // ── §7.2 ID management ────────────────────────────────────────────────────
@@ -838,11 +934,51 @@ function serializeError(err) {
838
934
  ...(errStack !== undefined ? { stack: errStack } : {}),
839
935
  };
840
936
  }
937
+ // Anything else was `String(err)`, which for a rejected object — the shape a
938
+ // custom fetch, a GraphQL client or a worker produces — is literally
939
+ // "[object Object]". The entry recorded no message and no `code`, so the log
940
+ // said only that something had failed. An error-shaped object keeps its
941
+ // `name`, `message`, `code` and `stack`; anything else is stringified
942
+ // structurally.
943
+ if (err !== null && typeof err === "object") {
944
+ const o = err;
945
+ const out = {
946
+ name: typeof o.name === "string" && o.name !== "" ? o.name : "Error",
947
+ message: typeof o.message === "string" ? o.message : safeStringify(err),
948
+ };
949
+ if (typeof o.code === "string" || typeof o.code === "number")
950
+ out.code = String(o.code);
951
+ if (typeof o.stack === "string")
952
+ out.stack = o.stack;
953
+ return out;
954
+ }
841
955
  return { name: "Error", message: String(err) };
842
956
  }
843
- /** Cross-runtime high-resolution timer. Falls back to Date.now() when performance.now() is unavailable. */
844
- function perfNow() {
845
- return typeof performance !== "undefined" ? performance.now() : Date.now();
957
+ /** `JSON.stringify` that never throws — falls back for circular values. */
958
+ // The marker is lowercase here and capitalised in the shared helper. Both are
959
+ // asserted by tests and both appear in log output, so this converges on one
960
+ // implementation without converging on one string.
961
+ const CIRCULAR_MARKER = "[circular]";
962
+ const safeStringify = (value) => stringifySafely(value, CIRCULAR_MARKER);
963
+ /**
964
+ * Truncate a string to at most `maxBytes` UTF-8 bytes, never splitting a
965
+ * character, and report how many bytes were actually removed.
966
+ */
967
+ function truncateToBytes(str, maxBytes) {
968
+ const enc = new TextEncoder();
969
+ const total = enc.encode(str).byteLength;
970
+ if (total <= maxBytes)
971
+ return { text: str, removed: 0 };
972
+ let used = 0;
973
+ let end = 0;
974
+ for (const ch of str) {
975
+ const width = enc.encode(ch).byteLength;
976
+ if (used + width > maxBytes)
977
+ break;
978
+ used += width;
979
+ end += ch.length;
980
+ }
981
+ return { text: str.slice(0, end), removed: total - used };
846
982
  }
847
983
  // ============================================================================
848
984
  // §10 FACTORY HELPERS
package/dist/cjs/mod.js CHANGED
@@ -44,13 +44,13 @@ export { parseCookieDate, getPublicSuffix, getRegistrableDomain, isPublicSuffix,
44
44
  // core.ts exports
45
45
  export { HAS_NATIVE_FETCH, createTransport, parseBody, sendWithTimeout, readRawBody, decompressBodyStream, } from "./core.js";
46
46
  // digest.ts exports
47
- export { parseDigestChallenge, computeDigestResponse, formatDigestAuth, createDigestAuthorization, } from "./digest.js";
47
+ export { parseDigestChallenge, computeDigestResponse, computeUsernameStar, formatDigestAuth, createDigestAuthorization, } from "./digest.js";
48
48
  // graphql.ts exports
49
49
  export { clearAPQCache, getAPQMetrics, authLink, errorLink, loggingLink, retryLink, } from "./graphql.js";
50
50
  // headers.ts exports
51
51
  export { HeaderName, formatContentType, parseContentDisposition, formatContentDisposition, parseWWWAuthenticate, formatBearer, formatBasic, parseAccept, parseAcceptEncoding, parseAcceptLanguage, negotiateContentType, parseRange, parseContentRange, formatLinkHeader, parseForwarded, normalizeForwardedHeaders, getClientIP, parseRetryAfter, parseHSTS, formatHSTS, parseCSP, formatCSP, parseServerTiming, formatServerTiming, parseAltSvc, parseContentLanguage, parseWarning, parseParams, fromNodeHeaders, toNodeHeaders, fromWebHeaders, securityHeaders, corsHeaders, RichHeaders, createHeaders, createRequestHeaders, createResponseHeaders, createImmutableHeaders, } from "./headers.js";
52
52
  // interceptors.ts exports
53
- export { createRetryInterceptor, createAuthInterceptor, createTimeoutInterceptor, createLoggingInterceptor, createCacheInterceptor, createDedupeInterceptor, createRateLimitInterceptor, RateLimitError, createHARInterceptor, createMetricsInterceptor, createInterceptorSuite, computeBodySize, } from "./interceptors.js";
53
+ export { createRetryInterceptor, createAuthInterceptor, createTimeoutInterceptor, createLoggingInterceptor, createCacheInterceptor, createDedupeInterceptor, createRateLimitInterceptor, RateLimitError, ConcurrencyLimiter, CONCURRENCY_DEFAULTS, ConcurrencyLimitError, createHARInterceptor, createMetricsInterceptor, createInterceptorSuite, computeBodySize, } from "./interceptors.js";
54
54
  // lifecycle.ts exports
55
55
  export { RedirectTracker, TooManyRedirectsError, tap, injectHeaders, withBaseURL, throwOnHTTPError, validateResponse, HTTPError, ResponseValidationError, composeBeforeRequest, composeBeforeResponse, composeAround, createLoggingHooks, createTimingHook, createBodyNormalizationHook, createAbortHook, createHookContext, } from "./lifecycle.js";
56
56
  // logging.ts exports
@@ -94,5 +94,6 @@ export { InterceptorManager } from "./interceptors.js";
94
94
  export { HookRegistry, HookEmitter } from "./lifecycle.js";
95
95
  export { CircuitBreaker, CircuitBreakerRegistry, CircuitOpenError, createCircuitBreaker, createCircuitBreakerRegistry, } from "./circuit-breaker.js";
96
96
  export { DedupMap, createDedupMap } from "./dedup.js";
97
+ export { generateIdempotencyKey, isValidIdempotencyKey } from "./headers.js";
97
98
  // ── Utilities ────────────────────────────────────────────────────────────────
98
99
  export { safeJSONParse, tryParseJSON, parseUntrustedJSON, sanitizeParsedJSON, isUint8Array, isArrayBuffer, isReadableStream, isHeaders, isAbortSignal, isPlainObject, isFormData, isBlob, isURLSearchParams, isValidHeaderName, isValidHeaderValue, isSafeURL, sanitizeURL, createStructuredError, formatError, perfNow, sleep, concatUint8Arrays, toUint8Array, uint8ArrayToBase64, deepClone, normalizeHeaders, mergeSignals, isAbortError, getRuntime, isNodeEnvironment, isBrowserEnvironment, hasNativeFetch, } from "./utils.js";