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 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.1.10` ou plus récent, puis relancer `claude-scionos auth login`
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.1.10` or later, then re-run `claude-scionos auth login`
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 { buildProxyRequestOptions, normalizeProxyHeaders, startProxyServer } from './src/proxy.js';
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
- while (true) {
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
- console.log(chalk.gray("Validating token..."));
317
- const validation = await validateToken(token, { baseUrl: serviceConfig.baseUrl });
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("✓ Token validated."));
322
+ console.log(chalk.green(`✓ ${serviceConfig.tokenPromptLabel} token validated.`));
320
323
  return { token, validation };
321
324
  }
322
325
 
323
- console.log(chalk.red(`❌ Token validation failed: ${validation.message || validation.status || validation.reason}. Try again.`));
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 validation = await validateToken(candidate.token, { baseUrl: serviceConfig.baseUrl });
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(`⚠ Strategy "${selected}" is only partially available on ${serviceConfig.availabilityLabel}.`));
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
- const state = !hasVerifiedModelIds(modelIds)
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(' Stored token removed.') : chalk.yellow(' No stored token was found.'));
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(`✓ Token saved securely in ${storage.backend}.`));
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(' No environment token or stored token is available.'));
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, { baseUrl: serviceConfig.baseUrl });
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(`✓ ${formatTokenSource(candidate.source)} token is valid.`)
506
- : chalk.red(`❌ ${formatTokenSource(candidate.source)} token is invalid: ${validation.message || validation.status || validation.reason}`));
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
- console.log('');
544
- return;
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, { baseUrl: serviceConfig.baseUrl });
568
- if (canProceedWithValidation(validation)) {
569
- showStrategies(validation.models, serviceConfig);
570
- return;
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.baseUrl;
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.baseUrl,
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
- normalizeProxyHeaders,
765
- startProxyServer,
781
+ resolveLaunchToken,
766
782
  validateToken
767
783
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-scionos",
3
- "version": "4.1.10",
3
+ "version": "4.2.0",
4
4
  "description": "RouterLab launcher, strategy proxy and secure token wrapper for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "index.js",
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 /messages: ${error.message}`);
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.url.includes('/messages') ? 500 : 502, {
184
+ writeJsonError(res, req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH ? 500 : 502, {
166
185
  error: {
167
- message: req.method === 'POST' && req.url.includes('/messages')
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.method === 'OPTIONS') {
209
- res.writeHead(200, {
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.method === 'POST' && req.url.includes('/messages')) {
219
- handleMessageRequest(req, res, {
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
- forwardRequest(req, res, {
238
+ handleMessageRequest(req, res, {
231
239
  baseUrl,
232
240
  debug,
233
241
  onDebug,
234
242
  onError,
235
- timeout: 60000,
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
- baseUrl = BASE_URL,
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
  };