@socketsecurity/lib 5.12.0 → 5.14.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/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.14.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.14.0) - 2026-04-06
9
+
10
+ ### Added — http-request
11
+
12
+ - `HttpResponseError` class — thrown on non-2xx when `throwOnError` is enabled, carries the full `HttpResponse`
13
+ - `throwOnError` option on `HttpRequestOptions` — non-2xx responses throw instead of resolving with `ok: false`, enabling retry of HTTP errors
14
+ - `onRetry` callback on `HttpRequestOptions` — customize retry behavior per-attempt (return `false` to stop, a `number` to override delay, `undefined` for default backoff)
15
+ - Streaming body support — `body` accepts `Readable` streams (incl. `form-data` npm package), auto-merges `getHeaders()` when present
16
+ - `parseRetryAfterHeader()` — standalone RFC 7231 §7.1.3 `Retry-After` header parser (strict integer seconds + HTTP-date formats)
17
+ - `sanitizeHeaders()` — redact sensitive headers (`authorization`, `cookie`, `set-cookie`, `proxy-authorization`, `proxy-authenticate`, `www-authenticate`) for safe logging
18
+
19
+ ### Changed — http-request
20
+
21
+ - `HttpRequestOptions.body` type widened from `Buffer | string` to `Buffer | Readable | string`
22
+ - Redirect responses now drained via `res.resume()` to free sockets
23
+ - `maxResponseSize` exceeded now cleans up both response and request
24
+ - `onResponse` hooks wrapped in try/catch — user hook errors can no longer leave promises pending
25
+
26
+ ## [5.13.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.13.0) - 2026-04-05
27
+
28
+ ### Added — http-request
29
+
30
+ - `readIncomingResponse()` — reads and buffers a Node.js `IncomingResponse` into an `HttpResponse` (#143)
31
+ - Useful for converting raw responses from code that bypasses `httpRequest()` (e.g. multipart form-data uploads) into the standard `HttpResponse` interface
32
+ - `IncomingResponse` type alias — disambiguates `IncomingMessage` as a client-side response
33
+ - `IncomingRequest` type alias — disambiguates `IncomingMessage` as a server-side request
34
+
35
+ ### Changed — http-request
36
+
37
+ - Internal `httpRequestAttempt` callbacks now use `IncomingResponse` type
38
+ - `HttpResponse.rawResponse` type narrowed from `IncomingMessage` to `IncomingResponse`
39
+
8
40
  ## [5.12.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.12.0) - 2026-04-04
9
41
 
10
42
  ### Added — http-request
@@ -1,4 +1,24 @@
1
+ /**
2
+ * @fileoverview HTTP/HTTPS request utilities using Node.js built-in modules with retry logic, redirects, and download support.
3
+ *
4
+ * This module provides a fetch-like API built on top of Node.js native `http` and `https` modules.
5
+ * It supports automatic retries with exponential backoff, redirect following, streaming downloads,
6
+ * and provides a familiar fetch-style response interface.
7
+ *
8
+ * Key Features:
9
+ * - Automatic retries with exponential backoff for failed requests.
10
+ * - Redirect following with configurable max redirects.
11
+ * - Streaming downloads with progress callbacks.
12
+ * - Fetch-like response interface (`.json()`, `.text()`, `.arrayBuffer()`).
13
+ * - Timeout support for all operations.
14
+ * - Zero dependencies on external HTTP libraries.
15
+ */
16
+ import type { Readable } from 'node:stream';
1
17
  import type { IncomingHttpHeaders, IncomingMessage } from 'http';
18
+ /** IncomingMessage received as a response to a client request (http.request callback). */
19
+ export type IncomingResponse = IncomingMessage;
20
+ /** IncomingMessage received as a request in a server handler (http.createServer callback). */
21
+ export type IncomingRequest = IncomingMessage;
2
22
  import type { Logger } from './logger.js';
3
23
  /**
4
24
  * Information passed to the onRequest hook before each request attempt.
@@ -35,7 +55,17 @@ export interface HttpHooks {
35
55
  export interface HttpRequestOptions {
36
56
  /**
37
57
  * Request body to send.
38
- * Can be a string (e.g., JSON) or Buffer (e.g., binary data).
58
+ * Can be a string, Buffer, or Readable stream.
59
+ *
60
+ * When a Readable stream is provided, it is piped directly to the request.
61
+ * If the stream has a `getHeaders()` method (duck-typed, e.g., the `form-data`
62
+ * npm package), its headers (Content-Type with boundary) are automatically
63
+ * merged into the request headers.
64
+ *
65
+ * **Note:** Streaming bodies are one-shot — they cannot be replayed. Using a
66
+ * Readable body with `retries > 0` throws an error. Buffer the body as a
67
+ * string/Buffer if retries are needed. Redirects are also disabled for
68
+ * streaming bodies since the stream is consumed on the first request.
39
69
  *
40
70
  * @example
41
71
  * ```ts
@@ -52,9 +82,18 @@ export interface HttpRequestOptions {
52
82
  * method: 'POST',
53
83
  * body: buffer
54
84
  * })
85
+ *
86
+ * // Stream form-data (npm package, not native FormData)
87
+ * import FormData from 'form-data'
88
+ * const form = new FormData()
89
+ * form.append('file', createReadStream('data.json'))
90
+ * await httpRequest('https://api.example.com/upload', {
91
+ * method: 'POST',
92
+ * body: form // auto-merges form.getHeaders()
93
+ * })
55
94
  * ```
56
95
  */
57
- body?: Buffer | string | undefined;
96
+ body?: Buffer | Readable | string | undefined;
58
97
  /**
59
98
  * Custom CA certificates for TLS connections.
60
99
  * When provided, these certificates are combined with the default trust
@@ -158,6 +197,38 @@ export interface HttpRequestOptions {
158
197
  * ```
159
198
  */
160
199
  method?: string | undefined;
200
+ /**
201
+ * Callback invoked before each retry attempt.
202
+ * Allows customizing retry behavior per-attempt (e.g., skip 4xx, honor Retry-After).
203
+ *
204
+ * @param attempt - Current retry attempt number (1-based)
205
+ * @param error - The error that triggered the retry (HttpResponseError for HTTP errors)
206
+ * @param delay - The calculated delay in ms before next retry
207
+ * @returns `false` to stop retrying and rethrow,
208
+ * a `number` to override the delay (ms),
209
+ * or `undefined` to use the calculated delay
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * await httpRequest('https://api.example.com/data', {
214
+ * retries: 3,
215
+ * throwOnError: true,
216
+ * onRetry: (attempt, error, delay) => {
217
+ * // Don't retry client errors (except 429)
218
+ * if (error instanceof HttpResponseError) {
219
+ * if (error.response.status === 429) {
220
+ * const retryAfter = parseRetryAfterHeader(error.response.headers['retry-after'])
221
+ * return retryAfter ?? undefined
222
+ * }
223
+ * if (error.response.status >= 400 && error.response.status < 500) {
224
+ * return false
225
+ * }
226
+ * }
227
+ * }
228
+ * })
229
+ * ```
230
+ */
231
+ onRetry?: ((attempt: number, error: unknown, delay: number) => boolean | number | undefined) | undefined;
161
232
  /**
162
233
  * Number of retry attempts for failed requests.
163
234
  * Uses exponential backoff: delay = `retryDelay` * 2^attempt.
@@ -190,6 +261,23 @@ export interface HttpRequestOptions {
190
261
  * ```
191
262
  */
192
263
  retryDelay?: number | undefined;
264
+ /**
265
+ * When true, non-2xx HTTP responses throw an `HttpResponseError` instead
266
+ * of resolving with `response.ok === false`. This makes HTTP error
267
+ * responses eligible for retry via the `retries` option.
268
+ *
269
+ * @default false
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * // Throw on 4xx/5xx responses (enabling retry for 5xx)
274
+ * await httpRequest('https://api.example.com/data', {
275
+ * throwOnError: true,
276
+ * retries: 3
277
+ * })
278
+ * ```
279
+ */
280
+ throwOnError?: boolean | undefined;
193
281
  /**
194
282
  * Request timeout in milliseconds.
195
283
  * If the request takes longer than this, it will be aborted.
@@ -314,12 +402,71 @@ export interface HttpResponse {
314
402
  */
315
403
  text(): string;
316
404
  /**
317
- * The underlying Node.js IncomingMessage for advanced use cases
405
+ * The underlying Node.js IncomingResponse for advanced use cases
318
406
  * (e.g., streaming, custom header inspection). Only available when
319
407
  * the response was not consumed by the convenience methods.
320
408
  */
321
- rawResponse?: IncomingMessage | undefined;
409
+ rawResponse?: IncomingResponse | undefined;
410
+ }
411
+ /**
412
+ * Read and buffer a client-side IncomingResponse into an HttpResponse.
413
+ *
414
+ * Useful when you have a raw response from code that bypasses
415
+ * `httpRequest()` (e.g., multipart form-data uploads via `http.request()`,
416
+ * or responses from third-party HTTP libraries) and need to convert it
417
+ * into the standard HttpResponse interface.
418
+ */
419
+ export declare function readIncomingResponse(msg: IncomingResponse): Promise<HttpResponse>;
420
+ /**
421
+ * Error thrown when an HTTP response has a non-2xx status code
422
+ * and `throwOnError` is enabled. Carries the full `HttpResponse`
423
+ * so callers can inspect status, headers, and body.
424
+ */
425
+ export declare class HttpResponseError extends Error {
426
+ response: HttpResponse;
427
+ constructor(response: HttpResponse, message?: string | undefined);
322
428
  }
