import { randomBytes } from 'node:crypto'; import { createServer, type Server } from 'node:http'; import log from 'electron-log'; import type { HostApiContext } from './context'; import type { HostApiRequest } from './route-utils'; import { parseRawJsonBody, requireJsonContentType, sendJsonResponse, setCorsHeaders, } from './route-utils'; const DEFAULT_HOST_API_PORT = 13210; type StartHostApiServerOptions = { ctx: HostApiContext; dispatchRequest: (request: HostApiRequest) => Promise; fallbackRequest?: (request: HostApiRequest) => Promise; port?: number; }; let hostApiToken = ''; export function getHostApiPort(): number { const raw = process.env['ZN_AI_HOST_API_PORT']; const parsed = raw ? Number.parseInt(raw, 10) : NaN; return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_HOST_API_PORT; } export function getHostApiToken(): string { return hostApiToken; } export function getHostApiBase(): string { return `http://127.0.0.1:${getHostApiPort()}`; } export function startHostApiServer(options: StartHostApiServerOptions): Server { const port = options.port ?? getHostApiPort(); hostApiToken = randomBytes(32).toString('hex'); const server = createServer(async (req, res) => { try { const requestUrl = new URL(req.url || '/', `http://127.0.0.1:${port}`); setCorsHeaders(res, req.headers.origin); if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return; } const bearerHeader = req.headers.authorization || ''; const bearerToken = bearerHeader.startsWith('Bearer ') ? bearerHeader.slice('Bearer '.length) : ''; const token = ( req.headers['x-host-api-token'] || requestUrl.searchParams.get('token') || bearerToken ); if (token !== hostApiToken) { sendJsonResponse(res, 401, { success: false, ok: false, error: 'Unauthorized', }); return; } if (requestUrl.pathname === '/api/events' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache, no-transform', Connection: 'keep-alive', }); res.write(': connected\n\n'); options.ctx.eventBus.addSseClient(res); res.write(`event: gateway:status\ndata: ${JSON.stringify(options.ctx.gatewayManager.getStatus())}\n\n`); return; } if (!requireJsonContentType(req)) { sendJsonResponse(res, 415, { success: false, ok: false, error: 'Content-Type must be application/json', }); return; } const body = req.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method) ? await parseRawJsonBody(req) : null; const forwardedHeaders: Record = {}; Object.entries(req.headers).forEach(([key, value]) => { if (typeof value === 'string') { forwardedHeaders[key] = value; } }); delete forwardedHeaders['x-host-api-token']; const request: HostApiRequest = { path: `${requestUrl.pathname}${requestUrl.search}`, method: req.method, headers: forwardedHeaders, body, }; const localResult = await options.dispatchRequest(request); const response = localResult ?? ( options.fallbackRequest ? await options.fallbackRequest(request) : null ); if (response == null) { sendJsonResponse(res, 404, { success: false, ok: false, error: `No route for ${req.method} ${requestUrl.pathname}`, }); return; } const result = response as { status?: number; data?: unknown; json?: unknown; success?: boolean; ok?: boolean; error?: string; text?: string; }; sendJsonResponse(res, result.status ?? 200, response); } catch (error) { sendJsonResponse(res, 500, { success: false, ok: false, error: error instanceof Error ? error.message : String(error), }); } }); server.on('error', (error) => { log.error('Host API server failed:', error); }); server.on('close', () => { hostApiToken = ''; }); server.listen(port, '127.0.0.1'); return server; }