@socketsecurity/lib 5.14.0 → 5.15.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,17 @@ 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.15.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.15.0) - 2026-04-06
9
+
10
+ ### Added — http-request
11
+
12
+ - `stream` option on `HttpRequestOptions` — resolves with `HttpResponse` immediately after headers arrive, leaving `rawResponse` unconsumed for piping to files
13
+ - `headers`, `ok`, `status`, `statusText` fields on `HttpDownloadResult`
14
+
15
+ ### Changed — http-request
16
+
17
+ - `httpDownload` now uses `httpRequest` with `stream: true` internally, eliminating ~120 lines of duplicated HTTP plumbing
18
+
8
19
  ## [5.14.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.14.0) - 2026-04-06
9
20
 
10
21
  ### Added — http-request
@@ -277,6 +277,20 @@ export interface HttpRequestOptions {
277
277
  * })
278
278
  * ```
279
279
  */
280
+ /**
281
+ * When true, resolve with an HttpResponse whose body is NOT buffered.
282
+ * The `rawResponse` property contains the unconsumed IncomingResponse
283
+ * stream for piping to files or other destinations.
284
+ *
285
+ * `body`, `text()`, `json()`, and `arrayBuffer()` return empty/zero
286
+ * values since the stream has not been read.
287
+ *
288
+ * Incompatible with `maxResponseSize` (size enforcement requires
289
+ * reading the body).
290
+ *
291
+ * @default false
292
+ */
293
+ stream?: boolean | undefined;
280
294
  throwOnError?: boolean | undefined;
281
295
  /**
282
296
  * Request timeout in milliseconds.
@@ -657,26 +671,18 @@ export interface HttpDownloadOptions {
657
671
  * Result of a successful file download.
658
672
  */
659
673
  export interface HttpDownloadResult {
660
- /**
661
- * Absolute path where the file was saved.
662
- *
663
- * @example
664
- * ```ts
665
- * const result = await httpDownload('https://example.com/file.zip', '/tmp/file.zip')
666
- * console.log(`Downloaded to: ${result.path}`)
667
- * ```
668
- */
674
+ /** HTTP response headers from the final response (after redirects). */
675
+ headers: IncomingHttpHeaders;
676
+ /** Whether the download succeeded (status 200-299). Always true on success (non-2xx throws). */
677
+ ok: true;
678
+ /** Absolute path where the file was saved. */
669
679
  path: string;
670
- /**
671
- * Total size of downloaded file in bytes.
672
- *
673
- * @example
674
- * ```ts
675
- * const result = await httpDownload('https://example.com/file.zip', '/tmp/file.zip')
676
- * console.log(`Downloaded ${result.size} bytes`)
677
- * ```
678
- */
680
+ /** Total size of downloaded file in bytes. */
679
681
  size: number;
682
+ /** HTTP status code from the final response (after redirects). */
683
+ status: number;
684
+ /** HTTP status message from the final response (after redirects). */
685
+ statusText: string;
680
686
  }
681
687
  /**
682
688
  * Map of filenames to their SHA256 hashes.
@@ -193,135 +193,61 @@ async function httpDownloadAttempt(url, destPath, options) {
193
193
  onProgress,
194
194
  timeout = 12e4
195
195
  } = { __proto__: null, ...options };
196
+ const response = await httpRequestAttempt(url, {
197
+ ca,
198
+ followRedirects,
199
+ headers,
200
+ maxRedirects,
201
+ method: "GET",
202
+ stream: true,
203
+ timeout
204
+ });
205
+ if (!response.ok) {
206
+ throw new Error(
207
+ `Download failed: HTTP ${response.status} ${response.statusText}`
208
+ );
209
+ }
210
+ const res = response.rawResponse;
211
+ if (!res) {
212
+ throw new Error("Stream response missing rawResponse");
213
+ }
214
+ const { createWriteStream } = /* @__PURE__ */ getFs();
215
+ const totalSize = Number.parseInt(
216
+ response.headers["content-length"] || "0",
217
+ 10
218
+ );
196
219
  return await new Promise((resolve, reject) => {
197
- const parsedUrl = new URL(url);
198
- const isHttps = parsedUrl.protocol === "https:";
199
- const httpModule = isHttps ? /* @__PURE__ */ getHttps() : /* @__PURE__ */ getHttp();
200
- const requestOptions = {
201
- headers: {
202
- "User-Agent": "socket-registry/1.0",
203
- ...headers
204
- },
205
- hostname: parsedUrl.hostname,
206
- method: "GET",
207
- path: parsedUrl.pathname + parsedUrl.search,
208
- port: parsedUrl.port,
209
- timeout
210
- };
211
- if (ca && isHttps) {
212
- requestOptions["ca"] = ca;
213
- }
214
- const { createWriteStream } = /* @__PURE__ */ getFs();
215
- let fileStream;
216
- let streamClosed = false;
217
- const closeStream = () => {
218
- if (!streamClosed && fileStream) {
219
- streamClosed = true;
220
- fileStream.close();
220
+ let downloadedSize = 0;
221
+ const fileStream = createWriteStream(destPath);
222
+ fileStream.on("error", (error) => {
223
+ fileStream.close();
224
+ reject(
225
+ new Error(`Failed to write file: ${error.message}`, { cause: error })
226
+ );
227
+ });
228
+ res.on("data", (chunk) => {
229
+ downloadedSize += chunk.length;
230
+ if (onProgress && totalSize > 0) {
231
+ onProgress(downloadedSize, totalSize);
221
232
  }
222
- };
223
- const request = httpModule.request(
224
- requestOptions,
225
- (res) => {
226
- if (followRedirects && res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
227
- if (maxRedirects <= 0) {
228
- reject(
229
- new Error(
230
- `Too many redirects (exceeded maximum: ${maxRedirects})`
231
- )
232
- );
233
- return;
234
- }
235
- const redirectUrl = res.headers.location.startsWith("http") ? res.headers.location : new URL(res.headers.location, url).toString();
236
- const redirectParsed = new URL(redirectUrl);
237
- if (isHttps && redirectParsed.protocol !== "https:") {
238
- reject(
239
- new Error(
240
- `Redirect from HTTPS to HTTP is not allowed: ${redirectUrl}`
241
- )
242
- );
243
- return;
244
- }
245
- resolve(
246
- httpDownloadAttempt(redirectUrl, destPath, {
247
- ca,
248
- followRedirects,
249
- headers,
250
- maxRedirects: maxRedirects - 1,
251
- onProgress,
252
- timeout
253
- })
254
- );
255
- return;
256
- }
257
- if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
258
- closeStream();
259
- reject(
260
- new Error(
261
- `Download failed: HTTP ${res.statusCode} ${res.statusMessage}`
262
- )
263
- );
264
- return;
265
- }
266
- const totalSize = Number.parseInt(
267
- res.headers["content-length"] || "0",
268
- 10
269
- );
270
- let downloadedSize = 0;
271
- fileStream = createWriteStream(destPath);
272
- fileStream.on("error", (error) => {
273
- closeStream();
274
- const err = new Error(`Failed to write file: ${error.message}`, {
275
- cause: error
276
- });
277
- reject(err);
278
- });
279
- res.on("data", (chunk) => {
280
- downloadedSize += chunk.length;
281
- if (onProgress && totalSize > 0) {
282
- onProgress(downloadedSize, totalSize);
283
- }
284
- });
285
- res.on("end", () => {
286
- fileStream?.close(() => {
287
- streamClosed = true;
288
- resolve({
289
- path: destPath,
290
- size: downloadedSize
291
- });
292
- });
293
- });
294
- res.on("error", (error) => {
295
- closeStream();
296
- reject(error);
233
+ });
234
+ res.on("end", () => {
235
+ fileStream.close(() => {
236
+ resolve({
237
+ headers: response.headers,
238
+ ok: true,
239
+ path: destPath,
240
+ size: downloadedSize,
241
+ status: response.status,
242
+ statusText: response.statusText
297
243
  });
298
- res.pipe(fileStream);
299
- }
300
- );
301
- request.on("error", (error) => {
302
- closeStream();
303
- const code = error.code;
304
- let message = `HTTP download failed for ${url}: ${error.message}
305
- `;
306
- if (code === "ENOTFOUND") {
307
- message += "DNS lookup failed. Check the hostname and your network connection.";
308
- } else if (code === "ECONNREFUSED") {
309
- message += "Connection refused. Verify the server is running and accessible.";
310
- } else if (code === "ETIMEDOUT") {
311
- message += "Request timed out. Check your network or increase the timeout value.";
312
- } else if (code === "ECONNRESET") {
313
- message += "Connection reset. The server may have closed the connection unexpectedly.";
314
- } else {
315
- message += "Check your network connection and verify the URL is correct.";
316
- }
317
- reject(new Error(message, { cause: error }));
244
+ });
318
245
  });
319
- request.on("timeout", () => {
320
- request.destroy();
321
- closeStream();
322
- reject(new Error(`Download timed out after ${timeout}ms`));
246
+ res.on("error", (error) => {
247
+ fileStream.close();
248
+ reject(error);
323
249
  });
324
- request.end();
250
+ res.pipe(fileStream);
325
251
  });
326
252
  }
327
253
  function enrichErrorMessage(url, method, error) {
@@ -355,6 +281,7 @@ async function httpRequestAttempt(url, options) {
355
281
  maxRedirects = 5,
356
282
  maxResponseSize,
357
283
  method = "GET",
284
+ stream = false,
358
285
  timeout = 3e4
359
286
  } = { __proto__: null, ...options };
360
287
  const startTime = Date.now();
@@ -452,11 +379,37 @@ async function httpRequestAttempt(url, options) {
452
379
  maxRedirects: maxRedirects - 1,
453
380
  maxResponseSize,
454
381
  method,
382
+ stream,
455
383
  timeout
456
384
  })
457
385
  );
458
386
  return;
459
387
  }
388
+ if (stream) {
389
+ const status = res.statusCode || 0;
390
+ const statusText = res.statusMessage || "";
391
+ const ok = status >= 200 && status < 300;
392
+ emitResponse({
393
+ headers: res.headers,
394
+ status,
395
+ statusText
396
+ });
397
+ const emptyBody = Buffer.alloc(0);
398
+ resolveOnce({
399
+ arrayBuffer: () => emptyBody.buffer,
400
+ body: emptyBody,
401
+ headers: res.headers,
402
+ json: () => {
403
+ throw new Error("Cannot parse JSON from a streaming response");
404
+ },
405
+ ok,
406
+ rawResponse: res,
407
+ status,
408
+ statusText,
409
+ text: () => ""
410
+ });
411
+ return;
412
+ }
460
413
  const chunks = [];
461
414
  let totalBytes = 0;
462
415
  res.on("data", (chunk) => {
@@ -533,12 +486,12 @@ async function httpRequestAttempt(url, options) {
533
486
  });
534
487
  if (body) {
535
488
  if (typeof body === "object" && typeof body.pipe === "function") {
536
- const stream = body;
537
- stream.on("error", (err) => {
489
+ const stream2 = body;
490
+ stream2.on("error", (err) => {
538
491
  request.destroy();
539
492
  rejectOnce(err);
540
493
  });
541
- stream.pipe(request);
494
+ stream2.pipe(request);
542
495
  return;
543
496
  }
544
497
  request.write(body);
@@ -613,8 +566,8 @@ Computed: ${computedHash}`
613
566
  }
