@socketsecurity/lib 5.5.3 → 5.7.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,100 @@ 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.7.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.7.0) - 2026-02-12
9
+
10
+ ### Added
11
+
12
+ - **env**: Added `isInEnv()` helper function to check if an environment variable key exists, regardless of its value
13
+ - Returns `true` even for empty strings, `"false"`, `"0"`, etc.
14
+ - Follows same override resolution order as `getEnvValue()` (isolated overrides → shared overrides → process.env)
15
+ - Useful for detecting presence of environment variables independent of their value
16
+
17
+ - **dlx**: Added new exported helper functions
18
+ - `downloadBinaryFile()` - Downloads a binary file from a URL to the dlx cache directory
19
+ - `ensurePackageInstalled()` - Ensures an npm package is installed and cached via Arborist
20
+ - `getBinaryCacheMetadataPath()` - Gets the file path to dlx binary cache metadata (`.dlx-metadata.json`)
21
+ - `isBinaryCacheValid()` - Checks if a cached dlx binary is still valid based on TTL and timestamp
22
+ - `makePackageBinsExecutable()` - Makes npm package binaries executable on Unix systems
23
+ - `parsePackageSpec()` - Parses npm package spec strings (e.g., `pkg@1.0.0`) into name and version
24
+ - `resolveBinaryPath()` - Resolves the absolute path to a binary within an installed package
25
+ - `writeBinaryCacheMetadata()` - Writes dlx binary cache metadata with integrity, size, and source info
26
+
27
+ - **releases**: Added `createAssetMatcher()` utility function for GitHub release asset pattern matching
28
+ - Creates matcher functions that test strings against glob patterns, prefix/suffix, or RegExp
29
+ - Used for dynamic asset discovery in GitHub releases (e.g., matching platform-specific binaries)
30
+
31
+ ### Changed
32
+
33
+ - **env**: Updated `getCI()` to use `isInEnv()` for more accurate CI detection
34
+ - Now returns `true` whenever the `CI` key exists in the environment, not just when truthy
35
+ - Matches standard CI detection behavior where the presence of the key (not its value) indicates a CI environment
36
+
37
+ ### Fixed
38
+
39
+ - **github**: Fixed JSON parsing crash vulnerability by adding try-catch around `JSON.parse()` in GitHub API responses
40
+ - Prevents crashes on malformed, incomplete, or binary responses
41
+ - Error messages now include the response URL for better debugging
42
+
43
+ - **dlx/binary**: Fixed clock skew vulnerabilities in cache validation
44
+ - Cache entries with future timestamps (clock skew) are now treated as expired
45
+ - Metadata writes now use atomic write-then-rename pattern to prevent corruption
46
+ - Added TOCTOU race protection by re-checking binary existence after metadata read
47
+
48
+ - **dlx/cache cleanup**: Fixed handling of future timestamps during cache cleanup
49
+ - Entries with future timestamps (due to clock skew) are now properly treated as expired
50
+
51
+ - **dlx/package**: Fixed scoped package parsing bug where `@scope/package` was incorrectly parsed
52
+ - Changed condition from `startsWith('@')` to `atIndex === 0` for more precise detection
53
+ - Fixes installation failures for scoped packages like `@socketregistry/lib`
54
+
55
+ - **cache-with-ttl**: Added clock skew detection to TTL cache
56
+ - Far-future `expiresAt` values (>2x TTL) are now treated as expired
57
+ - Protects against cache poisoning from clock skew
58
+
59
+ - **packages/specs**: Fixed unconditional `.git` truncation in Git URL parsing
60
+ - Now only removes `.git` suffix when URL actually ends with `.git`
61
+ - Prevents incorrect truncation of URLs containing `.git` in the middle
62
+
63
+ - **releases/github**: Fixed TOCTOU race condition in binary download verification
64
+ - Re-checks binary existence after reading version file
65
+ - Ensures binary is re-downloaded if missing despite version file presence
66
+
67
+ - **provenance**: Fixed incorrect package name in provenance workflow
68
+ - Changed from `@socketregistry/lib` to `@socketsecurity/lib`
69
+
70
+
71
+ ## [5.6.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.6.0) - 2026-02-08
72
+
73
+ ### Added
74
+
75
+ - **http-request**: Added automatic default headers for JSON and text requests
76
+ - `httpJson()` now automatically sets `Accept: application/json` header
77
+ - `httpJson()` automatically sets `Content-Type: application/json` when body is present
78
+ - `httpText()` now automatically sets `Accept: text/plain` header
79
+ - `httpText()` automatically sets `Content-Type: text/plain` when body is present
80
+ - User-provided headers always override defaults
81
+ - Simplifies API usage - no need to manually set common headers
82
+
83
+ ### Changed
84
+
85
+ - **http-request**: Renamed HTTP helper functions to support all HTTP methods (BREAKING CHANGE)
86
+ - `httpGetJson()` → `httpJson()` - Now supports GET, POST, PUT, DELETE, PATCH, etc.
87
+ - `httpGetText()` → `httpText()` - Now supports all HTTP methods via `method` option
88
+ - Functions now accept `method` parameter in options (defaults to 'GET')
89
+ - More flexible API that matches modern fetch-style conventions
90
+ - **Migration**: Replace `httpGetJson()` calls with `httpJson()` and `httpGetText()` with `httpText()`
91
+
92
+ ### Fixed
93
+
94
+ - **http-request**: Fixed Content-Type header incorrectly sent with empty string body
95
+ - Empty string body (`""`) no longer triggers Content-Type header
96
+ - Changed condition from `if (body !== undefined)` to `if (body)` for semantic correctness
97
+ - Empty string represents "no content" and should not declare a Content-Type
98
+ - Affects `httpJson()` and `httpText()` functions
99
+ - Fixes potential API compatibility issues with servers expecting no Content-Type for empty bodies
100
+ - Added comprehensive test coverage for empty string edge case
101
+
8
102
  ## [5.5.3](https://github.com/SocketDev/socket-lib/releases/tag/v5.5.3) - 2026-01-20
