subto 8.0.2 → 8.0.4

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.
@@ -40,7 +40,7 @@ Commands
40
40
  {
41
41
  "url": "https://example.com",
42
42
  "source": "cli",
43
- "client": { "name": "subto-cli", "version": "8.0.2" }
43
+ "client": { "name": "subto-cli", "version": "8.0.3" }
44
44
  }
45
45
  ```
46
46
 
@@ -98,11 +98,11 @@ Download
98
98
  After publishing or packing, a distributable tarball will be available under `./dist/` e.g.:
99
99
 
100
100
  ```
101
- ./dist/subto-8.0.2.tgz
101
+ ./dist/subto-8.0.3.tgz
102
102
  ```
103
103
 
104
104
  You can download that file directly and install locally with:
105
105
 
106
106
  ```bash
107
- npm install -g ./dist/subto-8.0.2.tgz
107
+ npm install -g ./dist/subto-8.0.3.tgz
108
108
  ```
@@ -11,7 +11,7 @@ const chalk = (_chalk && _chalk.default) ? _chalk.default : _chalk;
11
11
  const CONFIG_DIR = path.join(os.homedir(), '.subto');
12
12
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
13
13
  const DEFAULT_API_BASE = 'https://subto.one';
14
- const CLIENT_META = { name: 'subto-cli', version: '8.0.2' };
14
+ const CLIENT_META = { name: 'subto-cli', version: '8.0.3' };
15
15
 
16
16
  function configFilePath() { return CONFIG_PATH; }
17
17
 
@@ -101,7 +101,10 @@ async function postScan(url, apiKey) {
101
101
  }
102
102
  const fetchFn = global.fetch || (function(){ try{ const u = require('undici'); if (u && typeof u.fetch === 'function') { global.fetch = u.fetch; return global.fetch; } } catch(e){} return null; })();
103
103
  if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Install Node 18+ or run `npm install undici`.');
104
- const res = await fetchFn(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'User-Agent': `${CLIENT_META.name}/${CLIENT_META.version}` }, body: JSON.stringify(body) });
104
+
105
+ // Send both Authorization and x-api-key headers to maximize compatibility across deployments
106
+ const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'x-api-key': String(apiKey), 'User-Agent': `${CLIENT_META.name}/${CLIENT_META.version}` };
107
+ const res = await fetchFn(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
105
108
  const text = await res.text();
106
109
  let data = null; try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
107
110
  return { status: res.status, headers: res.headers, body: data };
@@ -212,8 +215,9 @@ async function run(argv) {
212
215
 
213
216
  let recheckedAfterTerminal = false;
214
217
  while (true) {
215
- const statusUrl = new URL(`/api/v1/scan/${scanId}`, base).toString();
216
- const r = await fetchFn(statusUrl, { headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Accept': 'application/json' } });
218
+ const statusUrl = new URL(`/api/v1/scan/${scanId}`, base).toString();
219
+ // Include both Authorization and x-api-key headers so the server accepts either format
220
+ const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
217
221
 
218
222
  if (r.status === 429) {
219
223
  let retrySeconds = null;
package/index.js CHANGED
@@ -11,7 +11,7 @@ const chalk = (_chalk && _chalk.default) ? _chalk.default : _chalk;
11
11
  const CONFIG_DIR = path.join(os.homedir(), '.subto');
12
12
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
13
13
  const DEFAULT_API_BASE = 'https://subto.one/api/v1';
14
- const CLIENT_META = { name: 'subto-cli', version: '8.0.2' };
14
+ const CLIENT_META = { name: 'subto-cli', version: '8.0.4' };
15
15
  const cp = require('child_process');
16
16
 
17
17
  // Normalize SUBTO API base so callers can set either
@@ -90,6 +90,33 @@ async function writeConfig(obj) {
90
90
  await fs.rename(tmp, configFilePath());
91
91
  }
92
92
 
93
+ // Helpers: normalization, header-safety check, and simple yes/no prompt
94
+ function isByteStringSafe(s){
95
+ if (typeof s !== 'string') return false;
96
+ for (let i = 0; i < s.length; i++) { if (s.charCodeAt(i) > 255) return false; }
97
+ return true;
98
+ }
99
+
100
+ function normalizeApiKey(s){
101
+ if (typeof s !== 'string') return s;
102
+ let out = s.normalize('NFKC');
103
+ out = out.replace(/[\u200B\uFEFF]/g, '');
104
+ out = out.replace(/\u00A0/g, ' ');
105
+ out = out.replace(/[\u2018\u2019]/g, "'");
106
+ out = out.replace(/[\u201C\u201D]/g, '"');
107
+ out = out.replace(/\u2026/g, '...');
108
+ out = out.replace(/[\u22C5\u00B7]/g, '.');
109
+ out = out.replace(/\u2022/g, '*');
110
+ return out.trim();
111
+ }
112
+
113
+ function askYesNo(promptText){
114
+ return new Promise(res => {
115
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
116
+ rl.question(promptText + ' ', ans => { rl.close(); res(String(ans||'').trim().toLowerCase().startsWith('y')); });
117
+ });
118
+ }
119
+
93
120
  // Interactive helper to store OpenRouter key + model into ~/.subto/config.json
94
121
  async function storeOpenRouterKeyInteractive(keyArg, modelArg) {
95
122
  try {
@@ -111,9 +138,9 @@ async function storeOpenRouterKeyInteractive(keyArg, modelArg) {
111
138
  // Best-effort validation
112
139
  let validated = false;
113
140
  const fetchFn = global.fetch;
114
- if (typeof fetchFn === 'function') {
115
- try {
116
- const res = await fetchFn('https://openrouter.ai/api/v1/models', { headers: { 'Authorization': `Bearer ${key}` } });
141
+ if (typeof fetchFn === 'function') {
142
+ try {
143
+ const res = await fetchWithKey('https://openrouter.ai/api/v1/models', { headers: { } }, key);
117
144
  if (res && res.ok) {
118
145
  const jd = await res.json().catch(()=>null);
119
146
  const modelsList = jd && (jd.models || jd.data || jd) ;
@@ -138,38 +165,7 @@ async function storeOpenRouterKeyInteractive(keyArg, modelArg) {
138
165
  }
139
166
 
140
167
  async function promptHidden(prompt) {
141
- function isByteStringSafe(s){
142
- if (typeof s !== 'string') return false;
143
- for (let i = 0; i < s.length; i++) {
144
- if (s.charCodeAt(i) > 255) return false;
145
- }
146
- return true;
147
- }
148
-
149
- function normalizeApiKey(s){
150
- if (typeof s !== 'string') return s;
151
- // Unicode Normalization + common visual substitutions
152
- let out = s.normalize('NFKC');
153
- // Remove invisible/zero-width marks
154
- out = out.replace(/[\u200B\uFEFF]/g, '');
155
- // Replace non-breaking spaces with regular space
156
- out = out.replace(/\u00A0/g, ' ');
157
- // Smart quotes -> ascii
158
- out = out.replace(/[\u2018\u2019]/g, "'");
159
- out = out.replace(/[\u201C\u201D]/g, '"');
160
- // Ellipsis and middle-dots -> simple replacements
161
- out = out.replace(/\u2026/g, '...');
162
- out = out.replace(/[\u22C5\u00B7]/g, '.');
163
- out = out.replace(/\u2022/g, '*');
164
- return out.trim();
165
- }
166
-
167
- function askYesNo(promptText){
168
- return new Promise(res => {
169
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
170
- rl.question(promptText + ' ', ans => { rl.close(); res(String(ans||'').trim().toLowerCase().startsWith('y')); });
171
- });
172
- }
168
+
173
169
  if (!process.stdin.isTTY) throw new Error('Interactive prompt required');
174
170
  return new Promise((resolve, reject) => {
175
171
  const stdin = process.stdin;
@@ -203,30 +199,84 @@ async function promptHidden(prompt) {
203
199
  function validateUrl(input) {
204
200
  try { const u = new URL(input); if (u.protocol !== 'http:' && u.protocol !== 'https:') return false; return true; } catch (err) { return false; }
205
201
  }
202
+ async function _maskHeaders(obj){
203
+ const out = {};
204
+ try{
205
+ for (const k of Object.keys(obj||{})){
206
+ let v = obj[k];
207
+ if (v === undefined || v === null) v = '';
208
+ let sv = String(v);
209
+ if (/authorization/i.test(k) || /api-?key/i.test(k)){
210
+ if (sv.length > 12) sv = sv.slice(0,6) + '...' + sv.slice(-4); else sv = '***';
211
+ }
212
+ out[k] = sv;
213
+ }
214
+ }catch(e){}
215
+ return out;
216
+ }
206
217
 
207
- async function postScan(url, apiKey) {
218
+ async function postScan(url, apiKey, debug=false) {
208
219
  // Use normalized API base
209
220
  const endpoint = new URL('scan', SUBTO_API_BASE_SLASH).toString();
210
221
  const body = { url, source: 'cli', client: CLIENT_META };
211
- const fetchFn = global.fetch;
212
- if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
222
+ const fetchFn = global.fetch;
223
+ if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
213
224
  // Validate header-safety to avoid undici/node fetch ByteString conversion errors
214
225
  if (!isByteStringSafe(String(apiKey || ''))) {
215
226
  throw new Error('API key contains unsupported characters (non-Latin-1). Re-run `subto login` and paste a plain ASCII API key.');
216
227
  }
217
- const res = await fetchFn(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'User-Agent': `${CLIENT_META.name}/${CLIENT_META.version}` }, body: JSON.stringify(body) });
228
+
229
+ // Send both Authorization and x-api-key headers to maximize compatibility across deployments
230
+ const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'x-api-key': String(apiKey), 'User-Agent': `${CLIENT_META.name}/${CLIENT_META.version}` };
231
+ if (debug) {
232
+ try { console.error(chalk.dim('Request headers:'), JSON.stringify(await _maskHeaders(headers))); } catch(e){}
233
+ }
234
+ const res = await fetchFn(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
218
235
  const text = await res.text();
219
236
  let data = null; try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
237
+ if (debug) {
238
+ try {
239
+ const hdrs = {};
240
+ if (res && res.headers && typeof res.headers.forEach === 'function') res.headers.forEach((v,k)=>{ hdrs[k]=v; });
241
+ console.error(chalk.dim('Response status:'), res.status);
242
+ console.error(chalk.dim('Response headers:'), JSON.stringify(await _maskHeaders(hdrs)));
243
+ console.error(chalk.dim('Response body:'), String(text).slice(0,4096));
244
+ } catch(e){}
245
+ }
220
246
  return { status: res.status, headers: res.headers, body: data };
221
247
  }
222
248
 
249
+ // Helper to fetch with both Authorization and x-api-key headers and optional debug logging
250
+ async function fetchWithKey(url, opts, apiKey, debug=false){
251
+ const fetchFn = global.fetch;
252
+ if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
253
+ const baseHeaders = (opts && opts.headers) ? Object.assign({}, opts.headers) : {};
254
+ const headers = Object.assign({}, baseHeaders, { 'Authorization': `Bearer ${apiKey}`, 'x-api-key': String(apiKey) });
255
+ if (debug) {
256
+ try { console.error(chalk.dim('Request headers:'), JSON.stringify(await _maskHeaders(headers))); } catch(e){}
257
+ }
258
+ const r = await fetchFn(url, Object.assign({}, opts || {}, { headers }));
259
+ if (debug) {
260
+ try {
261
+ const txt = await r.text();
262
+ const hdrs = {}; if (r && r.headers && typeof r.headers.forEach === 'function') r.headers.forEach((v,k)=>hdrs[k]=v);
263
+ console.error(chalk.dim('Response status:'), r.status);
264
+ console.error(chalk.dim('Response headers:'), JSON.stringify(await _maskHeaders(hdrs)));
265
+ console.error(chalk.dim('Response body:'), String(txt).slice(0,4096));
266
+ // put body back so callers expecting .json() will not fail
267
+ r._cached_text = txt;
268
+ } catch(e){}
269
+ }
270
+ return r;
271
+ }
272
+
223
273
  function extractScanIdFromHtml(html) {
224
274
  if (!html || typeof html !== 'string') return null;
225
275
  // common patterns: /scan/ID or scanId="..." or data-scan-id="..."
226
276
  const m1 = html.match(/\/scan\/(?:id\/)?([a-zA-Z0-9_-]{8,})/i);
227
277
  if (m1 && m1[1]) return m1[1];
228
278
  const m2 = html.match(/scanId["'\s:=]+([a-zA-Z0-9_-]{8,})/i);
229
- if (m2 && m2[1]) return m2[1];
279
+ program.command('scan <url>')
230
280
  const m3 = html.match(/data-scan-id["'\s=:\>]+([a-zA-Z0-9_-]{8,})/i);
231
281
  if (m3 && m3[1]) return m3[1];
232
282
  return null;
@@ -379,9 +429,7 @@ async function callOpenAI(prompt){
379
429
  } catch (e) { /* ignore config read errors */ }
380
430
  const fetchFn = global.fetch;
381
431
  if(typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
382
-
383
- // If OpenAI key present, call OpenAI API; otherwise, if OpenRouter key present, call OpenRouter endpoint.
384
- if (openaiKey) {
432
+
385
433
  const model = process.env.AI_MODEL || process.env.OPENAI_MODEL || 'gpt-4o-mini';
386
434
  const body = {
387
435
  model,
@@ -523,8 +571,9 @@ async function startChatREPL(scanData){
523
571
 
524
572
  async function run(argv) {
525
573
  const program = new Command();
526
- program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '8.0.2');
574
+ program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '8.0.4');
527
575
  program.option('-v, --verbose', 'Show verbose HTTP logs');
576
+ program.option('--debug', 'Show debug HTTP headers and responses');
528
577
  program.option('--chat', 'Start local AI assistant (no command required)');
529
578
  program.option('--no-auto-skip', 'Disable automatic skipping of external APIs when scans appear stuck');
530
579
  program.option('--skip-prompt-ms <n>', 'Milliseconds before prompting to skip external APIs (default 15000)');
@@ -559,7 +608,8 @@ async function run(argv) {
559
608
  if (!validateUrl(url)) { console.error(chalk.red('Invalid URL. Provide a full URL including http:// or https://')); process.exit(1); }
560
609
  const cfg = await readConfig(); if (!cfg || !cfg.apiKey) { console.error(chalk.red('Missing API key. Run:'), chalk.cyan('subto login')); process.exit(1); }
561
610
  try {
562
- const resp = await postScan(url, cfg.apiKey);
611
+ const DEBUG = (program && typeof program.opts === 'function') ? Boolean(program.opts().debug) : false;
612
+ const resp = await postScan(url, cfg.apiKey, DEBUG);
563
613
  if (resp.status === 429) {
564
614
  let retrySeconds = null;
565
615
  if (resp.body && typeof resp.body === 'object' && resp.body.retry_after) retrySeconds = resp.body.retry_after;
@@ -589,7 +639,7 @@ async function run(argv) {
589
639
  if (attemptId && typeof fetchFn === 'function') {
590
640
  try {
591
641
  const statusUrl = new URL(`scan/${attemptId}`, SUBTO_API_BASE_SLASH).toString();
592
- const r2 = await fetchFn(statusUrl, { headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Accept': 'application/json' } });
642
+ const r2 = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
593
643
  if (r2 && r2.ok) {
594
644
  try { resp.body = await r2.json(); } catch (e) { /* leave as-is */ }
595
645
  } else {
@@ -841,7 +891,7 @@ async function run(argv) {
841
891
  startSpinner();
842
892
  while (true) {
843
893
  const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
844
- const r = await fetchFn(statusUrl, { headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Accept': 'application/json' } });
894
+ const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
845
895
 
846
896
  if (r.status === 429) {
847
897
  let retrySeconds = null;
@@ -889,7 +939,7 @@ async function run(argv) {
889
939
  console.log(chalk.yellow('\nNo progress detected on external APIs — requesting server to skip external API calls (auto).'));
890
940
  const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
891
941
  markSkipRequested(lastServerPercent);
892
- await fetchFn(skipUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' }, body: '{}' });
942
+ await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
893
943
  console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
894
944
  } else {
895
945
  console.log(chalk.yellow('\nNo progress detected on external APIs. Automatic skip is disabled.'));
@@ -939,7 +989,7 @@ async function run(argv) {
939
989
  console.log(chalk.yellow('\nSkip requested by user.'));
940
990
  const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
941
991
  markSkipRequested(serverPct);
942
- await fetchFn(skipUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' }, body: '{}' });
992
+ await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
943
993
  console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
944
994
  } catch (e) {
945
995
  console.error(chalk.red('Failed to request skip:'), e && e.message ? e.message : e);
@@ -979,7 +1029,7 @@ async function run(argv) {
979
1029
  console.log(chalk.yellow('\nAuto-skip timeout reached — requesting server to skip external API calls.'));
980
1030
  const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
981
1031
  markSkipRequested(lastServerPercent);
982
- await fetchFn(skipUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' }, body: '{}' });
1032
+ await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
983
1033
  console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
984
1034
  } else {
985
1035
  console.log(chalk.yellow('\nAuto-skip timeout reached but automatic skip is disabled. Continuing to wait.'));
@@ -1021,7 +1071,7 @@ async function run(argv) {
1021
1071
  console.log(chalk.yellow('\nScan stuck at 50% for too long — auto-requesting skip of external APIs.'));
1022
1072
  const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
1023
1073
  markSkipRequested(lastServerPercent);
1024
- await fetchFn(skipUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' }, body: '{}' });
1074
+ await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
1025
1075
  console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
1026
1076
  } else {
1027
1077
  console.log(chalk.yellow('\nScan stuck at 50% for too long but automatic skip is disabled.'));
@@ -1046,7 +1096,7 @@ async function run(argv) {
1046
1096
  // give the server a moment to populate results
1047
1097
  await sleep(2000);
1048
1098
  // continue to refresh once more
1049
- try { const r2 = await fetchFn(statusUrl, { headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Accept': 'application/json' } }); data = await r2.json(); } catch (e) {}
1099
+ try { const r2 = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG); data = await r2.json(); } catch (e) {}
1050
1100
  // final render
1051
1101
  renderLine(data);
1052
1102
  }
@@ -1101,7 +1151,7 @@ async function run(argv) {
1101
1151
  const endpoint = new URL('/api/v1/upload', base).toString();
1102
1152
  const fetchFn = global.fetch; if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
1103
1153
  const body = { files: samples, meta: { collected: collected.length, totalBytes } };
1104
- const r = await fetchFn(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cfg.apiKey}` }, body: JSON.stringify(body) });
1154
+ const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey, DEBUG);
1105
1155
  if (!r.ok) {
1106
1156
  const txt = await r.text().catch(()=>null);
1107
1157
  console.error(chalk.red('Upload failed:'), r.status, txt || r.statusText);
@@ -1119,7 +1169,7 @@ async function run(argv) {
1119
1169
  const statusUrl = new URL(`scan/${j.scanId}`, SUBTO_API_BASE_SLASH).toString();
1120
1170
  const sleep = ms => new Promise(r=>setTimeout(r,ms));
1121
1171
  while (true) {
1122
- const s = await fetchFn(statusUrl, { headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Accept': 'application/json' } });
1172
+ const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
1123
1173
  if (!s.ok) { console.error(chalk.red('Failed to fetch scan status:', s.status)); break; }
1124
1174
  const data = await s.json(); if (data.status && ['completed','done','finished','success'].includes(String(data.status).toLowerCase())) { console.log(chalk.green('Scan complete.')); await printFullReport(data); break; }
1125
1175
  console.log(chalk.dim('Scan status:'), data.status || 'queued', '— polling again in 4s'); await sleep(4000);
@@ -1151,7 +1201,7 @@ async function run(argv) {
1151
1201
  const fetchFn = global.fetch;
1152
1202
  if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
1153
1203
  const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
1154
- const r = await fetchFn(statusUrl, { headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Accept': 'application/json' } });
1204
+ const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
1155
1205
  if (!r.ok) { throw new Error(`Failed to fetch scan ${scanId}: ${r.status}`); }
1156
1206
  scanData = await r.json();
1157
1207
  }
@@ -1368,7 +1418,7 @@ async function run(argv) {
1368
1418
  const base = SUBTO_HOST_BASE; const fetchFn = global.fetch;
1369
1419
  if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
1370
1420
  const statusUrl = new URL(`scan/${answer}`, SUBTO_API_BASE_SLASH).toString();
1371
- const r = await fetchFn(statusUrl, { headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Accept': 'application/json' } });
1421
+ const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
1372
1422
  if (!r.ok) throw new Error(`Failed to fetch scan ${answer}: ${r.status}`);
1373
1423
  scanData = await r.json();
1374
1424
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subto",
3
- "version": "8.0.2",
3
+ "version": "8.0.4",
4
4
  "description": "Subto CLI — thin wrapper around the Subto.One API",
5
5
  "bin": {
6
6
  "subto": "bin/subto.js"