/** * Provider Storage * Manages provider configurations and API keys. * Keys are stored in plain text alongside provider configs in a single electron-store. */ import { BUILTIN_PROVIDER_TYPES, type ProviderType } from './provider-registry'; import { getActiveOpenClawProviders } from './openclaw-auth'; // Lazy-load electron-store (ESM module) // eslint-disable-next-line @typescript-eslint/no-explicit-any let providerStore: any = null; async function getProviderStore() { if (!providerStore) { const Store = (await import('electron-store')).default; providerStore = new Store({ name: 'clawx-providers', defaults: { providers: {} as Record, apiKeys: {} as Record, defaultProvider: null as string | null, }, }); } return providerStore; } /** * Provider configuration */ export interface ProviderConfig { id: string; name: string; type: ProviderType; baseUrl?: string; model?: string; fallbackModels?: string[]; fallbackProviderIds?: string[]; enabled: boolean; createdAt: string; updatedAt: string; } // ==================== API Key Storage ==================== /** * Store an API key */ export async function storeApiKey(providerId: string, apiKey: string): Promise { try { const s = await getProviderStore(); const keys = (s.get('apiKeys') || {}) as Record; keys[providerId] = apiKey; s.set('apiKeys', keys); return true; } catch (error) { console.error('Failed to store API key:', error); return false; } } /** * Retrieve an API key */ export async function getApiKey(providerId: string): Promise { try { const s = await getProviderStore(); const keys = (s.get('apiKeys') || {}) as Record; return keys[providerId] || null; } catch (error) { console.error('Failed to retrieve API key:', error); return null; } } /** * Delete an API key */ export async function deleteApiKey(providerId: string): Promise { try { const s = await getProviderStore(); const keys = (s.get('apiKeys') || {}) as Record; delete keys[providerId]; s.set('apiKeys', keys); return true; } catch (error) { console.error('Failed to delete API key:', error); return false; } } /** * Check if an API key exists for a provider */ export async function hasApiKey(providerId: string): Promise { const s = await getProviderStore(); const keys = (s.get('apiKeys') || {}) as Record; return providerId in keys; } /** * List all provider IDs that have stored keys */ export async function listStoredKeyIds(): Promise { const s = await getProviderStore(); const keys = (s.get('apiKeys') || {}) as Record; return Object.keys(keys); } // ==================== Provider Configuration ==================== /** * Save a provider configuration */ export async function saveProvider(config: ProviderConfig): Promise { const s = await getProviderStore(); const providers = s.get('providers') as Record; providers[config.id] = config; s.set('providers', providers); } /** * Get a provider configuration */ export async function getProvider(providerId: string): Promise { const s = await getProviderStore(); const providers = s.get('providers') as Record; return providers[providerId] || null; } /** * Get all provider configurations */ export async function getAllProviders(): Promise { const s = await getProviderStore(); const providers = s.get('providers') as Record; return Object.values(providers); } /** * Delete a provider configuration and its API key */ export async function deleteProvider(providerId: string): Promise { try { // Delete the API key await deleteApiKey(providerId); // Delete the provider config const s = await getProviderStore(); const providers = s.get('providers') as Record; delete providers[providerId]; s.set('providers', providers); // Clear default if this was the default if (s.get('defaultProvider') === providerId) { s.delete('defaultProvider'); } return true; } catch (error) { console.error('Failed to delete provider:', error); return false; } } /** * Set the default provider */ export async function setDefaultProvider(providerId: string): Promise { const s = await getProviderStore(); s.set('defaultProvider', providerId); } /** * Get the default provider */ export async function getDefaultProvider(): Promise { const s = await getProviderStore(); return s.get('defaultProvider') as string | undefined; } /** * Get provider with masked key info (for UI display) */ export async function getProviderWithKeyInfo( providerId: string ): Promise<(ProviderConfig & { hasKey: boolean; keyMasked: string | null }) | null> { const provider = await getProvider(providerId); if (!provider) return null; const apiKey = await getApiKey(providerId); let keyMasked: string | null = null; if (apiKey) { if (apiKey.length > 12) { keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`; } else { keyMasked = '*'.repeat(apiKey.length); } } return { ...provider, hasKey: !!apiKey, keyMasked, }; } /** * Get all providers with key info (for UI display) * Also synchronizes ClawX local provider list with OpenClaw's actual config. */ export async function getAllProvidersWithKeyInfo(): Promise< Array > { const providers = await getAllProviders(); const results: Array = []; const activeOpenClawProviders = await getActiveOpenClawProviders(); for (const provider of providers) { // Sync check: If it's a custom/OAuth provider and it no longer exists in OpenClaw config // (e.g. wiped by Gateway due to missing plugin, or manually deleted by user) // we should remove it from ClawX UI to stay consistent. const isBuiltin = BUILTIN_PROVIDER_TYPES.includes(provider.type); // For custom/ollama providers, the OpenClaw config key is derived as // "-" where suffix = first 8 chars of providerId with hyphens stripped. // e.g. provider.id "custom-a1b2c3d4-..." → strip hyphens → "customa1b2c3d4..." → slice(0,8) → "customa1" // → openClawKey = "custom-customa1" // This must match getOpenClawProviderKey() in ipc-handlers.ts exactly. const openClawKey = (provider.type === 'custom' || provider.type === 'ollama') ? `${provider.type}-${provider.id.replace(/-/g, '').slice(0, 8)}` : provider.type === 'minimax-portal-cn' ? 'minimax-portal' : provider.type; if (!isBuiltin && !activeOpenClawProviders.has(provider.type) && !activeOpenClawProviders.has(provider.id) && !activeOpenClawProviders.has(openClawKey)) { console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, dropping from ClawX UI`); await deleteProvider(provider.id); continue; } const apiKey = await getApiKey(provider.id); let keyMasked: string | null = null; if (apiKey) { if (apiKey.length > 12) { keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`; } else { keyMasked = '*'.repeat(apiKey.length); } } results.push({ ...provider, hasKey: !!apiKey, keyMasked, }); } return results; }