{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/gateway/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAGX,eAAe,EACf,aAAa,EAGb,YAAY,EAEZ,MAAM,kBAAkB,CAAC;AAE1B,sBAAsB;AACtB,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,aAAa,CAAC;IAC7B,cAAc,EAAE,cAAc,CAAC;CAC/B;AAED,6BAA6B;AAC7B,qBAAa,OAAQ,YAAW,aAAa;IAC5C,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,UAAU,CAAC,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,CAAkB;IACnC,OAAO,CAAC,WAAW,CAA8C;IACjE,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,cAAc,CAAoE;IAC1F,OAAO,CAAC,SAAS,CAAC,CAAO;IACzB,OAAO,CAAC,mBAAmB,CAAK;IAEhC,YAAY,OAAO,EAAE,cAAc,EAQlC;IAED,+BAA+B;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CA+B3B;IAED,8BAA8B;IACxB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB1B;IAED,wBAAwB;IACxB,SAAS,IAAI,YAAY,CAQxB;YAGa,kBAAkB;YAgClB,oBAAoB;YAiCpB,YAAY;YA2BZ,iBAAiB;IAsB/B,uBAAuB;IACvB,OAAO,CAAC,cAAc;YAUR,gBAAgB;YAwDhB,gBAAgB;IA+B9B,oCAAoC;IACpC,OAAO,CAAC,cAAc;IAetB,uCAAuC;IACvC,OAAO,CAAC,aAAa;IAgBrB,kCAAkC;IAClC,OAAO,CAAC,kBAAkB;IAiD1B,+BAA+B;IAC/B,OAAO,CAAC,eAAe;IA8BvB,oCAAoC;IACpC,OAAO,CAAC,oBAAoB;IAI5B,iCAAiC;IACjC,OAAO,CAAC,UAAU;IAIlB,wBAAwB;IACxB,OAAO,CAAC,QAAQ;IAWhB,yBAAyB;IACzB,OAAO,CAAC,YAAY;IAMpB,0BAA0B;IAC1B,OAAO,CAAC,aAAa;IAMrB,6BAA6B;IAC7B,OAAO,CAAC,oBAAoB;IAS5B,yBAAyB;IACzB,EAAE,CAAC,CAAC,SAAS,MAAM,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,IAAI,CAKhG;IAED,4BAA4B;IAC5B,GAAG,CAAC,CAAC,SAAS,MAAM,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,IAAI,CAQjG;IAED,iBAAiB;IACjB,OAAO,CAAC,IAAI;CAYZ","sourcesContent":["/**\n * Gateway Server - WebSocket and HTTP server for OpenClaw\n *\n * Handles:\n * - WebSocket connections from clients, nodes, and channels\n * - HTTP API for configuration and management\n * - Static file serving for web UI\n * - Message routing between channels and agents\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as http from \"node:http\";\nimport * as path from \"node:path\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport type { ConfigManager } from \"../config/manager.js\";\nimport { type ChannelAdapter, globalChannelRegistry } from \"./channels/base.js\";\nimport { MessageRouter } from \"./router.js\";\nimport type { SessionManager } from \"./sessions.js\";\nimport type {\n\tConnectionType,\n\tGatewayConfig,\n\tGatewayEventMap,\n\tGatewayServer,\n\tInboundMessage,\n\tOutboundMessage,\n\tServerStatus,\n\tWebSocketConnection,\n} from \"./types/index.js\";\n\n/** Gateway options */\nexport interface GatewayOptions {\n\tconfigManager: ConfigManager;\n\tsessionManager: SessionManager;\n}\n\n/** Gateway implementation */\nexport class Gateway implements GatewayServer {\n\tprivate configManager: ConfigManager;\n\tprivate sessionManager: SessionManager;\n\tprivate router: MessageRouter;\n\tprivate config: GatewayConfig;\n\tprivate httpServer?: http.Server;\n\tprivate wsServer?: WebSocketServer;\n\tprivate connections = new Map<string, WebSocketConnectionImpl>();\n\tprivate channelAdapters = new Map<string, ChannelAdapter>();\n\tprivate eventListeners = new Map<keyof GatewayEventMap, Array<(data: unknown) => void>>();\n\tprivate startTime?: Date;\n\tprivate connectionIdCounter = 0;\n\n\tconstructor(options: GatewayOptions) {\n\t\tthis.configManager = options.configManager;\n\t\tthis.sessionManager = options.sessionManager;\n\t\tthis.router = new MessageRouter({\n\t\t\tconfigManager: options.configManager,\n\t\t\tsessionManager: options.sessionManager,\n\t\t});\n\t\tthis.config = options.configManager.getGatewayConfig();\n\t}\n\n\t/** Start the gateway server */\n\tasync start(): Promise<void> {\n\t\t// Initialize channel adapters\n\t\tawait this.initializeChannels();\n\n\t\t// Create HTTP server\n\t\tthis.httpServer = http.createServer(this.handleHttpRequest.bind(this));\n\n\t\t// Create WebSocket server\n\t\tthis.wsServer = new WebSocketServer({\n\t\t\tnoServer: true,\n\t\t\tmaxPayload: 10 * 1024 * 1024, // 10MB\n\t\t});\n\n\t\t// Handle WebSocket connections\n\t\tthis.wsServer.on(\"connection\", this.handleWsConnection.bind(this));\n\n\t\t// Handle upgrade from HTTP to WebSocket\n\t\tthis.httpServer.on(\"upgrade\", this.handleUpgrade.bind(this));\n\n\t\t// Start listening\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.httpServer!.listen(this.config.port, () => {\n\t\t\t\tthis.startTime = new Date();\n\t\t\t\tconsole.log(`OpenClaw Gateway listening on port ${this.config.port}`);\n\t\t\t\tresolve();\n\t\t\t});\n\t\t\tthis.httpServer!.on(\"error\", reject);\n\t\t});\n\n\t\t// Start cleanup interval\n\t\tthis.startCleanupInterval();\n\t}\n\n\t/** Stop the gateway server */\n\tasync stop(): Promise<void> {\n\t\t// Close all connections\n\t\tfor (const connection of this.connections.values()) {\n\t\t\tconnection.close();\n\t\t}\n\t\tthis.connections.clear();\n\n\t\t// Shutdown channel adapters\n\t\tfor (const adapter of this.channelAdapters.values()) {\n\t\t\tawait adapter.shutdown();\n\t\t}\n\t\tthis.channelAdapters.clear();\n\n\t\t// Close WebSocket server\n\t\tthis.wsServer?.close();\n\n\t\t// Close HTTP server\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tthis.httpServer?.close(() => resolve());\n\t\t});\n\n\t\tconsole.log(\"OpenClaw Gateway stopped\");\n\t}\n\n\t/** Get server status */\n\tgetStatus(): ServerStatus {\n\t\treturn {\n\t\t\trunning: !!this.startTime,\n\t\t\tport: this.config.port,\n\t\t\tstartTime: this.startTime,\n\t\t\tconnections: this.connections.size,\n\t\t\tsessions: this.sessionManager.getStats().total,\n\t\t};\n\t}\n\n\t/** Initialize channel adapters */\n\tprivate async initializeChannels(): Promise<void> {\n\t\tconst channels = this.configManager.getEnabledChannels();\n\n\t\tfor (const channelConfig of channels) {\n\t\t\ttry {\n\t\t\t\tif (globalChannelRegistry.has(channelConfig.type)) {\n\t\t\t\t\tconst adapter = globalChannelRegistry.create(channelConfig.type, channelConfig);\n\n\t\t\t\t\t// Set up message handler\n\t\t\t\t\tadapter.onMessage((message) => {\n\t\t\t\t\t\tthis.handleChannelMessage(message);\n\t\t\t\t\t});\n\n\t\t\t\t\t// Set up error handler\n\t\t\t\t\tadapter.onError((error) => {\n\t\t\t\t\t\tconsole.error(`Channel ${channelConfig.type} error:`, error);\n\t\t\t\t\t\tthis.emit(\"error\", { source: channelConfig.type, error });\n\t\t\t\t\t});\n\n\t\t\t\t\tawait adapter.initialize();\n\t\t\t\t\tthis.channelAdapters.set(channelConfig.type, adapter);\n\t\t\t\t\tconsole.log(`Channel ${channelConfig.type} initialized`);\n\t\t\t\t} else {\n\t\t\t\t\tconsole.warn(`Unknown channel type: ${channelConfig.type}`);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`Failed to initialize channel ${channelConfig.type}:`, error);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Handle incoming channel message */\n\tprivate async handleChannelMessage(message: InboundMessage): Promise<void> {\n\t\t// Emit event\n\t\tthis.emit(\"message:inbound\", message);\n\n\t\t// Check access\n\t\tconst access = await this.router.canProcess(message);\n\t\tif (!access.allowed) {\n\t\t\tconsole.log(`Message rejected: ${access.reason}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Resolve route\n\t\tconst route = await this.router.resolveRoute(message);\n\t\tif (!route) {\n\t\t\tconsole.log(\"No route found for message\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create session\n\t\tconst session = await this.router.getOrCreateSession(message);\n\n\t\t// Update session state\n\t\tthis.sessionManager.setSessionState(session.key, \"active\");\n\n\t\t// Emit session event\n\t\tthis.emit(\"session:update\", session);\n\n\t\t// TODO: Forward to agent runner\n\t\t// For now, just echo back a response\n\t\tawait this.sendResponse(message, route);\n\t}\n\n\t/** Send response (placeholder) */\n\tprivate async sendResponse(\n\t\tinbound: InboundMessage,\n\t\troute: { agentId: string; channelConfig: { type: string } },\n\t): Promise<void> {\n\t\tconst adapter = this.channelAdapters.get(route.channelConfig.type);\n\t\tif (!adapter) return;\n\n\t\t// Create outbound message\n\t\tconst outbound: OutboundMessage = {\n\t\t\tid: this.generateId(),\n\t\t\tsessionKey: inbound.sessionKey,\n\t\t\tchannel: route.channelConfig.type,\n\t\t\ttarget: inbound.sender.id,\n\t\t\tparts: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: `Received your message: \"${(inbound.content as { text: string }).text ?? \"(no text)\"}\"`,\n\t\t\t\t},\n\t\t\t],\n\t\t\treplyTo: inbound.id,\n\t\t};\n\n\t\tawait adapter.send(outbound);\n\t\tthis.emit(\"message:outbound\", outbound);\n\t}\n\n\t/** Handle HTTP request */\n\tprivate async handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n\t\tconst url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n\n\t\t// CORS headers\n\t\tthis.setCorsHeaders(res);\n\n\t\tif (req.method === \"OPTIONS\") {\n\t\t\tres.writeHead(200);\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\n\t\t// API routes\n\t\tif (url.pathname.startsWith(\"/api/\")) {\n\t\t\tawait this.handleApiRequest(req, res, url);\n\t\t\treturn;\n\t\t}\n\n\t\t// Static files\n\t\tawait this.handleStaticFile(req, res, url);\n\t}\n\n\t/** Set CORS headers */\n\tprivate setCorsHeaders(res: http.ServerResponse): void {\n\t\tconst origins = this.config.corsOrigins;\n\t\tif (origins.includes(\"*\")) {\n\t\t\tres.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n\t\t}\n\t\tres.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\");\n\t\tres.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\");\n\t}\n\n\t/** Handle API request */\n\tprivate async handleApiRequest(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<void> {\n\t\tconst path = url.pathname.slice(5); // Remove /api/\n\n\t\ttry {\n\t\t\tswitch (path) {\n\t\t\t\tcase \"status\":\n\t\t\t\t\tthis.jsonResponse(res, this.getStatus());\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"config\":\n\t\t\t\t\tif (req.method === \"GET\") {\n\t\t\t\t\t\tthis.jsonResponse(res, this.configManager.getConfig());\n\t\t\t\t\t} else if (req.method === \"POST\") {\n\t\t\t\t\t\t// Update config\n\t\t\t\t\t\tconst body = await this.readBody(req);\n\t\t\t\t\t\tconst updates = JSON.parse(body);\n\t\t\t\t\t\tthis.configManager.updateConfig(updates);\n\t\t\t\t\t\tawait this.configManager.save();\n\t\t\t\t\t\tthis.jsonResponse(res, { success: true });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.errorResponse(res, 405, \"Method not allowed\");\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"sessions\":\n\t\t\t\t\tif (req.method === \"GET\") {\n\t\t\t\t\t\tthis.jsonResponse(res, this.sessionManager.getStats());\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.errorResponse(res, 405, \"Method not allowed\");\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"channels\":\n\t\t\t\t\tif (req.method === \"GET\") {\n\t\t\t\t\t\tconst channels = this.configManager.getEnabledChannels().map((c) => ({\n\t\t\t\t\t\t\ttype: c.type,\n\t\t\t\t\t\t\tname: c.name,\n\t\t\t\t\t\t\tenabled: c.enabled,\n\t\t\t\t\t\t\tconnected: this.channelAdapters.get(c.type)?.isConnected() ?? false,\n\t\t\t\t\t\t}));\n\t\t\t\t\t\tthis.jsonResponse(res, channels);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.errorResponse(res, 405, \"Method not allowed\");\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\tthis.errorResponse(res, 404, \"Not found\");\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"API error:\", error);\n\t\t\tthis.errorResponse(res, 500, \"Internal server error\");\n\t\t}\n\t}\n\n\t/** Handle static file request */\n\tprivate async handleStaticFile(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<void> {\n\t\t// Default to index.html\n\t\tlet filePath = url.pathname === \"/\" ? \"index.html\" : url.pathname.slice(1);\n\n\t\t// Security: prevent directory traversal\n\t\tfilePath = path.normalize(filePath).replace(/^(\\.\\.[/\\\\])+/, \"\");\n\n\t\tconst fullPath = path.join(this.config.publicDir, filePath);\n\n\t\ttry {\n\t\t\tconst content = await fs.readFile(fullPath);\n\t\t\tconst ext = path.extname(fullPath);\n\t\t\tconst contentType = this.getContentType(ext);\n\n\t\t\tres.setHeader(\"Content-Type\", contentType);\n\t\t\tres.writeHead(200);\n\t\t\tres.end(content);\n\t\t} catch {\n\t\t\t// Try to serve index.html for SPA routes\n\t\t\ttry {\n\t\t\t\tconst indexPath = path.join(this.config.publicDir, \"index.html\");\n\t\t\t\tconst content = await fs.readFile(indexPath);\n\t\t\t\tres.setHeader(\"Content-Type\", \"text/html\");\n\t\t\t\tres.writeHead(200);\n\t\t\t\tres.end(content);\n\t\t\t} catch {\n\t\t\t\tthis.errorResponse(res, 404, \"Not found\");\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Get content type by extension */\n\tprivate getContentType(ext: string): string {\n\t\tconst types: Record<string, string> = {\n\t\t\t\".html\": \"text/html\",\n\t\t\t\".js\": \"application/javascript\",\n\t\t\t\".css\": \"text/css\",\n\t\t\t\".json\": \"application/json\",\n\t\t\t\".png\": \"image/png\",\n\t\t\t\".jpg\": \"image/jpeg\",\n\t\t\t\".gif\": \"image/gif\",\n\t\t\t\".svg\": \"image/svg+xml\",\n\t\t\t\".ico\": \"image/x-icon\",\n\t\t};\n\t\treturn types[ext] || \"application/octet-stream\";\n\t}\n\n\t/** Handle HTTP upgrade to WebSocket */\n\tprivate handleUpgrade(req: http.IncomingMessage, socket: any, head: Buffer): void {\n\t\tconst upgrade = req.headers.upgrade?.toLowerCase();\n\n\t\tif (upgrade === \"websocket\") {\n\t\t\t// Check if it's a WebSocket path\n\t\t\tconst url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n\t\t\tif (url.pathname === this.config.wsPath) {\n\t\t\t\tthis.wsServer?.handleUpgrade(req, socket, head, (ws) => {\n\t\t\t\t\tthis.wsServer?.emit(\"connection\", ws, req);\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tsocket.destroy();\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Handle WebSocket connection */\n\tprivate handleWsConnection(ws: WebSocket, req: http.IncomingMessage): void {\n\t\tconst connectionId = this.generateConnectionId();\n\t\tconst url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n\n\t\t// Determine connection type from query params\n\t\tconst type = (url.searchParams.get(\"type\") as ConnectionType) || \"client\";\n\t\tconst agentId = url.searchParams.get(\"agent\") || undefined;\n\t\tconst sessionKey = url.searchParams.get(\"session\") || undefined;\n\n\t\tconst connection = new WebSocketConnectionImpl({\n\t\t\tid: connectionId,\n\t\t\ttype,\n\t\t\tagentId,\n\t\t\tsessionKey,\n\t\t\tws,\n\t\t});\n\n\t\tthis.connections.set(connectionId, connection);\n\n\t\t// Handle messages\n\t\tws.on(\"message\", (data) => {\n\t\t\tthis.handleWsMessage(connection, data);\n\t\t});\n\n\t\t// Handle close\n\t\tws.on(\"close\", () => {\n\t\t\tthis.connections.delete(connectionId);\n\t\t\tthis.emit(\"connection:close\", {\n\t\t\t\tconnectionId,\n\t\t\t\treason: \"closed\",\n\t\t\t});\n\t\t});\n\n\t\t// Handle errors\n\t\tws.on(\"error\", (error) => {\n\t\t\tconsole.error(`WebSocket error on ${connectionId}:`, error);\n\t\t\tthis.emit(\"error\", { source: connectionId, error });\n\t\t});\n\n\t\tthis.emit(\"connection:open\", { connectionId, type });\n\n\t\t// Send welcome\n\t\tconnection.send({\n\t\t\ttype: \"connected\",\n\t\t\tconnectionId,\n\t\t\tserverTime: new Date().toISOString(),\n\t\t});\n\t}\n\n\t/** Handle WebSocket message */\n\tprivate handleWsMessage(connection: WebSocketConnectionImpl, data: Buffer | ArrayBuffer | Buffer[]): void {\n\t\ttry {\n\t\t\tconst message = JSON.parse(data.toString());\n\n\t\t\t// Handle different message types\n\t\t\tswitch (message.type) {\n\t\t\t\tcase \"ping\":\n\t\t\t\t\tconnection.send({ type: \"pong\", time: Date.now() });\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"subscribe\":\n\t\t\t\t\t// Subscribe to session events\n\t\t\t\t\tif (message.sessionKey) {\n\t\t\t\t\t\tconnection.sessionKey = message.sessionKey;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"message\":\n\t\t\t\t\t// Handle outgoing message from client\n\t\t\t\t\t// TODO: Process through agent\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\tconsole.log(`Unknown message type: ${message.type}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to parse WebSocket message:\", error);\n\t\t}\n\t}\n\n\t/** Generate unique connection ID */\n\tprivate generateConnectionId(): string {\n\t\treturn `conn-${Date.now()}-${++this.connectionIdCounter}`;\n\t}\n\n\t/** Generate unique message ID */\n\tprivate generateId(): string {\n\t\treturn `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\t}\n\n\t/** Read request body */\n\tprivate readBody(req: http.IncomingMessage): Promise<string> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet body = \"\";\n\t\t\treq.on(\"data\", (chunk) => {\n\t\t\t\tbody += chunk.toString();\n\t\t\t});\n\t\t\treq.on(\"end\", () => resolve(body));\n\t\t\treq.on(\"error\", reject);\n\t\t});\n\t}\n\n\t/** Send JSON response */\n\tprivate jsonResponse(res: http.ServerResponse, data: unknown): void {\n\t\tres.setHeader(\"Content-Type\", \"application/json\");\n\t\tres.writeHead(200);\n\t\tres.end(JSON.stringify(data));\n\t}\n\n\t/** Send error response */\n\tprivate errorResponse(res: http.ServerResponse, code: number, message: string): void {\n\t\tres.setHeader(\"Content-Type\", \"application/json\");\n\t\tres.writeHead(code);\n\t\tres.end(JSON.stringify({ error: message }));\n\t}\n\n\t/** Start cleanup interval */\n\tprivate startCleanupInterval(): void {\n\t\tsetInterval(async () => {\n\t\t\tconst closed = await this.sessionManager.cleanup();\n\t\t\tif (closed > 0) {\n\t\t\t\tconsole.log(`Cleaned up ${closed} idle sessions`);\n\t\t\t}\n\t\t}, 60000); // Every minute\n\t}\n\n\t/** Add event listener */\n\ton<T extends keyof GatewayEventMap>(event: T, listener: (data: GatewayEventMap[T]) => void): void {\n\t\tif (!this.eventListeners.has(event)) {\n\t\t\tthis.eventListeners.set(event, []);\n\t\t}\n\t\tthis.eventListeners.get(event)!.push(listener as (data: unknown) => void);\n\t}\n\n\t/** Remove event listener */\n\toff<T extends keyof GatewayEventMap>(event: T, listener: (data: GatewayEventMap[T]) => void): void {\n\t\tconst listeners = this.eventListeners.get(event);\n\t\tif (listeners) {\n\t\t\tconst index = listeners.indexOf(listener as (data: unknown) => void);\n\t\t\tif (index >= 0) {\n\t\t\t\tlisteners.splice(index, 1);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Emit event */\n\tprivate emit<T extends keyof GatewayEventMap>(event: T, data: GatewayEventMap[T]): void {\n\t\tconst listeners = this.eventListeners.get(event);\n\t\tif (listeners) {\n\t\t\tfor (const listener of listeners) {\n\t\t\t\ttry {\n\t\t\t\t\tlistener(data);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(`Error in event listener for ${event}:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** WebSocket connection implementation */\nclass WebSocketConnectionImpl implements WebSocketConnection {\n\tid: string;\n\ttype: ConnectionType;\n\tagentId?: string;\n\tsessionKey?: string;\n\tws: WebSocket;\n\tisAlive = true;\n\tlastActivity: Date;\n\n\tconstructor(params: {\n\t\tid: string;\n\t\ttype: ConnectionType;\n\t\tagentId?: string;\n\t\tsessionKey?: string;\n\t\tws: WebSocket;\n\t}) {\n\t\tthis.id = params.id;\n\t\tthis.type = params.type;\n\t\tthis.agentId = params.agentId;\n\t\tthis.sessionKey = params.sessionKey;\n\t\tthis.ws = params.ws;\n\t\tthis.lastActivity = new Date();\n\t}\n\n\tsend(data: unknown): void {\n\t\tif (this.ws.readyState === WebSocket.OPEN) {\n\t\t\tthis.ws.send(JSON.stringify(data));\n\t\t\tthis.lastActivity = new Date();\n\t\t}\n\t}\n\n\tclose(): void {\n\t\tthis.ws.close();\n\t\tthis.isAlive = false;\n\t}\n}\n"]}