@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
JavaScript
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
}
}