molt-cli 1.0.1 → 1.0.3

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.
Files changed (2) hide show
  1. package/cli.js +43 -3
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -120,10 +120,16 @@ function signMessage(message, privateKeyBase64) {
120
120
 
121
121
  function computeProofOfWork(publicKey) {
122
122
  let nonce = 0;
123
+ const startTime = Date.now();
123
124
  while (true) {
124
125
  const hash = crypto.createHash('sha256').update(publicKey + nonce).digest('hex');
125
- if (hash.startsWith('0000')) return nonce;
126
+ if (hash.startsWith('000000')) return nonce; // 6 zeros ≈ 5-15 seconds
126
127
  nonce++;
128
+ // Safety: log progress every 5M hashes
129
+ if (nonce % 5000000 === 0) {
130
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
131
+ process.stdout.write(`\r Mining proof-of-work... ${(nonce / 1000000).toFixed(0)}M hashes (${elapsed}s)`);
132
+ }
127
133
  }
128
134
  }
129
135
 
@@ -332,13 +338,45 @@ function extractTarGz(tarGzPath, destDir) {
332
338
  const extract = tar.extract();
333
339
  const gunzip = zlib.createGunzip();
334
340
 
341
+ const MAX_EXTRACTED_SIZE = 50 * 1024 * 1024; // 50MB max decompressed
342
+ const MAX_FILES = 500; // max files in package
343
+ let totalSize = 0;
344
+ let fileCount = 0;
345
+ const resolvedDest = path.resolve(destDir);
346
+
335
347
  extract.on('entry', (header, stream, next) => {
336
- const filePath = path.join(destDir, header.name);
348
+ fileCount++;
349
+ if (fileCount > MAX_FILES) {
350
+ stream.destroy();
351
+ return reject(new Error(`Too many files in package (max ${MAX_FILES}). Aborting.`));
352
+ }
353
+
354
+ // Path traversal protection: resolve and verify within destDir
355
+ const filePath = path.resolve(destDir, header.name);
356
+ if (!filePath.startsWith(resolvedDest + path.sep) && filePath !== resolvedDest) {
357
+ stream.resume(); // skip malicious entry
358
+ console.log(` ${C.red}⚠ Skipping suspicious path: ${header.name}${C.reset}`);
359
+ return next();
360
+ }
361
+
362
+ // Skip symlinks (potential attack vector)
363
+ if (header.type === 'symlink' || header.type === 'link') {
364
+ stream.resume();
365
+ return next();
366
+ }
367
+
337
368
  if (header.type === 'directory') {
338
369
  fs.mkdirSync(filePath, { recursive: true });
339
370
  stream.resume();
340
371
  next();
341
372
  } else {
373
+ // Tar bomb protection: track decompressed size
374
+ totalSize += header.size || 0;
375
+ if (totalSize > MAX_EXTRACTED_SIZE) {
376
+ stream.destroy();
377
+ return reject(new Error(`Package too large when extracted (>${MAX_EXTRACTED_SIZE / 1024 / 1024}MB). Possible tar bomb.`));
378
+ }
379
+
342
380
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
343
381
  const out = fs.createWriteStream(filePath);
344
382
  stream.pipe(out);
@@ -858,7 +896,9 @@ async function cmdInstall(skillName) {
858
896
  console.log(` ${C.dim}Installed to: ${C.cyan}${path.relative(process.cwd(), destDir) || destDir}${C.reset}`);
859
897
  console.log();
860
898
  } catch (err) {
861
- errorMsg(err.error || 'Install failed', err.hint);
899
+ // Clear any active spinners before showing error
900
+ process.stdout.write('\r\x1b[K');
901
+ errorMsg(err.error || err.message || 'Install failed', err.hint);
862
902
  }
863
903
  }
864
904
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "molt-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "CLI for MolTunes — the AI agent skill marketplace",
5
5
  "main": "cli.js",
6
6
  "bin": {