@socketsecurity/lib 5.11.4 → 5.13.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.
@@ -1,4 +1,38 @@
1
+ import type { IncomingHttpHeaders, IncomingMessage } from 'http';
2
+ /** IncomingMessage received as a response to a client request (http.request callback). */
3
+ export type IncomingResponse = IncomingMessage;
4
+ /** IncomingMessage received as a request in a server handler (http.createServer callback). */
5
+ export type IncomingRequest = IncomingMessage;
1
6
  import type { Logger } from './logger.js';
7
+ /**
8
+ * Information passed to the onRequest hook before each request attempt.
9
+ */
10
+ export interface HttpHookRequestInfo {
11
+ headers: Record<string, string>;
12
+ method: string;
13
+ timeout: number;
14
+ url: string;
15
+ }
16
+ /**
17
+ * Information passed to the onResponse hook after each request attempt.
18
+ */
19
+ export interface HttpHookResponseInfo {
20
+ duration: number;
21
+ error?: Error | undefined;
22
+ headers?: IncomingHttpHeaders | undefined;
23
+ method: string;
24
+ status?: number | undefined;
25
+ statusText?: string | undefined;
26
+ url: string;
27
+ }
28
+ /**
29
+ * Lifecycle hooks for observing HTTP request/response events.
30
+ * Hooks fire per-attempt (retries produce multiple hook calls).
31
+ */
32
+ export interface HttpHooks {
33
+ onRequest?: ((info: HttpHookRequestInfo) => void) | undefined;
34
+ onResponse?: ((info: HttpHookResponseInfo) => void) | undefined;
35
+ }
2
36
  /**
3
37
  * Configuration options for HTTP/HTTPS requests.
4
38
  */
@@ -61,6 +95,11 @@ export interface HttpRequestOptions {
61
95
  * ```
62
96
  */
63
97
  followRedirects?: boolean | undefined;
98
+ /**
99
+ * Lifecycle hooks for observing request/response events.
100
+ * Hooks fire per-attempt — retries and redirects each trigger separate hook calls.
101
+ */
102
+ hooks?: HttpHooks | undefined;
64
103
  /**
65
104
  * HTTP headers to send with the request.
66
105
  * A `User-Agent` header is automatically added if not provided.
@@ -92,6 +131,14 @@ export interface HttpRequestOptions {
92
131
  * ```
93
132
  */
94
133
  maxRedirects?: number | undefined;
134
+ /**
135
+ * Maximum response body size in bytes. Responses exceeding this limit
136
+ * will be rejected with an error. Prevents memory exhaustion from
137
+ * unexpectedly large responses.
138
+ *
139
+ * @default undefined (no limit)
140
+ */
141
+ maxResponseSize?: number | undefined;
95
142
  /**
96
143
  * HTTP method to use for the request.
97
144
  *
@@ -205,7 +252,7 @@ export interface HttpResponse {
205
252
  * console.log(response.headers['set-cookie']) // May be string[]
206
253
  * ```
207
254
  */
208
- headers: Record<string, string | string[] | undefined>;
255
+ headers: IncomingHttpHeaders;
209
256
  /**
210
257
  * Parse response body as JSON.
211
258
  * Type parameter `T` allows specifying the expected JSON structure.
@@ -270,7 +317,22 @@ export interface HttpResponse {
270
317
  * ```
271
318
  */
272
319
  text(): string;
320
+ /**
321
+ * The underlying Node.js IncomingResponse for advanced use cases
322
+ * (e.g., streaming, custom header inspection). Only available when
323
+ * the response was not consumed by the convenience methods.
324
+ */
325
+ rawResponse?: IncomingResponse | undefined;
273
326
  }
327
+ /**
328
+ * Read and buffer a client-side IncomingResponse into an HttpResponse.
329
+ *
330
+ * Useful when you have a raw response from code that bypasses
331
+ * `httpRequest()` (e.g., multipart form-data uploads via `http.request()`,
332
+ * or responses from third-party HTTP libraries) and need to convert it
333
+ * into the standard HttpResponse interface.
334
+ */
335
+ export declare function readIncomingResponse(msg: IncomingResponse): Promise<HttpResponse>;
274
336
  /**
275
337
  * Configuration options for file downloads.
276
338
  */
