iterate-skill-installer 3.2.2 → 3.3.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.
Files changed (2) hide show
  1. package/lib/installer.js +38 -9
  2. package/package.json +1 -1
package/lib/installer.js CHANGED
@@ -193,14 +193,43 @@ function isGithubApiUrl(url) {
193
193
  return hostname === 'api.github.com' || hostname.endsWith('.api.github.com');
194
194
  }
195
195
 
196
+ /**
197
+ * Normalize a caller-supplied GitHub token.
198
+ *
199
+ * Tokens frequently arrive with surrounding whitespace (a trailing newline
200
+ * from `export GITHUB_TOKEN=$(cat secret)` or a shell alias that echoes the
201
+ * value); an untrimmed token makes every authenticated call fail with 401
202
+ * while silently burning the user's GitHub API rate limit. An empty result is
203
+ * treated as "no token".
204
+ */
205
+ function normalizeToken(raw) {
206
+ if (raw == null) return null;
207
+ const trimmed = String(raw).trim();
208
+ return trimmed === '' ? null : trimmed;
209
+ }
210
+
211
+ /**
212
+ * Compose the curl flags that attach the caller's PAT — and only those. The
213
+ * token is never attached to a non-API host; and marking it up differently
214
+ * from install.py would let the two installers disagree about credentials.
215
+ *
216
+ * Critically, a custom `-H "Authorization: Bearer ..."` header is echoed by
217
+ * curl onto EVERY host in a `-L` redirect chain (unlike `--user`, curl has no
218
+ * notion of stripping auth on a host change for raw headers). When the token
219
+ * is attached we therefore forbid redirects outright: a redirect then fails
220
+ * loudly instead of leaking the PAT to a foreign host.
221
+ */
222
+ function buildAuthFlags(url, token) {
223
+ if (!token) return [];
224
+ if (!isGithubApiUrl(url)) return [];
225
+ return ['-H', `Authorization: Bearer ${token}`, '--max-redirs', '0'];
226
+ }
227
+
196
228
  async function fetchJson(url, token) {
197
229
  // Prefer curl over Node.js fetch because curl uses the system CA store
198
230
  // and avoids Node-specific certificate issues in some environments.
199
231
  const failFlag = (await supportsCurlFailWithBody()) ? '--fail-with-body' : '--fail';
200
- const args = ['-sSL', failFlag, '--max-time', CURL_MAX_TIME_SECONDS, '--max-filesize', String(JSON_MAX_BYTES), '-H', 'Accept: application/vnd.github+json', '-H', 'X-GitHub-Api-Version: 2022-11-28', '-H', 'User-Agent: iterate-skill-installer'];
201
- if (token && isGithubApiUrl(url)) {
202
- args.push('-H', `Authorization: Bearer ${token}`);
203
- }
232
+ const args = ['-sSL', failFlag, '--max-time', CURL_MAX_TIME_SECONDS, '--max-filesize', String(JSON_MAX_BYTES), '-H', 'Accept: application/vnd.github+json', '-H', 'X-GitHub-Api-Version: 2022-11-28', '-H', 'User-Agent: iterate-skill-installer', ...buildAuthFlags(url, token)];
204
233
  args.push(url);
205
234
  const stdout = await runCommand('curl', args);
206
235
  try {
@@ -224,11 +253,8 @@ async function fetchJson(url, token) {
224
253
  */
225
254
  async function downloadFile(url, destPath, token, { progress = false, maxBytes = TARBALL_MAX_BYTES } = {}) {
226
255
  const failFlag = (await supportsCurlFailWithBody()) ? '--fail-with-body' : '--fail';
227
- const args = ['-sSL', failFlag, '--max-time', CURL_MAX_TIME_SECONDS, '--max-filesize', String(maxBytes), '-o', destPath, '-H', 'User-Agent: iterate-skill-installer'];
256
+ const args = ['-sSL', failFlag, '--max-time', CURL_MAX_TIME_SECONDS, '--max-filesize', String(maxBytes), '-o', destPath, '-H', 'User-Agent: iterate-skill-installer', ...buildAuthFlags(url, token)];
228
257
  if (progress) args.push('--progress-bar');
229
- if (token && isGithubApiUrl(url)) {
230
- args.push('-H', `Authorization: Bearer ${token}`);
231
- }
232
258
  args.push(url);
233
259
  if (progress) {
234
260
  // Let curl write its progress bar directly to the terminal.
@@ -592,6 +618,7 @@ function parseArgs(argv) {
592
618
  force: false,
593
619
  noCli: false,
594
620
  token: process.env.GITHUB_TOKEN || null,
621
+ token: normalizeToken(process.env.GITHUB_TOKEN),
595
622
  // Non-install action requested via -h/--help/-v/--version. bin/cli.js
596
623
  // inspects this before running main() and exits 0 without installing.
597
624
  mode: null,
@@ -660,7 +687,7 @@ function parseArgs(argv) {
660
687
  console.error('Error: --token requires a value (a flag was found instead)');
661
688
  process.exit(1);
662
689
  }
663
- options.token = next;
690
+ options.token = normalizeToken(next);
664
691
  i++;
665
692
  break;
666
693
  default:
@@ -860,5 +887,7 @@ module.exports = {
860
887
  extractTarball,
861
888
  rejectLinkEntries,
862
889
  isGithubApiUrl,
890
+ normalizeToken,
891
+ buildAuthFlags,
863
892
  supportsCurlFailWithBody,
864
893
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-skill-installer",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "description": "One-command installer for iterate-skill across AI coding assistants",
5
5
  "main": "lib/installer.js",
6
6
  "files": [