60 lines
3.3 KiB
TypeScript
60 lines
3.3 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
|
const DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
|
|
|
|
export async function handleCaseRunHttp(request: IncomingMessage, response: ServerResponse, url: URL, options: any = {}) {
|
|
const env = options.env ?? process.env;
|
|
const baseUrl = String(env.HARNESSRL_API_URL ?? "").trim().replace(/\/+$/u, "");
|
|
if (!baseUrl) return sendJson(response, 503, { ok: false, error: { code: "harnessrl_api_url_required", message: "HARNESSRL_API_URL is required for CaseRun routes" } });
|
|
try {
|
|
const target = new URL(`${url.pathname}${url.search}`, `${baseUrl}/`);
|
|
const headers = new Headers();
|
|
for (const [name, value] of Object.entries(request.headers)) {
|
|
if (value === undefined || name.toLowerCase() === "host" || name.toLowerCase() === "content-length") continue;
|
|
headers.set(name, Array.isArray(value) ? value.join(",") : value);
|
|
}
|
|
headers.set("x-forwarded-host", String(request.headers["x-forwarded-host"] ?? request.headers.host ?? ""));
|
|
headers.set("x-forwarded-proto", String(request.headers["x-forwarded-proto"] ?? "http"));
|
|
const body = request.method === "GET" || request.method === "HEAD" ? undefined : await readBody(request, DEFAULT_BODY_LIMIT_BYTES);
|
|
const upstream = await (options.fetchImpl ?? globalThis.fetch)(target, { method: request.method, headers, body });
|
|
response.statusCode = upstream.status;
|
|
for (const [name, value] of upstream.headers) if (name.toLowerCase() !== "content-length" && name.toLowerCase() !== "transfer-encoding") response.setHeader(name, value);
|
|
response.end(Buffer.from(await upstream.arrayBuffer()));
|
|
} catch (error: any) {
|
|
sendJson(response, 502, { ok: false, error: { code: error?.code ?? "harnessrl_api_unavailable", message: error?.message ?? String(error) } });
|
|
}
|
|
}
|
|
|
|
export function caseRunInternalFetch(fetchImpl: typeof fetch, internalApiUrl: string, env: Record<string, string | undefined>) {
|
|
const apiKey = String(env.HWLAB_BOOTSTRAP_ADMIN_API_KEY ?? "").trim();
|
|
if (!apiKey) return fetchImpl;
|
|
const internalOrigin = new URL(internalApiUrl).origin;
|
|
return async (input: URL | RequestInfo, init: RequestInit = {}) => {
|
|
const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
if (target.origin !== internalOrigin) return fetchImpl(input, init);
|
|
const headers = new Headers(input instanceof Request ? input.headers : undefined);
|
|
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
|
|
if (!headers.has("authorization")) headers.set("authorization", `Bearer ${apiKey}`);
|
|
return fetchImpl(input, { ...init, headers });
|
|
};
|
|
}
|
|
|
|
async function readBody(request: IncomingMessage, limitBytes: number) {
|
|
let size = 0;
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of request) {
|
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
size += buffer.length;
|
|
if (size > limitBytes) throw Object.assign(new Error(`body exceeds ${limitBytes} bytes`), { code: "body_too_large" });
|
|
chunks.push(buffer);
|
|
}
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
function sendJson(response: ServerResponse, status: number, body: unknown) {
|
|
response.statusCode = status;
|
|
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
response.setHeader("cache-control", "no-store");
|
|
response.end(`${JSON.stringify(body)}\n`);
|
|
}
|