429
+ /**
430
+ * Parse a `Retry-After` HTTP header value into milliseconds.
431
+ *
432
+ * Supports both formats defined in RFC 7231 §7.1.3:
433
+ * - **delay-seconds**: integer number of seconds (e.g., `"120"`)
434
+ * - **HTTP-date**: an absolute date/time (e.g., `"Fri, 31 Dec 2027 23:59:59 GMT"`)
435
+ *
436
+ * When the header is an array (multiple values), the first element is used.
437
+ *
438
+ * @param value - The raw Retry-After header value(s)
439
+ * @returns Delay in milliseconds, or `undefined` if the value cannot be parsed
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * const delay = parseRetryAfterHeader(response.headers['retry-after'])
444
+ * if (delay !== undefined) {
445
+ * await new Promise(resolve => setTimeout(resolve, delay))
446
+ * }
447
+ * ```
448
+ */
449
+ export declare function parseRetryAfterHeader(value: string | string[] | undefined): number | undefined;
450
+ /**
451
+ * Redact sensitive HTTP headers for safe logging and telemetry.
452
+ *
453
+ * Replaces values of sensitive headers (Authorization, Cookie, etc.)
454
+ * with `[REDACTED]`. Non-sensitive headers are passed through unchanged.
455
+ * Array values are joined with `', '`.
456
+ *
457
+ * @param headers - HTTP headers to sanitize
458
+ * @returns A new object with sensitive values redacted
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * const safe = sanitizeHeaders({
463
+ * 'authorization': 'Bearer secret',
464
+ * 'content-type': 'application/json'
465
+ * })
466
+ * // { authorization: '[REDACTED]', 'content-type': 'application/json' }
467
+ * ```
468
+ */
469
+ export declare function sanitizeHeaders(headers: Record<string, unknown> | undefined): Record<string, string>;
323
470
  /**
324
471
  * Configuration options for file downloads.
325
472
  */
