{"version":3,"file":"ws-framing.cjs","names":["EventEmitter"],"sources":["../src/ws-framing.ts"],"sourcesContent":["/**\n * Minimal RFC 6455 WebSocket server implementation.\n *\n * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).\n * Supports text frames, ping/pong, close handshake, and client frame unmasking.\n * Designed for a mock server — no extensions, no binary frames, no compression.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport type * as net from \"node:net\";\nimport type * as http from \"node:http\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-5AB5DC799C07\";\n\n// Opcodes\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\nexport class WebSocketConnection extends EventEmitter {\n  private socket: net.Socket;\n  private buffer: Buffer = Buffer.alloc(0);\n  private closed = false;\n\n  // For fragmented messages (continuation frames)\n  private fragments: Buffer[] = [];\n\n  constructor(socket: net.Socket) {\n    super();\n    this.socket = socket;\n\n    socket.on(\"data\", (data: Buffer) => {\n      this.buffer = Buffer.concat([this.buffer, data]);\n      this.parseFrames();\n    });\n\n    socket.on(\"close\", () => {\n      if (!this.closed) {\n        this.closed = true;\n        this.emit(\"close\", 1006, \"Connection lost\");\n      }\n    });\n\n    socket.on(\"error\", (err: Error) => {\n      this.emit(\"error\", err);\n    });\n  }\n\n  send(data: string): void {\n    if (this.closed) return;\n    const payload = Buffer.from(data, \"utf-8\");\n    this.writeFrame(OP_TEXT, payload);\n  }\n\n  close(code = 1000, reason = \"\"): void {\n    if (this.closed) return;\n    this.closed = true;\n\n    const reasonBuf = Buffer.from(reason, \"utf-8\");\n    const payload = Buffer.alloc(2 + reasonBuf.length);\n    payload.writeUInt16BE(code, 0);\n    reasonBuf.copy(payload, 2);\n    this.writeFrame(OP_CLOSE, payload);\n\n    // Give the client a moment to receive the close frame before destroying.\n    // If writeFrame failed (socket already destroyed), this is a no-op.\n    setTimeout(() => {\n      if (!this.socket.destroyed) {\n        this.socket.destroy();\n      }\n      // Emit close event for server-initiated closes so listeners\n      // (e.g. activeConnections.delete) always fire.\n      this.emit(\"close\", code, reason);\n    }, 100);\n  }\n\n  destroy(): void {\n    if (this.closed) return;\n    this.closed = true;\n    if (!this.socket.destroyed) {\n      this.socket.destroy();\n    }\n    this.emit(\"close\", 1006, \"Connection destroyed\");\n  }\n\n  get isClosed(): boolean {\n    return this.closed;\n  }\n\n  private writeFrame(opcode: number, payload: Buffer): void {\n    if (this.socket.destroyed) return;\n\n    // Server-to-client frames are NOT masked (per RFC 6455 §5.1)\n    const length = payload.length;\n    let header: Buffer;\n\n    if (length < 126) {\n      header = Buffer.alloc(2);\n      header[0] = 0x80 | opcode; // FIN + opcode\n      header[1] = length;\n    } else if (length < 65536) {\n      header = Buffer.alloc(4);\n      header[0] = 0x80 | opcode;\n      header[1] = 126;\n      header.writeUInt16BE(length, 2);\n    } else {\n      header = Buffer.alloc(10);\n      header[0] = 0x80 | opcode;\n      header[1] = 127;\n      header.writeUInt32BE(0, 2);\n      header.writeUInt32BE(length, 6);\n    }\n\n    try {\n      this.socket.write(Buffer.concat([header, payload]));\n    } catch (err: unknown) {\n      // Expected when socket is destroyed between our check and write.\n      // Log unexpected errors so they don't vanish silently.\n      if (!this.socket.destroyed) {\n        const msg = err instanceof Error ? err.message : String(err);\n        console.error(`[LLMock] Unexpected writeFrame error: ${msg}`);\n      }\n    }\n  }\n\n  private parseFrames(): void {\n    while (this.buffer.length >= 2 && !this.closed) {\n      const byte0 = this.buffer[0];\n      const byte1 = this.buffer[1];\n\n      const fin = (byte0 & 0x80) !== 0;\n      const opcode = byte0 & 0x0f;\n      const masked = (byte1 & 0x80) !== 0;\n      let payloadLength = byte1 & 0x7f;\n      let offset = 2;\n\n      if (payloadLength === 126) {\n        if (this.buffer.length < 4) return; // need more data\n        payloadLength = this.buffer.readUInt16BE(2);\n        offset = 4;\n      } else if (payloadLength === 127) {\n        if (this.buffer.length < 10) return;\n        // Read lower 32 bits (upper 32 should be 0 for reasonable payloads)\n        payloadLength = this.buffer.readUInt32BE(6) + this.buffer.readUInt32BE(2) * 0x100000000;\n        offset = 10;\n      }\n\n      const maskSize = masked ? 4 : 0;\n      const totalFrameSize = offset + maskSize + payloadLength;\n\n      if (this.buffer.length < totalFrameSize) return; // need more data\n\n      let maskKey: Buffer | null = null;\n      if (masked) {\n        maskKey = this.buffer.subarray(offset, offset + 4);\n        offset += 4;\n      }\n\n      let payload = this.buffer.subarray(offset, offset + payloadLength);\n\n      // Unmask client payload\n      if (maskKey) {\n        payload = Buffer.from(payload); // copy before mutating\n        for (let i = 0; i < payload.length; i++) {\n          payload[i] ^= maskKey[i % 4];\n        }\n      }\n\n      // Consume the frame from the buffer\n      this.buffer = this.buffer.subarray(totalFrameSize);\n\n      this.handleFrame(fin, opcode, payload);\n    }\n  }\n\n  private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n    // Control frames (opcode >= 0x8) must not be fragmented\n    if (opcode === OP_PING) {\n      this.writeFrame(OP_PONG, payload);\n      return;\n    }\n\n    if (opcode === OP_PONG) {\n      // Ignore unsolicited pongs\n      return;\n    }\n\n    if (opcode === OP_CLOSE) {\n      const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;\n      const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf-8\") : \"\";\n\n      if (!this.closed) {\n        this.closed = true;\n        // Echo close frame back\n        this.writeFrame(OP_CLOSE, payload);\n        this.socket.end();\n        this.emit(\"close\", code, reason);\n      }\n      // If already closed (server-initiated or duplicate), ignore — the\n      // close event was already emitted by close() or the first OP_CLOSE.\n      return;\n    }\n\n    // Text or continuation frames\n    if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {\n      this.fragments.push(payload);\n\n      if (fin) {\n        const message = Buffer.concat(this.fragments).toString(\"utf-8\");\n        this.fragments = [];\n        this.emit(\"message\", message);\n      }\n      // If !fin, wait for more continuation frames\n      return;\n    }\n\n    // Binary or unknown — just ignore for a mock server\n  }\n}\n\nexport function computeAcceptKey(wsKey: string): string {\n  return createHash(\"sha1\")\n    .update(wsKey + WS_GUID)\n    .digest(\"base64\");\n}\n\nexport function upgradeToWebSocket(\n  req: http.IncomingMessage,\n  socket: net.Socket,\n): WebSocketConnection {\n  const key = req.headers[\"sec-websocket-key\"];\n  if (!key) {\n    socket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n    socket.destroy();\n    throw new Error(\"Missing Sec-WebSocket-Key header\");\n  }\n\n  const acceptKey = computeAcceptKey(key);\n\n  let responseHeaders =\n    \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n    \"Upgrade: websocket\\r\\n\" +\n    \"Connection: Upgrade\\r\\n\" +\n    `Sec-WebSocket-Accept: ${acceptKey}\\r\\n`;\n\n  // Echo back requested subprotocol if present\n  const protocol = req.headers[\"sec-websocket-protocol\"];\n  if (protocol) {\n    // Take the first offered protocol\n    const first = protocol.split(\",\")[0].trim();\n    responseHeaders += `Sec-WebSocket-Protocol: ${first}\\r\\n`;\n  }\n\n  responseHeaders += \"\\r\\n\";\n\n  socket.write(responseHeaders);\n\n  return new WebSocketConnection(socket);\n}\n"],"mappings":"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"}