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
JavaScript
"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