Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import 'dotenv/config'
import cors from '@fastify/cors'
import Fastify from 'fastify'
import http from 'http'
import net from 'net'
import { env } from './config/env.js'
import { registerJsonBodyParser } from './lib/jsonBodyParser.js'
import { errorHandler } from './middlewares/errorHandler.js'
import { registerAuthMiddleware } from './middlewares/authMiddleware.js'
import { cursorRoutes } from './routes/cursor.js'
import { voiceRoutes } from './routes/voice.js'
import { harnessShareRoutes } from './routes/harnessShares.js'
import { deskRoutes } from './routes/desk.js'
import { handleObserverUpgrade } from './lib/observerWs.js'
import { deviceAuthRoutes } from './routes/deviceAuth.js'
import { healthRoutes, authRoutes, userRoutes, machineRoutes, planRoutes, gridRoutes, deviceRoutes, mobileRoutes, appRoutes, analyticsRoutes, agentRouteRoutes, storeRoutes } from './routes/index.js'
import { startSubdomainProxy, startMeshProxy } from './lib/subdomainProxy.js'
import { handleDeviceUpgrade } from './lib/deviceWs.js'
import { handleWebUpgrade } from './lib/webWs.js'
import { handleManagerUpgrade } from './lib/managerWs.js'
import { handleAdapterUpgrade } from './lib/adapterWs.js'
import { logger } from './utils/logger.js'
import { startTurnCredentialRefresh } from './lib/turnCredentials.js'
import { closeBus, redisSsoProfileStore } from './lib/bus.js'
import { useSharedSsoProfileStore } from './lib/ssoAuth.js'
import { drainAllSockets, openSocketCount, RELEASE_UPGRADE_SLOT, type SlotSocket } from './lib/wsServer.js'
// Node ≥ 15 turns an unhandled rejection into a process exit. On a cluster worker that holds thousands of
// sockets, one DB/Redis hiccup inside a fire-and-forget promise would then drop every one of them and
// leave their presence keys as ghosts until TTL. Log it instead; `fireAndForget` (utils/async.ts) is the
// per-call-site fix, this is the net under it.
process.on('unhandledRejection', (reason) => {
logger.error('unhandledRejection', reason instanceof Error ? reason : new Error(String(reason)))
})
// Handshakes in flight: each one holds a raw socket AND an outbound SSO profile call (5s timeout) before
// we know whether it is legitimate. Without a ceiling, a flood of junk tokens is a cheap way to pin file
// descriptors and outbound HTTP on this worker. Over the cap → 503 + Retry-After; a healthy client's
// reconnect backoff handles that fine.
const MAX_PENDING_UPGRADES = 500
const UPGRADE_HANDSHAKE_TIMEOUT_MS = 10_000
let pendingUpgrades = 0
// One http.Server fronts everything. Inverted transport: there is NO data-plane HTTP reverse-proxy
// anymore (web + device data ride the hub WS). HTTP is entirely the Fastify control API
// (auth / users / agents / health); WS upgrades are routed below (web-ws / manager-ws / commander-ws / voice).
const app = Fastify({
logger: false,
requestIdLogLabel: 'reqId',
connectionTimeout: 120000,
keepAliveTimeout: 72000,
serverFactory: (handler) => {
const server = http.createServer((req, res) => {
handler(req, res)
})
server.on('upgrade', (req, socket, head) => {
// Interactive low-latency channels (terminal PTY bytes, voice frames, control chat) — disable
// Nagle's algorithm so small writes aren't held back up to ~40ms waiting to coalesce with more
// data. Nothing in this codebase called setNoDelay() before, so every socket here defaulted to
// Nagle-enabled.
if (socket instanceof net.Socket) socket.setNoDelay(true)
if (pendingUpgrades >= MAX_PENDING_UPGRADES) {
try { socket.write('HTTP/1.1 503 Service Unavailable\r\nRetry-After: 2\r\nConnection: close\r\nContent-Length: 0\r\n\r\n') } catch { /* ignore */ }
socket.destroy()
return
}
pendingUpgrades++
// The slot covers only the PRE-auth handshake (the expensive, unauthenticated phase: an open socket
// plus one outbound SSO call). It is released the instant that phase settles — on a successful
// upgrade (createWss fires RELEASE_UPGRADE_SLOT from the wss 'connection' event) or on the socket
// dying (auth-fail destroy / ws.close) — NOT held for the connection's life or a fixed window, so a
// burst of completed reconnects never starves new handshakes. The timer is only a backstop for a
// handshake that neither completes nor closes (a hung peer mid-negotiation).
let released = false
const release = (): void => { if (!released) { released = true; pendingUpgrades-- } }
;(socket as SlotSocket)[RELEASE_UPGRADE_SLOT] = release
socket.once('close', release)
setTimeout(release, UPGRADE_HANDSHAKE_TIMEOUT_MS).unref()
// A handshake that never completes (auth call hung, peer went silent) must not hold the socket.
// `ws` resets the timeout to 0 once it completes the upgrade, so this only ever fires pre-upgrade.
if (socket instanceof net.Socket) socket.setTimeout(UPGRADE_HANDSHAKE_TIMEOUT_MS, () => socket.destroy())
const path = (req.url ?? '').split('?')[0]
if (path === '/api/observer-ws') {
handleObserverUpgrade(req, socket, head)
return
}
// Inverted-transport hub endpoints (terminated here, not proxied):
// /api/web-ws — web clients (replaces the old proxied /proxy/api/ws)
// /api/manager-ws — managers dial in and multiplex all their agents
if (path === '/api/web-ws') {
handleWebUpgrade(req, socket, head)
return
}
if (path === '/api/manager-ws') {
handleManagerUpgrade(req, socket, head)
return
}
// Remote-machine adapters (machine-adapter CLI) dial in and play the node role for their agent.
if (path === '/api/adapter-ws') {
handleAdapterUpgrade(req, socket, head)
return
}
// Device WS: TERMINATE + relay (per-user; we sniff voice frames for STT). Auth is the owner's
// SSO access token as the first subprotocol, plus a required `?computer=` id.
if (path === '/api/device-ws') {
handleDeviceUpgrade(req, socket, head)
return
}
// Inverted transport: all WS is now terminated above (web-ws / manager-ws / commander-ws / voice).
// Nothing is reverse-proxied over WS anymore (REST still rides /proxy → handleProxyRequest).
socket.destroy()
})
return server
},
})
async function start(): Promise<void> {
// Mint the Cloudflare TURN credential before the first socket lands; no-op when TURN is unconfigured.
startTurnCredentialRefresh()
app.setErrorHandler(errorHandler)
registerJsonBodyParser(app)
app.addHook('onSend', async (_request, reply) => {
reply.header('X-Content-Type-Options', 'nosniff')
reply.header('Referrer-Policy', 'strict-origin-when-cross-origin')
if (env.NODE_ENV === 'production') {
reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
}
})
// Allow all origins (reflect the request origin so credentialed requests work too). Methods +
// headers are reflected (allowedHeaders omitted → echoes Access-Control-Request-Headers), so any
// client header (Authorization, x-api-key, …) passes preflight.
await app.register(cors, {
origin: true,
credentials: true,
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'],
})
// Every process shares one memory of validated tokens; without it each worker asks the profile API
// for itself. Attached before anything can authenticate — the REST gate below and the socket upgrades.
useSharedSsoProfileStore(redisSsoProfileStore)
// SSO access-token gate for the control API (data-plane auth happens before Fastify).
registerAuthMiddleware(app)
await app.register(healthRoutes)
await app.register(authRoutes)
await app.register(userRoutes)
await app.register(machineRoutes)
await app.register(planRoutes)
await app.register(gridRoutes) // the account's private harness grid name
await app.register(deviceRoutes)
await app.register(mobileRoutes)
await app.register(appRoutes)
await app.register(analyticsRoutes)
await app.register(agentRouteRoutes) // which agent a task belongs to — the prompt lives here, not in the CLI
await app.register(cursorRoutes) // standalone STT + LLM for the Cursor desktop app (self-contained)
// The cabled dial's transcription, authenticated by the caller's SSO token rather than a shared secret.
// Absent from the auth middleware's skip-list on purpose: that absence IS the gate.
await app.register(voiceRoutes)
await app.register(deviceAuthRoutes) // device-authorization grant: how the desktop app gets a machine key
await app.register(storeRoutes) // the Harness Store's ratings and reviews; the catalogue is the CLI's registry
await app.register(harnessShareRoutes)
await app.register(deskRoutes) // the account's tabs, the same on every computer (lib/desk.ts)
// Dedicated public subdomain app-proxy on its own port (Host-header routed → tunnelled to the node app).
const appProxyServer = startSubdomainProxy()
// Internal mesh listener (Phase 2, MESH_ENABLED) — peers forward here when this instance owns the socket.
const meshServer = startMeshProxy()
let shuttingDown = false
const shutdown = async (signal: string, exitCode = 0) => {
if (shuttingDown) return
shuttingDown = true
logger.info(`Received ${signal}, shutting down...`, { openSockets: openSocketCount() })
try {
appProxyServer.close()
meshServer?.close()
// Start Fastify's close (it tells the http server to stop accepting new connections) but DON'T await
// it yet: http.Server#close only resolves once every connection has ended, and an upgraded WebSocket
// is a connection that never ends on its own — so awaiting here would hang until clients happened to
// disconnect, and the drain below would never run.
const fastifyClosed = app.close().catch((err) => logger.error('fastify close error', err))
// Now end the WebSockets (invisible to http.Server#close), which is what lets fastifyClosed settle.
await drainAllSockets()
// Sockets terminated at the end of the drain fire their 'close' handler on the next tick, and those
// handlers clear presence/owner keys as fire-and-forget Redis writes. Let them land before closeBus
// quits the connection, else a half-open socket's key survives on TTL alone.
await new Promise((resolve) => setTimeout(resolve, 250))
// Bounded wait for Fastify to finish (onClose hooks, Prisma disconnect); never block shutdown on it.
await Promise.race([fastifyClosed, new Promise((resolve) => setTimeout(resolve, 5_000))])
await closeBus()
process.exit(exitCode)
} catch (err) {
logger.error('Error during shutdown', err)
process.exit(1)
}
}
process.on('SIGINT', () => void shutdown('SIGINT'))
process.on('SIGTERM', () => void shutdown('SIGTERM'))
// Truly unrecoverable: drain politely so clients reconnect elsewhere, then let PM2/k8s restart us.
process.on('uncaughtException', (err) => {
logger.error('uncaughtException — draining', err)
void shutdown('uncaughtException', 1)
setTimeout(() => process.exit(1), 8_000).unref()
})
await app.listen({ port: env.PORT, host: '0.0.0.0' })
logger.info(`backend listening on ${env.PORT}`)
}
start().catch((err) => {
logger.error('Failed to start backend', err)
process.exit(1)
})
710fbeaaeThis file is pinned to the selected release. Download the original to open it in your design tool.