74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import { WebSocketServer, type WebSocket } from "ws";
|
|
import { parseHttpRequestFrame, parseJsonFrame, type HttpResponseFrame } from "./protocol.js";
|
|
|
|
export type OpencodeProxyOptions = {
|
|
host: string;
|
|
port: number;
|
|
opencodeUrl: string;
|
|
username?: string;
|
|
password?: string;
|
|
maxRequestMs?: number;
|
|
};
|
|
|
|
const DEFAULT_MAX_REQUEST_MS = 600_000;
|
|
|
|
// WebSocket endpoint where the ComputerCraft client is the requester and the
|
|
// bridge is the responder: it performs the opencode http call server-side (no
|
|
// short timeout) and returns the raw response. This lets the in-game client use
|
|
// opencode's synchronous /message endpoint without hitting CC's ~60s http cap.
|
|
export function startOpencodeProxy(options: OpencodeProxyOptions): WebSocketServer {
|
|
const baseUrl = options.opencodeUrl.replace(/\/+$/, "");
|
|
const maxRequestMs = options.maxRequestMs ?? DEFAULT_MAX_REQUEST_MS;
|
|
const authHeader = options.password
|
|
? "Basic " + Buffer.from(`${options.username ?? "opencode"}:${options.password}`).toString("base64")
|
|
: undefined;
|
|
|
|
const server = new WebSocketServer({ host: options.host, port: options.port });
|
|
|
|
server.on("connection", (ws) => {
|
|
ws.on("message", (data) => {
|
|
void handleFrame(ws, data);
|
|
});
|
|
ws.on("error", () => {
|
|
/* connection-level errors just drop the socket; nothing to clean up */
|
|
});
|
|
});
|
|
|
|
async function handleFrame(ws: WebSocket, data: unknown): Promise<void> {
|
|
const frame = parseHttpRequestFrame(parseJsonFrame(data));
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
};
|
|
if (authHeader) {
|
|
headers.Authorization = authHeader;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(baseUrl + frame.path, {
|
|
method: frame.method,
|
|
headers,
|
|
body: frame.method === "POST" ? frame.body : undefined,
|
|
signal: AbortSignal.timeout(maxRequestMs),
|
|
});
|
|
const body = await res.text();
|
|
send(ws, { type: "http-response", id: frame.id, status: res.status, body });
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
send(ws, { type: "http-response", id: frame.id, status: 0, error: message });
|
|
}
|
|
}
|
|
|
|
return server;
|
|
}
|
|
|
|
function send(ws: WebSocket, frame: HttpResponseFrame): void {
|
|
if (ws.readyState === ws.OPEN) {
|
|
ws.send(JSON.stringify(frame));
|
|
}
|
|
}
|