Makelore 2.0 initial clean snapshot
This commit is contained in:
133
electron/api/server.ts
Normal file
133
electron/api/server.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
||||
import { getPort } from '../utils/config';
|
||||
import { logger } from '../utils/logger';
|
||||
import type { HostApiContext } from './context';
|
||||
import { handleAiProxyRoutes } from './routes/ai-proxy';
|
||||
import { handleAppRoutes } from './routes/app';
|
||||
import { handleAuthRoutes } from './routes/auth';
|
||||
import { handleOpencodeRoutes } from './routes/opencode';
|
||||
import { handleImageWorkspaceRoutes } from './routes/image-workspace';
|
||||
import { handleWorksRoutes } from './routes/works';
|
||||
import { handleUserSyncRoutes } from './routes/user-sync';
|
||||
import { handleSettingsRoutes } from './routes/settings';
|
||||
import { handleProviderRoutes } from './routes/providers';
|
||||
import { handleLogRoutes } from './routes/logs';
|
||||
import { handleUsageRoutes } from './routes/usage';
|
||||
import { handleFileRoutes } from './routes/files';
|
||||
import { handleMeowaGameAssetsRoutes } from './routes/meowa-game-assets';
|
||||
import { sendJson, setCorsHeaders, requireJsonContentType } from './route-utils';
|
||||
|
||||
type RouteHandler = (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
ctx: HostApiContext,
|
||||
) => Promise<boolean>;
|
||||
|
||||
const coreRouteHandlers: RouteHandler[] = [
|
||||
handleAiProxyRoutes,
|
||||
handleAppRoutes,
|
||||
handleAuthRoutes,
|
||||
handleImageWorkspaceRoutes,
|
||||
handleWorksRoutes,
|
||||
handleUserSyncRoutes,
|
||||
handleOpencodeRoutes,
|
||||
handleSettingsRoutes,
|
||||
handleProviderRoutes,
|
||||
handleFileRoutes,
|
||||
handleMeowaGameAssetsRoutes,
|
||||
handleLogRoutes,
|
||||
handleUsageRoutes,
|
||||
];
|
||||
|
||||
function buildRouteHandlers(): RouteHandler[] {
|
||||
return coreRouteHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-session secret token used to authenticate Host API requests.
|
||||
* Generated once at server start and shared with the renderer via IPC.
|
||||
* This prevents cross-origin attackers from reading sensitive data even
|
||||
* if they can reach 127.0.0.1:13210 (the CORS wildcard alone is not
|
||||
* sufficient because browsers attach the Origin header but not a secret).
|
||||
*/
|
||||
let hostApiToken: string = '';
|
||||
|
||||
/** Retrieve the current Host API auth token (for use by IPC proxy). */
|
||||
export function getHostApiToken(): string {
|
||||
return hostApiToken;
|
||||
}
|
||||
|
||||
export function startHostApiServer(ctx: HostApiContext, port = getPort('NIANCODE_HOST_API')): Server {
|
||||
// Generate a cryptographically random token for this session.
|
||||
hostApiToken = randomBytes(32).toString('hex');
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
const requestUrl = new URL(req.url || '/', `http://127.0.0.1:${port}`);
|
||||
// ── CORS headers ─────────────────────────────────────────
|
||||
// Set origin-aware CORS headers early so every response
|
||||
// (including error responses) carries them consistently.
|
||||
const origin = req.headers.origin;
|
||||
setCorsHeaders(res, origin);
|
||||
|
||||
// CORS preflight — respond before auth so browsers can negotiate.
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Auth gate ──────────────────────────────────────────────
|
||||
// Every non-preflight request must carry a valid Bearer token.
|
||||
// Accept via Authorization header (preferred) or ?token= query
|
||||
// parameter (for EventSource which cannot set custom headers).
|
||||
const authHeader = req.headers.authorization || '';
|
||||
const bearerToken = authHeader.startsWith('Bearer ')
|
||||
? authHeader.slice(7)
|
||||
: (requestUrl.searchParams.get('token') || '');
|
||||
if (bearerToken !== hostApiToken) {
|
||||
sendJson(res, 401, { success: false, error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Content-Type gate (anti-CSRF) ──────────────────────────
|
||||
// Mutation requests must use application/json to force a CORS
|
||||
// preflight, preventing "simple request" CSRF attacks.
|
||||
if (!requireJsonContentType(req)) {
|
||||
sendJson(res, 415, { success: false, error: 'Content-Type must be application/json' });
|
||||
return;
|
||||
}
|
||||
|
||||
const routeHandlers = buildRouteHandlers();
|
||||
for (const handler of routeHandlers) {
|
||||
if (await handler(req, res, requestUrl, ctx)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendJson(res, 404, { success: false, error: `No route for ${req.method} ${requestUrl.pathname}` });
|
||||
} catch (error) {
|
||||
logger.error('Host API request failed:', error);
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', (error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'EACCES' || error.code === 'EADDRINUSE') {
|
||||
logger.error(
|
||||
`Host API server failed to bind port ${port}: ${error.message}. ` +
|
||||
'On Windows this is often caused by Hyper-V reserving the port range. ' +
|
||||
`Set NIANCODE_PORT_NIANCODE_HOST_API env var to override the default port.`,
|
||||
);
|
||||
} else {
|
||||
logger.error('Host API server error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
logger.info(`Host API server listening on http://127.0.0.1:${port}`);
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
Reference in New Issue
Block a user