kinetex 1.4.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 (95) hide show
  1. package/README.md +170 -46
  2. package/dist/browser/kinetex.esm.js +29 -29
  3. package/dist/browser/kinetex.js +1143 -791
  4. package/dist/browser/kinetex.min.js +29 -29
  5. package/dist/cjs/aws-sigv4.js +7 -14
  6. package/dist/cjs/cache.js +9 -0
  7. package/dist/cjs/circuit-breaker.js +8 -1
  8. package/dist/cjs/client.js +350 -95
  9. package/dist/cjs/cookie-parser.js +28 -1
  10. package/dist/cjs/core.js +227 -5
  11. package/dist/cjs/dedup.js +93 -10
  12. package/dist/cjs/digest.js +36 -15
  13. package/dist/cjs/graphql.js +2 -11
  14. package/dist/cjs/headers.js +24 -1
  15. package/dist/cjs/interceptors.js +6 -17
  16. package/dist/cjs/lifecycle.js +3 -19
  17. package/dist/cjs/logging.js +12 -29
  18. package/dist/cjs/progress.js +1 -3
  19. package/dist/cjs/proxy.js +19 -2
  20. package/dist/cjs/response.js +202 -75
  21. package/dist/cjs/socks5.js +5 -0
  22. package/dist/cjs/utils.js +115 -27
  23. package/dist/cjs/ws.js +43 -2
  24. package/dist/esm/aws-sigv4.js +7 -14
  25. package/dist/esm/aws-sigv4.js.map +1 -1
  26. package/dist/esm/cache.js +9 -0
  27. package/dist/esm/cache.js.map +1 -1
  28. package/dist/esm/circuit-breaker.js +8 -1
  29. package/dist/esm/circuit-breaker.js.map +1 -1
  30. package/dist/esm/client.js +350 -95
  31. package/dist/esm/client.js.map +1 -1
  32. package/dist/esm/cookie-parser.js +28 -1
  33. package/dist/esm/cookie-parser.js.map +1 -1
  34. package/dist/esm/core.js +227 -5
  35. package/dist/esm/core.js.map +1 -1
  36. package/dist/esm/dedup.js +93 -10
  37. package/dist/esm/dedup.js.map +1 -1
  38. package/dist/esm/digest.js +36 -15
  39. package/dist/esm/digest.js.map +1 -1
  40. package/dist/esm/graphql.js +2 -11
  41. package/dist/esm/graphql.js.map +1 -1
  42. package/dist/esm/headers.js +24 -1
  43. package/dist/esm/headers.js.map +1 -1
  44. package/dist/esm/interceptors.js +6 -17
  45. package/dist/esm/interceptors.js.map +1 -1
  46. package/dist/esm/lifecycle.js +3 -19
  47. package/dist/esm/lifecycle.js.map +1 -1
  48. package/dist/esm/logging.js +12 -29
  49. package/dist/esm/logging.js.map +1 -1
  50. package/dist/esm/progress.js +1 -3
  51. package/dist/esm/progress.js.map +1 -1
  52. package/dist/esm/proxy.js +19 -2
  53. package/dist/esm/proxy.js.map +1 -1
  54. package/dist/esm/response.js +202 -75
  55. package/dist/esm/response.js.map +1 -1
  56. package/dist/esm/socks5.js +5 -0
  57. package/dist/esm/socks5.js.map +1 -1
  58. package/dist/esm/types.js.map +1 -1
  59. package/dist/esm/url.js.map +1 -1
  60. package/dist/esm/utils.js +115 -27
  61. package/dist/esm/utils.js.map +1 -1
  62. package/dist/esm/ws.js +43 -2
  63. package/dist/esm/ws.js.map +1 -1
  64. package/dist/types/aws-sigv4.d.ts.map +1 -1
  65. package/dist/types/cache.d.ts.map +1 -1
  66. package/dist/types/circuit-breaker.d.ts +1 -1
  67. package/dist/types/circuit-breaker.d.ts.map +1 -1
  68. package/dist/types/client.d.ts.map +1 -1
  69. package/dist/types/cookie-parser.d.ts.map +1 -1
  70. package/dist/types/core.d.ts +113 -0
  71. package/dist/types/core.d.ts.map +1 -1
  72. package/dist/types/dedup.d.ts +38 -3
  73. package/dist/types/dedup.d.ts.map +1 -1
  74. package/dist/types/digest.d.ts.map +1 -1
  75. package/dist/types/graphql.d.ts.map +1 -1
  76. package/dist/types/headers.d.ts +23 -4
  77. package/dist/types/headers.d.ts.map +1 -1
  78. package/dist/types/interceptors.d.ts +24 -38
  79. package/dist/types/interceptors.d.ts.map +1 -1
  80. package/dist/types/lifecycle.d.ts.map +1 -1
  81. package/dist/types/logging.d.ts.map +1 -1
  82. package/dist/types/progress.d.ts.map +1 -1
  83. package/dist/types/proxy.d.ts.map +1 -1
  84. package/dist/types/response.d.ts +42 -1
  85. package/dist/types/response.d.ts.map +1 -1
  86. package/dist/types/socks5.d.ts.map +1 -1
  87. package/dist/types/types.d.ts +105 -23
  88. package/dist/types/types.d.ts.map +1 -1
  89. package/dist/types/url.d.ts +0 -11
  90. package/dist/types/url.d.ts.map +1 -1
  91. package/dist/types/utils.d.ts +69 -1
  92. package/dist/types/utils.d.ts.map +1 -1
  93. package/dist/types/ws.d.ts +0 -1
  94. package/dist/types/ws.d.ts.map +1 -1
  95. package/package.json +10 -4
