import { ipcMain } from "electron/main"; import type { IpcMainEvent, WebContents } from "electron/main"; interface RendererProxyRequest { url: string; method: string; headers?: Record; body?: string; } interface RendererProxyResponse { ok: boolean; status: number; statusText: string; headers: Record; bodyText: string; error?: string; } let rendererWebContents: WebContents | null = null; let requestSequence = 0; const pendingRequests = new Map void; reject: (error: Error) => void; timeout: ReturnType; }>(); const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request"; const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response"; ipcMain.on(rendererProxyResponseChannel, handleRendererProxyResponse); export function registerRendererDevtoolsProxyTarget(webContents: WebContents): void { rendererWebContents = webContents; webContents.once("destroyed", () => { if (rendererWebContents === webContents) { rendererWebContents = null; } }); } export function canUseRendererDevtoolsProxy(): boolean { return Boolean(rendererWebContents && !rendererWebContents.isDestroyed()); } export async function rendererDevtoolsFetch( request: RendererProxyRequest, ): Promise { if (!rendererWebContents || rendererWebContents.isDestroyed()) { throw new Error("renderer_devtools_proxy_unavailable"); } const requestId = `renderer-proxy-${Date.now()}-${requestSequence += 1}`; return await new Promise((resolve, reject) => { const timeout = setTimeout(() => { pendingRequests.delete(requestId); reject(new Error("renderer_devtools_proxy_timeout")); }, 30_000); pendingRequests.set(requestId, { resolve, reject, timeout, }); try { rendererWebContents?.send(rendererProxyRequestChannel, { requestId, request, }); } catch (error) { clearTimeout(timeout); pendingRequests.delete(requestId); reject(error instanceof Error ? error : new Error("renderer_devtools_proxy_send_failed")); } }); } function handleRendererProxyResponse( event: IpcMainEvent, payload: { requestId?: unknown; response?: unknown; }, ): void { if (rendererWebContents && event.sender !== rendererWebContents) { return; } const requestId = typeof payload?.requestId === "string" ? payload.requestId : ""; if (!requestId) { return; } const pending = pendingRequests.get(requestId); if (!pending) { return; } clearTimeout(pending.timeout); pendingRequests.delete(requestId); const response = payload.response; if (!isRendererProxyResponse(response)) { pending.reject(new Error("renderer_devtools_proxy_invalid_response")); return; } pending.resolve(response); } function isRendererProxyResponse(value: unknown): value is RendererProxyResponse { if (!value || typeof value !== "object") { return false; } const candidate = value as Partial; return typeof candidate.ok === "boolean" && typeof candidate.status === "number" && typeof candidate.statusText === "string" && typeof candidate.bodyText === "string" && Boolean(candidate.headers && typeof candidate.headers === "object"); }