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.
Files changed (120) hide show
  1. package/README.md +246 -9
  2. package/dist/browser/kinetex.esm.js +38 -22
  3. package/dist/browser/kinetex.js +2545 -550
  4. package/dist/browser/kinetex.min.js +38 -22
  5. package/dist/cjs/aws-sigv4.js +133 -19
  6. package/dist/cjs/cache.js +49 -7
  7. package/dist/cjs/circuit-breaker.js +45 -3
  8. package/dist/cjs/client.js +387 -104
  9. package/dist/cjs/cookie-parser.js +103 -5
  10. package/dist/cjs/cookie-store.js +125 -28
  11. package/dist/cjs/core.js +465 -66
  12. package/dist/cjs/dedup.js +49 -11
  13. package/dist/cjs/digest.js +160 -24
  14. package/dist/cjs/graphql.js +164 -24
  15. package/dist/cjs/headers.js +303 -45
  16. package/dist/cjs/interceptors.js +221 -7
  17. package/dist/cjs/lifecycle.js +89 -40
  18. package/dist/cjs/logging.js +168 -15
  19. package/dist/cjs/mod.js +3 -2
  20. package/dist/cjs/pagination.js +247 -22
  21. package/dist/cjs/progress.js +177 -27
  22. package/dist/cjs/proxy.js +412 -0
  23. package/dist/cjs/response.js +316 -47
  24. package/dist/cjs/socks5.js +131 -15
  25. package/dist/cjs/sse.js +173 -43
  26. package/dist/cjs/url.js +191 -45
  27. package/dist/cjs/utils.js +222 -48
  28. package/dist/cjs/ws.js +19 -10
  29. package/dist/esm/aws-sigv4.js +133 -19
  30. package/dist/esm/aws-sigv4.js.map +1 -1
  31. package/dist/esm/cache.js +49 -7
  32. package/dist/esm/cache.js.map +1 -1
  33. package/dist/esm/circuit-breaker.js +45 -3
  34. package/dist/esm/circuit-breaker.js.map +1 -1
  35. package/dist/esm/client.js +387 -104
  36. package/dist/esm/client.js.map +1 -1
  37. package/dist/esm/cookie-parser.js +103 -5
  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 +465 -66
  42. package/dist/esm/core.js.map +1 -1
  43. package/dist/esm/dedup.js +49 -11
  44. package/dist/esm/dedup.js.map +1 -1
  45. package/dist/esm/digest.js +160 -24
  46. package/dist/esm/digest.js.map +1 -1
  47. package/dist/esm/graphql.js +164 -24
  48. package/dist/esm/graphql.js.map +1 -1
  49. package/dist/esm/headers.js +303 -45
  50. package/dist/esm/headers.js.map +1 -1
  51. package/dist/esm/interceptors.js +221 -7
  52. package/dist/esm/interceptors.js.map +1 -1
  53. package/dist/esm/lifecycle.js +89 -40
  54. package/dist/esm/lifecycle.js.map +1 -1
  55. package/dist/esm/logging.js +168 -15
  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 +177 -27
  62. package/dist/esm/progress.js.map +1 -1
  63. package/dist/esm/proxy.js +413 -0
  64. package/dist/esm/proxy.js.map +1 -0
  65. package/dist/esm/response.js +316 -47
  66. package/dist/esm/response.js.map +1 -1
  67. package/dist/esm/socks5.js +131 -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 +222 -48
  75. package/dist/esm/utils.js.map +1 -1
  76. package/dist/esm/ws.js +19 -10
  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 +14 -1
  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 +103 -25
  89. package/dist/types/core.d.ts.map +1 -1
  90. package/dist/types/dedup.d.ts.map +1 -1
  91. package/dist/types/digest.d.ts +17 -37
  92. package/dist/types/digest.d.ts.map +1 -1
  93. package/dist/types/graphql.d.ts.map +1 -1
  94. package/dist/types/headers.d.ts +45 -27
  95. package/dist/types/headers.d.ts.map +1 -1
  96. package/dist/types/interceptors.d.ts +102 -0
  97. package/dist/types/interceptors.d.ts.map +1 -1
  98. package/dist/types/lifecycle.d.ts +19 -2
  99. package/dist/types/lifecycle.d.ts.map +1 -1
  100. package/dist/types/logging.d.ts +22 -3
  101. package/dist/types/logging.d.ts.map +1 -1
  102. package/dist/types/mod.d.ts +5 -3
  103. package/dist/types/mod.d.ts.map +1 -1
  104. package/dist/types/pagination.d.ts +0 -25
  105. package/dist/types/pagination.d.ts.map +1 -1
  106. package/dist/types/progress.d.ts +1 -1
  107. package/dist/types/progress.d.ts.map +1 -1
  108. package/dist/types/proxy.d.ts +50 -0
  109. package/dist/types/proxy.d.ts.map +1 -0
  110. package/dist/types/response.d.ts +7 -1
  111. package/dist/types/response.d.ts.map +1 -1
  112. package/dist/types/socks5.d.ts.map +1 -1
  113. package/dist/types/sse.d.ts.map +1 -1
  114. package/dist/types/types.d.ts +114 -3
  115. package/dist/types/types.d.ts.map +1 -1
  116. package/dist/types/url.d.ts +0 -14
  117. package/dist/types/url.d.ts.map +1 -1
  118. package/dist/types/utils.d.ts.map +1 -1
  119. package/dist/types/ws.d.ts.map +1 -1
  120. package/package.json +1 -1
