162abdc97c
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports) - Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier - Add root scripts: format, format:check, lint, lint:fix - Reformat 257 files across admin-web, ops-web, desktop-client, packages - Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters - Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex - Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
448 lines
11 KiB
TypeScript
448 lines
11 KiB
TypeScript
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<Session>()
|
|
const activeRequests = new Map<string, ActiveObservedRequest>()
|
|
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.
|
|
}
|
|
}
|