UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

177 lines (176 loc) 7.98 kB
import express from "express"; import fs from "fs"; import http from "http"; import path from "path"; import { fileURLToPath } from "url"; import { setupWebSocket } from "./voiceWebSocketHandler.js"; import { timingSafeEqualString } from "./tokenCompare.js"; import { NeuroLink } from "../../neurolink.js"; import { logger } from "../../utils/logger.js"; import { withTimeout } from "../../utils/async/withTimeout.js"; import { getCartesiaWsUrl } from "../../adapters/tts/cartesiaHandler.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Resolve the public/ directory containing static assets. * The CLI build (tsc) only emits .ts → .js and does NOT copy non-TS assets, * so __dirname/public may not exist when running from dist/. * Fall back to the original source path in that case. */ function resolvePublicPath() { const compiled = path.join(__dirname, "public"); if (fs.existsSync(compiled)) { return compiled; } // Resolve from project root → src/lib/server/voice/public const source = path.resolve(__dirname, "../../../../src/lib/server/voice/public"); if (fs.existsSync(source)) { return source; } return compiled; // let express.static handle the 404 } export async function startVoiceServer(port = 3000) { const app = express(); // NEW11: refuse to bind to non-loopback interfaces unless the operator // has explicitly opted in. The voice server has minimal hardening and // exposing it publicly without a token leaks Soniox / Cartesia / LLM // credit usage to anyone who can reach the listener. const allowPublic = process.env.VOICE_SERVER_ALLOW_PUBLIC === "1"; const host = allowPublic ? (process.env.VOICE_SERVER_HOST ?? "0.0.0.0") : "127.0.0.1"; // NEW11: optional shared-secret bearer token for both HTTP and WebSocket // upgrade. When VOICE_SERVER_AUTH_TOKEN is set, every HTTP request must // carry `Authorization: Bearer <token>`. The WS upgrade additionally // accepts `?token=<token>` because browser WebSocket constructors cannot // set custom headers — see voiceWebSocketHandler.verifyClient. HTTP routes // intentionally reject `?token=` (would leak via Referer + access logs). const authToken = process.env.VOICE_SERVER_AUTH_TOKEN; /* ---------- BODY LIMITS + AUTH ---------- */ // NEW11: cap JSON / urlencoded body to 100kb. Express's default is 100kb // for json() but only when explicitly registered; without this any future // body parser would default to whatever its own limit is. app.use(express.json({ limit: "100kb" })); app.use(express.urlencoded({ limit: "100kb", extended: false })); // NEW11: minimal HTTP auth middleware. Skips when no token is configured // (back-compat — local-only dev keeps working). Skips for /health so // load-balancers can probe without credentials. if (authToken) { app.use((req, res, next) => { if (req.path === "/health") { return next(); } const header = req.header("authorization"); // Bug 3 fix: HTTP routes only accept the bearer header. The `?token=` // fallback exists only on the WS upgrade where the browser API cannot // attach headers — using it on regular HTTP would leak credentials via // Referer headers, browser history, server access logs, and proxies. const provided = header?.startsWith("Bearer ") ? header.slice(7) : undefined; if (!provided || !timingSafeEqualString(provided, authToken)) { res.status(401).json({ error: "Unauthorized" }); return; } next(); }); } /* ---------- STATIC FILES ---------- */ const publicPath = resolvePublicPath(); logger.info("[SERVER] Serving static from:", publicPath); app.use(express.static(publicPath)); app.get("/", (_, res) => { res.sendFile(path.join(publicPath, "index.html")); }); /* ---------- HEALTH CHECK ---------- */ app.get("/health", (_, res) => { res.json({ status: "ok" }); }); /* ---------- ERROR HANDLER ---------- */ // NEW11: global Express error handler so synchronous and async errors are // caught instead of crashing the process or leaking stack traces. app.use((err, _req, res, _next) => { logger.error(`[SERVER] Unhandled error: ${err instanceof Error ? err.message : String(err)}`); if (!res.headersSent) { res.status(500).json({ error: "Internal server error" }); } }); const server = http.createServer(app); /* ---------- WS ---------- */ // NEW11: pass the auth token + allow-public flag through to the WS handler // so it can verify clients on upgrade and apply maxPayload caps. setupWebSocket(server, { authToken, maxPayload: 1_048_576 }); /* ---------- START ---------- */ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, host, () => { server.removeListener("error", reject); const exposure = allowPublic ? `bound publicly on ${host}:${port} (VOICE_SERVER_ALLOW_PUBLIC=1)` : `bound to loopback ${host}:${port} (set VOICE_SERVER_ALLOW_PUBLIC=1 to expose externally)`; logger.info(`[SERVER] Voice server running — ${exposure}${authToken ? " (auth required)" : " (no auth — token via VOICE_SERVER_AUTH_TOKEN recommended)"}`); resolve(); }); }); /* ---------- WARMUP ---------- */ // Pre-warm NeuroLink + Azure on startup so the first real user request isn't // slow. NeuroLink's MCP init + Azure's connection pool both have cold-start // overhead that shows up as 3-4s on the very first call. We also open and // immediately close a Cartesia WS to prime the TLS handshake. warmup().catch((err) => { logger.warn("[WARMUP] Failed (non-fatal):", err.message); }); } async function warmup() { const t = Date.now(); logger.info("[WARMUP] Warming up LLM + TTS..."); const neurolink = new NeuroLink(); const provider = process.env.VOICE_LLM_PROVIDER ?? "azure"; const model = process.env.VOICE_LLM_MODEL ?? "gpt-4o-automatic"; try { const result = await withTimeout(neurolink.stream({ provider, model, input: { text: "hi" }, maxTokens: 3, disableTools: true, enableAnalytics: false, enableEvaluation: false, }), 15000, "LLM warmup timed out"); // Drain the stream so the connection is fully exercised. for await (const _chunk of result.stream) { /* drain */ } logger.info(`[WARMUP] LLM warmup done in ${Date.now() - t}ms`); } catch (err) { logger.warn("[WARMUP] LLM warmup failed (non-fatal):", err.message); } // Cartesia TLS warmup — open WS, wait for connect, then close. try { const { default: WebSocket } = await import("ws"); const apiKey = process.env.CARTESIA_API_KEY; await new Promise((resolve) => { const ws = new WebSocket(getCartesiaWsUrl(), { headers: apiKey ? { "X-API-Key": apiKey } : undefined, }); const timeout = setTimeout(() => { ws.terminate(); resolve(); // non-fatal, just move on }, 5000); ws.once("open", () => { clearTimeout(timeout); ws.close(); resolve(); }); ws.once("error", () => { clearTimeout(timeout); resolve(); // non-fatal }); }); logger.info(`[WARMUP] Cartesia warmup done in ${Date.now() - t}ms`); } catch { // non-fatal } }