From 688fcedd695f6952ee868ec252e8b6fdab87663a Mon Sep 17 00:00:00 2001 From: Vishnu Date: Tue, 9 Jun 2026 22:03:43 +0530 Subject: [PATCH] feat: add API Key authentication for REST and WebSocket servers --- .gitignore | 5 ++++ cli/proxima-cli.cjs | 39 ++++++++++++++++++++++++-- electron/main-v2.cjs | 17 ++++++++++-- electron/rest-api.cjs | 63 ++++++++++++++++++++++++++++++------------ electron/ws-server.cjs | 13 ++++++++- 5 files changed, 114 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index ae17432..a0254df 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,8 @@ cookies-backup/ # Dev audit files Version_4.1.0.txt installer.nsh + +# Local developer agents and graph output +.agents/ +graphify-out/ +graphify/ diff --git a/cli/proxima-cli.cjs b/cli/proxima-cli.cjs index 29afac5..4857a3e 100644 --- a/cli/proxima-cli.cjs +++ b/cli/proxima-cli.cjs @@ -18,6 +18,35 @@ const API_HOST = process.env.PROXIMA_HOST || '127.0.0.1'; const API_PORT = parseInt(process.env.PROXIMA_PORT) || 3210; const API_BASE = `http://${API_HOST}:${API_PORT}`; +// Helper to get the API key dynamically from settings.json or environment +function getApiKey() { + if (process.env.PROXIMA_API_KEY) { + return process.env.PROXIMA_API_KEY; + } + const homedir = require('os').homedir(); + const platform = process.platform; + let appDataDir = ''; + if (platform === 'win32') { + appDataDir = process.env.APPDATA || path.join(homedir, 'AppData', 'Roaming'); + } else if (platform === 'darwin') { + appDataDir = path.join(homedir, 'Library', 'Application Support'); + } else { + appDataDir = process.env.XDG_CONFIG_HOME || path.join(homedir, '.config'); + } + const settingsPath = path.join(appDataDir, 'proxima', 'settings.json'); + try { + if (fs.existsSync(settingsPath)) { + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + if (settings && settings.apiKey) { + return settings.apiKey; + } + } + } catch (e) { + // Ignore read errors + } + return null; +} + // ─── Colors (ANSI) ────────────────────────────────── const c = { reset: '\x1b[0m', @@ -47,12 +76,18 @@ function red(text) { return colorize(c.red, text); } function apiRequest(method, path, body = null) { return new Promise((resolve, reject) => { const url = new URL(path, API_BASE); + const headers = { 'Content-Type': 'application/json' }; + const key = getApiKey(); + if (key) { + headers['Authorization'] = `Bearer ${key}`; + } + const options = { hostname: url.hostname, port: url.port, - path: url.pathname, + path: url.pathname + url.search, method, - headers: { 'Content-Type': 'application/json' }, + headers, timeout: 120000 // 2 min timeout for slow providers }; diff --git a/electron/main-v2.cjs b/electron/main-v2.cjs index bbd0e9c..e1282c1 100644 --- a/electron/main-v2.cjs +++ b/electron/main-v2.cjs @@ -3,6 +3,7 @@ const { app, BrowserWindow, ipcMain, shell, session, clipboard } = require('electron'); const path = require('path'); const fs = require('fs'); +const crypto = require('crypto'); const net = require('net'); const BrowserManager = require('./browser-manager.cjs'); const { initRestAPI, startRestAPI, stopRestAPI, isRestAPIRunning } = require('./rest-api.cjs'); @@ -74,12 +75,21 @@ function loadSettings() { try { if (fs.existsSync(settingsPath)) { const saved = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - return { ...defaultSettings, ...saved }; + const settings = { ...defaultSettings, ...saved }; + if (!settings.apiKey) { + settings.apiKey = crypto.randomBytes(24).toString('hex'); + saveSettings(settings); + } + return settings; } } catch (e) { console.error('Error loading settings:', e); } - return defaultSettings; + const settings = { ...defaultSettings, apiKey: crypto.randomBytes(24).toString('hex') }; + try { + saveSettings(settings); + } catch(e) {} + return settings; } function saveSettings(settings) { @@ -396,6 +406,9 @@ function createWindow() { const s = loadSettings(); return Object.entries(s.providers) .filter(([_, c]) => c.enabled).map(([n]) => n); + }, + getApiKey: () => { + return loadSettings().apiKey; } }); // Only start if enabled (default: false — user must enable it) diff --git a/electron/rest-api.cjs b/electron/rest-api.cjs index da7317f..bb42a0b 100644 --- a/electron/rest-api.cjs +++ b/electron/rest-api.cjs @@ -36,6 +36,7 @@ const MODEL_ALIASES = { // ─── State ─────────────────────────────────────────────── let handleMCPRequest = null; let getEnabledProvidersList = null; +let getApiKey = null; let httpServer = null; // ─── Response Time Tracking ────────────────────────────── @@ -95,6 +96,7 @@ function getFormattedStats() { function initRestAPI(config) { handleMCPRequest = config.handleMCPRequest; getEnabledProvidersList = config.getEnabledProviders; + getApiKey = config.getApiKey; } // ─── Helpers ───────────────────────────────────────────── @@ -118,9 +120,17 @@ function parseBody(req) { } function sendJSON(res, code, data) { + const origin = res.req && res.req.headers ? res.req.headers.origin : null; + let allowedOrigin = '*'; + if (origin) { + const isLocal = origin.startsWith('http://localhost:') || + origin.startsWith('http://127.0.0.1:') || + origin.startsWith('chrome-extension://'); + allowedOrigin = isLocal ? origin : 'null'; + } res.writeHead(code, { 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Origin': allowedOrigin, 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'X-Powered-By': 'Proxima AI' @@ -368,14 +378,15 @@ function getChatHTML(accentColor = '#22c55e') { `; } -function getChatJS() { +function getChatJS(apiKey) { + const key = apiKey || (typeof getApiKey === 'function' ? getApiKey() : ''); return `