import { randomUUID } from 'node:crypto' import type { Session, WebContents } from 'electron/main' import { app } from 'electron/main' import type { DesktopObservedRequest, DesktopObservedRequestKind, DesktopObservedRequestSnapshot, DesktopObservedRequestSource, } from '../shared/network-debug' interface ActiveObservedRequest { id: string startedAt: number kind: DesktopObservedRequestKind source: DesktopObservedRequestSource label: string method: string url: string partition: string | null resourceType: string | null } interface StartObservedRequestOptions { kind?: DesktopObservedRequestKind source?: DesktopObservedRequestSource label: string method?: string url: string partition?: string | null resourceType?: string | null } const observedSessions = new WeakSet() const activeRequests = new Map() const observedRecords: DesktopObservedRequest[] = [] const maxObservedRecords = 240 const requestFilter = { urls: ['http://*/*', 'https://*/*', 'ws://*/*', 'wss://*/*'], } const redactedQueryKeys = /token|signature|sig|auth|key|password|session/i const networkObserverEnabled = !app.isPackaged let rendererTarget: WebContents | null = null let originalFetch: typeof globalThis.fetch | null = null export function installObservedGlobalFetch(): void { if (!networkObserverEnabled) { return } if (originalFetch || typeof globalThis.fetch !== 'function') { return } originalFetch = globalThis.fetch.bind(globalThis) globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const url = requestURL(input) if (!url || !isInspectableURL(url) || !originalFetch) { return originalFetch!(input, init) } const method = requestMethod(input, init) const requestId = startObservedRequest({ kind: inferKindFromHeaders(input, init), source: 'main', label: 'main.fetch', method, url, }) try { const response = await originalFetch(input, init) resolveObservedRequest(requestId, { phase: 'response', status: response.status, }) return response } catch (error) { failObservedRequest(requestId, error) throw error } }) as typeof globalThis.fetch } export function registerObservedRequestRendererTarget(webContents: WebContents): void { if (!networkObserverEnabled) { return } rendererTarget = webContents webContents.once('destroyed', () => { if (rendererTarget === webContents) { rendererTarget = null } }) } export function observeSessionRequests( target: Session, options: { label: string; partition?: string | null }, ): void { if (!networkObserverEnabled) { return } if (observedSessions.has(target)) { return } observedSessions.add(target) target.webRequest.onBeforeRequest(requestFilter, (details, callback) => { if (!isInspectableURL(details.url)) { callback({}) return } const requestId = sessionRequestId(details.id, options.partition ?? null) activeRequests.set(requestId, { id: requestId, startedAt: Date.now(), kind: details.resourceType === 'webSocket' ? 'ws' : 'http', source: 'session', label: options.label, method: normalizeMethod(details.method), url: sanitizeURL(details.url), partition: options.partition ?? null, resourceType: normalizeResourceType(details.resourceType), }) pushRecord( buildRecord(activeRequests.get(requestId)!, { at: Date.now(), phase: 'request', }), ) callback({}) }) target.webRequest.onCompleted(requestFilter, (details) => { if (!isInspectableURL(details.url)) { return } const requestId = sessionRequestId(details.id, options.partition ?? null) const active = activeRequests.get(requestId) ?? createFallbackActiveRequest(requestId, { source: 'session', label: options.label, method: normalizeMethod(details.method), url: details.url, partition: options.partition ?? null, resourceType: normalizeResourceType(details.resourceType), }) pushRecord( buildRecord(active, { at: Date.now(), phase: 'response', status: details.statusCode, durationMs: Date.now() - active.startedAt, }), ) activeRequests.delete(requestId) }) target.webRequest.onErrorOccurred(requestFilter, (details) => { if (!isInspectableURL(details.url)) { return } const requestId = sessionRequestId(details.id, options.partition ?? null) const active = activeRequests.get(requestId) ?? createFallbackActiveRequest(requestId, { source: 'session', label: options.label, method: normalizeMethod(details.method), url: details.url, partition: options.partition ?? null, resourceType: normalizeResourceType(details.resourceType), }) pushRecord( buildRecord(active, { at: Date.now(), phase: 'error', durationMs: Date.now() - active.startedAt, error: normalizeErrorMessage(details.error), }), ) activeRequests.delete(requestId) }) } export function startObservedRequest(options: StartObservedRequestOptions): string { if (!networkObserverEnabled) { return '' } const id = `main:${randomUUID()}` const active: ActiveObservedRequest = { id, startedAt: Date.now(), kind: options.kind ?? 'http', source: options.source ?? 'main', label: options.label, method: normalizeMethod(options.method), url: sanitizeURL(options.url), partition: options.partition ?? null, resourceType: options.resourceType ?? null, } activeRequests.set(id, active) pushRecord( buildRecord(active, { at: active.startedAt, phase: 'request', }), ) return id } export function resolveObservedRequest( id: string, options: { phase?: 'response' | 'close' status?: number | null error?: string | null keepActive?: boolean } = {}, ): void { const active = activeRequests.get(id) if (!active) { return } pushRecord( buildRecord(active, { at: Date.now(), phase: options.phase ?? 'response', status: options.status ?? null, durationMs: Date.now() - active.startedAt, error: options.error ?? null, }), ) if (!options.keepActive) { activeRequests.delete(id) } } export function failObservedRequest(id: string, error: unknown): void { const active = activeRequests.get(id) if (!active) { return } pushRecord( buildRecord(active, { at: Date.now(), phase: 'error', durationMs: Date.now() - active.startedAt, error: normalizeErrorMessage(error), }), ) activeRequests.delete(id) } export function getObservedRequestSnapshot(): DesktopObservedRequestSnapshot { if (!networkObserverEnabled) { return { rendererAttached: false, activeCount: 0, recent: [], } } return { rendererAttached: Boolean(rendererTarget && !rendererTarget.isDestroyed()), activeCount: activeRequests.size, recent: observedRecords.slice(-80), } } function requestURL(input: RequestInfo | URL): string | null { if (typeof input === 'string') { return input } if (input instanceof URL) { return input.toString() } return typeof input.url === 'string' ? input.url : null } function requestMethod(input: RequestInfo | URL, init?: RequestInit): string { if (init?.method) { return normalizeMethod(init.method) } if (typeof Request !== 'undefined' && input instanceof Request) { return normalizeMethod(input.method) } return 'GET' } function inferKindFromHeaders( input: RequestInfo | URL, init?: RequestInit, ): DesktopObservedRequestKind { const headers = new Headers(init?.headers) if (!headers.has('accept') && typeof Request !== 'undefined' && input instanceof Request) { const requestAccept = input.headers.get('accept') if (requestAccept) { headers.set('accept', requestAccept) } } const accept = headers.get('accept')?.toLowerCase() ?? '' if (accept.includes('text/event-stream')) { return 'sse' } return 'http' } function isInspectableURL(url: string): boolean { try { const protocol = new URL(url).protocol return ( protocol === 'http:' || protocol === 'https:' || protocol === 'ws:' || protocol === 'wss:' ) } catch { return false } } function sanitizeURL(url: string): string { try { const parsed = new URL(url) for (const key of parsed.searchParams.keys()) { if (redactedQueryKeys.test(key)) { parsed.searchParams.set(key, '[REDACTED]') } } return parsed.toString() } catch { return url } } function normalizeMethod(value: string | undefined | null): string { return typeof value === 'string' && value.trim() ? value.trim().toUpperCase() : 'GET' } function normalizeResourceType(value: unknown): string | null { return typeof value === 'string' && value.trim() ? value : null } function normalizeErrorMessage(error: unknown): string { if (typeof error === 'string' && error.trim()) { return error } if (error instanceof Error) { return error.message || error.name || 'request_failed' } if (error && typeof error === 'object' && 'message' in error) { const message = (error as { message?: unknown }).message if (typeof message === 'string' && message.trim()) { return message } } return 'request_failed' } function sessionRequestId(id: number, partition: string | null): string { return `session:${partition ?? 'default'}:${id}` } function createFallbackActiveRequest( id: string, options: { source: DesktopObservedRequestSource label: string method?: string url: string partition?: string | null resourceType?: string | null }, ): ActiveObservedRequest { return { id, startedAt: Date.now(), kind: options.resourceType === 'webSocket' ? 'ws' : 'http', source: options.source, label: options.label, method: normalizeMethod(options.method), url: sanitizeURL(options.url), partition: options.partition ?? null, resourceType: options.resourceType ?? null, } } function buildRecord( active: ActiveObservedRequest, input: { at: number phase: DesktopObservedRequest['phase'] status?: number | null durationMs?: number | null error?: string | null }, ): DesktopObservedRequest { return { id: active.id, at: input.at, phase: input.phase, kind: active.kind, source: active.source, label: active.label, method: active.method, url: active.url, partition: active.partition, resourceType: active.resourceType, status: input.status ?? null, durationMs: input.durationMs ?? null, error: input.error ?? null, } } function pushRecord(record: DesktopObservedRequest): void { observedRecords.push(record) if (observedRecords.length > maxObservedRecords) { observedRecords.splice(0, observedRecords.length - maxObservedRecords) } if (!rendererTarget || rendererTarget.isDestroyed()) { return } try { rendererTarget.send('desktop:network-observed', record) } catch { // Best-effort diagnostics only. } }