subto 8.0.6 → 9.0.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.
@@ -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.3' };
14
+ const CLIENT_META = { name: 'subto-cli', version: '9.0.0' };
15
15
 
16
16
  function configFilePath() { return CONFIG_PATH; }
17
17
 
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.6' };
14
+ const CLIENT_META = { name: 'subto-cli', version: '9.0.0' };
15
15
  const cp = require('child_process');
16
16
 
17
17
  // Normalize SUBTO API base so callers can set either
@@ -217,7 +217,10 @@ async function _maskHeaders(obj){
217
217
 
218
218
  async function postScan(url, apiKey, debug=false) {
219
219
  // Use normalized API base
220
- const endpoint = new URL('scan', SUBTO_API_BASE_SLASH).toString();
220
+ const endpointObj = new URL('scan', SUBTO_API_BASE_SLASH);
221
+ // API key is sent via headers only — never in query strings to prevent
222
+ // leakage via server logs, proxy logs, and Referer headers.
223
+ const endpoint = endpointObj.toString();
221
224
  const body = { url, source: 'cli', client: CLIENT_META };
222
225
  const fetchFn = global.fetch;
223
226
  if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
@@ -398,12 +401,12 @@ async function printFullReport(data) {
398
401
  function copyToClipboard(text){
399
402
  try{
400
403
  if (process.platform === 'darwin'){
401
- const p = cp.spawnSync('pbcopy');
402
- p.stdin && p.stdin.end(String(text));
404
+ cp.spawnSync('pbcopy', { input: String(text) });
403
405
  return true;
404
406
  }
405
407
  if (process.platform === 'win32'){
406
- const p = cp.spawnSync('clip'); p.stdin && p.stdin.end(String(text)); return true;
408
+ cp.spawnSync('clip', { input: String(text) });
409
+ return true;
407
410
  }
408
411
  }catch(e){}
409
412
  return false;
@@ -429,6 +432,8 @@ async function callOpenAI(prompt){
429
432
  const fetchFn = global.fetch;
430
433
  if(typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
431
434
 
435
+ // Try OpenAI first if key is available
436
+ if (openaiKey) {
432
437
  const model = process.env.AI_MODEL || process.env.OPENAI_MODEL || 'gpt-4o-mini';
433
438
  const body = {
434
439
  model,
@@ -444,9 +449,11 @@ async function callOpenAI(prompt){
444
449
  // Avoid including response bodies in errors to prevent accidental leakage of sensitive data
445
450
  throw new Error(`OpenAI API error ${res.status}: ${res.statusText || 'request failed'}`);
446
451
  }
447
- const j = await res.json();
452
+ let j;
453
+ try { j = await res.json(); } catch (_) { throw new Error('OpenAI returned non-JSON response'); }
448
454
  const msg = j && j.choices && j.choices[0] && (j.choices[0].message && j.choices[0].message.content || j.choices[0].text);
449
455
  return String(msg || '').trim();
456
+ }
450
457
 
451
458
  if (openrouterKey) {
452
459
  // Use OpenRouter chat completions endpoint
@@ -465,7 +472,8 @@ async function callOpenAI(prompt){
465
472
  // Avoid including response bodies in errors to prevent accidental leakage of sensitive data
466
473
  throw new Error(`OpenRouter API error ${res.status}: ${res.statusText || 'request failed'}`);
467
474
  }
468
- const j = await res.json();
475
+ let j;
476
+ try { j = await res.json(); } catch (_) { throw new Error('OpenRouter returned non-JSON response'); }
469
477
  // OpenRouter responses mimic OpenAI shape (choices[0].message.content)
470
478
  const msg = j && j.choices && j.choices[0] && (j.choices[0].message && j.choices[0].message.content || j.choices[0].text);
471
479
  return String(msg || '').trim();
@@ -569,7 +577,7 @@ async function startChatREPL(scanData){
569
577
 
570
578
  async function run(argv) {
571
579
  const program = new Command();
572
- program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '8.0.6');
580
+ program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '9.0.0');
573
581
  program.option('-v, --verbose', 'Show verbose HTTP logs');
574
582
  program.option('--debug', 'Show debug HTTP headers and responses');
575
583
  program.option('--chat', 'Start local AI assistant (no command required)');
@@ -887,7 +895,14 @@ async function run(argv) {
887
895
  let recheckedAfterTerminal = false;
888
896
  let data = null;
889
897
  startSpinner();
898
+ const MAX_POLL_ITERATIONS = 900; // ~30 min at 2s interval
899
+ let pollIteration = 0;
890
900
  while (true) {
901
+ if (++pollIteration > MAX_POLL_ITERATIONS) {
902
+ stopSpinner();
903
+ console.error(chalk.red('Polling timed out after ' + MAX_POLL_ITERATIONS + ' iterations. The scan may still be running on the server.'));
904
+ process.exit(1);
905
+ }
891
906
  const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
892
907
  const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
893
908
 
@@ -1149,7 +1164,7 @@ async function run(argv) {
1149
1164
  const endpoint = new URL('/api/v1/upload', base).toString();
1150
1165
  const fetchFn = global.fetch; if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
1151
1166
  const body = { files: samples, meta: { collected: collected.length, totalBytes } };
1152
- const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey, DEBUG);
1167
+ const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey, Boolean(program.opts && program.opts().debug));
1153
1168
  if (!r.ok) {
1154
1169
  const txt = await r.text().catch(()=>null);
1155
1170
  console.error(chalk.red('Upload failed:'), r.status, txt || r.statusText);
@@ -1161,13 +1176,14 @@ async function run(argv) {
1161
1176
  } catch (e) { /* ignore */ }
1162
1177
  process.exit(1);
1163
1178
  }
1164
- const j = await r.json(); console.log(chalk.green('Upload queued:'), j.uploadId, 'scanId:', j.scanId, 'expiresAt:', new Date(j.expiresAt).toString());
1179
+ let j; try { j = await r.json(); } catch (_) { console.error(chalk.red('Server returned non-JSON response')); process.exit(1); }
1180
+ console.log(chalk.green('Upload queued:'), j.uploadId, 'scanId:', j.scanId, 'expiresAt:', j.expiresAt ? new Date(j.expiresAt).toString() : 'N/A');
1165
1181
  if (opts.wait) {
1166
1182
  // Poll the scan resource until completed (reuse existing polling behavior)
1167
1183
  const statusUrl = new URL(`scan/${j.scanId}`, SUBTO_API_BASE_SLASH).toString();
1168
1184
  const sleep = ms => new Promise(r=>setTimeout(r,ms));
1169
1185
  while (true) {
1170
- const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
1186
+ const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, Boolean(program.opts && program.opts().debug));
1171
1187
  if (!s.ok) { console.error(chalk.red('Failed to fetch scan status:', s.status)); break; }
1172
1188
  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; }
1173
1189
  console.log(chalk.dim('Scan status:'), data.status || 'queued', '— polling again in 4s'); await sleep(4000);
@@ -1201,7 +1217,7 @@ async function run(argv) {
1201
1217
  const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
1202
1218
  const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
1203
1219
  if (!r.ok) { throw new Error(`Failed to fetch scan ${scanId}: ${r.status}`); }
1204
- scanData = await r.json();
1220
+ try { scanData = await r.json(); } catch (_) { throw new Error('Server returned non-JSON response for scan ' + scanId); }
1205
1221
  }
1206
1222
 
1207
1223
  if (!scanData) { console.error(chalk.red('No scan data available.')); return; }
@@ -1378,7 +1394,7 @@ async function run(argv) {
1378
1394
  const endpoint = new URL(`video/${scanId}/debug`, serverBase).toString();
1379
1395
  const res = await fetchFn(endpoint, { headers: { 'Accept': 'application/json' } });
1380
1396
  if (!res.ok) throw new Error(`Server returned ${res.status}`);
1381
- const json = await res.json();
1397
+ let json; try { json = await res.json(); } catch (_) { throw new Error('Server returned non-JSON response'); }
1382
1398
  console.log(chalk.bold('\nVideo diagnostic for:'), scanId);
1383
1399
  if (json && json.scan) {
1384
1400
  console.log(' URL:', json.scan.url || '(unknown)');
@@ -1416,9 +1432,9 @@ async function run(argv) {
1416
1432
  const base = SUBTO_HOST_BASE; const fetchFn = global.fetch;
1417
1433
  if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
1418
1434
  const statusUrl = new URL(`scan/${answer}`, SUBTO_API_BASE_SLASH).toString();
1419
- const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
1435
+ const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, false);
1420
1436
  if (!r.ok) throw new Error(`Failed to fetch scan ${answer}: ${r.status}`);
1421
- scanData = await r.json();
1437
+ try { scanData = await r.json(); } catch (_) { throw new Error('Server returned non-JSON response'); }
1422
1438
  }
1423
1439
  await startChatREPL(scanData);
1424
1440
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subto",
3
- "version": "8.0.6",
3
+ "version": "9.0.0",
4
4
  "description": "Subto CLI — thin wrapper around the Subto.One API",
5
5
  "bin": {
6
6
  "subto": "bin/subto.js"