w3wallets 0.10.2 → 1.0.0-beta.2

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.
@@ -1,81 +1,301 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- *
5
- * Downloads and extracts Chrome extensions by alias ("backpack" and "metamask")
4
+ * Downloads and extracts Chrome extensions from the Chrome Web Store.
6
5
  *
7
6
  * Usage:
8
- * npx w3wallets backpack
9
- * npx w3wallets metamask
10
- * npx w3wallets backpack metamask
7
+ * npx w3wallets metamask polkadotjs # Download by alias
8
+ * npx w3wallets mm pjs # Short aliases
9
+ * npx w3wallets <extension-id> # Download by extension ID
10
+ * npx w3wallets --help # Show help
11
11
  */
12
12
 
13
13
  const fs = require("fs");
14
14
  const https = require("https");
15
15
  const path = require("path");
16
- const url = require("url");
17
16
  const zlib = require("zlib");
18
17
 
19
18
  // ---------------------------------------------------------------------
20
- // 1. Known aliases -> extension IDs
19
+ // 1. Known aliases -> extension IDs (case-insensitive lookup)
21
20
  // ---------------------------------------------------------------------
22
- const ALIASES = {
23
- backpack: "aflkmfhebedbjioipglgcbcmnbpgliof",
21
+ const EXTENSION_REGISTRY = {
22
+ // MetaMask wallet
24
23
  metamask: "nkbihfbeogaeaoehlefnkodbefgpgknn",
25
- polkadotJS: "mopnmbcafieddcagagdcbnhejhlodfdd",
24
+ mm: "nkbihfbeogaeaoehlefnkodbefgpgknn",
25
+
26
+ // Polkadot.js wallet
27
+ polkadotjs: "mopnmbcafieddcagagdcbnhejhlodfdd",
28
+ pjs: "mopnmbcafieddcagagdcbnhejhlodfdd",
29
+ };
30
+
31
+ // Human-readable names for display
32
+ const EXTENSION_NAMES = {
33
+ nkbihfbeogaeaoehlefnkodbefgpgknn: "MetaMask",
34
+ mopnmbcafieddcagagdcbnhejhlodfdd: "Polkadot.js",
26
35
  };
27
36
 
37
+ // Canonical aliases for listing
38
+ const CANONICAL_ALIASES = [
39
+ { name: "metamask", short: "mm", id: "nkbihfbeogaeaoehlefnkodbefgpgknn" },
40
+ { name: "polkadotjs", short: "pjs", id: "mopnmbcafieddcagagdcbnhejhlodfdd" },
41
+ ];
42
+
28
43
  // ---------------------------------------------------------------------
29
- // 2. Read aliases from CLI
44
+ // ZIP format constants (per PKWARE APPNOTE.TXT specification)
30
45
  // ---------------------------------------------------------------------
31
- const inputAliases = process.argv.slice(2);
46
+ const ZIP_SIGNATURES = {
47
+ EOCD: 0x06054b50, // End of Central Directory
48
+ CENTRAL_DIR: 0x02014b50, // Central Directory file header
49
+ LOCAL_FILE: 0x04034b50, // Local file header
50
+ };
32
51
 
33
- if (!inputAliases.length) {
34
- console.error("Usage: npx w3wallets <aliases...>");
35
- console.error("Available aliases:", Object.keys(ALIASES).join(", "));
36
- process.exit(1);
52
+ const ZIP_FLAGS = {
53
+ ENCRYPTED: 0x0001, // File is encrypted
54
+ DATA_DESCRIPTOR: 0x0008, // Sizes in data descriptor after file data
55
+ UTF8_FILENAME: 0x0800, // Filename is UTF-8 encoded
56
+ };
57
+
58
+ const ZIP_METHODS = {
59
+ STORE: 0, // No compression
60
+ DEFLATE: 8, // Deflate compression
61
+ };
62
+
63
+ // Marker value indicating ZIP64 format is required
64
+ const ZIP64_MARKER = 0xffffffff;
65
+
66
+ // Maximum size of EOCD record (22 bytes + max 65535 comment)
67
+ const MAX_EOCD_SEARCH = 65557;
68
+
69
+ // HTTP request timeout in milliseconds
70
+ const REQUEST_TIMEOUT_MS = 30000;
71
+
72
+ // ---------------------------------------------------------------------
73
+ // 2. CLI Argument Parser
74
+ // ---------------------------------------------------------------------
75
+ const CLI_OPTIONS = {
76
+ help: false,
77
+ list: false,
78
+ output: ".w3wallets",
79
+ force: false,
80
+ debug: false,
81
+ targets: [], // aliases or extension IDs
82
+ };
83
+
84
+ function printHelp() {
85
+ console.log(`
86
+ w3wallets - Download Chrome extensions from the Chrome Web Store
87
+
88
+ USAGE:
89
+ npx w3wallets [OPTIONS] <targets...>
90
+
91
+ TARGETS:
92
+ Alias name Known wallet alias (e.g., metamask, polkadotjs)
93
+ Short alias Short form (e.g., mm, pjs)
94
+ Extension ID 32-character Chrome extension ID
95
+ URL Chrome Web Store URL
96
+
97
+ OPTIONS:
98
+ -h, --help Show this help message
99
+ -l, --list List available wallet aliases
100
+ -o, --output Output directory (default: .w3wallets)
101
+ -f, --force Force re-download even if already exists
102
+ --debug Save raw .crx file for debugging
103
+
104
+ EXAMPLES:
105
+ npx w3wallets metamask # Download MetaMask
106
+ npx w3wallets mm pjs # Download using short aliases
107
+ npx w3wallets --list # List available aliases
108
+ npx w3wallets -o ./extensions metamask # Custom output directory
109
+ npx w3wallets --force mm # Force re-download
110
+ npx w3wallets nkbihfbeogaeaoehlefnkodbefgpgknn # Download by extension ID
111
+ npx w3wallets "https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn"
112
+ `);
37
113
  }
38
114
 
39
- for (const alias of inputAliases) {
40
- if (!ALIASES[alias]) {
41
- console.error(
42
- `Unknown alias "${alias}". Must be one of: ${Object.keys(ALIASES).join(", ")}`,
43
- );
44
- process.exit(1);
115
+ function printList() {
116
+ console.log("\nAvailable wallet aliases:\n");
117
+ console.log(" ALIAS SHORT EXTENSION ID");
118
+ console.log(" " + "-".repeat(50));
119
+ for (const { name, short, id } of CANONICAL_ALIASES) {
120
+ console.log(` ${name.padEnd(12)} ${short.padEnd(7)} ${id}`);
45
121
  }
122
+ console.log(
123
+ "\nYou can also download any extension by ID or Chrome Web Store URL.\n",
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Parse extension ID from various input formats:
129
+ * - Known alias (case-insensitive): "metamask", "MetaMask", "MM"
130
+ * - Direct extension ID: "nkbihfbeogaeaoehlefnkodbefgpgknn"
131
+ * - Chrome Web Store URL: "https://chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn"
132
+ */
133
+ function parseExtensionTarget(input) {
134
+ // Check for known alias (case-insensitive)
135
+ const normalizedInput = input.toLowerCase();
136
+ if (EXTENSION_REGISTRY[normalizedInput]) {
137
+ const id = EXTENSION_REGISTRY[normalizedInput];
138
+ // Find canonical alias name for directory
139
+ const alias = CANONICAL_ALIASES.find((a) => a.id === id);
140
+ return {
141
+ id,
142
+ name: EXTENSION_NAMES[id] || normalizedInput,
143
+ dirName: alias ? alias.name : id,
144
+ };
145
+ }
146
+
147
+ // Check if it's a Chrome Web Store URL
148
+ const urlPatterns = [
149
+ /chromewebstore\.google\.com\/detail\/[^/]+\/([a-z]{32})/i,
150
+ /chrome\.google\.com\/webstore\/detail\/[^/]+\/([a-z]{32})/i,
151
+ ];
152
+ for (const pattern of urlPatterns) {
153
+ const match = input.match(pattern);
154
+ if (match) {
155
+ const id = match[1].toLowerCase();
156
+ const alias = CANONICAL_ALIASES.find((a) => a.id === id);
157
+ return {
158
+ id,
159
+ name: EXTENSION_NAMES[id] || id,
160
+ dirName: alias ? alias.name : id,
161
+ };
162
+ }
163
+ }
164
+
165
+ // Check if it's a direct extension ID (32 lowercase letters)
166
+ if (/^[a-z]{32}$/i.test(input)) {
167
+ const id = input.toLowerCase();
168
+ const alias = CANONICAL_ALIASES.find((a) => a.id === id);
169
+ return {
170
+ id,
171
+ name: EXTENSION_NAMES[id] || id,
172
+ dirName: alias ? alias.name : id,
173
+ };
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ function parseArgs(args) {
180
+ let i = 0;
181
+ while (i < args.length) {
182
+ const arg = args[i];
183
+
184
+ if (arg === "-h" || arg === "--help") {
185
+ CLI_OPTIONS.help = true;
186
+ } else if (arg === "-l" || arg === "--list") {
187
+ CLI_OPTIONS.list = true;
188
+ } else if (arg === "-o" || arg === "--output") {
189
+ i++;
190
+ if (i >= args.length) {
191
+ console.error("Error: --output requires a directory path");
192
+ process.exit(1);
193
+ }
194
+ CLI_OPTIONS.output = args[i];
195
+ } else if (arg === "-f" || arg === "--force") {
196
+ CLI_OPTIONS.force = true;
197
+ } else if (arg === "--debug") {
198
+ CLI_OPTIONS.debug = true;
199
+ } else if (arg.startsWith("-")) {
200
+ console.error(`Error: Unknown option "${arg}"`);
201
+ console.error("Use --help for usage information");
202
+ process.exit(1);
203
+ } else {
204
+ // It's a target (alias, ID, or URL)
205
+ const parsed = parseExtensionTarget(arg);
206
+ if (!parsed) {
207
+ console.error(
208
+ `Error: "${arg}" is not a valid alias, extension ID, or URL`,
209
+ );
210
+ console.error(
211
+ "Use --list to see available aliases, or provide a 32-character extension ID",
212
+ );
213
+ process.exit(1);
214
+ }
215
+ CLI_OPTIONS.targets.push(parsed);
216
+ }
217
+ i++;
218
+ }
219
+ }
220
+
221
+ // Parse command line arguments
222
+ parseArgs(process.argv.slice(2));
223
+
224
+ // Handle --help
225
+ if (CLI_OPTIONS.help) {
226
+ printHelp();
227
+ process.exit(0);
228
+ }
229
+
230
+ // Handle --list
231
+ if (CLI_OPTIONS.list) {
232
+ printList();
233
+ process.exit(0);
234
+ }
235
+
236
+ // Validate we have targets
237
+ if (CLI_OPTIONS.targets.length === 0) {
238
+ console.error("Error: No extension targets specified");
239
+ console.error(
240
+ "Use --help for usage information or --list to see available aliases",
241
+ );
242
+ process.exit(1);
46
243
  }
47
244
 
48
245
  // ---------------------------------------------------------------------
49
- // 3. Main: download and extract each requested alias
246
+ // 3. Main: download and extract each requested extension
50
247
  // ---------------------------------------------------------------------
51
248
  (async function main() {
52
- for (const alias of inputAliases) {
53
- const extensionId = ALIASES[alias];
54
- console.log(`\n=== Processing alias: "${alias}" (ID: ${extensionId}) ===`);
249
+ for (const target of CLI_OPTIONS.targets) {
250
+ const { id, name, dirName } = target;
251
+ const outDir = path.join(CLI_OPTIONS.output, dirName);
55
252
 
56
- try {
57
- // 1) Download CRX
58
- const crxBuffer = await downloadCrx(extensionId);
59
- console.log(`Got CRX data for "${alias}"! ${crxBuffer.length} bytes`);
253
+ console.log(`\n=== ${name} (${id}) ===`);
60
254
 
61
- // 2) Save raw CRX to disk
62
- const outDir = path.join(".w3wallets", alias);
63
- fs.mkdirSync(outDir, { recursive: true });
255
+ // Check if already exists (skip unless --force)
256
+ const manifestPath = path.join(outDir, "manifest.json");
257
+ if (!CLI_OPTIONS.force && fs.existsSync(manifestPath)) {
258
+ console.log(`Already exists: ${outDir}`);
259
+ console.log("Use --force to re-download");
260
+ continue;
261
+ }
64
262
 
65
- const debugPath = path.join(outDir, `debug-${alias}.crx`);
66
- fs.writeFileSync(debugPath, crxBuffer);
67
- console.log(`Saved ${debugPath}`);
263
+ try {
264
+ // 1) Download CRX with progress
265
+ console.log("Downloading...");
266
+ const crxBuffer = await downloadCrx(id);
267
+ console.log(`Downloaded ${formatBytes(crxBuffer.length)}`);
268
+
269
+ // 2) Optionally save raw CRX for debugging
270
+ if (CLI_OPTIONS.debug) {
271
+ fs.mkdirSync(outDir, { recursive: true });
272
+ const debugPath = path.join(outDir, `debug-${dirName}.crx`);
273
+ fs.writeFileSync(debugPath, crxBuffer);
274
+ console.log(`Debug CRX saved: ${debugPath}`);
275
+ }
68
276
 
69
- // 3) Extract CRX into "wallets/<alias>"
277
+ // 3) Extract CRX
278
+ console.log("Extracting...");
70
279
  extractCrxToFolder(crxBuffer, outDir);
71
- console.log(`Extraction complete! See folder: ${outDir}`);
280
+ console.log(`Done: ${outDir}`);
72
281
  } catch (err) {
73
- console.error(`Failed to process "${alias}":`, err.message);
282
+ console.error(`Failed: ${err.message}`);
74
283
  process.exit(1);
75
284
  }
76
285
  }
286
+
287
+ console.log("\nAll extensions downloaded successfully!");
77
288
  })();
78
289
 
290
+ // ---------------------------------------------------------------------
291
+ // Utility: format bytes for human display
292
+ // ---------------------------------------------------------------------
293
+ function formatBytes(bytes) {
294
+ if (bytes < 1024) return `${bytes} B`;
295
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
296
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
297
+ }
298
+
79
299
  // ---------------------------------------------------------------------
80
300
  // downloadCrx: Build CRX URL and fetch it
81
301
  // ---------------------------------------------------------------------
@@ -95,7 +315,7 @@ async function downloadCrx(extensionId) {
95
315
  }
96
316
 
97
317
  // ---------------------------------------------------------------------
98
- // fetchUrl: minimal GET + redirect handling
318
+ // fetchUrl: minimal GET + redirect handling with timeout and progress
99
319
  // ---------------------------------------------------------------------
100
320
  function fetchUrl(
101
321
  targetUrl,
@@ -108,12 +328,13 @@ function fetchUrl(
108
328
  return reject(new Error("Too many redirects"));
109
329
  }
110
330
 
111
- const req = https.get(targetUrl, options, (res) => {
331
+ const reqOptions = { ...options, timeout: REQUEST_TIMEOUT_MS };
332
+ const req = https.get(targetUrl, reqOptions, (res) => {
112
333
  const { statusCode, headers } = res;
113
334
 
114
335
  // Follow redirects
115
336
  if ([301, 302, 303, 307, 308].includes(statusCode) && headers.location) {
116
- const newUrl = url.resolve(targetUrl, headers.location);
337
+ const newUrl = new URL(headers.location, targetUrl).href;
117
338
  res.resume(); // discard body
118
339
  return resolve(
119
340
  fetchUrl(newUrl, options, redirectCount + 1, maxRedirects),
@@ -127,15 +348,60 @@ function fetchUrl(
127
348
  );
128
349
  }
129
350
 
351
+ const contentLength = parseInt(headers["content-length"], 10) || 0;
130
352
  const dataChunks = [];
131
- res.on("data", (chunk) => dataChunks.push(chunk));
132
- res.on("end", () => resolve(Buffer.concat(dataChunks)));
353
+ let downloadedBytes = 0;
354
+ let lastProgressUpdate = 0;
355
+
356
+ res.on("data", (chunk) => {
357
+ dataChunks.push(chunk);
358
+ downloadedBytes += chunk.length;
359
+
360
+ // Update progress at most every 100ms to avoid flickering
361
+ const now = Date.now();
362
+ if (contentLength > 0 && now - lastProgressUpdate > 100) {
363
+ lastProgressUpdate = now;
364
+ const percent = Math.round((downloadedBytes / contentLength) * 100);
365
+ const progressBar = createProgressBar(percent);
366
+ process.stdout.write(
367
+ `\r ${progressBar} ${percent}% (${formatBytes(downloadedBytes)})`,
368
+ );
369
+ }
370
+ });
371
+
372
+ res.on("end", () => {
373
+ // Clear the progress line
374
+ if (contentLength > 0) {
375
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
376
+ }
377
+ resolve(Buffer.concat(dataChunks));
378
+ });
379
+ });
380
+
381
+ req.on("timeout", () => {
382
+ req.destroy();
383
+ reject(
384
+ new Error(
385
+ `Request timed out after ${REQUEST_TIMEOUT_MS}ms: ${targetUrl}`,
386
+ ),
387
+ );
133
388
  });
134
389
 
135
- req.on("error", (err) => reject(err));
390
+ req.on("error", (err) => {
391
+ reject(new Error(`Failed to fetch ${targetUrl}: ${err.message}`));
392
+ });
136
393
  });
137
394
  }
138
395
 
396
+ // ---------------------------------------------------------------------
397
+ // createProgressBar: Generate ASCII progress bar
398
+ // ---------------------------------------------------------------------
399
+ function createProgressBar(percent, width = 20) {
400
+ const filled = Math.round((percent / 100) * width);
401
+ const empty = width - filled;
402
+ return "[" + "=".repeat(filled) + " ".repeat(empty) + "]";
403
+ }
404
+
139
405
  // ---------------------------------------------------------------------
140
406
  // extractCrxToFolder
141
407
  // 1) Checks "Cr24" magic
@@ -149,6 +415,7 @@ function extractCrxToFolder(crxBuffer, outFolder) {
149
415
 
150
416
  const version = crxBuffer.readUInt32LE(4);
151
417
  let zipStartOffset = 0;
418
+
152
419
  if (version === 2) {
153
420
  const pkLen = crxBuffer.readUInt32LE(8);
154
421
  const sigLen = crxBuffer.readUInt32LE(12);
@@ -174,16 +441,16 @@ function extractCrxToFolder(crxBuffer, outFolder) {
174
441
 
175
442
  // ---------------------------------------------------------------------
176
443
  // parseZipCentralDirectory(buffer, outFolder)
177
- // 1) Finds End of Central Directory (EOCD) record (0x06054b50).
444
+ // 1) Finds End of Central Directory (EOCD) record
178
445
  // 2) Reads central directory for file metadata
179
446
  // 3) For each file, decompress into outFolder
180
447
  // ---------------------------------------------------------------------
181
448
  function parseZipCentralDirectory(zipBuffer, outFolder) {
182
- const eocdSig = 0x06054b50;
449
+ // Find EOCD by scanning backwards from end of file
183
450
  let eocdPos = -1;
184
- const minPos = Math.max(0, zipBuffer.length - 65557);
451
+ const minPos = Math.max(0, zipBuffer.length - MAX_EOCD_SEARCH);
185
452
  for (let i = zipBuffer.length - 4; i >= minPos; i--) {
186
- if (zipBuffer.readUInt32LE(i) === eocdSig) {
453
+ if (zipBuffer.readUInt32LE(i) === ZIP_SIGNATURES.EOCD) {
187
454
  eocdPos = i;
188
455
  break;
189
456
  }
@@ -196,6 +463,11 @@ function parseZipCentralDirectory(zipBuffer, outFolder) {
196
463
  const cdSize = zipBuffer.readUInt32LE(eocdPos + 12);
197
464
  const cdOffset = zipBuffer.readUInt32LE(eocdPos + 16);
198
465
 
466
+ // ZIP64 check: marker values indicate ZIP64 format is required
467
+ if (cdOffset === ZIP64_MARKER || cdSize === ZIP64_MARKER) {
468
+ throw new Error("ZIP64 format is not supported.");
469
+ }
470
+
199
471
  if (cdOffset + cdSize > zipBuffer.length) {
200
472
  throw new Error("Central directory offset/size out of range.");
201
473
  }
@@ -204,23 +476,20 @@ function parseZipCentralDirectory(zipBuffer, outFolder) {
204
476
  const files = [];
205
477
  for (let i = 0; i < totalCD; i++) {
206
478
  const sig = zipBuffer.readUInt32LE(ptr);
207
- if (sig !== 0x02014b50) {
479
+ if (sig !== ZIP_SIGNATURES.CENTRAL_DIR) {
208
480
  throw new Error(`Central directory signature mismatch at ${ptr}`);
209
481
  }
210
482
  ptr += 4;
211
483
 
212
- /* const verMade = */ zipBuffer.readUInt16LE(ptr);
213
- ptr += 2;
484
+ ptr += 2; // version made by (unused)
214
485
  const verNeed = zipBuffer.readUInt16LE(ptr);
215
486
  ptr += 2;
216
487
  const flags = zipBuffer.readUInt16LE(ptr);
217
488
  ptr += 2;
218
489
  const method = zipBuffer.readUInt16LE(ptr);
219
490
  ptr += 2;
220
- /* const modTime = */ zipBuffer.readUInt16LE(ptr);
221
- ptr += 2;
222
- /* const modDate = */ zipBuffer.readUInt16LE(ptr);
223
- ptr += 2;
491
+ ptr += 2; // mod time (unused)
492
+ ptr += 2; // mod date (unused)
224
493
  const crc32 = zipBuffer.readUInt32LE(ptr);
225
494
  ptr += 4;
226
495
  const compSize = zipBuffer.readUInt32LE(ptr);
@@ -233,18 +502,31 @@ function parseZipCentralDirectory(zipBuffer, outFolder) {
233
502
  ptr += 2;
234
503
  const cLen = zipBuffer.readUInt16LE(ptr);
235
504
  ptr += 2;
236
- /* const diskNo = */ zipBuffer.readUInt16LE(ptr);
237
- ptr += 2;
238
- /* const intAttr = */ zipBuffer.readUInt16LE(ptr);
239
- ptr += 2;
240
- /* const extAttr = */ zipBuffer.readUInt32LE(ptr);
241
- ptr += 4;
505
+ ptr += 2; // disk number (unused)
506
+ ptr += 2; // internal attributes (unused)
507
+ ptr += 4; // external attributes (unused)
242
508
  const localHeaderOffset = zipBuffer.readUInt32LE(ptr);
243
509
  ptr += 4;
244
510
 
245
511
  const filename = zipBuffer.toString("utf8", ptr, ptr + fLen);
246
512
  ptr += fLen + xLen + cLen; // skip the extra + comment
247
513
 
514
+ // Validate: encrypted files not supported
515
+ if (flags & ZIP_FLAGS.ENCRYPTED) {
516
+ throw new Error(`Encrypted files are not supported: ${filename}`);
517
+ }
518
+
519
+ // Validate: ZIP64 extended sizes not supported
520
+ if (
521
+ compSize === ZIP64_MARKER ||
522
+ unCompSize === ZIP64_MARKER ||
523
+ localHeaderOffset === ZIP64_MARKER
524
+ ) {
525
+ throw new Error(
526
+ `ZIP64 extended information not supported for file: ${filename}`,
527
+ );
528
+ }
529
+
248
530
  files.push({
249
531
  filename,
250
532
  method,
@@ -257,19 +539,31 @@ function parseZipCentralDirectory(zipBuffer, outFolder) {
257
539
  });
258
540
  }
259
541
 
260
- fs.mkdirSync(outFolder, { recursive: true });
542
+ const resolvedOutFolder = path.resolve(outFolder);
543
+ fs.mkdirSync(resolvedOutFolder, { recursive: true });
261
544
 
262
545
  for (const file of files) {
263
546
  const { filename, method, compSize, localHeaderOffset } = file;
264
547
 
548
+ // Security: validate path to prevent directory traversal attacks
549
+ const outPath = path.join(resolvedOutFolder, filename);
550
+ if (
551
+ !outPath.startsWith(resolvedOutFolder + path.sep) &&
552
+ outPath !== resolvedOutFolder
553
+ ) {
554
+ throw new Error(
555
+ `Path traversal detected, refusing to extract: ${filename}`,
556
+ );
557
+ }
558
+
265
559
  if (filename.endsWith("/")) {
266
- fs.mkdirSync(path.join(outFolder, filename), { recursive: true });
560
+ fs.mkdirSync(outPath, { recursive: true });
267
561
  continue;
268
562
  }
269
563
 
270
564
  let lhPtr = localHeaderOffset;
271
565
  const localSig = zipBuffer.readUInt32LE(lhPtr);
272
- if (localSig !== 0x04034b50) {
566
+ if (localSig !== ZIP_SIGNATURES.LOCAL_FILE) {
273
567
  throw new Error(`Local file header mismatch at ${lhPtr} for ${filename}`);
274
568
  }
275
569
  lhPtr += 4;
@@ -290,12 +584,11 @@ function parseZipCentralDirectory(zipBuffer, outFolder) {
290
584
  lhPtr += lhFNameLen + lhXLen;
291
585
  const fileData = zipBuffer.slice(lhPtr, lhPtr + compSize);
292
586
 
293
- const outPath = path.join(outFolder, filename);
294
587
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
295
588
 
296
- if (method === 0) {
589
+ if (method === ZIP_METHODS.STORE) {
297
590
  fs.writeFileSync(outPath, fileData);
298
- } else if (method === 8) {
591
+ } else if (method === ZIP_METHODS.DEFLATE) {
299
592
  const unzipped = zlib.inflateRawSync(fileData);
300
593
  fs.writeFileSync(outPath, unzipped);
301
594
  } else {