@@ -30,6 +30,8 @@ import { ValidationError } from "./types.js";
30
30
  // FIX (H6): untrusted GraphQL responses are parsed with raw JSON.parse below —
31
31
  // sanitize prototype-pollution keys before they reach user code or merges.
32
32
  import { sanitizeParsedJSON } from "./utils.js";
33
+ import { sleep } from "./utils.js";
34
+ import { sha256Hex } from "./utils.js";
33
35
  // ============================================================================
34
36
  // §2 ERRORS
35
37
  // ============================================================================
@@ -82,9 +84,6 @@ function buildGraphQLError(graphQLErrors, req, res) {
82
84
  return new GraphQLClientError(message, "EGRAPHQL", graphQLErrors, req, res);
83
85
  }
84
86
  // Utility function (B-10 fix - moved before usage)
85
- function sleep(ms) {
86
- return new Promise((resolve) => setTimeout(resolve, ms));
87
- }
88
87
  function buildLinkChain(links, terminal) {
89
88
  return links.reduceRight((next, link) => (op) => link(op, next), terminal);
90
89
  }
@@ -121,14 +120,6 @@ export function extractOperationName(query) {
121
120
  // ============================================================================
122
121
  // §5 AUTOMATIC PERSISTED QUERIES (APQ)
123
122
  // ============================================================================
124
- async function sha256Hex(str) {
125
- const encoded = new TextEncoder().encode(str);
126
- const buf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
127
- const hash = await crypto.subtle.digest("SHA-256", buf);
128
- return Array.from(new Uint8Array(hash))
129
- .map((b) => b.toString(16).padStart(2, "0"))
130
- .join("");
131
- }
132
123
  /** Maximum APQ cache entries to prevent memory leaks */
133
124
  const MAX_APQ_CACHE_ENTRIES = 1000;
134
125
  /** APQ cache entry TTL in ms — entries older than this are evicted */
@@ -952,6 +952,16 @@ export function parseContentDisposition(value) {
952
952
  * @param cd - Parsed disposition value
953
953
  * @returns Formatted `Content-Disposition` header value
954
954
  */
955
+ /**
956
+ * Render a `Content-Disposition` header value.
957
+ *
958
+ * Takes the fields it actually reads rather than a whole
959
+ * `ContentDispositionValue`: `params` is never consulted, and `name` /
960
+ * `filename` are already optional in the sense that matters — a null or absent
961
+ * one is simply not emitted. Requiring callers to write `name: null,
962
+ * params: new Map()` to format `{ type: "inline" }` made the signature
963
+ * describe a parse result rather than what formatting needs.
964
+ */
955
965
  export function formatContentDisposition(cd) {
956
966
  let out = cd.type;
957
967
  if (cd.name)
@@ -1180,7 +1190,11 @@ export function parseAuthorization(value) {
1180
1190
  continue;
1181
1191
  const k = t.slice(0, eq).trim().toLowerCase();
1182
1192
  let v = t.slice(eq + 1).trim();
1183
- if (v.startsWith('"') && v.endsWith('"'))
1193
+ // `length >= 2` so a lone `"` is left alone instead of becoming the
1194
+ // empty string: `'"'.slice(1, -1)` is `""`, so without the guard a
1195
+ // one-character value silently lost its only character. The identical
1196
+ // strip further down (stripIPPortAndBrackets) already had this guard.
1197
+ if (v.length >= 2 && v.startsWith('"') && v.endsWith('"'))
1184
1198
  v = v.slice(1, -1);
1185
1199
  params.set(k, v);
1186
1200
  }
@@ -1463,6 +1477,15 @@ export function parseLinkHeader(value) {
1463
1477
  * @param links - Parsed Link entries
1464
1478
  * @returns Formatted `Link` header value
1465
1479
  */
1480
+ /**
1481
+ * Render an array of links as a `Link` header value.
1482
+ *
1483
+ * Only `uri` is required. `LinkValue` is the shape `parseLinkHeader` *returns*
1484
+ * — every parameter present, null when absent — and a caller building a header
1485
+ * by hand should not have to write `rel: null, type: null, hreflang: null,
1486
+ * title: null, media: null` to say `{ uri, rel }`. The body already guards
1487
+ * every one of them, so the parameter type now says what formatting needs.
1488
+ */
1466
1489
  export function formatLinkHeader(links) {
1467
1490
  return links
1468
1491
  .map((l) => {
@@ -28,6 +28,7 @@
28
28
  * - No dependencies, no runtime globals beyond Promise/Map/Set
29
29
  */
30
30
  import { getAuthFingerprint } from "./cache.js";
31
+ import { perfNow, sleep, redactHeaders as redactHeaderValues } from "./utils.js";
31
32
  import { KinetexError } from "./types.js";
32
33
  import { parseHTTPDate } from "./headers.js";
33
34
  // ============================================================================
@@ -607,13 +608,9 @@ let _reqIdSeq = 0;
607
608
  export function createLoggingInterceptor(config = {}) {
608
609
  const cfg = { ...LOG_DEFAULTS, ...config };
609
610
  const redactSet = new Set(cfg.redactHeaders.map((h) => h.toLowerCase()));
610
- function redactHeaders(headers) {
611
- const out = {};
612
- for (const [k, v] of Object.entries(headers)) {
613
- out[k] = redactSet.has(k.toLowerCase()) ? "**REDACTED**" : v;
614
- }
615
- return out;
616
- }
611
+ // The mask is "**REDACTED**" rather than the logger's "***" — that string is
612
+ // observable in this interceptor's own log output, so it is preserved here.
613
+ const redactHeaders = (headers) => redactHeaderValues(headers, redactSet, "**REDACTED**");
617
614
  function makeEntry(type, ctx) {
618
615
  const requestId = ctx.store.get(REQUEST_ID_KEY) ?? "unknown";
619
616
  return {
@@ -1276,16 +1273,8 @@ export function createMetricsInterceptor() {
1276
1273
  // ============================================================================
1277
1274
  // §14 UTILITIES
1278
1275
  // ============================================================================
1279
- function now() {
1280
- // Use performance.now() when available (browser, Deno, Node 16+, Bun)
1281
- // Fall back to Date.now() for edge runtimes that may not expose it
1282
- return typeof performance !== "undefined" && typeof performance.now === "function"
1283
- ? performance.now()
1284
- : Date.now();
1285
- }
1286
- function sleep(ms) {
1287
- return new Promise((resolve) => setTimeout(resolve, ms));
1288
- }
1276
+ /** High-resolution monotonic clock — the one `utils.ts` exports, under a local name. */
1277
+ const now = perfNow;
1289
1278
  /** @internal Compute byte size of a request/response body. Exported for testing. */
1290
1279
  export function computeBodySize(body) {
1291
1280
  if (!body)
@@ -23,6 +23,7 @@
23
23
  * - Hook timing + tracing
24
24
  * - Typed event emitter
25
25
  */
26
+ import { perfNow, redactHeaders, abortError } from "./utils.js";
26
27
  /**
27
28
  * Lightweight typed event emitter for lifecycle events.
28
29
  *
@@ -811,12 +812,7 @@ export function createLoggingHooks(options = {}) {
811
812
  // cleartext by default, on a hook whose entire purpose is to log. The
812
813
  // interceptors' own logging defaults already redact all four.
813
814
  const redact = new Set((options.redactHeaders ?? ["authorization", "cookie", "set-cookie", "proxy-authorization"]).map((h) => h.toLowerCase()));
814
- function safeHeaders(h) {
815
- const out = {};
816
- for (const [k, v] of Object.entries(h))
817
- out[k] = redact.has(k.toLowerCase()) ? "***" : v;
818
- return out;
819
- }
815
+ const safeHeaders = (h) => redactHeaders(h, redact, "***");
820
816
  return {
821
817
  beforeRequest: (req) => {
822
818
  log("→ request", {
@@ -893,7 +889,7 @@ export function createAbortHook() {
893
889
  beforeRequest: (req, ctx) => {
894
890
  if (req.signal?.aborted) {
895
891
  ctx.meta.aborted = true;
896
- throw createAbortError("Request aborted before sending");
892
+ throw abortError("Request aborted before sending");
897
893
  }
898
894
  },
899
895
  onCancel: (evt) => {
@@ -904,18 +900,6 @@ export function createAbortHook() {
904
900
  // ============================================================================
905
901
  // §9 UTILITIES
906
902
  // ============================================================================
907
- // Cross-runtime: safe AbortError creation (DOMException may not exist in all runtimes)
908
- function createAbortError(message) {
909
- if (typeof DOMException !== "undefined") {
910
- return new DOMException(message, "AbortError");
911
- }
912
- const err = new Error(message);
913
- err.name = "AbortError";
914
- return err;
915
- }
916
- function perfNow() {
917
- return typeof performance !== "undefined" ? performance.now() : Date.now();
918
- }
919
903
  /**
920
904
  * Create a new HookContext for the given request.
921
905
  *
@@ -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 {
@@ -462,11 +462,7 @@ export class Redactor {
462
462
  }
463
463
  /** Redact sensitive header values, replacing them with "***". */
464
464
  redactHeaders(headers) {
465
- const out = {};
466
- for (const [k, v] of Object.entries(headers)) {
467
- out[k] = this.headerSet.has(k.toLowerCase()) ? "***" : v;
468
- }
469
- return out;
465
+ return redactHeaders(headers, this.headerSet, "***");
470
466
  }
471
467
  /**
472
468
  * Redact sensitive query parameters from a URL.
@@ -516,9 +512,11 @@ export class Redactor {
516
512
  }
517
513
  if (!this.paramSet.has(decoded.toLowerCase()))
518
514
  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}=***`;
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}=***`;
522
520
  });
523
521
  return `${base}?${params.join("&")}${fragment}`;
524
522
  }
@@ -957,22 +955,11 @@ function serializeError(err) {
957
955
  return { name: "Error", message: String(err) };
958
956
  }
959
957
  /** `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
- }
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);
976
963
  /**
977
964
  * Truncate a string to at most `maxBytes` UTF-8 bytes, never splitting a
978
965
  * character, and report how many bytes were actually removed.
@@ -993,10 +980,6 @@ function truncateToBytes(str, maxBytes) {
993
980
  }
994
981
  return { text: str.slice(0, end), removed: total - used };
995
982
  }
996
- /** Cross-runtime high-resolution timer. Falls back to Date.now() when performance.now() is unavailable. */
997
- function perfNow() {
998
- return typeof performance !== "undefined" ? performance.now() : Date.now();
999
- }
1000
983
  // ============================================================================
1001
984
  // §10 FACTORY HELPERS
1002
985
  // ============================================================================
@@ -20,6 +20,7 @@
20
20
  * - Async iterator interface
21
21
  * - Readable progress formatting (human-readable bytes, speed)
22
22
  */
23
+ import { perfNow } from "./utils.js";
23
24
  // ============================================================================
24
25
  // §2 PROGRESS TRACKER
25
26
  // ============================================================================
@@ -926,6 +927,3 @@ export function throttleProgress(fn, hz = 10) {
926
927
  // ============================================================================
927
928
  // §10 UTILITIES
928
929
  // ============================================================================
929
- function perfNow() {
930
- return typeof performance !== "undefined" ? performance.now() : Date.now();
931
- }
package/dist/cjs/proxy.js CHANGED
@@ -18,6 +18,18 @@
18
18
  * @module
19
19
  */
20
20
  import { KinetexError } from "./types.js";
21
+ /**
22
+ * True when `host` is an IPv4/IPv6 literal rather than a DNS name.
23
+ *
24
+ * A textual shape check rather than `node:net.isIP`, because this is a
25
+ * synchronous guard and the module only reaches for `node:net` through a
26
+ * dynamic import. Being imprecise here is harmless: the worst case is that a
27
+ * name the resolver dislikes is sent as SNI, and the TLS stack rejects it.
28
+ */
29
+ function isIpLiteral(host) {
30
+ const h = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
31
+ return h.includes(":") || /^\d+(\.\d+)*$/.test(h);
32
+ }
21
33
  const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
22
34
  /** Maximum CONNECT response head we will buffer before giving up. */
23
35
  const MAX_HEAD_BYTES = 64 * 1024;
@@ -305,9 +317,14 @@ export async function connectThroughProxy(proxy, target, options = {}) {
305
317
  clearTimeout(tunnelTimer);
306
318
  options.signal?.removeEventListener("abort", onTunnelAbort);
307
319
  }
320
+ // RFC 6066 §3 forbids IP literals in SNI, and Node 26 rejects the
321
+ // handshake outright when one is sent; Node 22 and 24 ignored it. Omit
322
+ // `servername` for IP targets so certificate verification falls back to
323
+ // the `host`/`authorized` check, which still covers the certificate.
324
+ const sni = options.servername ?? target.hostname;
308
325
  const secure = tls.connect({
309
326
  socket: baseSocket,
310
- servername: options.servername ?? target.hostname,
327
+ ...(sni !== undefined && !isIpLiteral(sni) ? { servername: sni } : {}),
311
328
  ...(options.ca !== undefined ? { ca: options.ca } : {}),
312
329
  },
313
330
  // `rejectUnauthorized` is left at its default (true), so a peer that
@@ -372,7 +389,7 @@ async function openTlsSocket(tls, opts, timeoutMs, signal, errMeta, what) {
372
389
  const socket = tls.connect({
373
390
  host: opts.host,
374
391
  port: opts.port,
375
- servername: opts.servername,
392
+ ...(isIpLiteral(opts.servername) ? {} : { servername: opts.servername }),
376
393
  ...(opts.ca !== undefined ? { ca: opts.ca } : {}),
377
394
  }, () => {
378
395
  cleanup();