614
567
  await fs.promises.rename(tempPath, destPath);
615
568
  return {
616
- path: destPath,
617
- size: result.size
569
+ ...result,
570
+ path: destPath
618
571
  };
619
572
  } catch (e) {
620
573
  lastError = e;
@@ -676,6 +629,7 @@ async function httpRequest(url, options) {
676
629
  onRetry,
677
630
  retries = 0,
678
631
  retryDelay = 1e3,
632
+ stream = false,
679
633
  throwOnError = false,
680
634
  timeout = 3e4
681
635
  } = { __proto__: null, ...options };
@@ -696,6 +650,7 @@ async function httpRequest(url, options) {
696
650
  maxRedirects,
697
651
  maxResponseSize,
698
652
  method,
653
+ stream,
699
654
  timeout
700
655
  };
701
656
  let lastError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "5.14.0",
3
+ "version": "5.15.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,7 +717,7 @@
717
717
  "update": "node scripts/update.mjs"
718
718
  },
719
719
  "devDependencies": {
720
- "@anthropic-ai/claude-code": "2.1.89",
720
+ "@anthropic-ai/claude-code": "2.1.92",
721
721
  "@babel/core": "7.28.4",
722
722
  "@babel/parser": "7.28.4",
723
723
  "@babel/traverse": "7.28.4",
@@ -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.13.0",
738
+ "@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.14.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",