@@ -567,6 +629,11 @@ export interface FetchChecksumsOptions {
567
629
  * ```
568
630
  */
569
631
  export declare function fetchChecksums(url: string, options?: FetchChecksumsOptions | undefined): Promise<Checksums>;
632
+ /**
633
+ * Build an enriched error message based on the error code.
634
+ * Generic guidance (no product-specific branding).
635
+ */
636
+ export declare function enrichErrorMessage(url: string, method: string, error: NodeJS.ErrnoException): string;
570
637
  /**
571
638
  * Download a file from a URL to a local path with redirect support, retry logic, and progress callbacks.
572
639
  * Uses streaming to avoid loading entire file in memory.
@@ -19,12 +19,14 @@ 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
+ enrichErrorMessage: () => enrichErrorMessage,
22
23
  fetchChecksums: () => fetchChecksums,
23
24
  httpDownload: () => httpDownload,
24
25
  httpJson: () => httpJson,
25
26
  httpRequest: () => httpRequest,
26
27
  httpText: () => httpText,
27
- parseChecksums: () => parseChecksums
28
+ parseChecksums: () => parseChecksums,
29
+ readIncomingResponse: () => readIncomingResponse
28
30
  });
29
31
  module.exports = __toCommonJS(http_request_exports);
30
32
  var import_fs = require("./fs.js");
@@ -60,6 +62,29 @@ function getHttps() {
60
62
  }
61
63
  return _https;
62
64
  }
65
+ async function readIncomingResponse(msg) {
66
+ const chunks = [];
67
+ for await (const chunk of msg) {
68
+ chunks.push(chunk);
69
+ }
70
+ const body = Buffer.concat(chunks);
71
+ const status = msg.statusCode ?? 0;
72
+ const statusText = msg.statusMessage ?? "";
73
+ return {
74
+ arrayBuffer: () => body.buffer.slice(
75
+ body.byteOffset,
76
+ body.byteOffset + body.byteLength
77
+ ),
78
+ body,
79
+ headers: msg.headers,
80
+ json: () => JSON.parse(body.toString("utf8")),
81
+ ok: status >= 200 && status < 300,
82
+ rawResponse: msg,
83
+ status,
84
+ statusText,
85
+ text: () => body.toString("utf8")
86
+ };
87
+ }
63
88
  function parseChecksums(text) {
64
89
  const checksums = { __proto__: null };
65
90
  for (const line of text.split("\n")) {
@@ -238,25 +263,51 @@ async function httpDownloadAttempt(url, destPath, options) {
238
263
  request.end();
239
264
  });
240
265
  }
266
+ function enrichErrorMessage(url, method, error) {
267
+ const code = error.code;
268
+ let message = `${method} request failed: ${url}`;
269
+ if (code === "ECONNREFUSED") {
270
+ message += "\n\u2192 Connection refused. Server is unreachable.\n\u2192 Check: Network connectivity and firewall settings.";
271
+ } else if (code === "ENOTFOUND") {
272
+ message += "\n\u2192 DNS lookup failed. Cannot resolve hostname.\n\u2192 Check: Internet connection and DNS settings.";
273
+ } else if (code === "ETIMEDOUT") {
274
+ message += "\n\u2192 Connection timed out. Network or server issue.\n\u2192 Try: Check network connectivity and retry.";
275
+ } else if (code === "ECONNRESET") {
276
+ message += "\n\u2192 Connection reset by server. Possible network interruption.\n\u2192 Try: Retry the request.";
277
+ } else if (code === "EPIPE") {
278
+ message += "\n\u2192 Broken pipe. Server closed connection unexpectedly.\n\u2192 Check: Authentication credentials and permissions.";
279
+ } else if (code === "CERT_HAS_EXPIRED" || code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
280
+ message += "\n\u2192 SSL/TLS certificate error.\n\u2192 Check: System time and date are correct.\n\u2192 Try: Update CA certificates on your system.";
281
+ } else if (code) {
282
+ message += `
283
+ \u2192 Error code: ${code}`;
284
+ }
285
+ return message;
286
+ }
241
287
  async function httpRequestAttempt(url, options) {
242
288
  const {
243
289
  body,
244
290
  ca,
245
291
  followRedirects = true,
246
292
  headers = {},
293
+ hooks,
247
294
  maxRedirects = 5,
295
+ maxResponseSize,
248
296
  method = "GET",
249
297
  timeout = 3e4
250
298
  } = { __proto__: null, ...options };
299
+ const startTime = Date.now();
300
+ const mergedHeaders = {
301
+ "User-Agent": "socket-registry/1.0",
302
+ ...headers
303
+ };
304
+ hooks?.onRequest?.({ method, url, headers: mergedHeaders, timeout });
251
305
  return await new Promise((resolve, reject) => {
252
306
  const parsedUrl = new URL(url);
253
307
  const isHttps = parsedUrl.protocol === "https:";
254
308
  const httpModule = isHttps ? /* @__PURE__ */ getHttps() : /* @__PURE__ */ getHttp();
255
309
  const requestOptions = {
256
- headers: {
257
- "User-Agent": "socket-registry/1.0",
258
- ...headers
259
- },
310
+ headers: mergedHeaders,
260
311
  hostname: parsedUrl.hostname,
261
312
  method,
262
313
  path: parsedUrl.pathname + parsedUrl.search,
@@ -266,10 +317,23 @@ async function httpRequestAttempt(url, options) {
266
317
  if (ca && isHttps) {
267
318
  requestOptions["ca"] = ca;
268
319
  }
320
+ const emitResponse = (info) => {
321
+ hooks?.onResponse?.({
322
+ duration: Date.now() - startTime,
323
+ method,
324
+ url,
325
+ ...info
326
+ });
327
+ };
269
328
  const request = httpModule.request(
270
329
  requestOptions,
271
330
  (res) => {
272
331
  if (followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
332
+ emitResponse({
333
+ headers: res.headers,
334
+ status: res.statusCode,
335
+ statusText: res.statusMessage
336
+ });
273
337
  if (maxRedirects <= 0) {
274
338
  reject(
275
339
  new Error(
@@ -294,7 +358,9 @@ async function httpRequestAttempt(url, options) {
294
358
  ca,
295
359
  followRedirects,
296
360
  headers,
361
+ hooks,
297
362
  maxRedirects: maxRedirects - 1,
363
+ maxResponseSize,
298
364
  method,
299
365
  timeout
300
366
  })
@@ -302,7 +368,20 @@ async function httpRequestAttempt(url, options) {
302
368
  return;
303
369
  }
304
370
  const chunks = [];
371
+ let totalBytes = 0;
305
372
  res.on("data", (chunk) => {
373
+ totalBytes += chunk.length;
374
+ if (maxResponseSize && totalBytes > maxResponseSize) {
375
+ res.destroy();
376
+ const sizeMB = (totalBytes / (1024 * 1024)).toFixed(2);
377
+ const maxMB = (maxResponseSize / (1024 * 1024)).toFixed(2);
378
+ const err = new Error(
379
+ `Response exceeds maximum size limit (${sizeMB}MB > ${maxMB}MB)`
380
+ );
381
+ emitResponse({ error: err });
382
+ reject(err);
383
+ return;
384
+ }
306
385
  chunks.push(chunk);
307
386
  });
308
387
  res.on("end", () => {
@@ -321,39 +400,45 @@ async function httpRequestAttempt(url, options) {
321
400
  return JSON.parse(responseBody.toString("utf8"));
322
401
  },
323
402
  ok,
403
+ rawResponse: res,
324
404
  status: res.statusCode || 0,
325
405
  statusText: res.statusMessage || "",
326
406
  text() {
327
407
  return responseBody.toString("utf8");
328
408
  }
329
409
  };
410
+ emitResponse({
411
+ headers: res.headers,
412
+ status: res.statusCode,
413
+ statusText: res.statusMessage
414
+ });
330
415
  resolve(response);
331
416
  });
332
417
  res.on("error", (error) => {
418
+ emitResponse({ error });
333
419
  reject(error);
334
420
  });
335
421
  }
336
422
  );
337
423
  request.on("error", (error) => {
338
- const code = error.code;
339
- let message = `HTTP request failed for ${url}: ${error.message}
340
- `;
341
- if (code === "ENOTFOUND") {
342
- message += "DNS lookup failed. Check the hostname and your network connection.";
343
- } else if (code === "ECONNREFUSED") {
344
- message += "Connection refused. Verify the server is running and accessible.";
345
- } else if (code === "ETIMEDOUT") {
346
- message += "Request timed out. Check your network or increase the timeout value.";
347
- } else if (code === "ECONNRESET") {
348
- message += "Connection reset. The server may have closed the connection unexpectedly.";
349
- } else {
350
- message += "Check your network connection and verify the URL is correct.";
351
- }
352
- reject(new Error(message, { cause: error }));
424
+ const message = enrichErrorMessage(
425
+ url,
426
+ method,
427
+ error
428
+ );
429
+ const enhanced = new Error(message, { cause: error });
430
+ emitResponse({ error: enhanced });
431
+ reject(enhanced);
353
432
  });
354
433
  request.on("timeout", () => {
355
434
  request.destroy();
356
- reject(new Error(`Request timed out after ${timeout}ms`));
435
+ const err = new Error(
436
+ `${method} request timed out after ${timeout}ms: ${url}
437
+ \u2192 Server did not respond in time.
438
+ \u2192 Try: Increase timeout or check network connectivity.`
439
+ );
440
+ emitResponse({ error: err });
441
+ reject(err);
357
442
  });
358
443
  if (body) {
359
444
  request.write(body);
@@ -482,7 +567,9 @@ async function httpRequest(url, options) {
482
567
  ca,
483
568
  followRedirects = true,
484
569
  headers = {},
570
+ hooks,
485
571
  maxRedirects = 5,
572
+ maxResponseSize,
486
573
  method = "GET",
487
574
  retries = 0,
488
575
  retryDelay = 1e3,
@@ -496,7 +583,9 @@ async function httpRequest(url, options) {
496
583
  ca,
497
584
  followRedirects,
498
585
  headers,
586
+ hooks,
499
587
  maxRedirects,
588
+ maxResponseSize,
500
589
  method,
501
590
  timeout
502
591
  });
@@ -542,10 +631,12 @@ async function httpText(url, options) {
542
631
  }
543
632
  // Annotate the CommonJS export names for ESM import in node:
544
633
  0 && (module.exports = {
634
+ enrichErrorMessage,
545
635
  fetchChecksums,
546
636
  httpDownload,
547
637
  httpJson,
548
638
  httpRequest,
549
639
  httpText,
550
- parseChecksums
640
+ parseChecksums,
641
+ readIncomingResponse
551
642
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "5.11.4",
3
+ "version": "5.13.0",
4
4
  "packageManager": "pnpm@10.33.0",
5
5
  "license": "MIT",
6
6
  "description": "Core utilities and infrastructure for Socket.dev security tools",
@@ -717,6 +717,7 @@
717
717
  "update": "node scripts/update.mjs"
718
718
  },
719
719
  "devDependencies": {
720
+ "@anthropic-ai/claude-code": "2.1.89",
720
721
  "@babel/core": "7.28.4",
721
722
  "@babel/parser": "7.28.4",
722
723
  "@babel/traverse": "7.28.4",
@@ -732,9 +733,9 @@
732
733
  "@npmcli/package-json": "7.0.0",
733
734
  "@npmcli/promise-spawn": "8.0.3",
734
735
  "@socketregistry/is-unicode-supported": "1.0.5",
735
- "@socketregistry/packageurl-js": "1.3.5",
736
+ "@socketregistry/packageurl-js": "1.4.1",
736
737
  "@socketregistry/yocto-spinner": "1.0.25",
737
- "@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.11.3",
738
+ "@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.12.0",
738
739
  "@types/node": "24.9.2",
739
740
  "@typescript/native-preview": "7.0.0-dev.20250920.1",
740
741
  "@vitest/coverage-v8": "4.0.3",