import "dotenv/config"; import express from "express"; import { createServer } from "http"; import net from "net"; import { createExpressMiddleware } from "@trpc/server/adapters/express"; import { registerOAuthRoutes } from "./oauth"; import { appRouter } from "../routers"; import { createContext } from "./context"; import { serveStatic, setupVite } from "./vite"; import { seedDefaults } from "../seed"; const GATEWAY_BASE_URL = process.env.GATEWAY_URL ?? "http://localhost:18789"; function isPortAvailable(port: number): Promise { return new Promise(resolve => { const server = net.createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on("error", () => resolve(false)); }); } async function findAvailablePort(startPort: number = 3000): Promise { for (let port = startPort; port < startPort + 20; port++) { if (await isPortAvailable(port)) { return port; } } throw new Error(`No available port found starting from ${startPort}`); } async function startServer() { const app = express(); const server = createServer(app); // Configure body parser with larger size limit for file uploads app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); // OAuth callback under /api/oauth/callback registerOAuthRoutes(app); // ── SSE proxy: POST /api/orchestrator/stream → Go Gateway SSE ────────────── // This proxies the SSE stream from the Go Gateway to the browser. // We need to do it at the Express level because tRPC doesn't support SSE yet. app.post("/api/orchestrator/stream", async (req, res) => { try { const gwRes = await fetch(`${GATEWAY_BASE_URL}/api/orchestrator/stream`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req.body), }); if (!gwRes.ok || !gwRes.body) { res.status(gwRes.status).json({ error: "Gateway stream error" }); return; } // Set SSE headers res.setHeader("Content-Type", "text/event-stream; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("Access-Control-Allow-Origin", "*"); res.flushHeaders(); // Pipe the response body — use a single TextDecoder with stream:true // so multi-byte UTF-8 sequences (Cyrillic, CJK, etc.) are never split const reader = gwRes.body.getReader(); const decoder = new TextDecoder("utf-8"); const pump = async () => { try { while (true) { const { done, value } = await reader.read(); if (done) break; res.write(decoder.decode(value, { stream: true })); // @ts-ignore if (res.flush) (res as any).flush(); } } catch { // Client disconnected } finally { res.end(); } }; // Abort if client disconnects req.on("close", () => reader.cancel()); await pump(); } catch (err: any) { if (!res.headersSent) { res.status(502).json({ error: `Gateway unreachable: ${err.message}` }); } } }); // tRPC API app.use( "/api/trpc", createExpressMiddleware({ router: appRouter, createContext, }) ); // development mode uses Vite, production mode uses static files if (process.env.NODE_ENV === "development") { await setupVite(app, server); } else { serveStatic(app); } const preferredPort = parseInt(process.env.PORT || "3000"); const port = await findAvailablePort(preferredPort); if (port !== preferredPort) { console.log(`Port ${preferredPort} is busy, using port ${port} instead`); } // Run idempotent seed on every startup (no-op if data already exists) await seedDefaults(); server.listen(port, () => { console.log(`Server running on http://localhost:${port}/`); }); } startServer().catch(console.error);