/** * WebSocket detection — intercepts WS connections opened during page crawl. * Records connection URLs, message schemas, and message counts. * This lets ZeTa discover real-time API contracts (chat, notifications, presence) * that would be invisible to HTTP-only crawlers. */ export interface WsConnection { url: string; framesSent: number; framesReceived: number; /** Sampled message bodies (first 10 per connection, truncated to 500 chars) */ samples: Array<{ direction: 'sent' | 'received'; payload: string; ts: string }>; /** Inferred protocol: 'json-rpc' | 'socket.io' | 'graphql-ws' | 'raw' */ protocol: string; openedAt: string; } function inferProtocol(sample: string): string { try { const obj = JSON.parse(sample); if (obj?.jsonrpc) return 'json-rpc'; if (obj?.type === 'connection_init' || obj?.type === 'subscribe') return 'graphql-ws'; if (typeof obj?.sid === 'string' || sample.startsWith('0{')) return 'socket.io'; return 'json'; } catch { return 'raw'; } } export function attachWebSocketDetector(cdpSession: any): { getConnections(): WsConnection[]; detach(): void; } { const connections = new Map(); const onCreated = (evt: any) => { const { requestId, url, timestamp } = evt ?? {}; if (!requestId || !url) return; connections.set(requestId, { url, framesSent: 0, framesReceived: 0, samples: [], protocol: 'unknown', openedAt: new Date(timestamp * 1000).toISOString(), }); }; const onFrame = (evt: any, direction: 'sent' | 'received') => { const { requestId, response } = evt ?? {}; if (!requestId) return; const conn = connections.get(requestId); if (!conn) return; if (direction === 'sent') conn.framesSent++; else conn.framesReceived++; if (conn.samples.length < 10 && response?.payloadData) { const payload = String(response.payloadData).slice(0, 500); conn.samples.push({ direction, payload, ts: new Date().toISOString() }); if (conn.protocol === 'unknown') conn.protocol = inferProtocol(payload); } }; const onSent = (evt: any) => onFrame(evt, 'sent'); const onReceived = (evt: any) => onFrame(evt, 'received'); if (cdpSession?.on) { cdpSession.on('Network.webSocketCreated', onCreated); cdpSession.on('Network.webSocketFrameSent', onSent); cdpSession.on('Network.webSocketFrameReceived', onReceived); } return { getConnections: () => [...connections.values()], detach() { if (cdpSession?.off) { cdpSession.off('Network.webSocketCreated', onCreated); cdpSession.off('Network.webSocketFrameSent', onSent); cdpSession.off('Network.webSocketFrameReceived', onReceived); } }, }; }