Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
39 changes: 37 additions & 2 deletions cli/proxima-cli.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
};

Expand Down
17 changes: 15 additions & 2 deletions electron/main-v2.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
63 changes: 45 additions & 18 deletions electron/rest-api.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const MODEL_ALIASES = {
// ─── State ───────────────────────────────────────────────
let handleMCPRequest = null;
let getEnabledProvidersList = null;
let getApiKey = null;
let httpServer = null;

// ─── Response Time Tracking ──────────────────────────────
Expand Down Expand Up @@ -95,6 +96,7 @@ function getFormattedStats() {
function initRestAPI(config) {
handleMCPRequest = config.handleMCPRequest;
getEnabledProvidersList = config.getEnabledProviders;
getApiKey = config.getApiKey;
}

// ─── Helpers ─────────────────────────────────────────────
Expand All @@ -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'
Expand Down Expand Up @@ -368,14 +378,15 @@ function getChatHTML(accentColor = '#22c55e') {
</div>`;
}

function getChatJS() {
function getChatJS(apiKey) {
const key = apiKey || (typeof getApiKey === 'function' ? getApiKey() : '');
return `
<script>
let ws=null,reqTimer=null,reqStart=0,battleMode=false,battleId=0,battleResults={};
const msgArea=document.getElementById('chat-messages'),input=document.getElementById('chat-input'),statusEl=document.getElementById('ws-status'),connectBtn=document.getElementById('ws-connect-btn'),timerEl=document.getElementById('ws-timer');
function setStatus(t,c){statusEl.textContent=t;const r=c==='#22c55e'?'34,197,94':c==='#f97316'?'249,115,22':'239,68,68';statusEl.style.background='rgba('+r+',.1)';statusEl.style.color=c;statusEl.style.borderColor='rgba('+r+',.15)';}
function toggleWS(){if(ws&&ws.readyState===1){ws.close();return;}connectWS();}
function connectWS(){try{ws=new WebSocket('ws://localhost:${REST_PORT}/ws');}catch(e){addSystem('Connection failed');return;}
function connectWS(){try{ws=new WebSocket('ws://localhost:${REST_PORT}/ws?apiKey=${key}');}catch(e){addSystem('Connection failed');return;}
setStatus('Connecting...','#f97316');connectBtn.textContent='Connecting...';
ws.onopen=()=>{setStatus('Connected','#22c55e');connectBtn.textContent='Disconnect';connectBtn.style.background='rgba(239,68,68,.15)';connectBtn.style.color='#ef4444';connectBtn.style.borderColor='rgba(239,68,68,.25)';input.focus();};
ws.onmessage=(e)=>{handleMsg(JSON.parse(e.data));};
Expand Down Expand Up @@ -404,7 +415,7 @@ function getChatJS() {
}

// ─── API Docs HTML ───────────────────────────────────────
function getDocsPage() {
function getDocsPage(apiKey) {
const enabled = getEnabled();
const s = getFormattedStats();

Expand Down Expand Up @@ -578,13 +589,13 @@ POST /v1/chat/completions

<div class="foot">Proxima API v${VERSION} — Zen4-bit ⚡</div>
</div>
${getChatJS()}
${getChatJS(apiKey)}
</body>
</html>`;
}

// ─── CLI Docs Page ───────────────────────────────────────
function getCLIDocsPage() {
function getCLIDocsPage(apiKey) {
return `<!DOCTYPE html>
<html lang="en">
<head>
Expand Down Expand Up @@ -811,13 +822,13 @@ proxima status

<div class="foot">Proxima CLI v${VERSION} — Zen4-bit ⚡</div>
</div>
${getChatJS()}
${getChatJS(apiKey)}
</body>
</html>`;
}

// ─── WebSocket Docs Page ─────────────────────────────────
function getWSDocsPage() {
function getWSDocsPage(apiKey) {
return `<!DOCTYPE html>
<html lang="en">
<head>
Expand Down Expand Up @@ -901,7 +912,7 @@ ws.send(JSON.stringify({
<div class="cmd"><div class="cmd-name">debate</div><div class="cmd-desc">Multi-provider debate</div></div>
<div class="cmd"><div class="cmd-name">audit</div><div class="cmd-desc">Security vulnerability scan</div></div>
<div class="cmd"><div class="cmd-name">ping</div><div class="cmd-desc">Connection health check</div></div>
<div class="cmd"><div class="cmd-name">stats</div><div class="cmd-desc">Server statistics</div></div>
<div class="cmd"><div class="cmd-name">stats</div><div class="cmd-desc">Server statistics data</div></div>
</div>
</div>

Expand Down Expand Up @@ -1065,7 +1076,7 @@ print(result["content"])</pre>

<div class="foot">Proxima WebSocket v${VERSION} — Zen4-bit ⚡</div>
</div>
${getChatJS()}
${getChatJS(apiKey)}
</body>
</html>`;
}
Expand Down Expand Up @@ -1385,21 +1396,21 @@ End with a security score (0-100).`;

// Docs page
if (method === 'GET' && (pathname === '/' || pathname === '/docs')) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
res.end(getDocsPage());
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(getDocsPage(getApiKey ? getApiKey() : ''));
return;
}

// CLI docs page
if (method === 'GET' && pathname === '/cli') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
res.end(getCLIDocsPage());
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(getCLIDocsPage(getApiKey ? getApiKey() : ''));
return;
}
// WebSocket docs page
if (method === 'GET' && (pathname === '/ws' || pathname === '/websocket')) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
res.end(getWSDocsPage());
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(getWSDocsPage(getApiKey ? getApiKey() : ''));
return;
Comment thread
Vishnu-tppr marked this conversation as resolved.
}

Expand All @@ -1415,8 +1426,16 @@ function startRestAPI() {

httpServer = http.createServer(async (req, res) => {
if (req.method === 'OPTIONS') {
const origin = req.headers.origin;
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(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
Expand All @@ -1425,6 +1444,14 @@ function startRestAPI() {
}

const url = new URL(req.url, `http://localhost:${REST_PORT}`);
const apiKey = getApiKey ? getApiKey() : null;
if (apiKey && !['/', '/docs', '/cli', '/ws', '/websocket', '/api/status'].includes(url.pathname)) {
const authHeader = req.headers.authorization;
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
return sendError(res, 401, 'Unauthorized: Invalid or missing API key', 'unauthorized');
}
}

try {
const body = req.method === 'POST' ? await parseBody(req) : {};
await handleRoute(req.method, url.pathname, body, res);
Expand All @@ -1440,7 +1467,7 @@ function startRestAPI() {

// Init WebSocket on same server
try {
initWebSocket(httpServer, handleMCPRequest, getEnabled);
initWebSocket(httpServer, handleMCPRequest, getEnabled, getApiKey);
console.log(`[API] 🔌 WebSocket ready at ws://localhost:${REST_PORT}/ws`);
} catch (err) {
console.error('[API] WebSocket init failed:', err.message);
Expand Down
13 changes: 12 additions & 1 deletion electron/ws-server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { WebSocketServer } = require('ws');

let handleMCPRequest = null;
let getEnabledProviders = null;
let getApiKey = null;
let wss = null;
const clients = new Map();

Expand All @@ -21,9 +22,10 @@ const wsStats = {
};

// ─── Init ────────────────────────────────────────────
function initWebSocket(httpServer, mcpHandler, enabledProvidersFn) {
function initWebSocket(httpServer, mcpHandler, enabledProvidersFn, apiKeyFn) {
handleMCPRequest = mcpHandler;
getEnabledProviders = enabledProvidersFn || (() => []);
getApiKey = apiKeyFn;
wsStats.startTime = new Date();

wss = new WebSocketServer({ noServer: true });
Expand All @@ -33,6 +35,15 @@ function initWebSocket(httpServer, mcpHandler, enabledProvidersFn) {
const url = new URL(request.url, `http://${request.headers.host}`);

if (url.pathname === '/ws' || url.pathname === '/websocket') {
const apiKey = getApiKey ? getApiKey() : null;
if (apiKey) {
const reqApiKey = url.searchParams.get('apiKey');
if (reqApiKey !== apiKey) {
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
socket.destroy();
return;
}
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
Expand Down