From ba1179d4696dd12dbfbe452a71c96313019ed97d Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 6 Jul 2026 01:00:56 +0200 Subject: [PATCH] fix: keep API key setup and export in sync --- scripts/setup-api.sh | 19 +++------- src/__tests__/api-key-env-static.test.ts | 46 +++++++++++++++++++++--- src/config/index.ts | 13 ++++++- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/scripts/setup-api.sh b/scripts/setup-api.sh index 0aea96f..bd5c043 100755 --- a/scripts/setup-api.sh +++ b/scripts/setup-api.sh @@ -10,10 +10,11 @@ echo "║ T3MP3ST API KEY CONFIGURATION ║" echo "╚═══════════════════════════════════════════════════════════╝" echo "" -ENV_FILE="$(dirname "$0")/../.env" +ENV_FILE="$HOME/.t3mp3st/.env" +mkdir -p "$(dirname "$ENV_FILE")" if [ -f "$ENV_FILE" ]; then - echo "[*] Existing .env file found; it will be replaced after you enter a new key." + echo "[*] Existing T3MP3ST env file found; it will be replaced after you enter a new key." fi echo "" @@ -64,24 +65,12 @@ esac chmod 600 "$ENV_FILE" -# Add .env to gitignore if not already there -GITIGNORE="$(dirname "$0")/../.gitignore" -if [ -f "$GITIGNORE" ]; then - if ! grep -q "^\.env$" "$GITIGNORE"; then - echo ".env" >> "$GITIGNORE" - echo "[+] Added .env to .gitignore" - fi -else - echo ".env" > "$GITIGNORE" - echo "[+] Created .gitignore with .env" -fi - echo "" echo "╔═══════════════════════════════════════════════════════════╗" echo "║ CONFIGURATION COMPLETE! ║" echo "╚═══════════════════════════════════════════════════════════╝" echo "" -echo "Your API key has been saved to .env (mode 600)" +echo "Your API key has been saved to $ENV_FILE (mode 600)" echo "" echo "Start the server with:" echo " npm run server" diff --git a/src/__tests__/api-key-env-static.test.ts b/src/__tests__/api-key-env-static.test.ts index 5964acc..f463a3e 100644 --- a/src/__tests__/api-key-env-static.test.ts +++ b/src/__tests__/api-key-env-static.test.ts @@ -5,14 +5,26 @@ import { join } from 'path'; const configSource = readFileSync(join(process.cwd(), 'src/config/index.ts'), 'utf8'); const setupScript = readFileSync(join(process.cwd(), 'scripts/setup-api.sh'), 'utf8'); -function configLoadEnvBlock(): string { - const start = configSource.indexOf('private loadEnvVariables(): void'); - expect(start).toBeGreaterThanOrEqual(0); - const end = configSource.indexOf('\n /**\n * Get all settings', start); - expect(end).toBeGreaterThan(start); +function sourceBlock(startMarker: string, endMarker: string): string { + const start = configSource.indexOf(startMarker); + expect(start, `missing start marker ${startMarker}`).toBeGreaterThanOrEqual(0); + const end = configSource.indexOf(endMarker, start); + expect(end, `missing end marker ${endMarker}`).toBeGreaterThan(start); return configSource.slice(start, end); } +function configLoadEnvBlock(): string { + return sourceBlock('private loadEnvVariables(): void', '\n /**\n * Get all settings'); +} + +function exportConfigBlock(): string { + return sourceBlock('exportConfig(filePath: string): void', '\n /**\n * Create a .env template file'); +} + +function envTemplateBlock(): string { + return sourceBlock('createEnvTemplate(filePath:', '\n writeFileSync(filePath, template);'); +} + describe('API key environment handling hardening', () => { it('ConfigManager does not implicitly read .env from the caller working directory', () => { const block = configLoadEnvBlock(); @@ -21,6 +33,30 @@ describe('API key environment handling hardening', () => { expect(block).toContain("join(homedir(), '.t3mp3st', '.env')"); }); + it('setup-api.sh writes the same T3MP3ST-owned env file that ConfigManager reads', () => { + const block = configLoadEnvBlock(); + + expect(setupScript).not.toMatch(/dirname "\$0"\).*\.\.\/\.env/); + expect(setupScript).toMatch(/\.t3mp3st/); + expect(setupScript).toMatch(/mkdir\s+-p\s+"\$\(dirname "\$ENV_FILE"\)"/); + expect(block).toContain("join(homedir(), '.t3mp3st', '.env')"); + }); + + it('env template points users at the T3MP3ST-owned env file, not a caller-cwd .env', () => { + const block = envTemplateBlock(); + + expect(block).toContain('~/.t3mp3st/.env'); + expect(block).not.toContain('Copy this file to .env'); + }); + + it('exportConfig redacts every supported provider key slot', () => { + const block = exportConfigBlock(); + + for (const provider of ['openrouter', 'venice', 'anthropic', 'openai', 'xai']) { + expect(block).toContain(`${provider}: settings.apiKeys.${provider} ? '***REDACTED***' : undefined`); + } + }); + it('ConfigManager uses env keys in-memory and does not persist imported env keys', () => { const block = configLoadEnvBlock(); diff --git a/src/config/index.ts b/src/config/index.ts index d2b579d..fa2ea75 100755 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -793,8 +793,10 @@ class ConfigManager { ...settings, apiKeys: { openrouter: settings.apiKeys.openrouter ? '***REDACTED***' : undefined, + venice: settings.apiKeys.venice ? '***REDACTED***' : undefined, anthropic: settings.apiKeys.anthropic ? '***REDACTED***' : undefined, openai: settings.apiKeys.openai ? '***REDACTED***' : undefined, + xai: settings.apiKeys.xai ? '***REDACTED***' : undefined, }, }; writeFileSync(filePath, JSON.stringify(safeSettings, null, 2)); @@ -805,12 +807,17 @@ class ConfigManager { */ createEnvTemplate(filePath: string = join(process.cwd(), '.env.template')): void { const template = `# T3MP3ST Environment Configuration -# Copy this file to .env and fill in your API keys +# Save this file as ~/.t3mp3st/.env and fill in your API keys +# The setup script writes the same T3MP3ST-owned file. # OpenRouter API Key (recommended - access to multiple models) # Get your key at: https://openrouter.ai/keys OPENROUTER_API_KEY= +# Venice API Key +# Get your key at: https://venice.ai +VENICE_API_KEY= + # Anthropic API Key (direct Claude access) # Get your key at: https://console.anthropic.com/ ANTHROPIC_API_KEY= @@ -818,6 +825,10 @@ ANTHROPIC_API_KEY= # OpenAI API Key # Get your key at: https://platform.openai.com/api-keys OPENAI_API_KEY= + +# xAI API Key +# Get your key at: https://console.x.ai/ +XAI_API_KEY= `; writeFileSync(filePath, template); }