需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from 'http';
|
|
import { PORTS } from '../utils/config';
|
|
|
|
/**
|
|
* Allowed CORS origins — only the Electron renderer (Vite dev or production)
|
|
* and the local opencode runtime are permitted to make cross-origin requests.
|
|
*/
|
|
const ALLOWED_ORIGINS = new Set([
|
|
`http://127.0.0.1:${PORTS.NIANCODE_DEV}`,
|
|
`http://localhost:${PORTS.NIANCODE_DEV}`,
|
|
`http://127.0.0.1:${PORTS.OPENCODE_RUNTIME}`,
|
|
`http://localhost:${PORTS.OPENCODE_RUNTIME}`,
|
|
]);
|
|
|
|
export async function parseJsonBody<T>(req: IncomingMessage): Promise<T> {
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of req) {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
}
|
|
const raw = Buffer.concat(chunks).toString('utf8').trim();
|
|
if (!raw) {
|
|
return {} as T;
|
|
}
|
|
return JSON.parse(raw) as T;
|
|
}
|
|
|
|
/**
|
|
* Validate that mutation requests (POST/PUT/PATCH/DELETE) carry a JSON Content-Type.
|
|
* This prevents "simple request" CSRF where the browser skips the preflight
|
|
* when Content-Type is text/plain or application/x-www-form-urlencoded.
|
|
*/
|
|
export function requireJsonContentType(req: IncomingMessage): boolean {
|
|
if (req.method === 'GET' || req.method === 'OPTIONS' || req.method === 'HEAD') {
|
|
return true;
|
|
}
|
|
// Requests without a body (content-length 0 or absent) are safe — CSRF
|
|
// "simple request" attacks rely on sending a crafted body.
|
|
const contentLength = req.headers['content-length'];
|
|
if (contentLength === '0' || contentLength === undefined) {
|
|
return true;
|
|
}
|
|
const ct = req.headers['content-type'] || '';
|
|
return ct.includes('application/json');
|
|
}
|
|
|
|
export function setCorsHeaders(res: ServerResponse, origin?: string): void {
|
|
// Only reflect the Origin header back if it is in the allow-list.
|
|
// Omitting the header for unknown origins causes the browser to block
|
|
// the response — this is the intended behavior for untrusted callers.
|
|
if (origin && ALLOWED_ORIGINS.has(origin)) {
|
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
res.setHeader('Vary', 'Origin');
|
|
}
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-NianCode-Access-Token');
|
|
}
|
|
|
|
export function sendJson(res: ServerResponse, statusCode: number, payload: unknown): void {
|
|
res.statusCode = statusCode;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify(payload));
|
|
}
|
|
|
|
export function sendNoContent(res: ServerResponse): void {
|
|
res.statusCode = 204;
|
|
res.end();
|
|
}
|
|
|
|
export function sendText(res: ServerResponse, statusCode: number, text: string): void {
|
|
res.statusCode = statusCode;
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
res.end(text);
|
|
}
|
|
|
|
export function flushStreamingHeaders(res: ServerResponse): void {
|
|
res.socket?.setNoDelay(true);
|
|
res.flushHeaders?.();
|
|
}
|
|
|
|
function waitForResponseDrain(res: ServerResponse): Promise<boolean> {
|
|
if (res.destroyed || res.writableEnded) {
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const cleanup = () => {
|
|
res.off('drain', onDrain);
|
|
res.off('close', onClose);
|
|
res.off('error', onError);
|
|
};
|
|
const onDrain = () => {
|
|
cleanup();
|
|
resolve(true);
|
|
};
|
|
const onClose = () => {
|
|
cleanup();
|
|
resolve(false);
|
|
};
|
|
const onError = (error: Error) => {
|
|
cleanup();
|
|
reject(error);
|
|
};
|
|
|
|
res.once('drain', onDrain);
|
|
res.once('close', onClose);
|
|
res.once('error', onError);
|
|
});
|
|
}
|
|
|
|
export async function writeStreamingChunk(
|
|
res: ServerResponse,
|
|
chunk: string | Uint8Array,
|
|
): Promise<boolean> {
|
|
if (res.destroyed || res.writableEnded) {
|
|
return false;
|
|
}
|
|
|
|
if (res.write(chunk)) {
|
|
return true;
|
|
}
|
|
return await waitForResponseDrain(res);
|
|
}
|