import type { FastifyInstance } from 'fastify'; import WebSocket from 'ws'; function boocoderWsUrl(origin: string, path: string): string { const u = new URL(origin); u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:'; u.pathname = path; u.search = ''; return u.toString(); } /** * Reverse-proxy BooCoder HTTP + WebSocket through BooChat's single origin. * WS must be registered before the HTTP catch-all — fetch() cannot upgrade. */ export function registerCoderProxy(app: FastifyInstance, boocoderOrigin: string): void { app.get<{ Params: { sessionId: string } }>( '/api/coder/ws/sessions/:sessionId', { websocket: true }, (clientSocket, req) => { const sessionId = req.params.sessionId; const target = boocoderWsUrl(boocoderOrigin, `/api/ws/sessions/${sessionId}`); const upstream = new WebSocket(target); upstream.on('open', () => { app.log.debug({ sessionId }, 'coder ws proxy: upstream connected'); }); upstream.on('message', (data, isBinary) => { if (clientSocket.readyState !== clientSocket.OPEN) return; clientSocket.send(data, { binary: isBinary }); }); upstream.on('close', (code, reason) => { if (clientSocket.readyState === clientSocket.OPEN) { clientSocket.close(code, reason.toString()); } }); upstream.on('error', (err) => { app.log.warn({ err, sessionId, target }, 'coder ws proxy: upstream error'); if (clientSocket.readyState === clientSocket.OPEN) { clientSocket.close(1011, 'upstream error'); } }); clientSocket.on('message', (data, isBinary) => { if (upstream.readyState !== WebSocket.OPEN) return; upstream.send(data, { binary: isBinary }); }); clientSocket.on('close', () => { if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) { upstream.close(); } }); clientSocket.on('error', () => { if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) { upstream.close(); } }); }, ); app.all('/api/coder/*', async (req, reply) => { const targetPath = req.url.replace('/api/coder', '/api'); const targetUrl = `${boocoderOrigin}${targetPath}`; const headers: Record = {}; if (req.headers['content-type']) headers['content-type'] = req.headers['content-type'] as string; if (req.headers['authorization']) headers['authorization'] = req.headers['authorization'] as string; try { const res = await fetch(targetUrl, { method: req.method as string, headers, body: req.method !== 'GET' && req.method !== 'HEAD' ? JSON.stringify(req.body) : undefined, }); reply.code(res.status); for (const [key, value] of res.headers) { if (key === 'transfer-encoding') continue; reply.header(key, value); } const body = await res.text(); return reply.send(body); } catch (err) { app.log.error({ err, targetUrl }, 'coder proxy error'); reply.code(502).send({ error: 'boocoder backend unavailable' }); } }); }