@socketsecurity/lib 5.11.4 → 5.12.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,34 @@
1
+ import type { IncomingHttpHeaders, IncomingMessage } from 'http';
1
2
  import type { Logger } from './logger.js';
3
+ /**
4
+ * Information passed to the onRequest hook before each request attempt.
5
+ */
6
+ export interface HttpHookRequestInfo {
7
+ headers: Record<string, string>;
8
+ method: string;
9
+ timeout: number;
10
+ url: string;
11
+ }
12
+ /**
13
+ * Information passed to the onResponse hook after each request attempt.
14
+ */
15
+ export interface HttpHookResponseInfo {
16
+ duration: number;
17
+ error?: Error | undefined;
18
+ headers?: IncomingHttpHeaders | undefined;
19
+ method: string;
20
+ status?: number | undefined;
21
+ statusText?: string | undefined;
22
+ url: string;
23
+ }
24
+ /**
25
+ * Lifecycle hooks for observing HTTP request/response events.
26
+ * Hooks fire per-attempt (retries produce multiple hook calls).
27
+ */
28
+ export interface HttpHooks {
29
+ onRequest?: ((info: HttpHookRequestInfo) => void) | undefined;
30
+ onResponse?: ((info: HttpHookResponseInfo) => void) | undefined;
31
+ }
2
32
  /**
3
33
  * Configuration options for HTTP/HTTPS requests.
4
34
  */
@@ -61,6 +91,11 @@ export interface HttpRequestOptions {
61
91
  * ```
62
92
  */
63
93
  followRedirects?: boolean | undefined;
94
+ /**
95
+ * Lifecycle hooks for observing request/response events.
96
+ * Hooks fire per-attempt — retries and redirects each trigger separate hook calls.
97
+ */
98
+ hooks?: HttpHooks | undefined;
64
99
  /**
65
100
  * HTTP headers to send with the request.
66
101
  * A `User-Agent` header is automatically added if not provided.
@@ -92,6 +127,14 @@ export interface HttpRequestOptions {
92
127
  * ```
93
128
  */
94
129
  maxRedirects?: number | undefined;
130
+ /**
131
+ * Maximum response body size in bytes. Responses exceeding this limit
132
+ * will be rejected with an error. Prevents memory exhaustion from
133
+ * unexpectedly large responses.
134
+ *
135
+ * @default undefined (no limit)
136
+ */
137
+ maxResponseSize?: number | undefined;
95
138
  /**
96
139
  * HTTP method to use for the request.
97
140
  *
@@ -205,7 +248,7 @@ export interface HttpResponse {
205
248
  * console.log(response.headers['set-cookie']) // May be string[]
206
249
  * ```
207
250
  */
208
- headers: Record<string, string | string[] | undefined>;
251
+ headers: IncomingHttpHeaders;
209
252
  /**
210
253
  * Parse response body as JSON.
211
254
  * Type parameter `T` allows specifying the expected JSON structure.
@@ -270,6 +313,12 @@ export interface HttpResponse {
270
313
  * ```
271
314
  */
272
315
  text(): string;
316
+ /**
317
+ * The underlying Node.js IncomingMessage for advanced use cases
318
+ * (e.g., streaming, custom header inspection). Only available when
319
+ * the response was not consumed by the convenience methods.
320
+ */
321
+ rawResponse?: IncomingMessage | undefined;
273
322
  }
274
323
  /**
275
324
  * Configuration options for file downloads.
@@ -567,6 +616,11 @@ export interface FetchChecksumsOptions {
567
616
  * ```
568
617
  */
569
618
  export declare function fetchChecksums(url: string, options?: FetchChecksumsOptions | undefined): Promise<Checksums>;
