subto 8.0.1 → 8.0.2
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 +40 -3
- package/index.js +43 -5
- 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.2" }
|
|
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.2.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.2.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.2' };
|
|
15
15
|
|
|
16
16
|
function configFilePath() { return CONFIG_PATH; }
|
|
17
17
|
|
|
@@ -61,6 +61,32 @@ async function promptHidden(prompt) {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function isByteStringSafe(s){
|
|
65
|
+
if (typeof s !== 'string') return false;
|
|
66
|
+
for (let i = 0; i < s.length; i++) { if (s.charCodeAt(i) > 255) return false; }
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeApiKey(s){
|
|
71
|
+
if (typeof s !== 'string') return s;
|
|
72
|
+
let out = s.normalize('NFKC');
|
|
73
|
+
out = out.replace(/[\u200B\uFEFF]/g, '');
|
|
74
|
+
out = out.replace(/\u00A0/g, ' ');
|
|
75
|
+
out = out.replace(/[\u2018\u2019]/g, "'");
|
|
76
|
+
out = out.replace(/[\u201C\u201D]/g, '"');
|
|
77
|
+
out = out.replace(/\u2026/g, '...');
|
|
78
|
+
out = out.replace(/[\u22C5\u00B7]/g, '.');
|
|
79
|
+
out = out.replace(/\u2022/g, '*');
|
|
80
|
+
return out.trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function askYesNo(promptText){
|
|
84
|
+
return new Promise(res => {
|
|
85
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
86
|
+
rl.question(promptText + ' ', ans => { rl.close(); res(String(ans||'').trim().toLowerCase().startsWith('y')); });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
function validateUrl(input) {
|
|
65
91
|
try { const u = new URL(input); if (u.protocol !== 'http:' && u.protocol !== 'https:') return false; return true; } catch (err) { return false; }
|
|
66
92
|
}
|
|
@@ -69,6 +95,10 @@ async function postScan(url, apiKey) {
|
|
|
69
95
|
const base = process.env.SUBTO_API_BASE_URL || DEFAULT_API_BASE;
|
|
70
96
|
const endpoint = new URL('/api/v1/scan', base).toString();
|
|
71
97
|
const body = { url, source: 'cli', client: CLIENT_META };
|
|
98
|
+
// Ensure the provided API key is safe for use in HTTP headers
|
|
99
|
+
if (!isByteStringSafe(String(apiKey || ''))) {
|
|
100
|
+
throw new Error('API key contains unsupported characters (non-Latin-1). Re-run `subto login` and paste a plain ASCII API key.');
|
|
101
|
+
}
|
|
72
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; })();
|
|
73
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`.');
|
|
74
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) });
|
|
@@ -95,10 +125,17 @@ async function run(argv) {
|
|
|
95
125
|
|
|
96
126
|
program.command('login').description('Store your API key in ~/.subto/config.json').action(async () => {
|
|
97
127
|
try {
|
|
98
|
-
|
|
128
|
+
let apiKey = await promptHidden('API key: ');
|
|
99
129
|
if (!apiKey || !apiKey.trim()) throw new Error('No API key entered. Aborting.');
|
|
100
130
|
if (apiKey.length < 10) throw new Error('API key looks too short.');
|
|
101
|
-
|
|
131
|
+
const normalized = normalizeApiKey(apiKey);
|
|
132
|
+
if (normalized !== apiKey) {
|
|
133
|
+
console.log(chalk.yellow('Notice: API key contained invisible or fancy characters — it will be sanitized before saving.'));
|
|
134
|
+
const ok = await askYesNo('Save sanitized key? (Y/n)');
|
|
135
|
+
if (!ok) { console.log(chalk.yellow('Login aborted.')); process.exit(1); }
|
|
136
|
+
}
|
|
137
|
+
if (!isByteStringSafe(normalized)) throw new Error('API key contains unsupported characters (non-Latin-1). Provide a plain ASCII key.');
|
|
138
|
+
await writeConfig({ apiKey: normalized });
|
|
102
139
|
console.log(chalk.green('API key saved to'), chalk.cyan(configFilePath()));
|
|
103
140
|
} catch (err) { if (err && err.message === 'Aborted') { console.log('\n' + chalk.yellow('Login aborted.')); process.exit(1); } throw err; }
|
|
104
141
|
});
|
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.2' };
|
|
15
15
|
const cp = require('child_process');
|
|
16
16
|
|
|
17
17
|
// Normalize SUBTO API base so callers can set either
|
|
@@ -98,6 +98,12 @@ async function storeOpenRouterKeyInteractive(keyArg, modelArg) {
|
|
|
98
98
|
key = await promptHidden('OpenRouter API key: ');
|
|
99
99
|
if (!key || !key.trim()) { console.log('Aborted.'); return; }
|
|
100
100
|
}
|
|
101
|
+
const normalized = normalizeApiKey(key);
|
|
102
|
+
if (normalized !== key) {
|
|
103
|
+
console.log(chalk.yellow('Notice: OpenRouter key contained invisible or fancy characters — it will be sanitized before saving.'));
|
|
104
|
+
const ok = await askYesNo('Save sanitized OpenRouter key? (Y/n)');
|
|
105
|
+
if (!ok) { console.log(chalk.yellow('Aborted.')); return; }
|
|
106
|
+
}
|
|
101
107
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
102
108
|
const model = modelArg || await new Promise(res => rl.question('Model (e.g. openai/gpt-oss-120b:free): ', a => { rl.close(); res(a && a.trim()); }));
|
|
103
109
|
const chosenModel = model && model.length ? model : 'openai/gpt-oss-120b:free';
|
|
@@ -124,7 +130,7 @@ async function storeOpenRouterKeyInteractive(keyArg, modelArg) {
|
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
const cfg = await readConfig() || {};
|
|
127
|
-
cfg.openrouterKey = String(
|
|
133
|
+
cfg.openrouterKey = String(normalized).trim();
|
|
128
134
|
cfg.openrouterModel = chosenModel;
|
|
129
135
|
await writeConfig(cfg);
|
|
130
136
|
console.log(chalk.green('OpenRouter key and model saved to'), chalk.cyan(configFilePath()));
|
|
@@ -139,6 +145,31 @@ async function promptHidden(prompt) {
|
|
|
139
145
|
}
|
|
140
146
|
return true;
|
|
141
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
|
+
}
|
|
142
173
|
if (!process.stdin.isTTY) throw new Error('Interactive prompt required');
|
|
143
174
|
return new Promise((resolve, reject) => {
|
|
144
175
|
const stdin = process.stdin;
|
|
@@ -492,7 +523,7 @@ async function startChatREPL(scanData){
|
|
|
492
523
|
|
|
493
524
|
async function run(argv) {
|
|
494
525
|
const program = new Command();
|
|
495
|
-
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '8.0.
|
|
526
|
+
program.name('subto').description('Subto CLI — wrapper around Subto.One API').version(CLIENT_META.version || '8.0.2');
|
|
496
527
|
program.option('-v, --verbose', 'Show verbose HTTP logs');
|
|
497
528
|
program.option('--chat', 'Start local AI assistant (no command required)');
|
|
498
529
|
program.option('--no-auto-skip', 'Disable automatic skipping of external APIs when scans appear stuck');
|
|
@@ -502,10 +533,17 @@ async function run(argv) {
|
|
|
502
533
|
|
|
503
534
|
program.command('login').description('Store your API key in ~/.subto/config.json').action(async () => {
|
|
504
535
|
try {
|
|
505
|
-
|
|
536
|
+
let apiKey = await promptHidden('API key: ');
|
|
506
537
|
if (!apiKey || !apiKey.trim()) throw new Error('No API key entered. Aborting.');
|
|
507
538
|
if (apiKey.length < 10) throw new Error('API key looks too short.');
|
|
508
|
-
|
|
539
|
+
const normalized = normalizeApiKey(apiKey);
|
|
540
|
+
if (normalized !== apiKey) {
|
|
541
|
+
console.log(chalk.yellow('Notice: API key contained invisible or fancy characters — it will be sanitized before saving.'));
|
|
542
|
+
const ok = await askYesNo('Save sanitized key? (Y/n)');
|
|
543
|
+
if (!ok) { console.log(chalk.yellow('Login aborted.')); process.exit(1); }
|
|
544
|
+
}
|
|
545
|
+
if (!isByteStringSafe(normalized)) throw new Error('API key contains unsupported characters (non-Latin-1). Provide a plain ASCII key.');
|
|
546
|
+
await writeConfig({ apiKey: normalized });
|
|
509
547
|
console.log(chalk.green('API key saved to'), chalk.cyan(configFilePath()));
|
|
510
548
|
} catch (err) { if (err && err.message === 'Aborted') { console.log('\n' + chalk.yellow('Login aborted.')); process.exit(1); } throw err; }
|
|
511
549
|
});
|