- Added telemetry utility to capture application events and metrics. - Integrated PostHog for event tracking with distinct user identification. - Implemented telemetry initialization, event capturing, and shutdown procedures. feat: add UV environment setup for Python management - Created utilities to manage Python installation and configuration. - Implemented network optimization checks for Python installation mirrors. - Added functions to set up managed Python environments with error handling. feat: enhance host API communication with token management - Introduced host API token retrieval and management for secure requests. - Updated host API fetch functions to include token in headers. - Added support for creating event sources with authentication. test: add comprehensive tests for gateway protocol and startup helpers - Implemented unit tests for gateway protocol helpers, event dispatching, and state management. - Added tests for startup recovery strategies and process policies. - Ensured coverage for connection monitoring and restart governance logic.
159 lines
4.4 KiB
TypeScript
159 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;
|
|
|
|
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 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<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;
|
|
}
|