@@ -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. */
@@ -439,7 +468,16 @@ export class Redactor {
439
468
  }
440
469
  return out;
441
470
  }
442
- /** Redact sensitive query parameters from a URL. */
471
+ /**
472
+ * Redact sensitive query parameters from a URL.
473
+ *
474
+ * Absolute URLs go through `URL`, which also normalises the result. A URL the
475
+ * `URL` constructor rejects — every relative form, and relative forms are what
476
+ * `HTTPLogger.child`'s own example logs — used to be returned untouched, so
477
+ * `logRequest(id, "GET", "/users?token=abc", ...)` wrote the token in clear
478
+ * while the absolute spelling of the same request redacted it. Those fall back
479
+ * to a textual scan of the query string below.
480
+ */
443
481
  redactURL(url) {
444
482
  try {
445
483
  const u = new URL(url);
@@ -451,9 +489,39 @@ export class Redactor {
451
489
  return u.toString();
452
490
  }
453
491
  catch {
454
- return url;
492
+ return this._redactRelativeQuery(url);
455
493
  }
456
494
  }
495
+ /** Redact the query string of a URL the `URL` constructor cannot parse. */
496
+ _redactRelativeQuery(url) {
497
+ const q = url.indexOf("?");
498
+ if (q === -1)
499
+ return url;
500
+ const base = url.slice(0, q);
501
+ const rest = url.slice(q + 1);
502
+ const hash = rest.indexOf("#");
503
+ const fragment = hash === -1 ? "" : rest.slice(hash);
504
+ const query = hash === -1 ? rest : rest.slice(0, hash);
505
+ const params = query.split("&").map((pair) => {
506
+ if (pair === "")
507
+ return pair;
508
+ const eq = pair.indexOf("=");
509
+ const name = eq === -1 ? pair : pair.slice(0, eq);
510
+ let decoded = name;
511
+ try {
512
+ decoded = decodeURIComponent(name);
513
+ }
514
+ catch {
515
+ /* keep the raw name if it is not valid percent-encoding */
516
+ }
517
+ if (!this.paramSet.has(decoded.toLowerCase()))
518
+ return pair;
519
+ // A valueless parameter (`?token`) is redacted to `token=***` so the
520
+ // redaction is visible rather than leaving the bare name behind.
521
+ return eq === -1 ? `${name}=***` : `${name}=***`;
522
+ });
523
+ return `${base}?${params.join("&")}${fragment}`;
524
+ }
457
525
  /**
458
526
  * Redact and optionally truncate a request or response body.
459
527
  *
@@ -470,6 +538,12 @@ export class Redactor {
470
538
  if (!allowed) {
471
539
  return { body: `[${ct || "binary"}]`, size: body ? bodySize(body) : null };
472
540
  }
541
+ // The size of the body *as received*, measured before any lossy decoding.
542
+ // Two invalid UTF-8 bytes decode to two U+FFFD characters that re-encode to
543
+ // 6 bytes, and reporting 6 for a 2-byte body is a size of nothing the caller
544
+ // sent. `maxBodyLength` is applied to the re-encoded form below, which is
545
+ // what actually gets written out.
546
+ const size = bodySize(body);
473
547
  let str;
474
548
  if (body instanceof Uint8Array) {
475
549
  try {
@@ -482,9 +556,14 @@ export class Redactor {
482
556
  else {
483
557
  str = body;
484
558
  }
485
- const size = str.length;
486
- // Redact body fields (JSON)
487
- if (ct.includes("application/json") && this.bodyFields.length > 0) {
559
+ // Redact body fields (JSON). The test has to be at least as wide as the
560
+ // `allowedBodyTypes` gate above: that one uses `startsWith` on a
561
+ // caller-configured list, so a config of `["application/"]` lets a
562
+ // `application/vnd.api+json` body through — and this narrower
563
+ // `includes("application/json")` then logged its `password` field in the
564
+ // clear. `+json` is the registered suffix form (RFC 6839) and this library
565
+ // already parses it as JSON elsewhere, so the redactor has to agree.
566
+ if (isJSONContentType(ct) && this.bodyFields.length > 0) {
488
567
  try {
489
568
  // FIX (H6): log bodies are untrusted — sanitize before mutation so a
490
569
  // crafted payload cannot smuggle __proto__ paths into redaction writes.
@@ -501,9 +580,12 @@ export class Redactor {
501
580
  for (const pattern of this.patterns) {
502
581
  str = str.replace(pattern, "***");
503
582
  }
504
- // Truncate
505
- if (str.length > this.maxBody) {
506
- str = str.slice(0, this.maxBody) + `... [truncated ${str.length - this.maxBody} bytes]`;
583
+ // Truncate by UTF-8 bytes, which is the unit `maxBodyLength` is documented
584
+ // in, and never split a multi-byte character in half.
585
+ const afterRedaction = bodySize(str);
586
+ if (afterRedaction > this.maxBody) {
587
+ const cut = truncateToBytes(str, this.maxBody);
588
+ str = `${cut.text}... [truncated ${cut.removed} bytes]`;
507
589
  }
508
590
  return { body: str, size };
509
591
  }
@@ -512,6 +594,15 @@ export class Redactor {
512
594
  function redactObjectPath(obj, path) {
513
595
  if (!obj || typeof obj !== "object" || path.length === 0)
514
596
  return;
597
+ // A JSON array was skipped outright: `bodyFields: ["password"]` only ever
598
+ // matched a top-level key, so a bulk payload — `[{"password":"…"}]`, the
599
+ // shape of most collection endpoints — logged its secrets in clear while
600
+ // the identical object form was redacted. Apply the path to every element.
601
+ if (Array.isArray(obj)) {
602
+ for (const item of obj)
603
+ redactObjectPath(item, path);
604
+ return;
605
+ }
515
606
  const [head, ...rest] = path;
516
607
  if (head === undefined)
517
608
  return;
@@ -642,6 +733,13 @@ export class HTTPLogger {
642
733
  const dur = active ? perfNow() - active.startMs : 0;
643
734
  const method = active?.method ?? "GET";
644
735
  const url = active?.url ?? "";
736
+ // The active ID is released before the filter runs, not after. A response
737
+ // dropped by `statuses` / `level` / `excludeURLs` / sampling returned early
738
+ // and left its entry in `activeIds` forever: the only cleanup is a
739
+ // size-triggered sweep inside `logRequest`, so a service logging with a
740
+ // narrow status filter accumulated one dead entry per request until it
741
+ // reached 10000 and started evicting live ones.
742
+ this.activeIds.delete(requestId);
645
743
  if (!this._shouldLog("INFO", method, url, status))
646
744
  return;
647
745
  const ct = headers["content-type"] ?? headers["Content-Type"] ?? null;
@@ -665,7 +763,6 @@ export class HTTPLogger {
665
763
  cached,
666
764
  meta: { ...this.cfg.context, ...meta },
667
765
  };
668
- this.activeIds.delete(requestId);
669
766
  this._write(entry);
670
767
  }
671
768
  /**
@@ -682,6 +779,8 @@ export class HTTPLogger {
682
779
  const dur = active ? perfNow() - active.startMs : 0;
683
780
  const method = active?.method ?? "GET";
684
781
  const url = active?.url ?? "";
782
+ // Released before the filter, for the same reason as `logResponse`.
783
+ this.activeIds.delete(requestId);
685
784
  if (!this._shouldLog("ERROR", method, url))
686
785
  return;
687
786
  const entry = {
@@ -698,7 +797,6 @@ export class HTTPLogger {
698
797
  attempt,
699
798
  meta: { ...this.cfg.context, ...meta },
700
799
  };
701
- this.activeIds.delete(requestId);
702
800
  this._write(entry);
703
801
  }
704
802
  // ── §7.2 ID management ────────────────────────────────────────────────────
@@ -838,8 +936,63 @@ function serializeError(err) {
838
936
  ...(errStack !== undefined ? { stack: errStack } : {}),
839
937
  };
840
938
  }
939
+ // Anything else was `String(err)`, which for a rejected object — the shape a
940
+ // custom fetch, a GraphQL client or a worker produces — is literally
941
+ // "[object Object]". The entry recorded no message and no `code`, so the log
942
+ // said only that something had failed. An error-shaped object keeps its
943
+ // `name`, `message`, `code` and `stack`; anything else is stringified
944
+ // structurally.
945
+ if (err !== null && typeof err === "object") {
946
+ const o = err;
947
+ const out = {
948
+ name: typeof o.name === "string" && o.name !== "" ? o.name : "Error",
949
+ message: typeof o.message === "string" ? o.message : safeStringify(err),
950
+ };
951
+ if (typeof o.code === "string" || typeof o.code === "number")
952
+ out.code = String(o.code);
953
+ if (typeof o.stack === "string")
954
+ out.stack = o.stack;
955
+ return out;
956
+ }
841
957
  return { name: "Error", message: String(err) };
842
958
  }
959
+ /** `JSON.stringify` that never throws — falls back for circular values. */
960
+ function safeStringify(value) {
961
+ try {
962
+ const seen = new WeakSet();
963
+ return (JSON.stringify(value, (_k, v) => {
964
+ if (v !== null && typeof v === "object") {
965
+ if (seen.has(v))
966
+ return "[circular]";
967
+ seen.add(v);
968
+ }
969
+ return v;
970
+ }) ?? String(value));
971
+ }
972
+ catch {
973
+ return String(value);
974
+ }
975
+ }
976
+ /**
977
+ * Truncate a string to at most `maxBytes` UTF-8 bytes, never splitting a
978
+ * character, and report how many bytes were actually removed.
979
+ */
980
+ function truncateToBytes(str, maxBytes) {
981
+ const enc = new TextEncoder();
982
+ const total = enc.encode(str).byteLength;
983
+ if (total <= maxBytes)
984
+ return { text: str, removed: 0 };
985
+ let used = 0;
986
+ let end = 0;
987
+ for (const ch of str) {
988
+ const width = enc.encode(ch).byteLength;
989
+ if (used + width > maxBytes)
990
+ break;
991
+ used += width;
992
+ end += ch.length;
993
+ }
994
+ return { text: str.slice(0, end), removed: total - used };
995
+ }
843
996
  /** Cross-runtime high-resolution timer. Falls back to Date.now() when performance.now() is unavailable. */
844
997
  function perfNow() {
845
998
  return typeof performance !== "undefined" ? performance.now() : Date.now();
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";