const BOT_ORIGIN = 'http://127.0.0.1:3007'; const WEB_ORIGIN = 'http://127.0.0.1:3008'; function shouldRouteToBot(pathname: string): boolean { if (pathname === '/metrics' || pathname.startsWith('/metrics/')) return true; if (pathname === '/webhook' || pathname.startsWith('/webhook/')) return true; return false; } function buildForwardHeaders(req: Request): Headers { const headers = new Headers(req.headers); try { const proto = headers.get('x-forwarded-proto') || 'https'; const fwdFor = headers.get('x-forwarded-for'); headers.set('x-forwarded-proto', proto); headers.set('x-forwarded-for', fwdFor ? `${fwdFor}, 127.0.0.1` : '127.0.0.1'); const host = headers.get('host') || ''; if (!host) headers.set('host', 'localhost'); } catch {} return headers; } Bun.serve({ port: Number(process.env.PORT || process.env.ROUTER_PORT || 3000), fetch: async (req) => { const url = new URL(req.url); // Health local para el contenedor (evita 404 en healthcheck) if (url.pathname === '/health') { return new Response('ok', { status: 200, headers: { 'content-type': 'text/plain' } }); } const routeToBot = shouldRouteToBot(url.pathname); const targetOrigin = routeToBot ? BOT_ORIGIN : WEB_ORIGIN; const targetUrl = targetOrigin + url.pathname + url.search; const headers = buildForwardHeaders(req); if (!routeToBot) { try { headers.set('accept-encoding', 'identity'); } catch {} } const init: RequestInit = { method: req.method, headers, body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req.body, redirect: 'manual', }; const started = Date.now(); try { const res = await fetch(targetUrl, init); const ms = Date.now() - started; try { console.log(`[proxy] ${req.method} ${url.pathname}${url.search} -> ${routeToBot ? 'bot' : 'web'} ${res.status} (${ms}ms)`); } catch {} // Devuelve la respuesta (incluye Set-Cookie, Location, etc.), asegurando Content-Type en assets por si faltase const passthroughHeaders = new Headers(res.headers); if (!routeToBot) { try { // Forzar respuesta sin compresión hacia el cliente passthroughHeaders.delete('content-encoding'); passthroughHeaders.delete('vary'); passthroughHeaders.delete('content-length'); const cc = passthroughHeaders.get('cache-control'); if (cc && !/no-transform/i.test(cc)) { passthroughHeaders.set('cache-control', cc + ', no-transform'); } else if (!cc) { passthroughHeaders.set('cache-control', 'no-transform'); } } catch {} } if (!passthroughHeaders.get('content-type')) { if (url.pathname.endsWith('.js')) passthroughHeaders.set('content-type', 'application/javascript; charset=utf-8'); if (url.pathname.endsWith('.css')) passthroughHeaders.set('content-type', 'text/css; charset=utf-8'); } return new Response(res.body, { status: res.status, headers: passthroughHeaders }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`[proxy] ${req.method} ${url.pathname}${url.search} -> ERROR: ${msg}`); return new Response(`Proxy error: ${msg}\n`, { status: 502 }); } }, });