claude-scionos 4.1.10 → 4.2.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.
- package/README.fr.md +1 -1
- package/README.md +1 -1
- package/index.js +111 -95
- package/package.json +1 -1
- package/src/proxy.js +31 -29
- package/src/routerlab.js +22 -1
package/README.fr.md
CHANGED
|
@@ -137,7 +137,7 @@ Cas courants :
|
|
|
137
137
|
- `Claude Code CLI not found` : installer `@anthropic-ai/claude-code`
|
|
138
138
|
- `Git Bash is required on Windows` : installer Git for Windows
|
|
139
139
|
- `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt` : définir la variable d'environnement ou stocker le token au préalable
|
|
140
|
-
- `Secure token file was created but no encrypted content was written` : mettre à jour vers `4.
|
|
140
|
+
- `Secure token file was created but no encrypted content was written` : mettre à jour vers `4.2.0` ou plus récent, puis relancer `claude-scionos auth login`
|
|
141
141
|
- `Stored token` est indiqué comme absent sous Windows alors qu'un login a déjà été fait : relancer `claude-scionos auth login`, car le fichier DPAPI local peut être vide ou corrompu
|
|
142
142
|
- `secret-tool not found` : installer un client Secret Service sous Linux ou utiliser la variable d'environnement
|
|
143
143
|
|
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ Common cases:
|
|
|
137
137
|
- `Claude Code CLI not found`: install `@anthropic-ai/claude-code`
|
|
138
138
|
- `Git Bash is required on Windows`: install Git for Windows
|
|
139
139
|
- `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt`: set the environment variable or store the token first
|
|
140
|
-
- `Secure token file was created but no encrypted content was written`: update to `4.
|
|
140
|
+
- `Secure token file was created but no encrypted content was written`: update to `4.2.0` or later, then re-run `claude-scionos auth login`
|
|
141
141
|
- `Stored token` is missing on Windows even though you already logged in: re-run `claude-scionos auth login` because the local DPAPI token file may be empty or corrupted
|
|
142
142
|
- `secret-tool not found`: install a Secret Service client on Linux or rely on the environment variable
|
|
143
143
|
|
package/index.js
CHANGED
|
@@ -4,23 +4,23 @@ import { styleText } from 'node:util';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
const chalk = {
|
|
7
|
-
hex: (color) => {
|
|
8
|
-
if (color === '#3b82f6') return (t) => styleText('blueBright', t);
|
|
9
|
-
if (color === '#a855f7') return (t) => styleText('magentaBright', t);
|
|
10
|
-
if (color === '#D97757') return (t) => styleText('redBright', t);
|
|
11
|
-
return (t) => t;
|
|
12
|
-
},
|
|
13
|
-
white: (t) => styleText('white', t),
|
|
14
|
-
gray: (t) => styleText('gray', t),
|
|
15
|
-
yellow: (t) => styleText('yellow', t),
|
|
16
|
-
red: (t) => styleText('red', t),
|
|
17
|
-
cyan: (t) => styleText('cyan', t),
|
|
18
|
-
redBright: (t) => styleText('redBright', t),
|
|
19
|
-
blueBright: (t) => styleText('blueBright', t),
|
|
20
|
-
green: (t) => styleText('green', t),
|
|
21
|
-
magenta: (t) => styleText('magenta', t),
|
|
22
|
-
bold: (t) => styleText('bold', t)
|
|
23
|
-
};
|
|
7
|
+
hex: (color) => {
|
|
8
|
+
if (color === '#3b82f6') return (t) => styleText('blueBright', t);
|
|
9
|
+
if (color === '#a855f7') return (t) => styleText('magentaBright', t);
|
|
10
|
+
if (color === '#D97757') return (t) => styleText('redBright', t);
|
|
11
|
+
return (t) => t;
|
|
12
|
+
},
|
|
13
|
+
white: (t) => styleText('white', t),
|
|
14
|
+
gray: (t) => styleText('gray', t),
|
|
15
|
+
yellow: (t) => styleText('yellow', t),
|
|
16
|
+
red: (t) => styleText('red', t),
|
|
17
|
+
cyan: (t) => styleText('cyan', t),
|
|
18
|
+
redBright: (t) => styleText('redBright', t),
|
|
19
|
+
blueBright: (t) => styleText('blueBright', t),
|
|
20
|
+
green: (t) => styleText('green', t),
|
|
21
|
+
magenta: (t) => styleText('magenta', t),
|
|
22
|
+
bold: (t) => styleText('bold', t)
|
|
23
|
+
};
|
|
24
24
|
import { password, confirm, select, Separator } from '@inquirer/prompts';
|
|
25
25
|
import { spawn, spawnSync } from 'node:child_process';
|
|
26
26
|
import process from 'node:process';
|
|
@@ -43,11 +43,13 @@ import {
|
|
|
43
43
|
getStrategyChoices,
|
|
44
44
|
hasVerifiedModelIds,
|
|
45
45
|
listStrategies,
|
|
46
|
+
resolveServiceBaseUrl,
|
|
46
47
|
storeToken,
|
|
47
|
-
validateToken
|
|
48
|
+
validateToken,
|
|
49
|
+
validateTokenFormat
|
|
48
50
|
} from './src/routerlab.js';
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
+
import { startProxyServer } from './src/proxy.js';
|
|
52
|
+
|
|
51
53
|
const require = createRequire(import.meta.url);
|
|
52
54
|
const pkg = require('./package.json');
|
|
53
55
|
|
|
@@ -292,7 +294,7 @@ function formatTokenSource(source) {
|
|
|
292
294
|
if (source === 'manual') return 'manual input';
|
|
293
295
|
return 'not available';
|
|
294
296
|
}
|
|
295
|
-
|
|
297
|
+
|
|
296
298
|
function installClaudeCode() {
|
|
297
299
|
return spawnSync('npm', ['install', '-g', '@anthropic-ai/claude-code'], {
|
|
298
300
|
stdio: 'inherit',
|
|
@@ -301,26 +303,27 @@ function installClaudeCode() {
|
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
async function promptAndValidateToken(promptMessage, serviceConfig) {
|
|
304
|
-
|
|
305
|
-
if (serviceConfig.tokenHelpUrl) {
|
|
306
|
-
console.log(chalk.blueBright(`To retrieve your token, visit: ${serviceConfig.tokenHelpUrl}`));
|
|
307
|
-
} else if (serviceConfig.tokenHelpMessage) {
|
|
308
|
-
console.log(chalk.blueBright(serviceConfig.tokenHelpMessage));
|
|
309
|
-
}
|
|
306
|
+
const serviceBaseUrl = resolveServiceBaseUrl(serviceConfig.value);
|
|
310
307
|
|
|
308
|
+
while (true) {
|
|
311
309
|
const token = await password({
|
|
312
310
|
message: promptMessage,
|
|
313
311
|
mask: '*'
|
|
314
312
|
});
|
|
315
313
|
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
const formatValidation = validateTokenFormat(token);
|
|
315
|
+
if (!formatValidation.valid) {
|
|
316
|
+
console.log(chalk.red(`✗ ${formatValidation.message}\n`));
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const validation = await validateToken(token, { baseUrl: serviceBaseUrl, serviceValue: serviceConfig.value });
|
|
318
321
|
if (canProceedWithValidation(validation)) {
|
|
319
|
-
console.log(chalk.green(
|
|
322
|
+
console.log(chalk.green(`✓ ${serviceConfig.tokenPromptLabel} token validated.`));
|
|
320
323
|
return { token, validation };
|
|
321
324
|
}
|
|
322
325
|
|
|
323
|
-
console.log(chalk.red(
|
|
326
|
+
console.log(chalk.red(`✗ Token invalid: ${validation.message || validation.status || validation.reason}\n`));
|
|
324
327
|
}
|
|
325
328
|
}
|
|
326
329
|
|
|
@@ -347,9 +350,27 @@ async function maybeStoreToken(token, serviceConfig, replaceExisting = false) {
|
|
|
347
350
|
|
|
348
351
|
async function resolveLaunchToken(noPrompt, serviceConfig) {
|
|
349
352
|
const candidate = getAvailableTokenCandidate(serviceConfig.value);
|
|
353
|
+
const serviceBaseUrl = resolveServiceBaseUrl(serviceConfig.value);
|
|
350
354
|
|
|
351
355
|
if (candidate.token) {
|
|
352
|
-
const
|
|
356
|
+
const formatValidation = validateTokenFormat(candidate.token);
|
|
357
|
+
if (!formatValidation.valid) {
|
|
358
|
+
const sourceLabel = formatTokenSource(candidate.source);
|
|
359
|
+
if (noPrompt) {
|
|
360
|
+
throw new Error(`${sourceLabel} token is invalid: ${formatValidation.message}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log(chalk.yellow(`⚠ The ${sourceLabel} token is invalid: ${formatValidation.message} Please enter a new token.`));
|
|
364
|
+
const prompted = await promptAndValidateToken(`Please enter your ${serviceConfig.tokenPromptLabel} token:`, serviceConfig);
|
|
365
|
+
await maybeStoreToken(prompted.token, serviceConfig, candidate.source === 'secure-store');
|
|
366
|
+
return {
|
|
367
|
+
token: prompted.token,
|
|
368
|
+
source: 'manual',
|
|
369
|
+
validation: prompted.validation
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const validation = await validateToken(candidate.token, { baseUrl: serviceBaseUrl, serviceValue: serviceConfig.value });
|
|
353
374
|
if (canProceedWithValidation(validation)) {
|
|
354
375
|
return {
|
|
355
376
|
token: candidate.token,
|
|
@@ -398,7 +419,7 @@ async function resolveStrategyChoice(parsed, modelIds, serviceConfig) {
|
|
|
398
419
|
if (hasVerifiedModelIds(modelIds)) {
|
|
399
420
|
const availability = assessStrategy(selected, modelIds, serviceConfig.value);
|
|
400
421
|
if (availability.level === 'partial') {
|
|
401
|
-
console.log(chalk.yellow(
|
|
422
|
+
console.log(chalk.yellow(`WARN Strategy "${selected}" is only partially available on ${serviceConfig.availabilityLabel}.`));
|
|
402
423
|
}
|
|
403
424
|
}
|
|
404
425
|
|
|
@@ -421,12 +442,12 @@ async function resolveStrategyChoice(parsed, modelIds, serviceConfig) {
|
|
|
421
442
|
const strategyChoices = getStrategyChoices(modelIds, serviceConfig.value).map((choice) => {
|
|
422
443
|
const launchReadiness = assessStrategyLaunch(choice.value, modelIds, serviceConfig.value);
|
|
423
444
|
const disabled = hasVerifiedModelIds(modelIds) && !launchReadiness.ready ? launchReadiness.note : false;
|
|
424
|
-
|
|
425
445
|
return {
|
|
426
446
|
...choice,
|
|
427
447
|
disabled,
|
|
428
448
|
name: `${getStrategyIndicator(choice.value, modelIds, serviceConfig.value)} ${getStrategyMenuLabel(choice.value)}`,
|
|
429
|
-
short: getStrategyMenuLabel(choice.value)
|
|
449
|
+
short: getStrategyMenuLabel(choice.value),
|
|
450
|
+
description: launchReadiness.note
|
|
430
451
|
};
|
|
431
452
|
});
|
|
432
453
|
|
|
@@ -451,12 +472,7 @@ function showStrategies(modelIds = null, serviceConfig) {
|
|
|
451
472
|
const strategies = listStrategies(modelIds, serviceConfig.value);
|
|
452
473
|
showSection('Strategies', strategies.map((strategy) => {
|
|
453
474
|
const indicator = getStrategyIndicator(strategy.value, modelIds, serviceConfig.value);
|
|
454
|
-
|
|
455
|
-
? chalk.gray('Unknown')
|
|
456
|
-
: assessStrategyLaunch(strategy.value, modelIds, serviceConfig.value).ready
|
|
457
|
-
? chalk.green('Ready')
|
|
458
|
-
: chalk.red('Blocked');
|
|
459
|
-
return `${indicator} ${chalk.white(strategy.name)} ${state} ${chalk.gray(`(${strategy.value})`)}\n ${strategy.description} ${strategy.availability.note}`.trimEnd();
|
|
475
|
+
return `${indicator} ${chalk.white(getStrategyMenuLabel(strategy.value))} ${chalk.gray(`(${strategy.value})`)}\n ${strategy.description}`;
|
|
460
476
|
}));
|
|
461
477
|
}
|
|
462
478
|
|
|
@@ -478,7 +494,7 @@ async function runAuthCommand(action, serviceConfig) {
|
|
|
478
494
|
|
|
479
495
|
if (action === 'logout') {
|
|
480
496
|
const removed = deleteStoredToken(serviceConfig.value);
|
|
481
|
-
console.log(removed ? chalk.green('
|
|
497
|
+
console.log(removed ? chalk.green('OK Stored token removed.') : chalk.yellow('WARN No stored token was found.'));
|
|
482
498
|
return;
|
|
483
499
|
}
|
|
484
500
|
|
|
@@ -489,21 +505,24 @@ async function runAuthCommand(action, serviceConfig) {
|
|
|
489
505
|
|
|
490
506
|
const prompted = await promptAndValidateToken(`Enter the ${serviceConfig.tokenPromptLabel} token to save securely:`, serviceConfig);
|
|
491
507
|
storeToken(prompted.token, serviceConfig.value);
|
|
492
|
-
console.log(chalk.green(
|
|
508
|
+
console.log(chalk.green(`OK Token saved securely in ${storage.backend}.`));
|
|
493
509
|
return;
|
|
494
510
|
}
|
|
495
511
|
|
|
496
512
|
if (action === 'test') {
|
|
497
513
|
const candidate = getAvailableTokenCandidate(serviceConfig.value);
|
|
498
514
|
if (!candidate.token) {
|
|
499
|
-
console.log(chalk.yellow('
|
|
515
|
+
console.log(chalk.yellow('WARN No environment token or stored token is available.'));
|
|
500
516
|
return;
|
|
501
517
|
}
|
|
502
518
|
|
|
503
|
-
const validation = await validateToken(candidate.token, {
|
|
519
|
+
const validation = await validateToken(candidate.token, {
|
|
520
|
+
baseUrl: resolveServiceBaseUrl(serviceConfig.value),
|
|
521
|
+
serviceValue: serviceConfig.value
|
|
522
|
+
});
|
|
504
523
|
console.log(canProceedWithValidation(validation)
|
|
505
|
-
? chalk.green(
|
|
506
|
-
: chalk.red(
|
|
524
|
+
? chalk.green(`OK ${formatTokenSource(candidate.source)} token is valid for ${serviceConfig.label}.`)
|
|
525
|
+
: chalk.red(`FAIL ${formatTokenSource(candidate.source)} token is invalid for ${serviceConfig.label}: ${validation.message || validation.status || validation.reason}`));
|
|
507
526
|
|
|
508
527
|
if (canProceedWithValidation(validation)) {
|
|
509
528
|
showStrategies(validation.models, serviceConfig);
|
|
@@ -538,16 +557,19 @@ async function runDoctor(serviceConfig) {
|
|
|
538
557
|
console.log('');
|
|
539
558
|
|
|
540
559
|
const candidate = getAvailableTokenCandidate(serviceConfig.value);
|
|
560
|
+
let validation = null;
|
|
561
|
+
|
|
541
562
|
if (!candidate.token) {
|
|
542
563
|
showStatus(`${serviceConfig.tokenPromptLabel} auth`, 'warn', 'Skipped: no environment or stored token available');
|
|
543
|
-
|
|
544
|
-
|
|
564
|
+
} else {
|
|
565
|
+
validation = await validateToken(candidate.token, {
|
|
566
|
+
baseUrl: resolveServiceBaseUrl(serviceConfig.value),
|
|
567
|
+
serviceValue: serviceConfig.value
|
|
568
|
+
});
|
|
569
|
+
showStatus(`${serviceConfig.tokenPromptLabel} auth`, canProceedWithValidation(validation) ? 'ok' : 'error', canProceedWithValidation(validation)
|
|
570
|
+
? `validated via ${formatTokenSource(candidate.source)} token`
|
|
571
|
+
: validation.message || validation.status || validation.reason);
|
|
545
572
|
}
|
|
546
|
-
|
|
547
|
-
const validation = await validateToken(candidate.token, { baseUrl: serviceConfig.baseUrl });
|
|
548
|
-
showStatus(`${serviceConfig.tokenPromptLabel} auth`, canProceedWithValidation(validation) ? 'ok' : 'error', canProceedWithValidation(validation)
|
|
549
|
-
? `validated via ${formatTokenSource(candidate.source)} token`
|
|
550
|
-
: validation.message || validation.status || validation.reason);
|
|
551
573
|
console.log('');
|
|
552
574
|
|
|
553
575
|
if (canProceedWithValidation(validation)) {
|
|
@@ -560,18 +582,14 @@ async function runListStrategies(serviceConfig) {
|
|
|
560
582
|
const candidate = getAvailableTokenCandidate(serviceConfig.value);
|
|
561
583
|
if (!candidate.token) {
|
|
562
584
|
showStrategies(null, serviceConfig);
|
|
563
|
-
console.log(chalk.gray(`Tip: save a token with \`claude-scionos auth login${serviceConfig.value === DEFAULT_SERVICE ? '' : ` --service ${serviceConfig.value}`}\` to verify availability live.`));
|
|
564
585
|
return;
|
|
565
586
|
}
|
|
566
587
|
|
|
567
|
-
const validation = await validateToken(candidate.token, {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
console.log(chalk.yellow(`⚠ Unable to verify strategy availability with the ${formatTokenSource(candidate.source)} token.`));
|
|
574
|
-
showStrategies(null, serviceConfig);
|
|
588
|
+
const validation = await validateToken(candidate.token, {
|
|
589
|
+
baseUrl: resolveServiceBaseUrl(serviceConfig.value),
|
|
590
|
+
serviceValue: serviceConfig.value
|
|
591
|
+
});
|
|
592
|
+
showStrategies(canProceedWithValidation(validation) ? validation.models : null, serviceConfig);
|
|
575
593
|
}
|
|
576
594
|
|
|
577
595
|
async function ensureClaudeInstallation(osInfo, interactive) {
|
|
@@ -611,7 +629,7 @@ async function ensureClaudeInstallation(osInfo, interactive) {
|
|
|
611
629
|
|
|
612
630
|
return claudeStatus;
|
|
613
631
|
}
|
|
614
|
-
|
|
632
|
+
|
|
615
633
|
async function main() {
|
|
616
634
|
const parsed = parseWrapperArgs(process.argv.slice(2));
|
|
617
635
|
|
|
@@ -667,7 +685,7 @@ async function main() {
|
|
|
667
685
|
}
|
|
668
686
|
|
|
669
687
|
const modelChoice = await resolveStrategyChoice(parsed, validation.models, serviceConfig);
|
|
670
|
-
let finalBaseUrl = serviceConfig.
|
|
688
|
+
let finalBaseUrl = resolveServiceBaseUrl(serviceConfig.value);
|
|
671
689
|
let proxyServer = null;
|
|
672
690
|
|
|
673
691
|
if (modelChoice !== 'default') {
|
|
@@ -676,7 +694,7 @@ async function main() {
|
|
|
676
694
|
}
|
|
677
695
|
|
|
678
696
|
const proxyInfo = await startProxyServer(modelChoice, token, {
|
|
679
|
-
baseUrl: serviceConfig.
|
|
697
|
+
baseUrl: resolveServiceBaseUrl(serviceConfig.value),
|
|
680
698
|
debug: isDebug,
|
|
681
699
|
onDebug: (message) => console.log(chalk.yellow(message)),
|
|
682
700
|
onError: (message) => console.error(chalk.red(message))
|
|
@@ -689,10 +707,10 @@ async function main() {
|
|
|
689
707
|
const env = {
|
|
690
708
|
...process.env,
|
|
691
709
|
ANTHROPIC_BASE_URL: finalBaseUrl,
|
|
692
|
-
ANTHROPIC_AUTH_TOKEN: token,
|
|
693
|
-
ANTHROPIC_API_KEY: "" // Force empty
|
|
694
|
-
};
|
|
695
|
-
|
|
710
|
+
ANTHROPIC_AUTH_TOKEN: token,
|
|
711
|
+
ANTHROPIC_API_KEY: "" // Force empty
|
|
712
|
+
};
|
|
713
|
+
|
|
696
714
|
if (interactive) {
|
|
697
715
|
showSection('Launch Summary', [
|
|
698
716
|
`${chalk.white('Service:')} ${serviceConfig.label}`,
|
|
@@ -721,31 +739,31 @@ async function main() {
|
|
|
721
739
|
cleanup();
|
|
722
740
|
process.exit(code ?? 0);
|
|
723
741
|
});
|
|
724
|
-
|
|
725
|
-
child.on('error', (err) => {
|
|
726
|
-
cleanup();
|
|
727
|
-
console.error(chalk.red(`\n❌ Error launching Claude CLI:`));
|
|
728
|
-
if (err.code === 'ENOENT') {
|
|
729
|
-
console.error(chalk.yellow(` Executable not found. Try 'npm install -g @anthropic-ai/claude-code'`));
|
|
730
|
-
} else if (err.code === 'EACCES') {
|
|
731
|
-
console.error(chalk.yellow(` Permission denied.`));
|
|
732
|
-
} else {
|
|
733
|
-
console.error(chalk.yellow(` ${err.message}`));
|
|
734
|
-
}
|
|
735
|
-
process.exit(1);
|
|
736
|
-
});
|
|
742
|
+
|
|
743
|
+
child.on('error', (err) => {
|
|
744
|
+
cleanup();
|
|
745
|
+
console.error(chalk.red(`\n❌ Error launching Claude CLI:`));
|
|
746
|
+
if (err.code === 'ENOENT') {
|
|
747
|
+
console.error(chalk.yellow(` Executable not found. Try 'npm install -g @anthropic-ai/claude-code'`));
|
|
748
|
+
} else if (err.code === 'EACCES') {
|
|
749
|
+
console.error(chalk.yellow(` Permission denied.`));
|
|
750
|
+
} else {
|
|
751
|
+
console.error(chalk.yellow(` ${err.message}`));
|
|
752
|
+
}
|
|
753
|
+
process.exit(1);
|
|
754
|
+
});
|
|
737
755
|
|
|
738
756
|
process.on('SIGINT', () => {
|
|
739
757
|
// Claude handles SIGINT; we keep the wrapper alive for cleanup on child exit.
|
|
740
758
|
});
|
|
741
|
-
|
|
742
|
-
process.on('SIGTERM', () => {
|
|
743
|
-
if (child) child.kill('SIGTERM');
|
|
744
|
-
cleanup();
|
|
745
|
-
process.exit(0);
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
|
|
759
|
+
|
|
760
|
+
process.on('SIGTERM', () => {
|
|
761
|
+
if (child) child.kill('SIGTERM');
|
|
762
|
+
cleanup();
|
|
763
|
+
process.exit(0);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
749
767
|
const isEntrypoint = normalizeEntrypointPath(process.argv[1]) === normalizeEntrypointPath(fileURLToPath(import.meta.url));
|
|
750
768
|
|
|
751
769
|
if (isEntrypoint) {
|
|
@@ -754,14 +772,12 @@ if (isEntrypoint) {
|
|
|
754
772
|
process.exit(1);
|
|
755
773
|
});
|
|
756
774
|
}
|
|
757
|
-
|
|
775
|
+
|
|
758
776
|
export {
|
|
759
|
-
buildProxyRequestOptions,
|
|
760
777
|
canProceedWithValidation,
|
|
761
778
|
installClaudeCode,
|
|
762
779
|
main,
|
|
763
780
|
normalizeEntrypointPath,
|
|
764
|
-
|
|
765
|
-
startProxyServer,
|
|
781
|
+
resolveLaunchToken,
|
|
766
782
|
validateToken
|
|
767
783
|
};
|
package/package.json
CHANGED
package/src/proxy.js
CHANGED
|
@@ -14,6 +14,8 @@ const HOP_BY_HOP_HEADERS = new Set([
|
|
|
14
14
|
'transfer-encoding',
|
|
15
15
|
'upgrade',
|
|
16
16
|
]);
|
|
17
|
+
const PROXY_AUTH_HEADER = 'x-scionos-proxy-secret';
|
|
18
|
+
const MESSAGES_PATH = '/v1/messages';
|
|
17
19
|
|
|
18
20
|
function normalizeProxyHeaders(headers) {
|
|
19
21
|
const normalizedHeaders = {};
|
|
@@ -32,6 +34,7 @@ function normalizeProxyHeaders(headers) {
|
|
|
32
34
|
function buildProxyRequestOptions(url, method, upstreamHeaders, validToken, bodyLength, timeout) {
|
|
33
35
|
const headers = normalizeProxyHeaders(upstreamHeaders);
|
|
34
36
|
delete headers.authorization;
|
|
37
|
+
delete headers[PROXY_AUTH_HEADER];
|
|
35
38
|
headers['x-api-key'] = validToken;
|
|
36
39
|
headers['anthropic-version'] ??= DEFAULT_ANTHROPIC_VERSION;
|
|
37
40
|
|
|
@@ -74,6 +77,22 @@ function writeJsonError(res, statusCode, payload) {
|
|
|
74
77
|
res.end(JSON.stringify(payload));
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
function getRequestPath(req) {
|
|
81
|
+
return new URL(req.url, 'http://127.0.0.1').pathname;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isAuthorizedProxyRequest(req, proxySecret) {
|
|
85
|
+
if (!proxySecret) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return req.headers[PROXY_AUTH_HEADER] === proxySecret;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isAllowedProxyRoute(req) {
|
|
93
|
+
return req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH;
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
async function handleMessageRequest(req, res, options) {
|
|
78
97
|
const {baseUrl, debug, onDebug, onError, targetModel, validToken} = options;
|
|
79
98
|
const chunks = [];
|
|
@@ -123,7 +142,7 @@ async function handleMessageRequest(req, res, options) {
|
|
|
123
142
|
validToken,
|
|
124
143
|
});
|
|
125
144
|
} catch (error) {
|
|
126
|
-
onError(`[Proxy Error] POST
|
|
145
|
+
onError(`[Proxy Error] POST ${MESSAGES_PATH}: ${error.message}`);
|
|
127
146
|
writeJsonError(res, 500, {
|
|
128
147
|
error: {
|
|
129
148
|
message: 'Scionos Proxy Error',
|
|
@@ -162,9 +181,9 @@ async function forwardRequest(req, res, options) {
|
|
|
162
181
|
onError(`[Proxy Error] Code: ${error.code}`);
|
|
163
182
|
}
|
|
164
183
|
|
|
165
|
-
writeJsonError(res, req.method === 'POST' && req
|
|
184
|
+
writeJsonError(res, req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH ? 500 : 502, {
|
|
166
185
|
error: {
|
|
167
|
-
message: req.method === 'POST' && req
|
|
186
|
+
message: req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH
|
|
168
187
|
? 'Proxy Error'
|
|
169
188
|
: 'Scionos Proxy Error: Failed to connect to upstream',
|
|
170
189
|
details: error.message,
|
|
@@ -201,47 +220,28 @@ function startProxyServer(targetModel, validToken, options = {}) {
|
|
|
201
220
|
debug = false,
|
|
202
221
|
onDebug = () => {},
|
|
203
222
|
onError = () => {},
|
|
223
|
+
proxySecret = null,
|
|
204
224
|
} = options;
|
|
205
225
|
|
|
206
226
|
return new Promise((resolve, reject) => {
|
|
207
227
|
const server = http.createServer((req, res) => {
|
|
208
|
-
if (req
|
|
209
|
-
res
|
|
210
|
-
'Access-Control-Allow-Origin': '*',
|
|
211
|
-
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
|
212
|
-
'Access-Control-Allow-Headers': '*',
|
|
213
|
-
});
|
|
214
|
-
res.end();
|
|
228
|
+
if (!isAuthorizedProxyRequest(req, proxySecret)) {
|
|
229
|
+
writeJsonError(res, 403, {error: {message: 'Forbidden'}});
|
|
215
230
|
return;
|
|
216
231
|
}
|
|
217
232
|
|
|
218
|
-
if (req
|
|
219
|
-
|
|
220
|
-
baseUrl,
|
|
221
|
-
debug,
|
|
222
|
-
onDebug,
|
|
223
|
-
onError,
|
|
224
|
-
targetModel,
|
|
225
|
-
validToken,
|
|
226
|
-
});
|
|
233
|
+
if (!isAllowedProxyRoute(req)) {
|
|
234
|
+
writeJsonError(res, 404, {error: {message: 'Not Found'}});
|
|
227
235
|
return;
|
|
228
236
|
}
|
|
229
237
|
|
|
230
|
-
|
|
238
|
+
handleMessageRequest(req, res, {
|
|
231
239
|
baseUrl,
|
|
232
240
|
debug,
|
|
233
241
|
onDebug,
|
|
234
242
|
onError,
|
|
235
|
-
|
|
243
|
+
targetModel,
|
|
236
244
|
validToken,
|
|
237
|
-
}).catch((error) => {
|
|
238
|
-
onError(`[Proxy Error] ${req.method} ${req.url}: ${error.message}`);
|
|
239
|
-
writeJsonError(res, 502, {
|
|
240
|
-
error: {
|
|
241
|
-
message: 'Scionos Proxy Error: Failed to connect to upstream',
|
|
242
|
-
details: error.message,
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
245
|
});
|
|
246
246
|
});
|
|
247
247
|
|
|
@@ -257,6 +257,8 @@ function startProxyServer(targetModel, validToken, options = {}) {
|
|
|
257
257
|
export {
|
|
258
258
|
buildProxyRequestOptions,
|
|
259
259
|
normalizeProxyHeaders,
|
|
260
|
+
PROXY_AUTH_HEADER,
|
|
260
261
|
resolveMappedModel,
|
|
261
262
|
startProxyServer,
|
|
262
263
|
};
|
|
264
|
+
|
package/src/routerlab.js
CHANGED
|
@@ -102,7 +102,8 @@ const STRATEGIES = [
|
|
|
102
102
|
|
|
103
103
|
async function fetchModels(apiKey, options = {}) {
|
|
104
104
|
const {
|
|
105
|
-
|
|
105
|
+
serviceValue = DEFAULT_SERVICE,
|
|
106
|
+
baseUrl = resolveServiceBaseUrl(serviceValue),
|
|
106
107
|
anthropicVersion = DEFAULT_ANTHROPIC_VERSION,
|
|
107
108
|
timeoutMs = 30000,
|
|
108
109
|
} = options;
|
|
@@ -190,6 +191,24 @@ function getServiceConfig(serviceValue = DEFAULT_SERVICE) {
|
|
|
190
191
|
return SERVICES[normalizeServiceValue(serviceValue)] ?? null;
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
function resolveServiceBaseUrl(serviceValue = DEFAULT_SERVICE, env = process.env) {
|
|
195
|
+
return env.ANTHROPIC_BASE_URL?.trim() || getServiceConfig(serviceValue)?.baseUrl || BASE_URL;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateTokenFormat(apiKey) {
|
|
199
|
+
const token = apiKey?.trim() ?? '';
|
|
200
|
+
|
|
201
|
+
if (!token) {
|
|
202
|
+
return {valid: false, reason: 'missing', message: 'Token is required.'};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (token.length < 20) {
|
|
206
|
+
return {valid: false, reason: 'too_short', message: 'Token seems invalid (too short).'};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {valid: true};
|
|
210
|
+
}
|
|
211
|
+
|
|
193
212
|
function getServiceLabel(serviceValue = DEFAULT_SERVICE) {
|
|
194
213
|
return getServiceConfig(serviceValue)?.availabilityLabel ?? 'RouterLab';
|
|
195
214
|
}
|
|
@@ -652,6 +671,8 @@ export {
|
|
|
652
671
|
getStrategyChoices,
|
|
653
672
|
hasVerifiedModelIds,
|
|
654
673
|
listStrategies,
|
|
674
|
+
resolveServiceBaseUrl,
|
|
655
675
|
storeToken,
|
|
656
676
|
validateToken,
|
|
677
|
+
validateTokenFormat,
|
|
657
678
|
};
|