@socketsecurity/lib 5.10.0 → 5.11.1

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,30 @@ 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.11.1](https://github.com/SocketDev/socket-lib/releases/tag/v5.11.1) - 2026-03-24
9
+
10
+ ### Added
11
+
12
+ - **dlx/binary**: Added `sha256` option to `dlxBinary()`, `downloadBinary()`, and `downloadBinaryFile()`
13
+ - Enables SHA-256 checksum verification for binary downloads via httpDownload
14
+ - Verification happens during download (fails early if checksum mismatches)
15
+ - Complements existing `integrity` option (SRI sha512 format, verified post-download)
16
+
17
+ ## [5.11.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.11.0) - 2026-03-23
18
+
19
+ ### Added
20
+
21
+ - **http-request**: Checksum verification for secure downloads
22
+ - `parseChecksums(text)`: Parse checksums file text into filename→hash map
23
+ - Supports GNU style (`hash filename`), BSD style (`SHA256 (file) = hash`), and single-space format
24
+ - Handles Windows CRLF and Unix LF line endings
25
+ - Returns null-prototype object to prevent prototype pollution
26
+ - `fetchChecksums(url, options?)`: Fetch and parse checksums from URL
27
+ - Supports `headers` and `timeout` options
28
+ - `httpDownload` now accepts `sha256` option to verify downloaded files
29
+ - Verification happens before atomic rename (file not saved if hash mismatches)
30
+ - Accepts uppercase hashes (normalized to lowercase internally)
31
+
8
32
  ## [5.10.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.10.0) - 2026-03-14
9
33
 
10
34
  ### Changed
@@ -99,7 +99,10 @@ function createTtlCache(options) {
99
99
  }
100
100
  return entry.data;
101
101
  }
102
- await cacache.remove(fullKey);
102
+ try {
103
+ await cacache.remove(fullKey);
104
+ } catch {
105
+ }
103
106
  }
104
107
  return void 0;
105
108
  }
@@ -206,7 +209,10 @@ function createTtlCache(options) {
206
209
  }
207
210
  const fullKey = buildKey(key);
208
211
  memoCache.delete(fullKey);
209
- await cacache.remove(fullKey);
212
+ try {
213
+ await cacache.remove(fullKey);
214
+ } catch {
215
+ }
210
216
  }
