Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
46 lines
2.1 KiB
TypeScript
46 lines
2.1 KiB
TypeScript
import { IncomingMessage, ServerResponse } from "node:http";
|
|
import { Duplex } from "node:stream";
|
|
import type { Socket } from "node:net";
|
|
|
|
/** Runs real Node/Express HTTP handlers without binding a port (restricted CI). */
|
|
export async function injectHttp(
|
|
handler: (request: IncomingMessage, response: ServerResponse) => void,
|
|
input: string | URL | Request,
|
|
init: RequestInit = {},
|
|
): Promise<Response> {
|
|
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
const output: Buffer[] = [];
|
|
const socket = new Duplex({
|
|
read() {},
|
|
write(chunk: Buffer, _encoding, callback) { output.push(Buffer.from(chunk)); callback(); },
|
|
});
|
|
// ServerResponse uses only Duplex operations; no network or unsafe dynamic values.
|
|
const httpSocket = socket as unknown as Socket;
|
|
const request = new IncomingMessage(httpSocket);
|
|
request.method = init.method ?? "GET";
|
|
request.complete = true;
|
|
request.url = `${url.pathname}${url.search}`;
|
|
request.headers = Object.fromEntries(new Headers(init.headers).entries());
|
|
request.headers.host = url.host;
|
|
const body = typeof init.body === "string" ? init.body : "";
|
|
if (body) request.headers["content-length"] = String(Buffer.byteLength(body));
|
|
request.push(body || null);
|
|
if (body) request.push(null);
|
|
const response = new ServerResponse(request);
|
|
response.assignSocket(httpSocket);
|
|
const completed = new Promise<Response>((resolve, reject) => {
|
|
response.once("error", reject);
|
|
response.once("finish", () => {
|
|
const bytes = Buffer.concat(output);
|
|
const bodyOffset = bytes.indexOf("\r\n\r\n") + 4;
|
|
const headers = new Headers();
|
|
for (const line of bytes.subarray(0, bodyOffset - 4).toString().split("\r\n").slice(1)) {
|
|
const separator = line.indexOf(":");
|
|
if (separator > 0) headers.append(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
}
|
|
resolve(new Response([204, 304].includes(response.statusCode) ? null : bytes.subarray(bodyOffset), { status: response.statusCode, headers }));
|
|
});
|
|
});
|
|
handler(request, response);
|
|
return completed;
|
|
}
|