@socketsecurity/lib 5.11.3 → 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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ 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.12.0](https://github.com/SocketDev/socket-lib/releases/tag/v5.12.0) - 2026-04-04
9
+
10
+ ### Added — http-request
11
+
12
+ - Lifecycle hooks (`onRequest`/`onResponse`) on `HttpRequestOptions` (#133)
13
+ - Fire per-attempt — retries and redirects each trigger separate hook calls
14
+ - `HttpHooks`, `HttpHookRequestInfo`, `HttpHookResponseInfo` types exported
15
+ - `maxResponseSize` option to reject responses exceeding a byte limit
16
+ - Works through redirects, `httpJson`, and `httpText`
17
+ - `rawResponse` property on `HttpResponse` exposing the underlying `IncomingMessage`
18
+ - `enrichErrorMessage()` exported for reusable error enrichment
19
+
20
+ ### Changed — http-request
21
+
22
+ - Error messages now include HTTP method and URL for easier debugging
23
+ - `HttpResponse.headers` type changed from `Record<string, string | string[] | undefined>` to `IncomingHttpHeaders`
24
+
25
+ ## [5.11.4](https://github.com/SocketDev/socket-lib/releases/tag/v5.11.4) - 2026-03-28
26
+
27
+ ### Changed
28
+
29
+ - **perf**: Lazy-load heavy external sub-bundles across 7 modules (#119)
30
+ - `sorts.ts`: Defer semver (2.5 MB via npm-pack) and fastSort until first use
31
+ - `versions.ts`: Defer semver until first use
32
+ - `archives.ts`: Defer adm-zip (102 KB) and tar-fs (105 KB) until extraction
33
+ - `globs.ts`: Defer fast-glob and picomatch (260 KB via pico-pack) until glob execution
34
+ - `fs.ts`: Defer del (260 KB via pico-pack) until safeDelete call
35
+ - `spawn.ts`: Defer @npmcli/promise-spawn (17 KB) until async spawn
36
+ - `strings.ts`: Defer get-east-asian-width (10 KB) until stringWidth call
37
+ - Importing lightweight exports (isObject, httpJson, localeCompare, readJsonSync, stripAnsi) no longer loads heavy externals at module init time
38
+
8
39
  ## [5.11.3](https://github.com/SocketDev/socket-lib/releases/tag/v5.11.3) - 2026-03-26
9
40
 
10
41
  ### Fixed
@@ -10,6 +10,8 @@ export interface ExtractOptions {
10
10
  quiet?: boolean;
11
11
  /** Strip leading path components (like tar --strip-components) */
12
12
  strip?: number;
13
+ /** Maximum number of entries to extract (default: 100,000) */
14
+ maxEntries?: number;
13
15
  /** Maximum size of a single extracted file in bytes (default: 100MB) */
14
16
  maxFileSize?: number;
15
17
  /** Maximum total extracted size in bytes (default: 1GB) */
package/dist/archives.js CHANGED
@@ -40,10 +40,24 @@ var import_node_fs = require("node:fs");
40
40
  var import_promises = require("node:stream/promises");
41
41
  var import_node_zlib = require("node:zlib");
42
42
  var import_node_process = __toESM(require("node:process"));
43
- var import_adm_zip = __toESM(require("./external/adm-zip.js"));
44
- var import_tar_fs = __toESM(require("./external/tar-fs.js"));
45
43
  var import_fs = require("./fs.js");
46
44
  var import_normalize = require("./paths/normalize.js");
45
+ let _AdmZip;
46
+ // @__NO_SIDE_EFFECTS__
47
+ function getAdmZip() {
48
+ if (_AdmZip === void 0) {
49
+ _AdmZip = require("./external/adm-zip.js");
50
+ }
51
+ return _AdmZip;
52
+ }
53
+ let _tarFs;
54
+ // @__NO_SIDE_EFFECTS__
55
+ function getTarFs() {
56
+ if (_tarFs === void 0) {
57
+ _tarFs = require("./external/tar-fs.js");
58
+ }
59
+ return _tarFs;
60
+ }
47
61
  let _path;
48
62
  // @__NO_SIDE_EFFECTS__
49
63
  function getPath() {
@@ -54,6 +68,7 @@ function getPath() {
54
68
  }
55
69
  const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024;
56
70
  const DEFAULT_MAX_TOTAL_SIZE = 1024 * 1024 * 1024;
71
+ const DEFAULT_MAX_ENTRIES = 1e5;
57
72
  function validatePathWithinBase(targetPath, baseDir, entryName) {
58
73
  const path = /* @__PURE__ */ getPath();
59
74
  const resolvedTarget = path.resolve(targetPath);
@@ -82,6 +97,7 @@ function detectArchiveFormat(filePath) {
82
97
  }
83
98
  async function extractTar(archivePath, outputDir, options = {}) {
84
99
  const {
100
+ maxEntries = DEFAULT_MAX_ENTRIES,
85
101
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
86
102
  maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,
87
103
  strip = 0
@@ -89,12 +105,37 @@ async function extractTar(archivePath, outputDir, options = {}) {
89
105
  const normalizedOutputDir = (0, import_normalize.normalizePath)(outputDir);
90
106
  await (0, import_fs.safeMkdir)(normalizedOutputDir);
91
107
  let totalExtractedSize = 0;
108
+ let entryCount = 0;
92
109
  let destroyScheduled = false;
93
- const extractStream = import_tar_fs.default.extract(normalizedOutputDir, {
110
+ const tarFs = /* @__PURE__ */ getTarFs();
111
+ const extractStream = tarFs.extract(normalizedOutputDir, {
94
112
  map: (header) => {
95
113
  if (destroyScheduled) {
96
114
  return header;
97
115
  }
116
+ entryCount += 1;
117
+ if (entryCount > maxEntries) {
118
+ destroyScheduled = true;
119
+ import_node_process.default.nextTick(() => {
120
+ extractStream.destroy(
121
+ new Error(
122
+ `Archive has too many entries: exceeded limit of ${maxEntries}`
123
+ )
124
+ );
125
+ });
126
+ return header;
127
+ }
128
+ if (header.name.includes("\0")) {
129
+ destroyScheduled = true;
130
+ import_node_process.default.nextTick(() => {
131
+ extractStream.destroy(
132
+ new Error(
133
+ `Invalid null byte in archive entry name: ${header.name}`
134
+ )
135
+ );
136
+ });
137
+ return header;
138
+ }
98
139
  if (header.type === "symlink" || header.type === "link") {
99
140
  destroyScheduled = true;
100
141
  import_node_process.default.nextTick(() => {
@@ -147,6 +188,7 @@ async function extractTar(archivePath, outputDir, options = {}) {
147
188
  }
148
189
  async function extractTarGz(archivePath, outputDir, options = {}) {
149
190
  const {
191
+ maxEntries = DEFAULT_MAX_ENTRIES,
150
192
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
151
193
  maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,
152
194
  strip = 0
@@ -154,12 +196,37 @@ async function extractTarGz(archivePath, outputDir, options = {}) {
154
196
  const normalizedOutputDir = (0, import_normalize.normalizePath)(outputDir);
155
197
  await (0, import_fs.safeMkdir)(normalizedOutputDir);
156
198
  let totalExtractedSize = 0;
199
+ let entryCount = 0;
157
200
  let destroyScheduled = false;
158
- const extractStream = import_tar_fs.default.extract(normalizedOutputDir, {
201
+ const tarFs = /* @__PURE__ */ getTarFs();
202
+ const extractStream = tarFs.extract(normalizedOutputDir, {
159
203
  map: (header) => {
160
204
  if (destroyScheduled) {
161
205
  return header;
162
206
  }
207
+ entryCount += 1;
208
+ if (entryCount > maxEntries) {
209
+ destroyScheduled = true;
210
+ import_node_process.default.nextTick(() => {
211
+ extractStream.destroy(
212
+ new Error(
213
+ `Archive has too many entries: exceeded limit of ${maxEntries}`
214
+ )
215
+ );
216
+ });
217
+ return header;
218
+ }
219
+ if (header.name.includes("\0")) {
220
+ destroyScheduled = true;
221
+ import_node_process.default.nextTick(() => {
222
+ extractStream.destroy(
223
+ new Error(
224
+ `Invalid null byte in archive entry name: ${header.name}`
225
+ )
226
+ );
227
+ });
228
+ return header;
229
+ }
163
230
  if (header.type === "symlink" || header.type === "link") {
164
231
  destroyScheduled = true;
165
232
  import_node_process.default.nextTick(() => {
@@ -212,20 +279,32 @@ async function extractTarGz(archivePath, outputDir, options = {}) {
212
279
  }
213
280
  async function extractZip(archivePath, outputDir, options = {}) {
214
281
  const {
282
+ maxEntries = DEFAULT_MAX_ENTRIES,
215
283
  maxFileSize = DEFAULT_MAX_FILE_SIZE,
216
284
  maxTotalSize = DEFAULT_MAX_TOTAL_SIZE,
217
285
  strip = 0
218
286
  } = options;
219
287
  const normalizedOutputDir = (0, import_normalize.normalizePath)(outputDir);
220
288
  await (0, import_fs.safeMkdir)(normalizedOutputDir);
221
- const zip = new import_adm_zip.default(archivePath);
289
+ const AdmZip = /* @__PURE__ */ getAdmZip();
290
+ const zip = new AdmZip(archivePath);
222
291
  const path = /* @__PURE__ */ getPath();
223
292
  const entries = zip.getEntries();
293
+ if (entries.length > maxEntries) {
294
+ throw new Error(
295
+ `Archive has too many entries: ${entries.length} (limit: ${maxEntries})`
296
+ );
297
+ }
224
298
  let totalExtractedSize = 0;
225
299
  for (const entry of entries) {
226
300
  if (entry.isDirectory) {
227
301
  continue;
228
302
  }
303
+ if (entry.entryName.includes("\0")) {
304
+ throw new Error(
305
+ `Invalid null byte in archive entry name: ${entry.entryName}`
306
+ );
307
+ }
229
308
  const uncompressedSize = entry.header.size;
230
309
  if (uncompressedSize > maxFileSize) {
231
310
  throw new Error(
@@ -306,11 +306,17 @@ Check your internet connection or verify the URL is accessible.`,
306
306
  const fileBuffer = await fs.promises.readFile(destPath);
307
307
  const hash = crypto.createHash("sha512").update(fileBuffer).digest("base64");
308
308
  const actualIntegrity = `sha512-${hash}`;
309
- if (integrity && actualIntegrity !== integrity) {
310
- await (0, import_fs.safeDelete)(destPath);
311
- throw new Error(
312
- `Integrity mismatch: expected ${integrity}, got ${actualIntegrity}`
309
+ if (integrity) {
310
+ const integrityMatch = actualIntegrity.length === integrity.length && crypto.timingSafeEqual(
311
+ Buffer.from(actualIntegrity),
312
+ Buffer.from(integrity)
313
313
  );
314
+ if (!integrityMatch) {
315
+ await (0, import_fs.safeDelete)(destPath);
316
+ throw new Error(
317
+ `Integrity mismatch: expected ${integrity}, got ${actualIntegrity}`
318
+ );
319
+ }
314
320
  }
315
321
  if (!import_platform.WIN32) {
316
322
  await fs.promises.chmod(destPath, 493);