subto 8.0.3 → 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.
- package/dist/package/index.js +7 -3
- package/index.js +79 -25
- package/package.json +1 -1
package/dist/package/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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.
|
|
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
|
|
@@ -138,9 +138,9 @@ async function storeOpenRouterKeyInteractive(keyArg, modelArg) {
|
|
|
138
138
|
// Best-effort validation
|
|
139
139
|
let validated = false;
|
|
140
140
|
const fetchFn = global.fetch;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
if (typeof fetchFn === 'function') {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetchWithKey('https://openrouter.ai/api/v1/models', { headers: { } }, key);
|
|
144
144
|
if (res && res.ok) {
|
|
145
145
|
const jd = await res.json().catch(()=>null);
|
|
146
146
|
const modelsList = jd && (jd.models || jd.data || jd) ;
|
|
@@ -199,30 +199,84 @@ async function promptHidden(prompt) {
|
|
|
199
199
|
function validateUrl(input) {
|
|
200
200
|
try { const u = new URL(input); if (u.protocol !== 'http:' && u.protocol !== 'https:') return false; return true; } catch (err) { return false; }
|
|
201
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
|
+
}
|
|
202
217
|
|
|
203
|
-
async function postScan(url, apiKey) {
|
|
218
|
+
async function postScan(url, apiKey, debug=false) {
|
|
204
219
|
// Use normalized API base
|
|
205
220
|
const endpoint = new URL('scan', SUBTO_API_BASE_SLASH).toString();
|
|
206
221
|
const body = { url, source: 'cli', client: CLIENT_META };
|
|
207
|
-
|
|
208
|
-
|
|
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+');
|
|
209
224
|
// Validate header-safety to avoid undici/node fetch ByteString conversion errors
|
|
210
225
|
if (!isByteStringSafe(String(apiKey || ''))) {
|
|
211
226
|
throw new Error('API key contains unsupported characters (non-Latin-1). Re-run `subto login` and paste a plain ASCII API key.');
|
|
212
227
|
}
|
|
213
|
-
|
|
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) });
|
|
214
235
|
const text = await res.text();
|
|
215
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
|
+
}
|
|
216
246
|
return { status: res.status, headers: res.headers, body: data };
|
|
217
247
|
}
|
|
218
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
|
+
|
|
219
273
|
function extractScanIdFromHtml(html) {
|
|
220
274
|
if (!html || typeof html !== 'string') return null;
|
|
221
275
|
// common patterns: /scan/ID or scanId="..." or data-scan-id="..."
|
|
222
276
|
const m1 = html.match(/\/scan\/(?:id\/)?([a-zA-Z0-9_-]{8,})/i);
|
|
223
277
|
if (m1 && m1[1]) return m1[1];
|
|
224
278
|
const m2 = html.match(/scanId["'\s:=]+([a-zA-Z0-9_-]{8,})/i);
|
|
225
|
-
|
|
279
|
+
program.command('scan <url>')
|
|
226
280
|
const m3 = html.match(/data-scan-id["'\s=:\>]+([a-zA-Z0-9_-]{8,})/i);
|
|
227
281
|
if (m3 && m3[1]) return m3[1];
|
|
228
282
|
return null;
|
|
@@ -375,9 +429,7 @@ async function callOpenAI(prompt){
|
|
|
375
429
|
} catch (e) { /* ignore config read errors */ }
|
|
376
430
|
const fetchFn = global.fetch;
|
|
377
431
|
if(typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
378
|
-
|
|
379
|
-
// If OpenAI key present, call OpenAI API; otherwise, if OpenRouter key present, call OpenRouter endpoint.
|
|
380
|
-
if (openaiKey) {
|
|
432
|
+
|
|
381
433
|
const model = process.env.AI_MODEL || process.env.OPENAI_MODEL || 'gpt-4o-mini';
|
|
382
434
|
const body = {
|
|
383
435
|
model,
|
|
@@ -519,8 +571,9 @@ async function startChatREPL(scanData){
|
|
|
519
571
|
|
|
520
572
|
async function run(argv) {
|
|
521
573
|
const program = new Command();
|
|
522
|
-
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '8.0.
|
|
574
|
+
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '8.0.4');
|
|
523
575
|
program.option('-v, --verbose', 'Show verbose HTTP logs');
|
|
576
|
+
program.option('--debug', 'Show debug HTTP headers and responses');
|
|
524
577
|
program.option('--chat', 'Start local AI assistant (no command required)');
|
|
525
578
|
program.option('--no-auto-skip', 'Disable automatic skipping of external APIs when scans appear stuck');
|
|
526
579
|
program.option('--skip-prompt-ms <n>', 'Milliseconds before prompting to skip external APIs (default 15000)');
|
|
@@ -555,7 +608,8 @@ async function run(argv) {
|
|
|
555
608
|
if (!validateUrl(url)) { console.error(chalk.red('Invalid URL. Provide a full URL including http:// or https://')); process.exit(1); }
|
|
556
609
|
const cfg = await readConfig(); if (!cfg || !cfg.apiKey) { console.error(chalk.red('Missing API key. Run:'), chalk.cyan('subto login')); process.exit(1); }
|
|
557
610
|
try {
|
|
558
|
-
const
|
|
611
|
+
const DEBUG = (program && typeof program.opts === 'function') ? Boolean(program.opts().debug) : false;
|
|
612
|
+
const resp = await postScan(url, cfg.apiKey, DEBUG);
|
|
559
613
|
if (resp.status === 429) {
|
|
560
614
|
let retrySeconds = null;
|
|
561
615
|
if (resp.body && typeof resp.body === 'object' && resp.body.retry_after) retrySeconds = resp.body.retry_after;
|
|
@@ -585,7 +639,7 @@ async function run(argv) {
|
|
|
585
639
|
if (attemptId && typeof fetchFn === 'function') {
|
|
586
640
|
try {
|
|
587
641
|
const statusUrl = new URL(`scan/${attemptId}`, SUBTO_API_BASE_SLASH).toString();
|
|
588
|
-
const r2 = await
|
|
642
|
+
const r2 = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
589
643
|
if (r2 && r2.ok) {
|
|
590
644
|
try { resp.body = await r2.json(); } catch (e) { /* leave as-is */ }
|
|
591
645
|
} else {
|
|
@@ -837,7 +891,7 @@ async function run(argv) {
|
|
|
837
891
|
startSpinner();
|
|
838
892
|
while (true) {
|
|
839
893
|
const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
840
|
-
const r = await
|
|
894
|
+
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
841
895
|
|
|
842
896
|
if (r.status === 429) {
|
|
843
897
|
let retrySeconds = null;
|
|
@@ -885,7 +939,7 @@ async function run(argv) {
|
|
|
885
939
|
console.log(chalk.yellow('\nNo progress detected on external APIs — requesting server to skip external API calls (auto).'));
|
|
886
940
|
const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
|
|
887
941
|
markSkipRequested(lastServerPercent);
|
|
888
|
-
await
|
|
942
|
+
await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
|
|
889
943
|
console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
|
|
890
944
|
} else {
|
|
891
945
|
console.log(chalk.yellow('\nNo progress detected on external APIs. Automatic skip is disabled.'));
|
|
@@ -935,7 +989,7 @@ async function run(argv) {
|
|
|
935
989
|
console.log(chalk.yellow('\nSkip requested by user.'));
|
|
936
990
|
const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
|
|
937
991
|
markSkipRequested(serverPct);
|
|
938
|
-
await
|
|
992
|
+
await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
|
|
939
993
|
console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
|
|
940
994
|
} catch (e) {
|
|
941
995
|
console.error(chalk.red('Failed to request skip:'), e && e.message ? e.message : e);
|
|
@@ -975,7 +1029,7 @@ async function run(argv) {
|
|
|
975
1029
|
console.log(chalk.yellow('\nAuto-skip timeout reached — requesting server to skip external API calls.'));
|
|
976
1030
|
const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
|
|
977
1031
|
markSkipRequested(lastServerPercent);
|
|
978
|
-
await
|
|
1032
|
+
await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
|
|
979
1033
|
console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
|
|
980
1034
|
} else {
|
|
981
1035
|
console.log(chalk.yellow('\nAuto-skip timeout reached but automatic skip is disabled. Continuing to wait.'));
|
|
@@ -1017,7 +1071,7 @@ async function run(argv) {
|
|
|
1017
1071
|
console.log(chalk.yellow('\nScan stuck at 50% for too long — auto-requesting skip of external APIs.'));
|
|
1018
1072
|
const skipUrl = new URL(`scan/${scanId}/skip-apis`, SUBTO_API_BASE_SLASH).toString();
|
|
1019
1073
|
markSkipRequested(lastServerPercent);
|
|
1020
|
-
await
|
|
1074
|
+
await fetchWithKey(skipUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }, cfg.apiKey, DEBUG);
|
|
1021
1075
|
console.log(chalk.yellow('Requested server to skip external API calls for this scan.'));
|
|
1022
1076
|
} else {
|
|
1023
1077
|
console.log(chalk.yellow('\nScan stuck at 50% for too long but automatic skip is disabled.'));
|
|
@@ -1042,7 +1096,7 @@ async function run(argv) {
|
|
|
1042
1096
|
// give the server a moment to populate results
|
|
1043
1097
|
await sleep(2000);
|
|
1044
1098
|
// continue to refresh once more
|
|
1045
|
-
try { const r2 = await
|
|
1099
|
+
try { const r2 = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG); data = await r2.json(); } catch (e) {}
|
|
1046
1100
|
// final render
|
|
1047
1101
|
renderLine(data);
|
|
1048
1102
|
}
|
|
@@ -1097,7 +1151,7 @@ async function run(argv) {
|
|
|
1097
1151
|
const endpoint = new URL('/api/v1/upload', base).toString();
|
|
1098
1152
|
const fetchFn = global.fetch; if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
1099
1153
|
const body = { files: samples, meta: { collected: collected.length, totalBytes } };
|
|
1100
|
-
const r = await
|
|
1154
|
+
const r = await fetchWithKey(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }, cfg.apiKey, DEBUG);
|
|
1101
1155
|
if (!r.ok) {
|
|
1102
1156
|
const txt = await r.text().catch(()=>null);
|
|
1103
1157
|
console.error(chalk.red('Upload failed:'), r.status, txt || r.statusText);
|
|
@@ -1115,7 +1169,7 @@ async function run(argv) {
|
|
|
1115
1169
|
const statusUrl = new URL(`scan/${j.scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
1116
1170
|
const sleep = ms => new Promise(r=>setTimeout(r,ms));
|
|
1117
1171
|
while (true) {
|
|
1118
|
-
const s = await
|
|
1172
|
+
const s = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
1119
1173
|
if (!s.ok) { console.error(chalk.red('Failed to fetch scan status:', s.status)); break; }
|
|
1120
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; }
|
|
1121
1175
|
console.log(chalk.dim('Scan status:'), data.status || 'queued', '— polling again in 4s'); await sleep(4000);
|
|
@@ -1147,7 +1201,7 @@ async function run(argv) {
|
|
|
1147
1201
|
const fetchFn = global.fetch;
|
|
1148
1202
|
if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
1149
1203
|
const statusUrl = new URL(`scan/${scanId}`, SUBTO_API_BASE_SLASH).toString();
|
|
1150
|
-
const r = await
|
|
1204
|
+
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
1151
1205
|
if (!r.ok) { throw new Error(`Failed to fetch scan ${scanId}: ${r.status}`); }
|
|
1152
1206
|
scanData = await r.json();
|
|
1153
1207
|
}
|
|
@@ -1364,7 +1418,7 @@ async function run(argv) {
|
|
|
1364
1418
|
const base = SUBTO_HOST_BASE; const fetchFn = global.fetch;
|
|
1365
1419
|
if (typeof fetchFn !== 'function') throw new Error('Global fetch() is not available in this Node runtime. Use Node 18+');
|
|
1366
1420
|
const statusUrl = new URL(`scan/${answer}`, SUBTO_API_BASE_SLASH).toString();
|
|
1367
|
-
|
|
1421
|
+
const r = await fetchWithKey(statusUrl, { headers: { 'Accept': 'application/json' } }, cfg.apiKey, DEBUG);
|
|
1368
1422
|
if (!r.ok) throw new Error(`Failed to fetch scan ${answer}: ${r.status}`);
|
|
1369
1423
|
scanData = await r.json();
|
|
1370
1424
|
}
|