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.
- package/dist/package/README.md +3 -3
- package/dist/package/index.js +8 -4
- package/index.js +107 -57
- package/package.json +1 -1
package/dist/package/README.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
107
|
+
npm install -g ./dist/subto-8.0.3.tgz
|
|
108
108
|
```
|
package/dist/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';
|
|
14
|
-
const CLIENT_META = { name: 'subto-cli', version: '8.0.
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|