import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' function healthCheckPlugin() { return { name: 'health-check-proxy', configureServer(server) { server.middlewares.use('/api/health', async (req, res) => { const params = new URL(req.url, 'http://localhost').searchParams; const url = params.get('url'); const mode = params.get('mode'); // 'tcp' for raw TCP port check if (!url) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing url param' })); return; } const start = Date.now(); const parsedUrl = new URL(url); try { if (mode === 'tcp') { // TCP socket connect check for non-HTTP services (e.g. Wyoming) const { default: net } = await import('net'); await new Promise((resolve, reject) => { const socket = net.createConnection( { host: parsedUrl.hostname, port: parseInt(parsedUrl.port), timeout: 5000 }, () => { socket.destroy(); resolve(); } ); socket.on('error', reject); socket.on('timeout', () => { socket.destroy(); reject(new Error('timeout')); }); }); } else { // HTTP/HTTPS health check const { default: https } = await import('https'); const { default: http } = await import('http'); const client = parsedUrl.protocol === 'https:' ? https : http; await new Promise((resolve, reject) => { const reqObj = client.get(url, { rejectUnauthorized: false, timeout: 5000 }, (resp) => { resp.resume(); resolve(); }); reqObj.on('error', reject); reqObj.on('timeout', () => { reqObj.destroy(); reject(new Error('timeout')); }); }); } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'online', responseTime: Date.now() - start })); } catch { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'offline', responseTime: null })); } }); // Service restart — runs launchctl or docker restart server.middlewares.use('/api/service/restart', async (req, res) => { if (req.method === 'OPTIONS') { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'Content-Type' }); res.end(); return; } if (req.method !== 'POST') { res.writeHead(405); res.end(); return; } try { const chunks = []; for await (const chunk of req) chunks.push(chunk); const { type, id } = JSON.parse(Buffer.concat(chunks).toString()); if (!type || !id) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'Missing type or id' })); return; } // Whitelist valid service IDs to prevent command injection const ALLOWED_LAUNCHD = [ 'gui/501/com.homeai.ollama', 'gui/501/com.homeai.openclaw', 'gui/501/com.homeai.openclaw-bridge', 'gui/501/com.homeai.wyoming-stt', 'gui/501/com.homeai.wyoming-tts', 'gui/501/com.homeai.wyoming-satellite', 'gui/501/com.homeai.dashboard', ]; const ALLOWED_DOCKER = [ 'homeai-open-webui', 'homeai-uptime-kuma', 'homeai-n8n', 'homeai-code-server', ]; let cmd; if (type === 'launchd' && ALLOWED_LAUNCHD.includes(id)) { cmd = ['launchctl', 'kickstart', '-k', id]; } else if (type === 'docker' && ALLOWED_DOCKER.includes(id)) { cmd = ['docker', 'restart', id]; } else { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'Service not in allowed list' })); return; } const { execFile } = await import('child_process'); const { promisify } = await import('util'); const execFileAsync = promisify(execFile); const { stdout, stderr } = await execFileAsync(cmd[0], cmd.slice(1), { timeout: 30000 }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, stdout: stdout.trim(), stderr: stderr.trim() })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: err.message })); } }); }, }; } function bridgeProxyPlugin() { return { name: 'bridge-proxy', configureServer(server) { // Proxy a request to the OpenClaw bridge const proxyRequest = (targetPath) => async (req, res) => { if (req.method === 'OPTIONS') { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }) res.end() return } try { const { default: http } = await import('http') const chunks = [] for await (const chunk of req) chunks.push(chunk) const body = Buffer.concat(chunks) await new Promise((resolve, reject) => { const proxyReq = http.request( `http://localhost:8081${targetPath}`, { method: req.method, headers: { 'Content-Type': req.headers['content-type'] || 'application/json', 'Content-Length': body.length, }, timeout: 120000, }, (proxyRes) => { res.writeHead(proxyRes.statusCode, { 'Content-Type': proxyRes.headers['content-type'] || 'application/json', 'Access-Control-Allow-Origin': '*', }) proxyRes.pipe(res) proxyRes.on('end', resolve) proxyRes.on('error', resolve) } ) proxyReq.on('error', reject) proxyReq.on('timeout', () => { proxyReq.destroy() reject(new Error('timeout')) }) proxyReq.write(body) proxyReq.end() }) } catch { if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: 'Bridge unreachable' })) } } } server.middlewares.use('/api/agent/message', proxyRequest('/api/agent/message')) server.middlewares.use('/api/tts', proxyRequest('/api/tts')) server.middlewares.use('/api/stt', proxyRequest('/api/stt')) }, } } export default defineConfig({ plugins: [ healthCheckPlugin(), bridgeProxyPlugin(), tailwindcss(), react(), ], server: { host: '0.0.0.0', port: 5173, }, })