import type { IpcMainEvent, WebContents } from 'electron/main' import { app, ipcMain } 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< string, { resolve: (value: RendererProxyResponse) => void reject: (error: Error) => void timeout: ReturnType } >() const rendererProxyRequestChannel = 'desktop:renderer-devtools-proxy:request' const rendererProxyResponseChannel = 'desktop:renderer-devtools-proxy:response' const rendererDevtoolsProxyEnabled = !app.isPackaged if (rendererDevtoolsProxyEnabled) { ipcMain.on(rendererProxyResponseChannel, handleRendererProxyResponse) } export function registerRendererDevtoolsProxyTarget(webContents: WebContents): void { if (!rendererDevtoolsProxyEnabled) { return } rendererWebContents = webContents webContents.once('destroyed', () => { if (rendererWebContents === webContents) { rendererWebContents = null } }) } export function canUseRendererDevtoolsProxy(): boolean { return ( rendererDevtoolsProxyEnabled && Boolean(rendererWebContents && !rendererWebContents.isDestroyed()) ) } export async function rendererDevtoolsFetch( request: RendererProxyRequest, ): Promise { if (!rendererDevtoolsProxyEnabled) { throw new Error('renderer_devtools_proxy_unavailable') } 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') ) }