/** * IPC Handlers * Registers all IPC handlers for main-renderer communication */ import { ipcMain, BrowserWindow, shell, dialog, app } from 'electron'; import { GatewayManager } from '../gateway/manager'; import { storeApiKey, getApiKey, deleteApiKey, hasApiKey, saveProvider, getProvider, getAllProviders, deleteProvider, setDefaultProvider, getDefaultProvider, getAllProvidersWithKeyInfo, isEncryptionAvailable, type ProviderConfig, } from '../utils/secure-storage'; import { getOpenClawStatus } from '../utils/paths'; /** * Register all IPC handlers */ export function registerIpcHandlers( gatewayManager: GatewayManager, mainWindow: BrowserWindow ): void { // Gateway handlers registerGatewayHandlers(gatewayManager, mainWindow); // OpenClaw handlers registerOpenClawHandlers(); // Provider handlers registerProviderHandlers(); // Shell handlers registerShellHandlers(); // Dialog handlers registerDialogHandlers(); // App handlers registerAppHandlers(); } /** * Gateway-related IPC handlers */ function registerGatewayHandlers( gatewayManager: GatewayManager, mainWindow: BrowserWindow ): void { // Get Gateway status ipcMain.handle('gateway:status', () => { return gatewayManager.getStatus(); }); // Check if Gateway is connected ipcMain.handle('gateway:isConnected', () => { return gatewayManager.isConnected(); }); // Start Gateway ipcMain.handle('gateway:start', async () => { try { await gatewayManager.start(); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Stop Gateway ipcMain.handle('gateway:stop', async () => { try { await gatewayManager.stop(); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Restart Gateway ipcMain.handle('gateway:restart', async () => { try { await gatewayManager.restart(); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Gateway RPC call ipcMain.handle('gateway:rpc', async (_, method: string, params?: unknown, timeoutMs?: number) => { try { const result = await gatewayManager.rpc(method, params, timeoutMs); return { success: true, result }; } catch (error) { return { success: false, error: String(error) }; } }); // Health check ipcMain.handle('gateway:health', async () => { try { const health = await gatewayManager.checkHealth(); return { success: true, ...health }; } catch (error) { return { success: false, ok: false, error: String(error) }; } }); // Forward Gateway events to renderer gatewayManager.on('status', (status) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:status-changed', status); } }); gatewayManager.on('message', (message) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:message', message); } }); gatewayManager.on('notification', (notification) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:notification', notification); } }); gatewayManager.on('channel:status', (data) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:channel-status', data); } }); gatewayManager.on('chat:message', (data) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:chat-message', data); } }); gatewayManager.on('exit', (code) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:exit', code); } }); gatewayManager.on('error', (error) => { if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('gateway:error', error.message); } }); } /** * OpenClaw-related IPC handlers * For checking submodule status */ function registerOpenClawHandlers(): void { // Get OpenClaw submodule status ipcMain.handle('openclaw:status', () => { return getOpenClawStatus(); }); // Check if OpenClaw is ready (submodule present and dependencies installed) ipcMain.handle('openclaw:isReady', () => { const status = getOpenClawStatus(); return status.submoduleExists && status.isInstalled; }); } /** * Provider-related IPC handlers */ function registerProviderHandlers(): void { // Check if encryption is available ipcMain.handle('provider:encryptionAvailable', () => { return isEncryptionAvailable(); }); // Get all providers with key info ipcMain.handle('provider:list', async () => { return await getAllProvidersWithKeyInfo(); }); // Get a specific provider ipcMain.handle('provider:get', async (_, providerId: string) => { return await getProvider(providerId); }); // Save a provider configuration ipcMain.handle('provider:save', async (_, config: ProviderConfig, apiKey?: string) => { try { // Save the provider config await saveProvider(config); // Store the API key if provided if (apiKey) { await storeApiKey(config.id, apiKey); } return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Delete a provider ipcMain.handle('provider:delete', async (_, providerId: string) => { try { await deleteProvider(providerId); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Update API key for a provider ipcMain.handle('provider:setApiKey', async (_, providerId: string, apiKey: string) => { try { await storeApiKey(providerId, apiKey); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Delete API key for a provider ipcMain.handle('provider:deleteApiKey', async (_, providerId: string) => { try { await deleteApiKey(providerId); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Check if a provider has an API key ipcMain.handle('provider:hasApiKey', async (_, providerId: string) => { return await hasApiKey(providerId); }); // Get the actual API key (for internal use only - be careful!) ipcMain.handle('provider:getApiKey', async (_, providerId: string) => { return await getApiKey(providerId); }); // Set default provider ipcMain.handle('provider:setDefault', async (_, providerId: string) => { try { await setDefaultProvider(providerId); return { success: true }; } catch (error) { return { success: false, error: String(error) }; } }); // Get default provider ipcMain.handle('provider:getDefault', async () => { return await getDefaultProvider(); }); // Validate API key by making a real test request to the provider // providerId can be either a stored provider ID or a provider type (e.g., 'openrouter', 'anthropic') ipcMain.handle('provider:validateKey', async (_, providerId: string, apiKey: string) => { try { // First try to get existing provider const provider = await getProvider(providerId); // Use provider.type if provider exists, otherwise use providerId as the type // This allows validation during setup when provider hasn't been saved yet const providerType = provider?.type || providerId; console.log(`Validating API key for provider type: ${providerType}`); return await validateApiKeyWithProvider(providerType, apiKey); } catch (error) { console.error('Validation error:', error); return { valid: false, error: String(error) }; } }); } /** * Validate API key by making a real chat completion API call to the provider * This sends a minimal "hi" message to verify the key works */ async function validateApiKeyWithProvider( providerType: string, apiKey: string ): Promise<{ valid: boolean; error?: string }> { const trimmedKey = apiKey.trim(); if (!trimmedKey) { return { valid: false, error: 'API key is required' }; } try { switch (providerType) { case 'anthropic': return await validateAnthropicKey(trimmedKey); case 'openai': return await validateOpenAIKey(trimmedKey); case 'google': return await validateGoogleKey(trimmedKey); case 'openrouter': return await validateOpenRouterKey(trimmedKey); case 'ollama': // Ollama doesn't require API key validation return { valid: true }; default: // For custom providers, just check the key is not empty return { valid: true }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { valid: false, error: errorMessage }; } } /** * Parse error message from API response */ function parseApiError(data: unknown): string { if (!data || typeof data !== 'object') return 'Unknown error'; // Anthropic format: { error: { message: "..." } } // OpenAI format: { error: { message: "..." } } // Google format: { error: { message: "..." } } const obj = data as { error?: { message?: string; type?: string }; message?: string }; if (obj.error?.message) return obj.error.message; if (obj.message) return obj.message; return 'Unknown error'; } /** * Validate Anthropic API key by making a minimal chat completion request */ async function validateAnthropicKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: 'claude-3-haiku-20240307', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }], }), }); const data = await response.json().catch(() => ({})); if (response.ok) { return { valid: true }; } // Authentication error if (response.status === 401) { return { valid: false, error: 'Invalid API key' }; } // Permission error (invalid key format, etc.) if (response.status === 403) { return { valid: false, error: parseApiError(data) }; } // Rate limit or overloaded - key is valid but service is busy if (response.status === 429 || response.status === 529) { return { valid: true }; } // Model not found or bad request but auth passed - key is valid if (response.status === 400 || response.status === 404) { const errorType = (data as { error?: { type?: string } })?.error?.type; if (errorType === 'authentication_error' || errorType === 'invalid_api_key') { return { valid: false, error: 'Invalid API key' }; } // Other errors like invalid_request_error mean the key is valid return { valid: true }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Validate OpenAI API key by making a minimal chat completion request */ async function validateOpenAIKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model: 'gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }], }), }); const data = await response.json().catch(() => ({})); if (response.ok) { return { valid: true }; } // Authentication error if (response.status === 401) { return { valid: false, error: 'Invalid API key' }; } // Rate limit - key is valid if (response.status === 429) { return { valid: true }; } // Model not found or bad request but auth passed - key is valid if (response.status === 400 || response.status === 404) { const errorCode = (data as { error?: { code?: string } })?.error?.code; if (errorCode === 'invalid_api_key') { return { valid: false, error: 'Invalid API key' }; } return { valid: true }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Validate Google (Gemini) API key by making a minimal generate content request */ async function validateGoogleKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ contents: [{ parts: [{ text: 'hi' }] }], generationConfig: { maxOutputTokens: 1 }, }), } ); const data = await response.json().catch(() => ({})); if (response.ok) { return { valid: true }; } // Authentication error if (response.status === 400 || response.status === 401 || response.status === 403) { const errorStatus = (data as { error?: { status?: string } })?.error?.status; if (errorStatus === 'UNAUTHENTICATED' || errorStatus === 'PERMISSION_DENIED') { return { valid: false, error: 'Invalid API key' }; } // Check if it's actually an auth error const errorMessage = parseApiError(data).toLowerCase(); if (errorMessage.includes('api key') || errorMessage.includes('invalid') || errorMessage.includes('unauthorized')) { return { valid: false, error: parseApiError(data) }; } // Other errors mean key is valid return { valid: true }; } // Rate limit - key is valid if (response.status === 429) { return { valid: true }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Validate OpenRouter API key by making a minimal chat completion request */ async function validateOpenRouterKey(apiKey: string): Promise<{ valid: boolean; error?: string }> { try { // Use a popular free model for validation const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'HTTP-Referer': 'https://clawx.app', 'X-Title': 'ClawX', }, body: JSON.stringify({ model: 'meta-llama/llama-3.2-3b-instruct:free', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }], }), }); const data = await response.json().catch(() => ({})); console.log('OpenRouter validation response:', response.status, JSON.stringify(data)); // Helper to check if error message indicates auth failure const isAuthError = (d: unknown): boolean => { const errorObj = (d as { error?: { message?: string; code?: number | string; type?: string } })?.error; if (!errorObj) return false; const message = (errorObj.message || '').toLowerCase(); const code = errorObj.code; const type = (errorObj.type || '').toLowerCase(); // Check for explicit auth-related errors if (code === 401 || code === '401' || code === 403 || code === '403') return true; if (type.includes('auth') || type.includes('invalid')) return true; if (message.includes('invalid api key') || message.includes('invalid key') || message.includes('unauthorized') || message.includes('authentication') || message.includes('invalid credentials') || message.includes('api key is not valid')) { return true; } return false; }; if (response.ok) { return { valid: true }; } // Always check for auth errors in the response body first if (isAuthError(data)) { // Return user-friendly message instead of raw API errors like "User not found." return { valid: false, error: 'Invalid API key' }; } // Authentication error status codes - always return user-friendly message if (response.status === 401 || response.status === 403) { return { valid: false, error: 'Invalid API key' }; } // Rate limit - key is valid if (response.status === 429) { return { valid: true }; } // Payment required or insufficient credits - key format is valid if (response.status === 402) { return { valid: true }; } // For 400/404, we must be very careful - only consider valid if clearly not an auth issue if (response.status === 400 || response.status === 404) { // If we got here without detecting auth error, it might be a model issue // But be conservative - require explicit success indication const errorObj = (data as { error?: { message?: string; code?: number } })?.error; const message = (errorObj?.message || '').toLowerCase(); // Only consider valid if the error is clearly about the model, not the key if (message.includes('model') && !message.includes('key') && !message.includes('auth')) { return { valid: true }; } // Default to invalid for ambiguous 400/404 errors return { valid: false, error: parseApiError(data) || 'Invalid API key or request' }; } return { valid: false, error: parseApiError(data) || `API error: ${response.status}` }; } catch (error) { return { valid: false, error: `Connection error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Shell-related IPC handlers */ function registerShellHandlers(): void { // Open external URL ipcMain.handle('shell:openExternal', async (_, url: string) => { await shell.openExternal(url); }); // Open path in file explorer ipcMain.handle('shell:showItemInFolder', async (_, path: string) => { shell.showItemInFolder(path); }); // Open path ipcMain.handle('shell:openPath', async (_, path: string) => { return await shell.openPath(path); }); } /** * Dialog-related IPC handlers */ function registerDialogHandlers(): void { // Show open dialog ipcMain.handle('dialog:open', async (_, options: Electron.OpenDialogOptions) => { const result = await dialog.showOpenDialog(options); return result; }); // Show save dialog ipcMain.handle('dialog:save', async (_, options: Electron.SaveDialogOptions) => { const result = await dialog.showSaveDialog(options); return result; }); // Show message box ipcMain.handle('dialog:message', async (_, options: Electron.MessageBoxOptions) => { const result = await dialog.showMessageBox(options); return result; }); } /** * App-related IPC handlers */ function registerAppHandlers(): void { // Get app version ipcMain.handle('app:version', () => { return app.getVersion(); }); // Get app name ipcMain.handle('app:name', () => { return app.getName(); }); // Get app path ipcMain.handle('app:getPath', (_, name: Parameters[0]) => { return app.getPath(name); }); // Get platform ipcMain.handle('app:platform', () => { return process.platform; }); // Quit app ipcMain.handle('app:quit', () => { app.quit(); }); // Relaunch app ipcMain.handle('app:relaunch', () => { app.relaunch(); app.quit(); }); }