import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; export const registerOpenCodeProxy = (app, deps) => { const { fs, os, path, OPEN_CODE_READY_GRACE_MS, getRuntime, getOpenCodeAuthHeaders, buildOpenCodeUrl, ensureOpenCodeApiPrefix, } = deps; if (app.get('opencodeProxyConfigured')) { return; } const runtime = getRuntime(); if (runtime.openCodePort) { console.log(`Setting up proxy to OpenCode on port ${runtime.openCodePort}`); } else { console.log('Setting up OpenCode API gate (OpenCode not started yet)'); } app.set('opencodeProxyConfigured', true); // Ensure API prefix is detected before proxying app.use('/api', (_req, _res, next) => { ensureOpenCodeApiPrefix(); next(); }); // Readiness gate — return 503 while OpenCode is starting/restarting app.use('/api', (req, res, next) => { if ( req.path.startsWith('/themes/custom') || req.path.startsWith('/push') || req.path.startsWith('/config/agents') || req.path.startsWith('/config/opencode-resolution') || req.path.startsWith('/config/settings') || req.path.startsWith('/config/skills') || req.path === '/config/reload' || req.path === '/health' ) { return next(); } const runtimeState = getRuntime(); const waitElapsed = runtimeState.openCodeNotReadySince === 0 ? 0 : Date.now() - runtimeState.openCodeNotReadySince; const stillWaiting = (!runtimeState.isOpenCodeReady && (runtimeState.openCodeNotReadySince === 0 || waitElapsed < OPEN_CODE_READY_GRACE_MS)) || runtimeState.isRestartingOpenCode || !runtimeState.openCodePort; if (stillWaiting) { return res.status(503).json({ error: 'OpenCode is restarting', restarting: true, }); } next(); }); // Windows: session merge for cross-directory session listing if (process.platform === 'win32') { app.get('/api/session', async (req, res, next) => { const rawUrl = req.originalUrl || req.url || ''; if (rawUrl.includes('directory=')) return next(); try { const authHeaders = getOpenCodeAuthHeaders(); const fetchOpts = { method: 'GET', headers: { Accept: 'application/json', ...authHeaders }, signal: AbortSignal.timeout(10000), }; const globalRes = await fetch(buildOpenCodeUrl('/session', ''), fetchOpts); const globalPayload = globalRes.ok ? await globalRes.json().catch(() => []) : []; const globalSessions = Array.isArray(globalPayload) ? globalPayload : []; const settingsPath = path.join(os.homedir(), '.config', 'openchamber', 'settings.json'); let projectDirs = []; try { const settingsRaw = fs.readFileSync(settingsPath, 'utf8'); const settings = JSON.parse(settingsRaw); projectDirs = (settings.projects || []) .map((project) => (typeof project?.path === 'string' ? project.path.trim() : '')) .filter(Boolean); } catch { } const seen = new Set( globalSessions .map((session) => (session && typeof session.id === 'string' ? session.id : null)) .filter((id) => typeof id === 'string') ); const extraSessions = []; for (const dir of projectDirs) { const candidates = Array.from(new Set([ dir, dir.replace(/\\/g, '/'), dir.replace(/\//g, '\\'), ])); for (const candidateDir of candidates) { const encoded = encodeURIComponent(candidateDir); try { const dirRes = await fetch(buildOpenCodeUrl(`/session?directory=${encoded}`, ''), fetchOpts); if (dirRes.ok) { const dirPayload = await dirRes.json().catch(() => []); const dirSessions = Array.isArray(dirPayload) ? dirPayload : []; for (const session of dirSessions) { const id = session && typeof session.id === 'string' ? session.id : null; if (id && !seen.has(id)) { seen.add(id); extraSessions.push(session); } } } } catch { } } } const merged = [...globalSessions, ...extraSessions]; merged.sort((a, b) => { const aTime = a && typeof a.time_updated === 'number' ? a.time_updated : 0; const bTime = b && typeof b.time_updated === 'number' ? b.time_updated : 0; return bTime - aTime; }); console.log(`[SessionMerge] ${globalSessions.length} global + ${extraSessions.length} extra = ${merged.length} total`); return res.json(merged); } catch (error) { console.log(`[SessionMerge] Error: ${error.message}, falling through`); next(); } }); } // http-proxy-middleware handles SSE, large bodies, timeouts correctly const apiProxy = createProxyMiddleware({ target: `http://127.0.0.1:${runtime.openCodePort || 3902}`, changeOrigin: true, pathRewrite: { '^/api': '' }, // Dynamic target — port can change after restart router: () => { const rt = getRuntime(); return `http://127.0.0.1:${rt.openCodePort || 3902}`; }, on: { proxyReq: (proxyReq) => { // Inject OpenCode auth headers const authHeaders = getOpenCodeAuthHeaders(); if (authHeaders.Authorization) { proxyReq.setHeader('Authorization', authHeaders.Authorization); } // Defensive: request identity encoding from upstream OpenCode. // This avoids compressed-body/header mismatches in multi-proxy setups. proxyReq.setHeader('accept-encoding', 'identity'); }, error: (err, _req, res) => { console.error('[proxy] OpenCode proxy error:', err.message); if (res && !res.headersSent && typeof res.status === 'function') { res.status(503).json({ error: 'OpenCode service unavailable' }); } }, }, }); app.use('/api', apiProxy); };