9
103
 
10
104
  ### Fixed
@@ -780,7 +874,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
780
874
 
781
875
  ### Changed
782
876
 
783
- - **DLX binary metadata structure**: Updated `writeMetadata()` to use unified schema with additional fields
877
+ - **DLX binary metadata structure**: Updated `writeBinaryCacheMetadata()` to use unified schema with additional fields
784
878
  - Now includes `cache_key` (first 16 chars of SHA-512 hash)
785
879
  - Added `size` field for cached binary size
786
880
  - Added `checksum_algorithm` field (currently "sha256")
@@ -54,7 +54,11 @@ function createTtlCache(options) {
54
54
  return `${opts.prefix}:${key}`;
55
55
  }
56
56
  function isExpired(entry) {
57
- return Date.now() > entry.expiresAt;
57
+ const now = Date.now();
58
+ if (entry.expiresAt > now + ttl * 2) {
59
+ return true;
60
+ }
61
+ return now > entry.expiresAt;
58
62
  }
59
63
  function createMatcher(pattern) {
60
64
  const fullPattern = buildKey(pattern);
@@ -10,9 +10,9 @@ export interface DlxBinaryOptions {
10
10
  */
11
11
  name?: string | undefined;
12
12
  /**
13
- * Expected checksum (sha256) for verification.
13
+ * Expected SRI integrity hash (sha512-<base64>) for verification.
14
14
  */
15
- checksum?: string | undefined;
15
+ integrity?: string | undefined;
16
16
  /**
17
17
  * Cache TTL in milliseconds (default: 7 days).
18
18
  */
@@ -47,65 +47,40 @@ export interface DlxBinaryResult {
47
47
  }
48
48
  /**
49
49
  * Metadata structure for cached binaries (.dlx-metadata.json).
50
- * Unified schema shared across TypeScript (dlxBinary) and C++ (socket_macho_decompress).
50
+ * Unified schema shared across TypeScript (dlxBinary) and C++ stub extractor.
51
51
  *
52
- * Core Fields (present in all implementations):
52
+ * Fields:
53
53
  * - version: Schema version (currently "1.0.0")
54
54
  * - cache_key: First 16 chars of SHA-512 hash (matches directory name)
55
55
  * - timestamp: Unix timestamp in milliseconds
56
- * - checksum: Full hash of cached binary (SHA-512 for C++, SHA-256 for TypeScript)
57
- * - checksum_algorithm: "sha512" or "sha256"
58
- * - platform: "darwin" | "linux" | "win32"
59
- * - arch: "x64" | "arm64"
56
+ * - integrity: SRI hash (sha512-<base64>, aligned with npm)
60
57
  * - size: Size of cached binary in bytes
61
58
  * - source: Origin information
62
- * - type: "download" (from URL) or "decompression" (from embedded binary)
59
+ * - type: "download" | "extract" | "package"
63
60
  * - url: Download URL (if type is "download")
64
- * - path: Source binary path (if type is "decompression")
61
+ * - path: Source binary path (if type is "extract")
62
+ * - spec: Package spec (if type is "package")
63
+ * - update_check: Update checking metadata (optional)
64
+ * - last_check: Timestamp of last update check
65
+ * - last_notification: Timestamp of last user notification
66
+ * - latest_known: Latest known version string
65
67
  *
66
- * Extra Fields (implementation-specific):
67
- * - For C++ decompression:
68
- * - compressed_size: Size of compressed data in bytes
69
- * - compression_algorithm: Brotli level (numeric)
70
- * - compression_ratio: original_size / compressed_size
71
- *
72
- * Example (TypeScript download):
68
+ * Example:
73
69
  * ```json
74
70
  * {
75
71
  * "version": "1.0.0",
76
72
  * "cache_key": "a1b2c3d4e5f67890",
77
73
  * "timestamp": 1730332800000,
78
- * "checksum": "sha256-abc123...",
79
- * "checksum_algorithm": "sha256",
80
- * "platform": "darwin",
81
- * "arch": "arm64",
74
+ * "integrity": "sha512-abc123base64...",
82
75
  * "size": 15000000,
83
76
  * "source": {
84
77
  * "type": "download",
85
78
  * "url": "https://example.com/binary"
86
- * }
87
- * }
88
- * ```
89
- *
90
- * Example (C++ decompression):
91
- * ```json
92
- * {
93
- * "version": "1.0.0",
94
- * "cache_key": "0123456789abcdef",
95
- * "timestamp": 1730332800000,
96
- * "checksum": "sha512-def456...",
97
- * "checksum_algorithm": "sha512",
98
- * "platform": "darwin",
99
- * "arch": "arm64",
100
- * "size": 13000000,
101
- * "source": {
102
- * "type": "decompression",
103
- * "path": "/usr/local/bin/socket"
104
79
  * },
105
- * "extra": {
106
- * "compressed_size": 1700000,
107
- * "compression_algorithm": 3,
108
- * "compression_ratio": 7.647
80
+ * "update_check": {
81
+ * "last_check": 1730332800000,
82
+ * "last_notification": 1730246400000,
83
+ * "latest_known": "2.1.0"
109
84
  * }
110
85
  * }
111
86
  * ```
@@ -116,17 +91,19 @@ export interface DlxMetadata {
116
91
  version: string;
117
92
  cache_key: string;
118
93
  timestamp: number;
119
- checksum: string;
120
- checksum_algorithm: string;
121
- platform: string;
122
- arch: string;
94
+ integrity: string;
123
95
  size: number;
124
96
  source?: {
125
- type: 'download' | 'decompression';
97
+ type: 'download' | 'extract' | 'package';
126
98
  url?: string;
127
99
  path?: string;
100
+ spec?: string;
101
+ };
102
+ update_check?: {
103
+ last_check: number;
104
+ last_notification: number;
105
+ latest_known: string;
128
106
  };
129
- extra?: Record<string, unknown>;
130
107
  }
131
108
  /**
132
109
  * Clean expired entries from the DLX cache.
@@ -146,6 +123,12 @@ export declare function downloadBinary(options: Omit<DlxBinaryOptions, 'spawnOpt
146
123
  binaryPath: string;
147
124
  downloaded: boolean;
148
125
  }>;
126
+ /**
127
+ * Download a file from a URL with integrity checking and concurrent download protection.
128
+ * Uses processLock to prevent multiple processes from downloading the same binary simultaneously.
129
+ * Internal helper function for downloading binary files.
130
+ */
131
+ export declare function downloadBinaryFile(url: string, destPath: string, integrity?: string | undefined): Promise<string>;
149
132
  /**
150
133
  * Execute a cached binary without re-downloading.
151
134
  * Similar to executePackage from dlx-package.
@@ -164,15 +147,27 @@ export declare function executeBinary(binaryPath: string, args: readonly string[
164
147
  * Uses same directory as dlx-package for unified DLX storage.
165
148
  */
166
149
  export declare function getDlxCachePath(): string;
150
+ /**
151
+ * Get metadata file path for a cached binary.
152
+ */
153
+ export declare function getBinaryCacheMetadataPath(cacheEntryPath: string): string;
154
+ /**
155
+ * Check if a cached binary is still valid.
156
+ */
157
+ export declare function isBinaryCacheValid(cacheEntryPath: string, cacheTtl: number): Promise<boolean>;
167
158
  /**
168
159
  * Get information about cached binaries.
169
160
  */
170
161
  export declare function listDlxCache(): Promise<Array<{
171
162
  age: number;
172
- arch: string;
173
- checksum: string;
163
+ integrity: string;
174
164
  name: string;
175
- platform: string;
176
165
  size: number;
177
166
  url: string;
178
167
  }>>;
168
+ /**
169
+ * Write metadata for a cached binary.
170
+ * Uses unified schema shared with C++ decompressor and CLI dlxBinary.
171
+ * Schema documentation: See DlxMetadata interface in this file (exported).
172
+ */
173
+ export declare function writeBinaryCacheMetadata(cacheEntryPath: string, cacheKey: string, url: string, integrity: string, size: number): Promise<void>;
@@ -22,15 +22,18 @@ __export(binary_exports, {
22
22
  cleanDlxCache: () => cleanDlxCache,
23
23
  dlxBinary: () => dlxBinary,
24
24
  downloadBinary: () => downloadBinary,
25
+ downloadBinaryFile: () => downloadBinaryFile,
25
26
  executeBinary: () => executeBinary,
27
+ getBinaryCacheMetadataPath: () => getBinaryCacheMetadataPath,
26
28
  getDlxCachePath: () => getDlxCachePath,
27
- listDlxCache: () => listDlxCache
29
+ isBinaryCacheValid: () => isBinaryCacheValid,
30
+ listDlxCache: () => listDlxCache,
31
+ writeBinaryCacheMetadata: () => writeBinaryCacheMetadata
28
32
  });
29
33
  module.exports = __toCommonJS(binary_exports);
30
34
  var import_platform = require("../constants/platform");
31
35
  var import_time = require("../constants/time");
32
36
  var import_cache = require("./cache");
33
- var import_manifest = require("./manifest");
34
37
  var import_http_request = require("../http-request");
35
38
  var import_fs = require("../fs");
36
39
  var import_objects = require("../objects");
@@ -62,115 +65,6 @@ function getPath() {
62
65
  }
63
66
  return _path;
64
67
  }
65
- function getMetadataPath(cacheEntryPath) {
66
- return (/* @__PURE__ */ getPath()).join(cacheEntryPath, ".dlx-metadata.json");
67
- }
68
- async function isCacheValid(cacheEntryPath, cacheTtl) {
69
- const fs = /* @__PURE__ */ getFs();
70
- try {
71
- const metaPath = getMetadataPath(cacheEntryPath);
72
- if (!fs.existsSync(metaPath)) {
73
- return false;
74
- }
75
- const metadata = await (0, import_fs.readJson)(metaPath, { throws: false });
76
- if (!(0, import_objects.isObjectObject)(metadata)) {
77
- return false;
78
- }
79
- const now = Date.now();
80
- const timestamp = metadata["timestamp"];
81
- if (typeof timestamp !== "number" || timestamp <= 0) {
82
- return false;
83
- }
84
- const age = now - timestamp;
85
- return age < cacheTtl;
86
- } catch {
87
- return false;
88
- }
89
- }
90
- async function downloadBinaryFile(url, destPath, checksum) {
91
- const crypto = /* @__PURE__ */ getCrypto();
92
- const fs = /* @__PURE__ */ getFs();
93
- const path = /* @__PURE__ */ getPath();
94
- const cacheEntryDir = path.dirname(destPath);
95
- const lockPath = path.join(cacheEntryDir, "concurrency.lock");
96
- return await import_process_lock.processLock.withLock(
97
- lockPath,
98
- async () => {
99
- if (fs.existsSync(destPath)) {
100
- const stats = await fs.promises.stat(destPath);
101
- if (stats.size > 0) {
102
- const fileBuffer2 = await fs.promises.readFile(destPath);
103
- const hasher2 = crypto.createHash("sha256");
104
- hasher2.update(fileBuffer2);
105
- return hasher2.digest("hex");
106
- }
107
- }
108
- try {
109
- await (0, import_http_request.httpDownload)(url, destPath);
110
- } catch (e) {
111
- throw new Error(
112
- `Failed to download binary from ${url}
113
- Destination: ${destPath}
114
- Check your internet connection or verify the URL is accessible.`,
115
- { cause: e }
116
- );
117
- }
118
- const fileBuffer = await fs.promises.readFile(destPath);
119
- const hasher = crypto.createHash("sha256");
120
- hasher.update(fileBuffer);
121
- const actualChecksum = hasher.digest("hex");
122
- if (checksum && actualChecksum !== checksum) {
123
- await (0, import_fs.safeDelete)(destPath);
124
- throw new Error(
125
- `Checksum mismatch: expected ${checksum}, got ${actualChecksum}`
126
- );
127
- }
128
- if (!import_platform.WIN32) {
129
- await fs.promises.chmod(destPath, 493);
130
- }
131
- return actualChecksum;
132
- },
133
- {
134
- // Align with npm npx locking strategy.
135
- staleMs: 5e3,
136
- touchIntervalMs: 2e3
137
- }
138
- );
139
- }
140
- async function writeMetadata(cacheEntryPath, cacheKey, url, binaryName, checksum, size) {
141
- const metaPath = getMetadataPath(cacheEntryPath);
142
- const metadata = {
143
- version: "1.0.0",
144
- cache_key: cacheKey,
145
- timestamp: Date.now(),
146
- checksum,
147
- checksum_algorithm: "sha256",
148
- platform: (0, import_platform.getPlatform)(),
149
- arch: (0, import_platform.getArch)(),
150
- size,
151
- source: {
152
- type: "download",
153
- url
154
- }
155
- };
156
- const fs = /* @__PURE__ */ getFs();
157
- await fs.promises.writeFile(metaPath, JSON.stringify(metadata, null, 2));
158
- try {
159
- const spec = `${url}:${binaryName}`;
160
- await import_manifest.dlxManifest.setBinaryEntry(spec, cacheKey, {
161
- checksum,
162
- checksum_algorithm: metadata.checksum_algorithm,
163
- platform: metadata.platform,
164
- arch: metadata.arch,
165
- size,
166
- source: {
167
- type: "download",
168
- url
169
- }
170
- });
171
- } catch {
172
- }
173
- }
174
68
  async function cleanDlxCache(maxAge = import_time.DLX_BINARY_CACHE_TTL) {
175
69
  const cacheDir = getDlxCachePath();
176
70
  const fs = /* @__PURE__ */ getFs();
@@ -183,7 +77,7 @@ async function cleanDlxCache(maxAge = import_time.DLX_BINARY_CACHE_TTL) {
183
77
  const entries = await fs.promises.readdir(cacheDir);
184
78
  for (const entry of entries) {
185
79
  const entryPath = path.join(cacheDir, entry);
186
- const metaPath = getMetadataPath(entryPath);
80
+ const metaPath = getBinaryCacheMetadataPath(entryPath);
187
81
  try {
188
82
  if (!await (0, import_fs.isDir)(entryPath)) {
189
83
  continue;
@@ -194,7 +88,7 @@ async function cleanDlxCache(maxAge = import_time.DLX_BINARY_CACHE_TTL) {
194
88
  }
195
89
  const timestamp = metadata["timestamp"];
196
90
  const age = typeof timestamp === "number" && timestamp > 0 ? now - timestamp : Number.POSITIVE_INFINITY;
197
- if (age > maxAge) {
91
+ if (age < 0 || age > maxAge) {
198
92
  await (0, import_fs.safeDelete)(entryPath, { force: true, recursive: true });
199
93
  cleaned += 1;
200
94
  }
@@ -214,8 +108,8 @@ async function cleanDlxCache(maxAge = import_time.DLX_BINARY_CACHE_TTL) {
214
108
  async function dlxBinary(args, options, spawnExtra) {
215
109
  const {
216
110
  cacheTtl = import_time.DLX_BINARY_CACHE_TTL,
217
- checksum,
218
111
  force: userForce = false,
112
+ integrity,
219
113
  name,
220
114
  spawnOptions,
221
115
  url,
@@ -231,13 +125,16 @@ async function dlxBinary(args, options, spawnExtra) {
231
125
  const cacheEntryDir = path.join(cacheDir, cacheKey);
232
126
  const binaryPath = (0, import_normalize.normalizePath)(path.join(cacheEntryDir, binaryName));
233
127
  let downloaded = false;
234
- let computedChecksum = checksum;
235
- if (!force && fs.existsSync(cacheEntryDir) && await isCacheValid(cacheEntryDir, cacheTtl)) {
128
+ let computedIntegrity = integrity;
129
+ if (!force && fs.existsSync(cacheEntryDir) && await isBinaryCacheValid(cacheEntryDir, cacheTtl)) {
236
130
  try {
237
- const metaPath = getMetadataPath(cacheEntryDir);
131
+ const metaPath = getBinaryCacheMetadataPath(cacheEntryDir);
238
132
  const metadata = await (0, import_fs.readJson)(metaPath, { throws: false });
239
- if (metadata && typeof metadata === "object" && !Array.isArray(metadata) && typeof metadata["checksum"] === "string") {
240
- computedChecksum = metadata["checksum"];
133
+ if (metadata && typeof metadata === "object" && !Array.isArray(metadata) && typeof metadata["integrity"] === "string") {
134
+ computedIntegrity = metadata["integrity"];
135
+ if (!fs.existsSync(binaryPath)) {
136
+ downloaded = true;
137
+ }
241
138
  } else {
242
139
  downloaded = true;
243
140
  }
@@ -271,14 +168,13 @@ Ensure the filesystem is writable or set SOCKET_DLX_DIR to a writable location.`
271
168
  { cause: e }
272
169
  );
273
170
  }
274
- computedChecksum = await downloadBinaryFile(url, binaryPath, checksum);
171
+ computedIntegrity = await downloadBinaryFile(url, binaryPath, integrity);
275
172
  const stats = await fs.promises.stat(binaryPath);
276
- await writeMetadata(
173
+ await writeBinaryCacheMetadata(
277
174
  cacheEntryDir,
278
175
  cacheKey,
279
176
  url,
280
- binaryName,
281
- computedChecksum || "",
177
+ computedIntegrity || "",
282
178
  stats.size
283
179
  );
284
180
  }
@@ -301,8 +197,8 @@ Ensure the filesystem is writable or set SOCKET_DLX_DIR to a writable location.`
301
197
  async function downloadBinary(options) {
302
198
  const {
303
199
  cacheTtl = import_time.DLX_BINARY_CACHE_TTL,
304
- checksum,
305
200
  force = false,
201
+ integrity,
306
202
  name,
307
203
  url
308
204
  } = { __proto__: null, ...options };
@@ -315,7 +211,7 @@ async function downloadBinary(options) {
315
211
  const cacheEntryDir = path.join(cacheDir, cacheKey);
316
212
  const binaryPath = (0, import_normalize.normalizePath)(path.join(cacheEntryDir, binaryName));
317
213
  let downloaded = false;
318
- if (!force && fs.existsSync(cacheEntryDir) && await isCacheValid(cacheEntryDir, cacheTtl)) {
214
+ if (!force && fs.existsSync(cacheEntryDir) && await isBinaryCacheValid(cacheEntryDir, cacheTtl)) {
319
215
  downloaded = false;
320
216
  } else {
321
217
  try {
@@ -341,14 +237,17 @@ Ensure the filesystem is writable or set SOCKET_DLX_DIR to a writable location.`
341
237
  { cause: e }
342
238
  );
343
239
  }
344
- const computedChecksum = await downloadBinaryFile(url, binaryPath, checksum);
240
+ const computedIntegrity = await downloadBinaryFile(
241
+ url,
242
+ binaryPath,
243
+ integrity
244
+ );
345
245
  const stats = await fs.promises.stat(binaryPath);
346
- await writeMetadata(
246
+ await writeBinaryCacheMetadata(
347
247
  cacheEntryDir,
348
248
  cacheKey,
349
249
  url,
350
- binaryName,
351
- computedChecksum || "",
250
+ computedIntegrity || "",
352
251
  stats.size
353
252
  );
354
253
  downloaded = true;
@@ -358,6 +257,54 @@ Ensure the filesystem is writable or set SOCKET_DLX_DIR to a writable location.`
358
257
  downloaded
359
258
  };
360
259
  }
260
+ async function downloadBinaryFile(url, destPath, integrity) {
261
+ const crypto = /* @__PURE__ */ getCrypto();
262
+ const fs = /* @__PURE__ */ getFs();
263
+ const path = /* @__PURE__ */ getPath();
264
+ const cacheEntryDir = path.dirname(destPath);
265
+ const lockPath = path.join(cacheEntryDir, "concurrency.lock");
266
+ return await import_process_lock.processLock.withLock(
267
+ lockPath,
268
+ async () => {
269
+ if (fs.existsSync(destPath)) {
270
+ const stats = await fs.promises.stat(destPath);
271
+ if (stats.size > 0) {
272
+ const fileBuffer2 = await fs.promises.readFile(destPath);
273
+ const hash2 = crypto.createHash("sha512").update(fileBuffer2).digest("base64");
274
+ return `sha512-${hash2}`;
275
+ }
276
+ }
277
+ try {
278
+ await (0, import_http_request.httpDownload)(url, destPath);
279
+ } catch (e) {
280
+ throw new Error(
281
+ `Failed to download binary from ${url}
282
+ Destination: ${destPath}
283
+ Check your internet connection or verify the URL is accessible.`,
284
+ { cause: e }
285
+ );
286
+ }
287
+ const fileBuffer = await fs.promises.readFile(destPath);
288
+ const hash = crypto.createHash("sha512").update(fileBuffer).digest("base64");
289
+ const actualIntegrity = `sha512-${hash}`;
290
+ if (integrity && actualIntegrity !== integrity) {
291
+ await (0, import_fs.safeDelete)(destPath);
292
+ throw new Error(
293
+ `Integrity mismatch: expected ${integrity}, got ${actualIntegrity}`
294
+ );
295
+ }
296
+ if (!import_platform.WIN32) {
297
+ await fs.promises.chmod(destPath, 493);
298
+ }
299
+ return actualIntegrity;
300
+ },
301
+ {
302
+ // Align with npm npx locking strategy.
303
+ staleMs: 5e3,
304
+ touchIntervalMs: 2e3
305
+ }
306
+ );
307
+ }
361
308
  function executeBinary(binaryPath, args, spawnOptions, spawnExtra) {
362
309
  const needsShell = import_platform.WIN32 && /\.(?:bat|cmd|ps1)$/i.test(binaryPath);
363
310
  const path = /* @__PURE__ */ getPath();
@@ -375,6 +322,34 @@ function executeBinary(binaryPath, args, spawnOptions, spawnExtra) {
375
322
  function getDlxCachePath() {
376
323
  return (0, import_socket.getSocketDlxDir)();
377
324
  }
325
+ function getBinaryCacheMetadataPath(cacheEntryPath) {
326
+ return (/* @__PURE__ */ getPath()).join(cacheEntryPath, ".dlx-metadata.json");
327
+ }
328
+ async function isBinaryCacheValid(cacheEntryPath, cacheTtl) {
329
+ const fs = /* @__PURE__ */ getFs();
330
+ try {
331
+ const metaPath = getBinaryCacheMetadataPath(cacheEntryPath);
332
+ if (!fs.existsSync(metaPath)) {
333
+ return false;
334
+ }
335
+ const metadata = await (0, import_fs.readJson)(metaPath, { throws: false });
336
+ if (!(0, import_objects.isObjectObject)(metadata)) {
337
+ return false;
338
+ }
339
+ const now = Date.now();
340
+ const timestamp = metadata["timestamp"];
341
+ if (typeof timestamp !== "number" || timestamp <= 0) {
342
+ return false;
343
+ }
344
+ const age = now - timestamp;
345
+ if (age < 0) {
346
+ return false;
347
+ }
348
+ return age < cacheTtl;
349
+ } catch {
350
+ return false;
351
+ }
352
+ }
378
353
  async function listDlxCache() {
379
354
  const cacheDir = getDlxCachePath();
380
355
  const fs = /* @__PURE__ */ getFs();
@@ -391,7 +366,7 @@ async function listDlxCache() {
391
366
  if (!await (0, import_fs.isDir)(entryPath)) {
392
367
  continue;
393
368
  }
394
- const metaPath = getMetadataPath(entryPath);
369
+ const metaPath = getBinaryCacheMetadataPath(entryPath);
395
370
  const metadata = await (0, import_fs.readJson)(metaPath, { throws: false });
396
371
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
397
372
  continue;
@@ -406,10 +381,8 @@ async function listDlxCache() {
406
381
  const binaryStats = await fs.promises.stat(binaryPath);
407
382
  results.push({
408
383
  age: now - (metaObj["timestamp"] || 0),
409
- arch: metaObj["arch"] || "unknown",
410
- checksum: metaObj["checksum"] || "",
384
+ integrity: metaObj["integrity"] || "",
411
385
  name: binaryFile,
412
- platform: metaObj["platform"] || "unknown",
413
386
  size: binaryStats.size,
414
387
  url
415
388
  });
@@ -419,12 +392,34 @@ async function listDlxCache() {
419
392
  }
420
393
  return results;
421
394
  }
395
+ async function writeBinaryCacheMetadata(cacheEntryPath, cacheKey, url, integrity, size) {
396
+ const metaPath = getBinaryCacheMetadataPath(cacheEntryPath);
397
+ const metadata = {
398
+ version: "1.0.0",
399
+ cache_key: cacheKey,
400
+ timestamp: Date.now(),
401
+ integrity,
402
+ size,
403
+ source: {
404
+ type: "download",
405
+ url
406
+ }
407
+ };
408
+ const fs = /* @__PURE__ */ getFs();
409
+ const tmpPath = `${metaPath}.tmp.${process.pid}`;
410
+ await fs.promises.writeFile(tmpPath, JSON.stringify(metadata, null, 2));
411
+ await fs.promises.rename(tmpPath, metaPath);
412
+ }
422
413
  // Annotate the CommonJS export names for ESM import in node:
423
414
  0 && (module.exports = {
424
415
  cleanDlxCache,
425
416
  dlxBinary,
426
417
  downloadBinary,
418
+ downloadBinaryFile,
427
419
  executeBinary,
420
+ getBinaryCacheMetadataPath,
428
421
  getDlxCachePath,
429
- listDlxCache
422
+ isBinaryCacheValid,
423
+ listDlxCache,
424
+ writeBinaryCacheMetadata
430
425
  });