158 lines
4.4 KiB
TypeScript
158 lines
4.4 KiB
TypeScript
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;
|
|
const HOST_API_UNAUTHORIZED_CODE = 'HOST_API_UNAUTHORIZED';
|
|
|
|
type StartHostApiServerOptions = {
|
|
ctx: HostApiContext;
|
|
dispatchRequest: (request: HostApiRequest) => Promise<unknown | null>;
|
|
fallbackRequest?: (request: HostApiRequest) => Promise<unknown>;
|
|
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 headerToken = req.headers['x-host-api-token'];
|
|
const token = typeof headerToken === 'string'
|
|
? headerToken
|
|
: Array.isArray(headerToken)
|
|
? headerToken[0]
|
|
: (requestUrl.searchParams.get('token') || '');
|
|
|
|
if (token !== hostApiToken) {
|
|
sendJsonResponse(res, 401, {
|
|
success: false,
|
|
ok: false,
|
|
code: HOST_API_UNAUTHORIZED_CODE,
|
|
error: 'Host API authentication failed',
|
|
});
|
|
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<unknown>(req)
|
|
: null;
|
|
|
|
const forwardedHeaders: Record<string, string> = {};
|
|
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;
|
|
}
|