@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 +11 -0
- package/dist/http-request.d.ts +24 -18
- package/dist/http-request.js +84 -129
- package/package.json +3 -3
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
|
package/dist/http-request.d.ts
CHANGED
|
@@ -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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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.
|
package/dist/http-request.js
CHANGED
|
@@ -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
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
reject(new Error(`Download timed out after ${timeout}ms`));
|
|
246
|
+
res.on("error", (error) => {
|
|
247
|
+
fileStream.close();
|
|
248
|
+
reject(error);
|
|
323
249
|
});
|
|
324
|
-
|
|
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
|
|
537
|
-
|
|
489
|
+
const stream2 = body;
|
|
490
|
+
stream2.on("error", (err) => {
|
|
538
491
|
request.destroy();
|
|
539
492
|
rejectOnce(err);
|
|
540
493
|
});
|
|
541
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|