UNPKG

unbound-claude-code

Version:

Claude Code with Unbound integration - Drop-in replacement for Claude Code with multi-provider routing and cost optimization

400 lines 13.2 kB
"use strict"; /** * Unbound Code Storage - API Key and Config Management * * Stores API key securely in system keychain when available, * falls back to file-based storage for cross-platform compatibility */ Object.defineProperty(exports, "__esModule", { value: true }); exports.UnboundStorage = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const os_1 = require("os"); const crypto_1 = require("crypto"); const global_1 = require("./global"); // Import keytar for keychain integration let keytar = null; try { keytar = require('keytar'); } catch (error) { // keytar not available } class UnboundStorage { constructor() { this.SERVICE_NAME = 'unbound-claude-code'; this.ACCOUNT_NAME = 'api-key'; this.CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.unbound-claude-code'); this.CONFIG_FILE = (0, path_1.join)(this.CONFIG_DIR, 'config.json'); // Ensure config directory exists this.ensureConfigDir(); } /** * Generate machine-specific encryption key */ getMachineKey() { try { const machineInfo = (0, os_1.hostname)() + JSON.stringify((0, os_1.networkInterfaces)()) + (0, os_1.homedir)(); return (0, crypto_1.createHash)('sha256').update(machineInfo).digest(); // Returns 32 bytes } catch (error) { // Fallback to a default key if machine info fails return (0, crypto_1.createHash)('sha256').update('unbound-fallback-key').digest(); // Returns 32 bytes } } /** * Encrypt sensitive data */ encrypt(text) { try { const key = this.getMachineKey(); // Already a 32-byte Buffer const iv = (0, crypto_1.randomBytes)(16); // 16 bytes IV for AES-256-CBC const cipher = (0, crypto_1.createCipheriv)('aes-256-cbc', key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); // Prepend IV to encrypted data return iv.toString('hex') + ':' + encrypted; } catch (error) { // If encryption fails, return original (fallback) return text; } } /** * Decrypt sensitive data */ decrypt(encryptedText) { try { // Check if data contains IV (new format) if (encryptedText.includes(':')) { const [ivHex, encrypted] = encryptedText.split(':'); const key = this.getMachineKey(); // Already a 32-byte Buffer const iv = Buffer.from(ivHex, 'hex'); const decipher = (0, crypto_1.createDecipheriv)('aes-256-cbc', key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } else { // Fallback for old format or plain text (backward compatibility) return encryptedText; } } catch (error) { // If decryption fails, assume it's plain text (backward compatibility) return encryptedText; } } async ensureConfigDir() { try { await fs_1.promises.mkdir(this.CONFIG_DIR, { recursive: true }); } catch (error) { // Directory already exists or creation failed - ignore } } async readConfig() { try { await this.ensureConfigDir(); const configData = await fs_1.promises.readFile(this.CONFIG_FILE, 'utf8'); const config = JSON.parse(configData); // Decrypt API key if it exists if (config.apiKey) { config.apiKey = this.decrypt(config.apiKey); } return config; } catch (error) { return {}; } } async writeConfig(config) { try { await this.ensureConfigDir(); const configToWrite = { ...config }; // Encrypt API key if it exists if (configToWrite.apiKey) { configToWrite.apiKey = this.encrypt(configToWrite.apiKey); } await fs_1.promises.writeFile(this.CONFIG_FILE, JSON.stringify(configToWrite, null, 2), 'utf8'); } catch (error) { console.warn('Failed to write config file:', error); } } /** * Store API key securely (keychain preferred, file fallback) */ async setApiKey(apiKey, silent = false) { // Try keychain first if (keytar) { try { await keytar.setPassword(this.SERVICE_NAME, this.ACCOUNT_NAME, apiKey); if (!silent) { console.log('✓ API key stored securely in system keychain'); } return; } catch (error) { //console.warn('Failed to store in keychain, using file fallback:', error); } } // Fallback to file storage try { const config = await this.readConfig(); config.apiKey = apiKey; await this.writeConfig(config); if (!silent) { console.log('✓ API key stored in config file (consider installing keytar for secure storage)'); } } catch (error) { throw new Error(`Failed to store API key: ${error}`); } } /** * Get stored API key (keychain preferred, file fallback) */ async getApiKey() { // Try keychain first if (keytar) { try { const keychainKey = await keytar.getPassword(this.SERVICE_NAME, this.ACCOUNT_NAME); if (keychainKey) { return keychainKey; } } catch (error) { console.warn('Keychain access failed, trying file fallback:', error); } } // Fallback to file storage try { const config = await this.readConfig(); return config.apiKey || null; } catch (error) { console.warn('Failed to read config file:', error); return null; } } /** * Get stored model */ async getModel() { try { const config = await this.readConfig(); return config.model || 'anthropic/claude-sonnet-4-20250514'; } catch (error) { return 'anthropic/claude-sonnet-4-20250514'; } } /** * Store model preference */ async setModel(model) { try { const config = await this.readConfig(); config.model = model; await this.writeConfig(config); console.log(`✓ Model preference set to: ${model}`); } catch (error) { console.warn('Failed to save model preference:', error); } } /** * Get full configuration */ async getFullConfig() { try { const config = await this.readConfig(); return { model: config.model || 'anthropic/claude-sonnet-4-20250514', baseUrl: config.baseUrl || 'https://api.getunbound.ai/v1', logLevel: config.logLevel || 'info', lastUsed: config.lastUsed }; } catch (error) { return { model: 'anthropic/claude-sonnet-4-20250514', baseUrl: `${global_1.UNBOUND_BASE_URL}/v1`, logLevel: 'info' }; } } /** * Update configuration */ async updateConfig(updates) { try { const config = await this.readConfig(); Object.assign(config, updates); await this.writeConfig(config); console.log('Configuration updated successfully'); } catch (error) { console.warn('Failed to update configuration:', error); } } /** * Clear all stored data including keychain and config file */ async clearConfig() { try { // Clear from keychain if available if (keytar) { try { await keytar.deletePassword(this.SERVICE_NAME, this.ACCOUNT_NAME); console.log('✓ API key cleared from keychain'); } catch (error) { console.warn('Failed to clear API key from keychain:', error); } } // Clear config file try { await fs_1.promises.unlink(this.CONFIG_FILE); console.log('✓ Config file cleared'); } catch (error) { // File might not exist, which is fine } console.log('✓ Configuration cleared successfully'); } catch (error) { console.error('Failed to clear config:', error); } } async clearConfigDueToInvalidKey() { try { // Clear from keychain if available if (keytar) { try { await keytar.deletePassword(this.SERVICE_NAME, this.ACCOUNT_NAME); } catch (error) { // Ignore keychain errors } } // Clear config file completely try { await fs_1.promises.unlink(this.CONFIG_FILE); } catch (error) { // File might not exist, which is fine } } catch (error) { console.error('Failed to clear config:', error); } } /** * Check if API key is stored */ async hasApiKey() { const apiKey = await this.getApiKey(); return apiKey !== null && apiKey.trim().length > 0; } /** * Check if API key is stored (sync version for backward compatibility) */ hasApiKeySync() { // This method is deprecated since we only use async now // Return false to force async check return false; } /** * Get vertex configuration */ async getVertexConfig() { try { const config = await this.readConfig(); return { useVertex: config.useVertex || false, model: config.vertexPrimaryModel || 'anthropic.claude-sonnet-4@20250514', smallModel: config.vertexSmallModel || 'anthropic.claude-3-5-haiku@20241022' }; } catch (error) { return { useVertex: false, model: 'anthropic.claude-sonnet-4@20250514', smallModel: 'anthropic.claude-3-5-haiku@20241022' }; } } /** * Store vertex configuration */ async setVertexConfig(vertexConfig) { try { const config = await this.readConfig(); config.useVertex = vertexConfig.useVertex; config.vertexPrimaryModel = vertexConfig.model; config.vertexSmallModel = vertexConfig.smallModel; config.isConfigured = true; // Mark as configured when preferences are set await this.writeConfig(config); console.log('✓ Vertex AI configuration saved'); } catch (error) { console.warn('Failed to save vertex configuration:', error); } } /** * Check if user has completed initial configuration */ async isConfigured() { try { const config = await this.readConfig(); return config.isConfigured === true; } catch (error) { return false; } } /** * Mark configuration as complete */ async markAsConfigured() { try { const config = await this.readConfig(); config.isConfigured = true; await this.writeConfig(config); } catch (error) { console.warn('Failed to mark configuration as complete:', error); } } /** * Get startup display configuration */ async getStartupConfig() { try { const config = await this.readConfig(); const vertexConfig = await this.getVertexConfig(); return { hasApiKey: true, // We know we have one since we just authenticated model: config.model || 'anthropic/claude-sonnet-4-20250514', useVertex: vertexConfig.useVertex, vertexPrimaryModel: vertexConfig.model, vertexSmallModel: vertexConfig.smallModel, baseUrl: config.baseUrl || `${global_1.UNBOUND_BASE_URL}/v1` }; } catch (error) { return { hasApiKey: false, model: 'anthropic/claude-sonnet-4-20250514', useVertex: false, baseUrl: `${global_1.UNBOUND_BASE_URL}/v1` }; } } } exports.UnboundStorage = UnboundStorage; //# sourceMappingURL=storage.js.map