211
217
  async function deleteAll(pattern) {
212
218
  const fullPrefix = pattern ? `${opts.prefix}:${pattern}` : opts.prefix;
@@ -13,6 +13,12 @@ export interface DlxBinaryOptions {
13
13
  * Expected SRI integrity hash (sha512-<base64>) for verification.
14
14
  */
15
15
  integrity?: string | undefined;
16
+ /**
17
+ * Expected SHA-256 hex checksum for verification.
18
+ * Passed to httpDownload for inline verification during download.
19
+ * This is more secure than post-download verification as it fails early.
20
+ */
21
+ sha256?: string | undefined;
16
22
  /**
17
23
  * Cache TTL in milliseconds (default: 7 days).
18
24
  */
@@ -127,8 +133,14 @@ export declare function downloadBinary(options: Omit<DlxBinaryOptions, 'spawnOpt
127
133
  * Download a file from a URL with integrity checking and concurrent download protection.
128
134
  * Uses processLock to prevent multiple processes from downloading the same binary simultaneously.
129
135
  * Internal helper function for downloading binary files.
136
+ *
137
+ * Supports two integrity verification methods:
138
+ * - sha256: Hex SHA-256 checksum (verified inline during download via httpDownload)
139
+ * - integrity: SRI format sha512-<base64> (verified post-download)
140
+ *
141
+ * The sha256 option is preferred as it fails early during download if the checksum doesn't match.
130
142
  */
131
- export declare function downloadBinaryFile(url: string, destPath: string, integrity?: string | undefined): Promise<string>;
143
+ export declare function downloadBinaryFile(url: string, destPath: string, integrity?: string | undefined, sha256?: string | undefined): Promise<string>;
132
144
  /**
133
145
  * Execute a cached binary without re-downloading.
134
146
  * Similar to executePackage from dlx-package.
@@ -111,6 +111,7 @@ async function dlxBinary(args, options, spawnExtra) {
111
111
  force: userForce = false,
112
112
  integrity,
113
113
  name,
114
+ sha256,
114
115
  spawnOptions,
115
116
  url,
116
117
  yes
@@ -168,7 +169,12 @@ Ensure the filesystem is writable or set SOCKET_DLX_DIR to a writable location.`
168
169
  { cause: e }
169
170
  );
170
171
  }
171
- computedIntegrity = await downloadBinaryFile(url, binaryPath, integrity);
172
+ computedIntegrity = await downloadBinaryFile(
173
+ url,
174
+ binaryPath,
175
+ integrity,
176
+ sha256
177
+ );
172
178
  const stats = await fs.promises.stat(binaryPath);
173
179
  await writeBinaryCacheMetadata(
174
180
  cacheEntryDir,
@@ -200,6 +206,7 @@ async function downloadBinary(options) {
200
206
  force = false,
201
207
  integrity,
202
208
  name,
209
+ sha256,
203
210
  url
204
211
  } = { __proto__: null, ...options };
205
212
  const fs = /* @__PURE__ */ getFs();
@@ -240,7 +247,8 @@ Ensure the filesystem is writable or set SOCKET_DLX_DIR to a writable location.`
240
247
  const computedIntegrity = await downloadBinaryFile(
241
248
  url,
242
249
  binaryPath,
243
- integrity
250
+ integrity,
251
+ sha256
244
252
  );
245
253
  const stats = await fs.promises.stat(binaryPath);
246
254
  await writeBinaryCacheMetadata(
@@ -257,7 +265,7 @@ Ensure the filesystem is writable or set SOCKET_DLX_DIR to a writable location.`
257
265
  downloaded
258
266
  };
259
267
  }
260
- async function downloadBinaryFile(url, destPath, integrity) {
268
+ async function downloadBinaryFile(url, destPath, integrity, sha256) {
261
269
  const crypto = /* @__PURE__ */ getCrypto();
262
270
  const fs = /* @__PURE__ */ getFs();
263
271
  const path = /* @__PURE__ */ getPath();
@@ -275,7 +283,7 @@ async function downloadBinaryFile(url, destPath, integrity) {
275
283
  }
276
284
  }
277
285
  try {
278
- await (0, import_http_request.httpDownload)(url, destPath);
286
+ await (0, import_http_request.httpDownload)(url, destPath, sha256 ? { sha256 } : void 0);
279
287
  } catch (e) {
280
288
  throw new Error(
281
289
  `Failed to download binary from ${url}
@@ -409,6 +409,29 @@ export interface HttpDownloadOptions {
409
409
  * ```
410
410
  */
411
411
  timeout?: number | undefined;
412
+ /**
413
+ * Expected SHA256 hash of the downloaded file.
414
+ * If provided, the download will fail if the computed hash doesn't match.
415
+ * The hash should be a lowercase hex string (64 characters).
416
+ *
417
+ * Use `fetchChecksums()` to fetch hashes from a checksums URL, then pass
418
+ * the specific hash here.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * // Verify download integrity with direct hash
423
+ * await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {
424
+ * sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
425
+ * })
426
+ *
427
+ * // Verify using checksums from a URL
428
+ * const checksums = await fetchChecksums('https://example.com/checksums.txt')
429
+ * await httpDownload('https://example.com/file.zip', '/tmp/file.zip', {
430
+ * sha256: checksums['file.zip']
431
+ * })
432
+ * ```
433
+ */
434
+ sha256?: string | undefined;
412
435
  }
413
436
  /**
414
437
  * Result of a successful file download.
@@ -435,6 +458,86 @@ export interface HttpDownloadResult {
435
458
  */
436
459
  size: number;
437
460
  }
461
+ /**
462
+ * Map of filenames to their SHA256 hashes.
463
+ * Keys are filenames (not paths), values are lowercase hex-encoded SHA256 hashes.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * const checksums: Checksums = {
468
+ * 'file.zip': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
469
+ * 'other.tar.gz': 'abc123...'
470
+ * }
471
+ * ```
472
+ */
473
+ export type Checksums = Record<string, string>;
474
+ /**
475
+ * Parse a checksums file text into a filename-to-hash map.
476
+ *
477
+ * Supports standard checksums file formats:
478
+ * - BSD style: "SHA256 (filename) = hash"
479
+ * - GNU style: "hash filename" (two spaces)
480
+ * - Simple style: "hash filename" (single space)
481
+ *
482
+ * Lines starting with '#' are treated as comments and ignored.
483
+ * Empty lines are ignored.
484
+ *
485
+ * @param text - Raw text content of a checksums file
486
+ * @returns Map of filenames to lowercase SHA256 hashes
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * const text = `
491
+ * # SHA256 checksums
492
+ * e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 file.zip
493
+ * abc123def456... other.tar.gz
494
+ * `
495
+ * const checksums = parseChecksums(text)
496
+ * console.log(checksums['file.zip']) // 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
497
+ * ```
498
+ */
499
+ export declare function parseChecksums(text: string): Checksums;
500
+ /**
501
+ * Options for fetching checksums from a URL.
502
+ */
503
+ export interface FetchChecksumsOptions {
504
+ /**
505
+ * HTTP headers to send with the request.
506
+ */
507
+ headers?: Record<string, string> | undefined;
508
+ /**
509
+ * Request timeout in milliseconds.
510
+ * @default 30000
511
+ */
512
+ timeout?: number | undefined;
513
+ }
514
+ /**
515
+ * Fetch and parse a checksums file from a URL.
516
+ *
517
+ * This is useful for verifying downloads from GitHub releases which typically
518
+ * publish a checksums.txt file alongside release assets.
519
+ *
520
+ * @param url - URL to the checksums file
521
+ * @param options - Request options
522
+ * @returns Map of filenames to lowercase SHA256 hashes
523
+ * @throws {Error} When the checksums file cannot be fetched
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * // Fetch checksums from GitHub release
528
+ * const checksums = await fetchChecksums(
529
+ * 'https://github.com/org/repo/releases/download/v1.0.0/checksums.txt'
530
+ * )
531
+ *
532
+ * // Use with httpDownload
533
+ * await httpDownload(
534
+ * 'https://github.com/org/repo/releases/download/v1.0.0/tool_linux.tar.gz',
535
+ * '/tmp/tool.tar.gz',
536
+ * { sha256: checksums['tool_linux.tar.gz'] }
537
+ * )
538
+ * ```
539
+ */
540
+ export declare function fetchChecksums(url: string, options?: FetchChecksumsOptions | undefined): Promise<Checksums>;
438
541
  /**
439
542
  * Download a file from a URL to a local path with redirect support, retry logic, and progress callbacks.
440
543
  * Uses streaming to avoid loading entire file in memory.
@@ -19,10 +19,12 @@ 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
+ fetchChecksums: () => fetchChecksums,
22
23
  httpDownload: () => httpDownload,
23
24
  httpJson: () => httpJson,
24
25
  httpRequest: () => httpRequest,
25
- httpText: () => httpText
26
+ httpText: () => httpText,
27
+ parseChecksums: () => parseChecksums
26
28
  });
27
29
  module.exports = __toCommonJS(http_request_exports);
28
30
  var import_fs = require("./fs.js");
@@ -34,9 +36,17 @@ function getFs() {
34
36
  }
35
37
  return _fs;
36
38
  }
39
+ let _crypto;
37
40
  let _http;
38
41
  let _https;
39
42
  // @__NO_SIDE_EFFECTS__
43
+ function getCrypto() {
44
+ if (_crypto === void 0) {
45
+ _crypto = require("crypto");
46
+ }
47
+ return _crypto;
48
+ }
49
+ // @__NO_SIDE_EFFECTS__
40
50
  function getHttp() {
41
51
  if (_http === void 0) {
42
52
  _http = require("http");
@@ -50,6 +60,40 @@ function getHttps() {
50
60
  }
51
61
  return _https;
52
62
  }
63
+ function parseChecksums(text) {
64
+ const checksums = { __proto__: null };
65
+ for (const line of text.split("\n")) {
66
+ const trimmed = line.trim();
67
+ if (!trimmed || trimmed.startsWith("#")) {
68
+ continue;
69
+ }
70
+ const bsdMatch = trimmed.match(
71
+ /^SHA256\s+\((.+)\)\s+=\s+([a-fA-F0-9]{64})$/
72
+ );
73
+ if (bsdMatch) {
74
+ checksums[bsdMatch[1]] = bsdMatch[2].toLowerCase();
75
+ continue;
76
+ }
77
+ const gnuMatch = trimmed.match(/^([a-fA-F0-9]{64})\s+(.+)$/);
78
+ if (gnuMatch) {
79
+ checksums[gnuMatch[2]] = gnuMatch[1].toLowerCase();
80
+ }
81
+ }
82
+ return checksums;
83
+ }
84
+ async function fetchChecksums(url, options) {
85
+ const { headers = {}, timeout = 3e4 } = {
86
+ __proto__: null,
87
+ ...options
88
+ };
89
+ const response = await httpRequest(url, { headers, timeout });
90
+ if (!response.ok) {
91
+ throw new Error(
92
+ `Failed to fetch checksums from ${url}: ${response.status} ${response.statusText}`
93
+ );
94
+ }
95
+ return parseChecksums(response.body.toString("utf8"));
96
+ }
53
97
  async function httpDownloadAttempt(url, destPath, options) {
54
98
  const {
55
99
  followRedirects = true,
@@ -292,6 +336,7 @@ async function httpDownload(url, destPath, options) {
292
336
  progressInterval = 10,
293
337
  retries = 0,
294
338
  retryDelay = 1e3,
339
+ sha256,
295
340
  timeout = 12e4
296
341
  } = { __proto__: null, ...options };
297
342
  let progressCallback;
@@ -324,6 +369,19 @@ async function httpDownload(url, destPath, options) {
324
369
  onProgress: progressCallback,
325
370
  timeout
326
371
  });
372
+ if (sha256) {
373
+ const crypto = /* @__PURE__ */ getCrypto();
374
+ const fileContent = await fs.promises.readFile(tempPath);
375
+ const computedHash = crypto.createHash("sha256").update(fileContent).digest("hex");
376
+ if (computedHash !== sha256.toLowerCase()) {
377
+ await (0, import_fs.safeDelete)(tempPath);
378
+ throw new Error(
379
+ `Checksum verification failed for ${url}
380
+ Expected: ${sha256.toLowerCase()}
381
+ Computed: ${computedHash}`
382
+ );
383
+ }
384
+ }
327
385
  await fs.promises.rename(tempPath, destPath);
328
386
  return {
329
387
  path: destPath,
@@ -440,8 +498,10 @@ async function httpText(url, options) {
440
498
  }
441
499
  // Annotate the CommonJS export names for ESM import in node:
442
500
  0 && (module.exports = {
501
+ fetchChecksums,
443
502
  httpDownload,
444
503
  httpJson,
445
504
  httpRequest,
446
- httpText
505
+ httpText,
506
+ parseChecksums
447
507
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "5.10.0",
4
- "packageManager": "pnpm@10.32.1",
3
+ "version": "5.11.1",
4
+ "packageManager": "pnpm@10.33.0",
5
5
  "license": "MIT",
6
6
  "description": "Core utilities and infrastructure for Socket.dev security tools",
7
7
  "keywords": [
@@ -734,7 +734,7 @@
734
734
  "@socketregistry/is-unicode-supported": "1.0.5",
735
735
  "@socketregistry/packageurl-js": "1.3.5",
736
736
  "@socketregistry/yocto-spinner": "1.0.25",
737
- "@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.9.0",
737
+ "@socketsecurity/lib-stable": "npm:@socketsecurity/lib@5.11.0",
738
738
  "@types/node": "24.9.2",
739
739
  "@typescript/native-preview": "7.0.0-dev.20250920.1",
740
740
  "@vitest/coverage-v8": "4.0.3",