diff --git a/src/components/ConfigPreview.tsx b/src/components/ConfigPreview.tsx index 1ca4499..59253ef 100644 --- a/src/components/ConfigPreview.tsx +++ b/src/components/ConfigPreview.tsx @@ -28,7 +28,7 @@ export const ConfigPreview: React.FC = ({ const configContent = tool.sampleTemplate(connection, selectedModels) const installScript = buildInstallScript(tool, connection, selectedModels, selectedOS, { - keySource: 'runtime-env', + keySource: connection.apiKey ? 'embedded' : 'runtime-env', }) const handleCopyConfig = async () => { @@ -182,14 +182,10 @@ export const ConfigPreview: React.FC = ({
- Secure API Key Storage in VS Code + API Key Setup for VS Code

- No API key entered above. The generated config uses{' '} - - ${'{input:9router-api-key}'} - - . When VS Code prompts you for 9router-api-key, paste your key. + No API key entered above. Enter your API key under Gateway Connection to embed it directly into the config, or use the 1-Command installer which will prompt you for your key securely during installation.

)} diff --git a/src/data/tools.ts b/src/data/tools.ts index 4452de0..71bc82d 100644 --- a/src/data/tools.ts +++ b/src/data/tools.ts @@ -37,7 +37,7 @@ export const tools: Record = { } const endpointUrl = connection.baseUrl.replace(/\/+$/, '') + '/chat/completions' - const apiKeyVal = connection.apiKey ? connection.apiKey : '${input:9router-api-key}' + const apiKeyVal = connection.apiKey ? connection.apiKey : '' const groups = Array.from(groupsMap.entries()).map(([provider, models]) => { return { diff --git a/src/services/installEndpoint.test.ts b/src/services/installEndpoint.test.ts index 8d281b4..021ad5b 100644 --- a/src/services/installEndpoint.test.ts +++ b/src/services/installEndpoint.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'bun:test' import { encodeInstallToken, decodeInstallToken, buildOneClickCommand } from './installLink' import { renderInstallScript } from './installEndpoint' +import { buildInstallScript } from './installScript' import { tools } from '../data/tools' describe('Install Link & Endpoint', () => { @@ -62,11 +63,44 @@ describe('Install Link & Endpoint', () => { expect(shRes.contentType).toContain('text/x-shellscript') expect(shRes.body).toContain('__NINEROUTER_API_KEY__') expect(shRes.body).toContain('NINEROUTER_API_KEY') + expect(shRes.body).toContain('/dev/tty') + expect(shRes.body).toContain('exit 1') + expect(shRes.body).not.toContain('${input:9router-api-key}') const ps1Res = renderInstallScript(token, 'ps1') expect(ps1Res.status).toBe(200) expect(ps1Res.contentType).toContain('text/plain') expect(ps1Res.body).toContain('__NINEROUTER_API_KEY__') + expect(ps1Res.body).toContain('exit 1') + expect(ps1Res.body).not.toContain('${input:9router-api-key}') + }) + + it('builds embedded install script with real API key inline and no prompts', () => { + const scriptUnix = buildInstallScript( + tools.copilot, + { baseUrl: 'http://localhost:20128/v1', apiKey: 'sk-real-secret-123' }, + [{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5' }], + 'unix', + { keySource: 'embedded' } + ) + + expect(scriptUnix).not.toBeNull() + expect(scriptUnix!.content).toContain('sk-real-secret-123') + expect(scriptUnix!.content).not.toContain('__NINEROUTER_API_KEY__') + expect(scriptUnix!.content).not.toContain('read -rsp') + + const scriptWin = buildInstallScript( + tools.copilot, + { baseUrl: 'http://localhost:20128/v1', apiKey: 'sk-real-secret-123' }, + [{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5' }], + 'windows', + { keySource: 'embedded' } + ) + + expect(scriptWin).not.toBeNull() + expect(scriptWin!.content).toContain('sk-real-secret-123') + expect(scriptWin!.content).not.toContain('__NINEROUTER_API_KEY__') + expect(scriptWin!.content).not.toContain('Read-Host') }) it('replaces every API key placeholder in generated runtime scripts', () => { diff --git a/src/services/installScript.ts b/src/services/installScript.ts index 5abba24..acab4dd 100644 --- a/src/services/installScript.ts +++ b/src/services/installScript.ts @@ -39,10 +39,14 @@ export function buildInstallScript( keySource === 'runtime-env' ? { writeConfigSection: `API_KEY="\${NINEROUTER_API_KEY:-}" -if [[ -z "$API_KEY" ]]; then - read -rsp "Enter your 9router API key: " API_KEY +if [[ -z "$API_KEY" && -r /dev/tty ]]; then + read -rsp "Enter your 9router API key: " API_KEY < /dev/tty echo fi +if [[ -z "$API_KEY" ]]; then + echo "❌ No 9router API key provided (set NINEROUTER_API_KEY or run in an interactive terminal)." >&2 + exit 1 +fi CONFIG=$(cat <<'EOF' ${configContent} @@ -85,11 +89,7 @@ replace_api_key_placeholder() { done } -if [[ -n "$API_KEY" ]]; then - replace_api_key_placeholder "$(json_escape "$API_KEY")" -else - replace_api_key_placeholder '\${input:9router-api-key}' -fi +replace_api_key_placeholder "$(json_escape "$API_KEY")" printf '%s\\n' "$CONFIG" > "$TARGET_FILE"` , @@ -165,16 +165,17 @@ if (-not $apiKey) { $apiKey = [System.Net.NetworkCredential]::new('', $secureApiKey).Password } +if (-not $apiKey) { + Write-Error "No 9router API key provided. Set NINEROUTER_API_KEY or run in an interactive terminal." + exit 1 +} + $config = @' ${configContent} '@ -if ($apiKey) { - $jsonApiKey = ConvertTo-Json -InputObject $apiKey -Compress - $config = $config.Replace('"__NINEROUTER_API_KEY__"', $jsonApiKey) -} else { - $config = $config.Replace('"__NINEROUTER_API_KEY__"', '"\${input:9router-api-key}"') -} +$jsonApiKey = ConvertTo-Json -InputObject $apiKey -Compress +$config = $config.Replace('"__NINEROUTER_API_KEY__"', $jsonApiKey) Set-Content -Path $targetFile -Value $config -Encoding UTF8`, psPostInstallNotes: `Write-Host ""