619
+ /**
620
+ * Build an enriched error message based on the error code.
621
+ * Generic guidance (no product-specific branding).
622
+ */
623
+ export declare function enrichErrorMessage(url: string, method: string, error: NodeJS.ErrnoException): string;
570
624
  /**
571
625
  * Download a file from a URL to a local path with redirect support, retry logic, and progress callbacks.
572
626
  * Uses streaming to avoid loading entire file in memory.
@@ -19,6 +19,7 @@ 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,
@@ -238,25 +239,51 @@ async function httpDownloadAttempt(url, destPath, options) {
238
239
  request.end();
239
240
  });
240
241
  }
242
+ function enrichErrorMessage(url, method, error) {
243
+ const code = error.code;
244
+ let message = `${method} request failed: ${url}`;
245
+ if (code === "ECONNREFUSED") {
246
+ message += "\n\u2192 Connection refused. Server is unreachable.\n\u2192 Check: Network connectivity and firewall settings.";
247
+ } else if (code === "ENOTFOUND") {
248
+ message += "\n\u2192 DNS lookup failed. Cannot resolve hostname.\n\u2192 Check: Internet connection and DNS settings.";
249
+ } else if (code === "ETIMEDOUT") {
250
+ message += "\n\u2192 Connection timed out. Network or server issue.\n\u2192 Try: Check network connectivity and retry.";
251
+ } else if (code === "ECONNRESET") {
252
+ message += "\n\u2192 Connection reset by server. Possible network interruption.\n\u2192 Try: Retry the request.";
253
+ } else if (code === "EPIPE") {
254
+ message += "\n\u2192 Broken pipe. Server closed connection unexpectedly.\n\u2192 Check: Authentication credentials and permissions.";
255
+ } else if (code === "CERT_HAS_EXPIRED" || code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
256
+ 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.";
257
+ } else if (code) {
258
+ message += `
259
+ \u2192 Error code: ${code}`;
260
+ }
261
+ return message;
262
+ }
241
263
  async function httpRequestAttempt(url, options) {
242
264
  const {
243
265
  body,
244
266
  ca,
245
267
  followRedirects = true,
246
268
  headers = {},
269
+ hooks,
247
270
  maxRedirects = 5,
271
+ maxResponseSize,
248
272
  method = "GET",
249
273
  timeout = 3e4
250
274
  } = { __proto__: null, ...options };
275
+ const startTime = Date.now();
276
+ const mergedHeaders = {
277
+ "User-Agent": "socket-registry/1.0",
278
+ ...headers
279
+ };
280
+ hooks?.onRequest?.({ method, url, headers: mergedHeaders, timeout });
251
281
  return await new Promise((resolve, reject) => {
252
282
  const parsedUrl = new URL(url);
253
283
  const isHttps = parsedUrl.protocol === "https:";
254
284
  const httpModule = isHttps ? /* @__PURE__ */ getHttps() : /* @__PURE__ */ getHttp();