@@ -19,13 +19,17 @@ var __copyProps = (to, from, except, desc) => {
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
  var http_request_exports = {};
21
21
  __export(http_request_exports, {
22
+ HttpResponseError: () => HttpResponseError,
22
23
  enrichErrorMessage: () => enrichErrorMessage,
23
24
  fetchChecksums: () => fetchChecksums,
24
25
  httpDownload: () => httpDownload,
25
26
  httpJson: () => httpJson,
26
27
  httpRequest: () => httpRequest,
27
28
  httpText: () => httpText,
28
- parseChecksums: () => parseChecksums
29
+ parseChecksums: () => parseChecksums,
30
+ parseRetryAfterHeader: () => parseRetryAfterHeader,
31
+ readIncomingResponse: () => readIncomingResponse,
32
+ sanitizeHeaders: () => sanitizeHeaders
29
33
  });
30
34
  module.exports = __toCommonJS(http_request_exports);
31
35
  var import_fs = require("./fs.js");
@@ -61,6 +65,87 @@ function getHttps() {
61
65
  }
62
66
  return _https;
63
67
  }
68
+ async function readIncomingResponse(msg) {
69
+ const chunks = [];
70
+ for await (const chunk of msg) {
71
+ chunks.push(chunk);
72
+ }
73
+ const body = Buffer.concat(chunks);
74
+ const status = msg.statusCode ?? 0;
75
+ const statusText = msg.statusMessage ?? "";
76
+ return {
77
+ arrayBuffer: () => body.buffer.slice(
78
+ body.byteOffset,
79
+ body.byteOffset + body.byteLength
80
+ ),
81
+ body,
82
+ headers: msg.headers,
83
+ json: () => JSON.parse(body.toString("utf8")),
84
+ ok: status >= 200 && status < 300,
85
+ rawResponse: msg,
86
+ status,
87
+ statusText,
88
+ text: () => body.toString("utf8")
89
+ };
90
+ }
91
+ class HttpResponseError extends Error {
92
+ response;
93
+ constructor(response, message) {
94
+ const statusCode = response.status ?? "unknown";
95
+ const statusMessage = response.statusText || "No status message";
96
+ super(message ?? `HTTP ${statusCode}: ${statusMessage}`);
97
+ this.name = "HttpResponseError";
98
+ this.response = response;
99
+ Error.captureStackTrace(this, HttpResponseError);
100
+ }
101
+ }
102
+ function parseRetryAfterHeader(value) {
103
+ if (!value) {
104
+ return void 0;
105
+ }
106
+ const raw = Array.isArray(value) ? value[0] : value;
107
+ if (!raw) {
108
+ return void 0;
109
+ }
110
+ const trimmed = raw.trim();
111
+ if (/^\d+$/.test(trimmed)) {
112
+ const seconds = Number(trimmed);
113
+ return seconds * 1e3;
114
+ }
115
+ const date = new Date(raw);
116
+ if (!Number.isNaN(date.getTime())) {
117
+ const delayMs = date.getTime() - Date.now();
118
+ if (delayMs > 0) {
119
+ return delayMs;
120
+ }
121
+ }
122
+ return void 0;
123
+ }
124
+ function sanitizeHeaders(headers) {
125
+ if (!headers) {
126
+ return {};
127
+ }
128
+ const sensitiveHeaders = /* @__PURE__ */ new Set([
129
+ "authorization",
130
+ "cookie",
131
+ "proxy-authorization",
132
+ "proxy-authenticate",
133
+ "set-cookie",
134
+ "www-authenticate"
135
+ ]);
136
+ const result = { __proto__: null };
137
+ for (const key of Object.keys(headers)) {
138
+ const value = headers[key];
139
+ if (sensitiveHeaders.has(key.toLowerCase())) {
140
+ result[key] = "[REDACTED]";
141
+ } else if (Array.isArray(value)) {
142
+ result[key] = value.join(", ");
143
+ } else if (value !== void 0 && value !== null) {
144
+ result[key] = String(value);
145
+ }
146
+ }
147
+ return result;
148
+ }
64
149
  function parseChecksums(text) {
65
150
  const checksums = { __proto__: null };
66
151
  for (const line of text.split("\n")) {
@@ -273,12 +358,34 @@ async function httpRequestAttempt(url, options) {
273
358
  timeout = 3e4
274
359
  } = { __proto__: null, ...options };
275
360
  const startTime = Date.now();
361
+ const streamHeaders = body && typeof body === "object" && "getHeaders" in body && typeof body.getHeaders === "function" ? body.getHeaders() : void 0;
276
362
  const mergedHeaders = {
277
363
  "User-Agent": "socket-registry/1.0",
364
+ ...streamHeaders,
278
365
  ...headers
279
366
  };
280
367
  hooks?.onRequest?.({ method, url, headers: mergedHeaders, timeout });
281
368
  return await new Promise((resolve, reject) => {
369
+ let settled = false;
370
+ const resolveOnce = (response) => {
371
+ if (settled) {
372
+ return;
373
+ }
374
+ settled = true;
375
+ resolve(response);
376
+ };
377
+ const rejectOnce = (err) => {
378
+ if (settled) {
379
+ return;
380
+ }
381
+ settled = true;
382
+ if (body && typeof body === "object" && typeof body.destroy === "function") {
383
+ ;
384
+ body.destroy();
385
+ }
386
+ emitResponse({ error: err });
387
+ reject(err);
388
+ };
282
389
  const parsedUrl = new URL(url);
283
390
  const isHttps = parsedUrl.protocol === "https:";
284
391
  const httpModule = isHttps ? /* @__PURE__ */ getHttps() : /* @__PURE__ */ getHttp();
@@ -294,23 +401,28 @@ async function httpRequestAttempt(url, options) {
294
401
  requestOptions["ca"] = ca;
295
402
  }
296
403
  const emitResponse = (info) => {
297
- hooks?.onResponse?.({
298
- duration: Date.now() - startTime,
299
- method,
300
- url,
301
- ...info
302
- });
404
+ try {
405
+ hooks?.onResponse?.({
406
+ duration: Date.now() - startTime,
407
+ method,
408
+ url,
409
+ ...info
410
+ });
411
+ } catch {
412
+ }
303
413
  };
304
414
  const request = httpModule.request(
305
415
  requestOptions,
306
416
  (res) => {
307
417
  if (followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
418
+ res.resume();
308
419
  emitResponse({
309
420
  headers: res.headers,
310
421
  status: res.statusCode,
311
422
  statusText: res.statusMessage
312
423
  });
313
424
  if (maxRedirects <= 0) {
425
+ settled = true;
314
426
  reject(
315
427
  new Error(
316
428
  `Too many redirects (exceeded maximum: ${maxRedirects})`
@@ -321,6 +433,7 @@ async function httpRequestAttempt(url, options) {
321
433
  const redirectUrl = res.headers.location.startsWith("http") ? res.headers.location : new URL(res.headers.location, url).toString();
322
434
  const redirectParsed = new URL(redirectUrl);
323
435
  if (isHttps && redirectParsed.protocol !== "https:") {
436
+ settled = true;
324
437
  reject(
325
438
  new Error(
326
439
  `Redirect from HTTPS to HTTP is not allowed: ${redirectUrl}`
@@ -328,6 +441,7 @@ async function httpRequestAttempt(url, options) {
328
441
  );
329
442
  return;
330
443
  }
444
+ settled = true;
331
445
  resolve(
332
446
  httpRequestAttempt(redirectUrl, {
333
447
  body,
@@ -349,18 +463,22 @@ async function httpRequestAttempt(url, options) {
349
463
  totalBytes += chunk.length;
350
464
  if (maxResponseSize && totalBytes > maxResponseSize) {
351
465
  res.destroy();
466
+ request.destroy();
352
467
  const sizeMB = (totalBytes / (1024 * 1024)).toFixed(2);
353
468
  const maxMB = (maxResponseSize / (1024 * 1024)).toFixed(2);
354
- const err = new Error(
355
- `Response exceeds maximum size limit (${sizeMB}MB > ${maxMB}MB)`
469
+ rejectOnce(
470
+ new Error(
471
+ `Response exceeds maximum size limit (${sizeMB}MB > ${maxMB}MB)`
472
+ )
356
473
  );
357
- emitResponse({ error: err });
358
- reject(err);
359
474
  return;
360
475
  }
361
476
  chunks.push(chunk);
362
477
  });
363
478
  res.on("end", () => {
479
+ if (settled) {
480
+ return;
481
+ }
364
482
  const responseBody = Buffer.concat(chunks);
365
483
  const ok = res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300;
366
484
  const response = {
@@ -388,11 +506,10 @@ async function httpRequestAttempt(url, options) {
388
506
  status: res.statusCode,
389
507
  statusText: res.statusMessage
390
508
  });
391
- resolve(response);
509
+ resolveOnce(response);
392
510
  });
393
511
  res.on("error", (error) => {
394
- emitResponse({ error });
395
- reject(error);
512
+ rejectOnce(error);
396
513
  });
397
514
  }
398
515
  );
@@ -402,24 +519,33 @@ async function httpRequestAttempt(url, options) {
402
519
  method,
403
520
  error
404
521
  );
405
- const enhanced = new Error(message, { cause: error });
406
- emitResponse({ error: enhanced });
407
- reject(enhanced);
522
+ rejectOnce(new Error(message, { cause: error }));
408
523
  });
409
524
  request.on("timeout", () => {
410
525
  request.destroy();
411
- const err = new Error(
412
- `${method} request timed out after ${timeout}ms: ${url}
526
+ rejectOnce(
527
+ new Error(
528
+ `${method} request timed out after ${timeout}ms: ${url}
413
529
  \u2192 Server did not respond in time.
414
530
  \u2192 Try: Increase timeout or check network connectivity.`
531
+ )
415
532
  );
416
- emitResponse({ error: err });
417
- reject(err);
418
533
  });
419
534
  if (body) {
535
+ if (typeof body === "object" && typeof body.pipe === "function") {
536
+ const stream = body;
537
+ stream.on("error", (err) => {
538
+ request.destroy();
539
+ rejectOnce(err);
540
+ });
541
+ stream.pipe(request);
542
+ return;
543
+ }
420
544
  request.write(body);
545
+ request.end();
546
+ } else {
547
+ request.end();
421
548
  }
422
- request.end();
423
549
  });
424
550
  }
425
551
  async function httpDownload(url, destPath, options) {
@@ -547,31 +673,55 @@ async function httpRequest(url, options) {
547
673
  maxRedirects = 5,
548
674
  maxResponseSize,
549
675
  method = "GET",
676
+ onRetry,
550
677
  retries = 0,
551
678
  retryDelay = 1e3,
679
+ throwOnError = false,
552
680
  timeout = 3e4
553
681
  } = { __proto__: null, ...options };
682
+ const isStreamBody = body !== void 0 && typeof body === "object" && typeof body.pipe === "function";
683
+ if (isStreamBody && retries > 0) {
684
+ throw new Error(
685
+ "Streaming body (Readable/FormData) cannot be used with retries. Streams are consumed on first attempt and cannot be replayed. Set retries: 0 or buffer the body as a string/Buffer."
686
+ );
687
+ }
688
+ const attemptOpts = {
689
+ body,
690
+ ca,
691
+ // Disable redirect following for stream bodies — the stream is consumed
692
+ // on the first request and cannot be re-piped to the redirect target.
693
+ followRedirects: isStreamBody ? false : followRedirects,
694
+ headers,
695
+ hooks,
696
+ maxRedirects,
697
+ maxResponseSize,
698
+ method,
699
+ timeout
700
+ };
554
701
  let lastError;
555
702
  for (let attempt = 0; attempt <= retries; attempt++) {
556
703
  try {
557
- return await httpRequestAttempt(url, {
558
- body,
559
- ca,
560
- followRedirects,
561
- headers,
562
- hooks,
563
- maxRedirects,
564
- maxResponseSize,
565
- method,
566
- timeout
567
- });
704
+ const response = await httpRequestAttempt(url, attemptOpts);
705
+ if (throwOnError && !response.ok) {
706
+ throw new HttpResponseError(response);
707
+ }
708
+ return response;
568
709
  } catch (e) {
569
710
  lastError = e;
570
711
  if (attempt === retries) {
571
712
  break;
572
713
  }
573
714
  const delayMs = retryDelay * 2 ** attempt;
574
- await new Promise((resolve) => setTimeout(resolve, delayMs));
715
+ if (onRetry) {
716
+ const retryResult = onRetry(attempt + 1, e, delayMs);
717
+ if (retryResult === false) {
718
+ break;
719
+ }
720
+ const actualDelay = typeof retryResult === "number" && !Number.isNaN(retryResult) ? Math.max(0, retryResult) : delayMs;
721
+ await new Promise((resolve) => setTimeout(resolve, actualDelay));
722
+ } else {
723
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
724
+ }
575
725
  }
576
726
  }
577
727
  throw lastError || new Error("Request failed after retries");
@@ -607,11 +757,15 @@ async function httpText(url, options) {
607
757
  }
608
758
  // Annotate the CommonJS export names for ESM import in node:
609
759
  0 && (module.exports = {
760
+ HttpResponseError,
610
761
  enrichErrorMessage,
611
762
  fetchChecksums,
612
763
  httpDownload,
613
764
  httpJson,
614
765
  httpRequest,
615
766
  httpText,
616
- parseChecksums
767
+ parseChecksums,
768
+ parseRetryAfterHeader,
769
+ readIncomingResponse,
770
+ sanitizeHeaders
617
771
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "5.12.0",
3
+ "version": "5.14.0",
4
4
  "packageManager": "pnpm@10.33.0",
5
5
  "license": "MIT",
6
6
  "description": "Core utilities and infrastructure for Socket.dev security tools",
@@ -735,7 +735,7 @@
735
735
  "@socketregistry/is-unicode-supported": "1.0.5",
736
736
  "@socketregistry/packageurl-js": "1.4.1",
737
737
  "@socketregistry/yocto-spinner": "1.0.25",
738
- "@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.11.4",
738
+ "@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.13.0",
739
739
  "@types/node": "24.9.2",
740
740
  "@typescript/native-preview": "7.0.0-dev.20250920.1",
741
741
  "@vitest/coverage-v8": "4.0.3",