iterate-skill-installer 2.3.0 → 2.3.5

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/lib/installer.js CHANGED
@@ -162,7 +162,11 @@ function parseChecksums(text) {
162
162
  if (!trimmed) continue;
163
163
  const parts = trimmed.split(/\s+/);
164
164
  if (parts.length >= 2) {
165
- map.set(parts[1], parts[0]);
165
+ // Strip the GNU tar binary-mode marker ('*') so that both
166
+ // "HASH filename" and "HASH *filename" formats match the target
167
+ // filename, consistent with scripts/install.py (name.lstrip('*')).
168
+ const name = parts[1].replace(/^\*/, '');
169
+ map.set(name, parts[0]);
166
170
  }
167
171
  }
168
172
  return map;
@@ -187,9 +191,32 @@ function runCommand(bin, args, options = {}) {
187
191
  }
188
192
 
189
193
  async function extractTarball(tarballPath, destDir) {
190
- // Use the system tar command (available on macOS, Linux, and Windows 10+).
191
- // Strip the top-level directory so files land directly in destDir.
192
- await runCommand('tar', ['-xzf', tarballPath, '-C', destDir, '--strip-components=1']);
194
+ // Use the system tar command (available on macOS, Linux, and Windows 10+),
195
+ // but defend against path-traversal tarballs: before extracting we list the
196
+ // archive contents and refuse to extract any entry whose resolved path
197
+ // escapes destDir (e.g. a "../" prefix). This keeps the installer
198
+ // dependency-free while closing the theoretical escape vector.
199
+ const absoluteDest = path.resolve(destDir);
200
+ fs.mkdirSync(absoluteDest, { recursive: true });
201
+
202
+ // 1. List archive entries (no extraction yet).
203
+ const listing = await runCommand('tar', ['-tzf', tarballPath]);
204
+
205
+ // 2. Validate every entry stays inside destDir after stripping the single
206
+ // top-level directory component (--strip-components=1).
207
+ const entries = listing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
208
+ for (const entry of entries) {
209
+ const stripped = entry.split('/').slice(1).join('/');
210
+ const resolved = path.resolve(absoluteDest, stripped || entry);
211
+ if (resolved !== absoluteDest && !resolved.startsWith(absoluteDest + path.sep)) {
212
+ throw new InstallerError(
213
+ `Refusing to extract tarball: entry "${entry}" escapes destination directory.`,
214
+ );
215
+ }
216
+ }
217
+
218
+ // 3. Now extract safely — the content has been validated above.
219
+ await runCommand('tar', ['-xzf', tarballPath, '-C', absoluteDest, '--strip-components=1']);
193
220
  }
194
221
 
195
222
  function runPythonInstall(pythonBin, installScript, args, options = {}) {
@@ -487,4 +514,5 @@ module.exports = {
487
514
  InstallerError,
488
515
  resolveInstallMode,
489
516
  askYesNo,
517
+ parseChecksums,
490
518
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-skill-installer",
3
- "version": "2.3.0",
3
+ "version": "2.3.5",
4
4
  "description": "One-command installer for iterate-skill across AI coding assistants",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {
package/test/mode.test.js CHANGED
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Unit tests for resolveInstallMode() — the global vs project install-mode
3
- * decision in the npx installer. Uses node:assert so no extra test runner is
4
- * required; run via `npm test`.
3
+ * decision — and parseChecksums() — the SHA256SUMS.txt parser — in the npx
4
+ * installer. Uses node:assert so no extra test runner is required; run via
5
+ * `npm test`.
5
6
  */
6
7
 
7
8
  const assert = require('node:assert');
8
9
  const fs = require('node:fs');
9
10
  const path = require('node:path');
10
11
  const os = require('node:os');
11
- const { resolveInstallMode } = require('../lib/installer');
12
+ const { resolveInstallMode, parseChecksums } = require('../lib/installer');
12
13
 
13
14
  const originalCwd = process.cwd();
14
15
  const originalHome = os.homedir;
@@ -79,6 +80,23 @@ async function run() {
79
80
  });
80
81
 
81
82
  console.log('mode.test.js: all resolveInstallMode tests passed');
83
+
84
+ // parseChecksums: must strip the GNU tar binary-mode marker ('*') so both
85
+ // "HASH name" and "HASH *name" formats resolve, matching the Python side.
86
+ const plain = parseChecksums('abc123 iterate-skill.tar.gz\n');
87
+ assert.strictEqual(plain.get('iterate-skill.tar.gz'), 'abc123', 'plain format should parse');
88
+
89
+ const starred = parseChecksums('def456 *iterate-skill.tar.gz\n');
90
+ assert.strictEqual(starred.get('iterate-skill.tar.gz'), 'def456', 'starred format should strip the * marker');
91
+
92
+ const crlf = parseChecksums('abc123 iterate-skill.tar.gz\r\n');
93
+ assert.strictEqual(crlf.get('iterate-skill.tar.gz'), 'abc123', 'CRLF line endings should parse');
94
+
95
+ const multi = parseChecksums('aa a.txt\nbb b.txt\n');
96
+ assert.strictEqual(multi.get('a.txt'), 'aa');
97
+ assert.strictEqual(multi.get('b.txt'), 'bb');
98
+
99
+ console.log('mode.test.js: all parseChecksums tests passed');
82
100
  }
83
101
 
84
102
  run().catch((err) => {