255
285
  const requestOptions = {
256
- headers: {
257
- "User-Agent": "socket-registry/1.0",
258
- ...headers
259
- },
286
+ headers: mergedHeaders,
260
287
  hostname: parsedUrl.hostname,
261
288
  method,
262
289
  path: parsedUrl.pathname + parsedUrl.search,
@@ -266,10 +293,23 @@ async function httpRequestAttempt(url, options) {
266
293
  if (ca && isHttps) {
267
294
  requestOptions["ca"] = ca;
268
295
  }
296
+ const emitResponse = (info) => {
297
+ hooks?.onResponse?.({
298
+ duration: Date.now() - startTime,
299
+ method,
300
+ url,
301
+ ...info
302
+ });
303
+ };
269
304
  const request = httpModule.request(
270
305
  requestOptions,
271
306
  (res) => {
272
307
  if (followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
308
+ emitResponse({
309
+ headers: res.headers,
310
+ status: res.statusCode,
311
+ statusText: res.statusMessage
312
+ });
273
313
  if (maxRedirects <= 0) {
274
314
  reject(
275
315
  new Error(
@@ -294,7 +334,9 @@ async function httpRequestAttempt(url, options) {
294
334
  ca,
295
335
  followRedirects,
296
336
  headers,
337
+ hooks,
297
338
  maxRedirects: maxRedirects - 1,
339
+ maxResponseSize,
298
340
  method,
299
341
  timeout
300
342
  })
@@ -302,7 +344,20 @@ async function httpRequestAttempt(url, options) {
302
344
  return;
303
345
  }
304
346
  const chunks = [];
347
+ let totalBytes = 0;
305
348
  res.on("data", (chunk) => {
349
+ totalBytes += chunk.length;
350
+ if (maxResponseSize && totalBytes > maxResponseSize) {
351
+ res.destroy();
352
+ const sizeMB = (totalBytes / (1024 * 1024)).toFixed(2);
353
+ const maxMB = (maxResponseSize / (1024 * 1024)).toFixed(2);
354
+ const err = new Error(
355
+ `Response exceeds maximum size limit (${sizeMB}MB > ${maxMB}MB)`
356
+ );
357
+ emitResponse({ error: err });
358
+ reject(err);
359
+ return;
360
+ }
306
361
  chunks.push(chunk);
307
362
  });
308
363
  res.on("end", () => {
@@ -321,39 +376,45 @@ async function httpRequestAttempt(url, options) {
321
376
  return JSON.parse(responseBody.toString("utf8"));
322
377
  },
323
378
  ok,
379
+ rawResponse: res,
324
380
  status: res.statusCode || 0,
325
381
  statusText: res.statusMessage || "",
326
382
  text() {
327
383
  return responseBody.toString("utf8");
328
384
  }
329
385
  };
386
+ emitResponse({
387
+ headers: res.headers,
388
+ status: res.statusCode,
389
+ statusText: res.statusMessage
390
+ });
330
391
  resolve(response);
331
392
  });
332
393
  res.on("error", (error) => {
394
+ emitResponse({ error });
333
395
  reject(error);
334
396
  });
335
397
  }
336
398
  );
337
399
  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 }));
400
+ const message = enrichErrorMessage(
401
+ url,
402
+ method,
403
+ error
404
+ );
405
+ const enhanced = new Error(message, { cause: error });
406
+ emitResponse({ error: enhanced });
407
+ reject(enhanced);
353
408
  });
354
409
  request.on("timeout", () => {
355
410
  request.destroy();
356
- reject(new Error(`Request timed out after ${timeout}ms`));
411
+ const err = new Error(
412
+ `${method} request timed out after ${timeout}ms: ${url}
413
+ \u2192 Server did not respond in time.
414
+ \u2192 Try: Increase timeout or check network connectivity.`
415
+ );
416
+ emitResponse({ error: err });
417
+ reject(err);
357
418
  });
358
419
  if (body) {
359
420
  request.write(body);
@@ -482,7 +543,9 @@ async function httpRequest(url, options) {
482
543
  ca,
483
544
  followRedirects = true,
484
545
  headers = {},
546
+ hooks,
485
547
  maxRedirects = 5,
548
+ maxResponseSize,
486
549
  method = "GET",
487
550
  retries = 0,
488
551
  retryDelay = 1e3,
@@ -496,7 +559,9 @@ async function httpRequest(url, options) {
496
559
  ca,
497
560
  followRedirects,
498
561
  headers,
562
+ hooks,
499
563
  maxRedirects,
564
+ maxResponseSize,
500
565
  method,
501
566
  timeout
502
567
  });
@@ -542,6 +607,7 @@ async function httpText(url, options) {
542
607
  }
543
608
  // Annotate the CommonJS export names for ESM import in node:
544
609
  0 && (module.exports = {
610
+ enrichErrorMessage,
545
611
  fetchChecksums,
546
612
  httpDownload,
547
613
  httpJson,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "5.11.4",
3
+ "version": "5.12.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.11.4",
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",