@orkestrel/websocket 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/src/server/index.cjs +608 -0
- package/dist/src/server/index.cjs.map +1 -0
- package/dist/src/server/index.d.cts +399 -0
- package/dist/src/server/index.d.ts +399 -0
- package/dist/src/server/index.js +580 -0
- package/dist/src/server/index.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#emitter","#socket","#protocol","#masked","#payload","#timeout","#requireMask","#signal","#onData","#readyState","#bytes","#ingest","#onClose","#finish","#onError","#onAbort","#write","#code","#reason","#encodeClose","#closeTimer","#destroyed","#buffer","#dispatch","#fail","#close","#messageOpcode","#fragments","#fragmentBytes","#decodeClose","#drain"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/NodeWebSocket.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { WebSocketReadyState } from './types.js'\n\n// The WebSocket wrapper's wire constants (AGENTS §5 constants file) — the RFC 6455\n// magic values the codec and the handshake are built on: the accept GUID, the\n// supported protocol version, the frame opcodes, the four ready states, and the\n// normal-closure status code. Every member is exported; the codec helpers and the\n// `NodeWebSocket` wrapper read them by name rather than re-spelling the bit values.\n\n/**\n * The RFC 6455 GUID concatenated to a client's `Sec-WebSocket-Key` before the SHA-1\n * hash that yields the `Sec-WebSocket-Accept` response value.\n *\n * @remarks\n * A fixed, spec-mandated constant (RFC 6455 §4.2.2) — read only by\n * {@link computeWebSocketAccept}.\n */\nexport const WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'\n\n/** The WebSocket protocol version this wrapper speaks (`Sec-WebSocket-Version: 13`). */\nexport const WEBSOCKET_VERSION = '13'\n\n/** Text frame opcode — a UTF-8 payload (RFC 6455 §5.6). */\nexport const WEBSOCKET_OPCODE_TEXT = 0x01\n\n/** Binary frame opcode — a raw byte payload (RFC 6455 §5.6). */\nexport const WEBSOCKET_OPCODE_BINARY = 0x02\n\n/** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */\nexport const WEBSOCKET_OPCODE_CLOSE = 0x08\n\n/** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */\nexport const WEBSOCKET_OPCODE_PING = 0x09\n\n/** Pong frame opcode — a control frame answering a ping (RFC 6455 §5.5.3). */\nexport const WEBSOCKET_OPCODE_PONG = 0x0a\n\n/** Ready state for a connecting WebSocket (before the handshake completes). */\nexport const WEBSOCKET_READY_CONNECTING: WebSocketReadyState = 0\n\n/** Ready state for an open WebSocket (the handshake completed; frames flow). */\nexport const WEBSOCKET_READY_OPEN: WebSocketReadyState = 1\n\n/** Ready state for a closing WebSocket (a close frame was sent or received). */\nexport const WEBSOCKET_READY_CLOSING: WebSocketReadyState = 2\n\n/** Ready state for a closed WebSocket (the socket ended). */\nexport const WEBSOCKET_READY_CLOSED: WebSocketReadyState = 3\n\n/** Normal-closure status code (RFC 6455 §7.4.1) — the default `close` code. */\nexport const WEBSOCKET_CLOSE_NORMAL = 1000\n\n/** Protocol-error status code (RFC 6455 §7.4.1) — a framing/state rule was violated. */\nexport const WEBSOCKET_CLOSE_PROTOCOL = 1002\n\n/** Unsupported-data status code (RFC 6455 §7.4.1) — the endpoint received a data type it cannot accept (e.g. binary on a text-only endpoint). */\nexport const WEBSOCKET_CLOSE_UNSUPPORTED = 1003\n\n/** Invalid-frame-payload-data status code (RFC 6455 §7.4.1) — e.g. non-UTF-8 text or an unparseable close reason. */\nexport const WEBSOCKET_CLOSE_INVALID = 1007\n\n/** Message-too-big status code (RFC 6455 §7.4.1) — a reassembled message exceeded the payload cap. */\nexport const WEBSOCKET_CLOSE_TOOBIG = 1009\n\n/** The default maximum inbound single-frame length AND reassembled-message total byte count (100 MiB — the `ws` package default). */\nexport const WEBSOCKET_MAX_PAYLOAD = 104_857_600\n\n/** The default close-handshake timeout in milliseconds — how long `close()` waits for the peer's echo before tearing the socket down. */\nexport const WEBSOCKET_CLOSE_TIMEOUT_MS = 30_000\n\n/** The post-`#fail` flush grace in milliseconds — how long a validation-breach close frame is given to flush through the socket's write buffer before the hard `destroy()` fallback fires (the normal path destroys sooner, on the `end()` flush callback). */\nexport const WEBSOCKET_FAIL_TIMEOUT_MS = 1_000\n\n/** The maximum control-frame payload length in bytes (RFC 6455 §5.5). */\nexport const WEBSOCKET_CONTROL_MAXLEN = 125\n","import type { WebSocketEncodeOptions, WebSocketFrame } from './types.js'\nimport { createHash, randomBytes } from 'node:crypto'\nimport { WEBSOCKET_GUID } from './constants.js'\n\n// The RFC 6455 codec — three pure, exported, exhaustively unit-tested functions that\n// are the entire bit-level surface of the WebSocket wrapper (AGENTS §5: the codec\n// branches are exported helpers, not hidden privates). `computeWebSocketAccept`\n// derives the handshake token; `parseWebSocketFrame` decodes ONE frame off a buffer,\n// returning `undefined` when the buffer holds an incomplete frame so the caller\n// accumulates across `data` chunks (the same streaming-decoder contract as the core\n// `SSEParser`); `encodeWebSocketFrame` is the inverse — it builds the wire bytes for a\n// frame. `parse` and `encode` are exact inverses, proven by the round-trip tests.\n//\n// Numeric byte reads are narrowed with `?? 0` rather than `!` (AGENTS §14): a read\n// past the buffer is impossible once the length guards pass, and `?? 0` keeps the\n// arithmetic total without an assertion.\n\n/**\n * Compute the `Sec-WebSocket-Accept` response value for an RFC 6455 upgrade.\n *\n * @remarks\n * The base64-encoded SHA-1 of the client's `Sec-WebSocket-Key` concatenated with the\n * fixed {@link WEBSOCKET_GUID} (RFC 6455 §4.2.2) — the proof the server understood the\n * handshake. Pure and deterministic.\n *\n * @param key - The client's `Sec-WebSocket-Key` header value\n * @returns The base64 accept token to send back as `Sec-WebSocket-Accept`\n */\nexport function computeWebSocketAccept(key: string): string {\n\treturn createHash('sha1')\n\t\t.update(key + WEBSOCKET_GUID)\n\t\t.digest('base64')\n}\n\n/**\n * Decode a single RFC 6455 frame from the front of a buffer.\n *\n * @remarks\n * Reads the FIN bit and opcode (byte 0), the mask bit and 7-bit payload length (byte\n * 1) — extended to a 16-bit length when the 7-bit field is `126`, or a 64-bit length\n * when it is `127` — the optional 4-byte mask key, then the payload, XOR-unmasking it\n * against the key when the mask bit is set (client→server frames MUST be masked, RFC\n * 6455 §5.3; an unmasked frame still decodes, leaving the payload as-is, so the caller\n * can enforce policy). Returns `undefined` the moment the buffer is too short for the\n * part it is up to (the length prefix, the mask, or the full payload) — the signal to\n * the caller to read more bytes and retry, exactly like {@link SSEParser} on a partial\n * line. `consumed` is the total bytes the frame occupied, so the caller slices the\n * remainder. Pure; never throws on a short buffer.\n *\n * @param buffer - The accumulation buffer to decode the next frame from\n * @returns The parsed {@link WebSocketFrame}, or `undefined` when the buffer is incomplete\n */\nexport function parseWebSocketFrame(buffer: Buffer): WebSocketFrame | undefined {\n\tif (buffer.length < 2) return undefined\n\n\tconst firstByte = buffer[0] ?? 0\n\tconst secondByte = buffer[1] ?? 0\n\n\tconst fin = (firstByte & 0x80) !== 0\n\tconst rsv = (firstByte & 0x70) >> 4\n\tconst opcode = firstByte & 0x0f\n\tconst masked = (secondByte & 0x80) !== 0\n\tlet length = secondByte & 0x7f\n\tlet offset = 2\n\n\tif (length === 126) {\n\t\tif (buffer.length < offset + 2) return undefined\n\t\tlength = buffer.readUInt16BE(offset)\n\t\toffset += 2\n\t} else if (length === 127) {\n\t\tif (buffer.length < offset + 8) return undefined\n\t\t// Split into two 32-bit reads — a payload past 2^53 is beyond any real frame,\n\t\t// and this keeps the arithmetic in safe-integer range.\n\t\tconst high = buffer.readUInt32BE(offset)\n\t\tconst low = buffer.readUInt32BE(offset + 4)\n\t\tlength = high * 0x1_0000_0000 + low\n\t\toffset += 8\n\t}\n\n\tlet mask: Buffer | undefined\n\tif (masked) {\n\t\tif (buffer.length < offset + 4) return undefined\n\t\tmask = buffer.subarray(offset, offset + 4)\n\t\toffset += 4\n\t}\n\n\tif (buffer.length < offset + length) return undefined\n\n\tconst payload = Buffer.alloc(length)\n\tbuffer.copy(payload, 0, offset, offset + length)\n\n\tif (mask !== undefined) {\n\t\tfor (let index = 0; index < length; index += 1) {\n\t\t\tpayload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0)\n\t\t}\n\t}\n\n\treturn { fin, opcode, payload, consumed: offset + length, masked, rsv }\n}\n\n/**\n * Read the declared payload length off the front of a buffer, without buffering or\n * reading the payload itself.\n *\n * @remarks\n * Decodes only byte 1's 7-bit length field, extended by the 16-bit (`126`) or 64-bit\n * (`127`) form exactly like {@link parseWebSocketFrame} — but stops there, so a caller\n * can reject an over-cap frame the moment its length is known, before the payload\n * bytes have even arrived. Returns `undefined` until the length field itself is fully\n * buffered (mirrors the parser's incomplete-buffer contract). Pure; never throws.\n *\n * @param buffer - The accumulation buffer to read the next frame's length from\n * @returns The declared payload length, or `undefined` when the buffer is too short to know it yet\n *\n * @example\n * ```ts\n * const declared = measureWebSocketFrame(buffer)\n * if (declared !== undefined && declared > limit) fail(WEBSOCKET_CLOSE_TOOBIG)\n * ```\n */\nexport function measureWebSocketFrame(buffer: Buffer): number | undefined {\n\tif (buffer.length < 2) return undefined\n\n\tconst secondByte = buffer[1] ?? 0\n\tlet length = secondByte & 0x7f\n\tconst offset = 2\n\n\tif (length === 126) {\n\t\tif (buffer.length < offset + 2) return undefined\n\t\tlength = buffer.readUInt16BE(offset)\n\t} else if (length === 127) {\n\t\tif (buffer.length < offset + 8) return undefined\n\t\tconst high = buffer.readUInt32BE(offset)\n\t\tconst low = buffer.readUInt32BE(offset + 4)\n\t\tlength = high * 0x1_0000_0000 + low\n\t}\n\n\treturn length\n}\n\n/**\n * Decode a byte sequence as strict UTF-8, or signal it is malformed.\n *\n * @remarks\n * Wraps `TextDecoder('utf-8', { fatal: true })` in a try/catch so a malformed sequence\n * returns `undefined` instead of throwing (AGENTS §14 — a guard-adjacent coercer never\n * throws on bad input). Pure.\n *\n * @param bytes - The raw bytes to decode\n * @returns The decoded string, or `undefined` when `bytes` is not valid UTF-8\n *\n * @example\n * ```ts\n * const text = parseUTF8(payload)\n * if (text === undefined) fail(WEBSOCKET_CLOSE_INVALID)\n * ```\n */\nexport function parseUTF8(bytes: Buffer): string | undefined {\n\ttry {\n\t\treturn new TextDecoder('utf-8', { fatal: true }).decode(bytes)\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Whether a numeric value is a valid RFC 6455 close status code to RECEIVE (§7.4.1).\n *\n * @remarks\n * True for `1000`–`1003`, `1007`–`1014`, and the application range `3000`–`4999`; false\n * for anything below `1000`, the reserved-for-local-use-only codes `1004`–`1006` and\n * `1015`, and the unassigned `1016`–`2999` range. The `1012`–`1014` extension of the\n * strict RFC 6455 receivable set is a deliberate IANA-interop choice: those three codes\n * (Service Restart, Try Again Later, Bad Gateway) are IANA-registered in the WebSocket\n * Close Code Number Registry and accepted by the `ws` ecosystem and modern conformance\n * suites, so a peer sending one is not treated as a protocol violation. Pure predicate,\n * never throws.\n *\n * @param code - The close status code to validate\n * @returns `true` when `code` is a valid RFC 6455 close code\n *\n * @example\n * ```ts\n * if (!isCloseCode(code)) fail(WEBSOCKET_CLOSE_PROTOCOL)\n * ```\n */\nexport function isCloseCode(code: number): boolean {\n\tif (code >= 1000 && code <= 1003) return true\n\tif (code >= 1007 && code <= 1014) return true\n\tif (code >= 3000 && code <= 4999) return true\n\treturn false\n}\n\n/**\n * Encode a single RFC 6455 frame to its wire bytes — the inverse of\n * {@link parseWebSocketFrame}.\n *\n * @remarks\n * Builds a final (FIN-set) frame: byte 0 is `0x80 | opcode`; the payload length uses\n * the 7-bit form below 126, the `126` + 16-bit form below 65 536, or the `127` +\n * 64-bit form beyond; when `masked` is set the mask bit is set, a 4-byte key (supplied\n * via `options.mask`, else random) is written, and the payload is XOR-masked. Server→\n * client frames are unmasked (the default); pass `masked: true` to encode a CLIENT\n * frame (e.g. to feed the parser in a test). A `string` payload is encoded as UTF-8.\n * Returns one contiguous `Buffer` (header + payload), so the wrapper writes it with a\n * single `socket.write`. Pure.\n *\n * @param opcode - The frame opcode (a `WEBSOCKET_OPCODE_*` value)\n * @param payload - The payload, a `Buffer` or a UTF-8 `string`\n * @param options - Masking control ({@link WebSocketEncodeOptions}); defaults to unmasked\n * @returns The complete frame as wire bytes\n */\nexport function encodeWebSocketFrame(\n\topcode: number,\n\tpayload: Buffer | string,\n\toptions?: WebSocketEncodeOptions,\n): Buffer {\n\tconst body = typeof payload === 'string' ? Buffer.from(payload, 'utf-8') : payload\n\tconst length = body.length\n\tconst masked = options?.masked === true\n\tconst mask = masked ? (options?.mask ?? randomBytes(4)) : undefined\n\tconst maskBit = masked ? 0x80 : 0\n\n\t// The header size: 2 base bytes + the extended-length bytes (0 / 2 / 8) + the mask\n\t// key (0 / 4). The length prefix and the mask key write into this header.\n\tconst extended = length < 126 ? 0 : length < 65_536 ? 2 : 8\n\tconst header = Buffer.alloc(2 + extended + (mask !== undefined ? 4 : 0))\n\theader[0] = 0x80 | opcode\n\n\tif (length < 126) {\n\t\theader[1] = maskBit | length\n\t} else if (length < 65_536) {\n\t\theader[1] = maskBit | 126\n\t\theader.writeUInt16BE(length, 2)\n\t} else {\n\t\theader[1] = maskBit | 127\n\t\theader.writeUInt32BE(Math.floor(length / 0x1_0000_0000), 2)\n\t\theader.writeUInt32BE(length % 0x1_0000_0000, 6)\n\t}\n\n\tif (mask === undefined) return Buffer.concat([header, body])\n\n\tmask.copy(header, header.length - 4)\n\tconst maskedBody = Buffer.alloc(length)\n\tfor (let index = 0; index < length; index += 1) {\n\t\tmaskedBody[index] = (body[index] ?? 0) ^ (mask[index % 4] ?? 0)\n\t}\n\treturn Buffer.concat([header, maskedBody])\n}\n","import type { Duplex } from 'node:stream'\nimport type {\n\tNodeWebSocketEventMap,\n\tNodeWebSocketInterface,\n\tNodeWebSocketOptions,\n\tWebSocketReadyState,\n} from './types.js'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcomputeWebSocketAccept,\n\tencodeWebSocketFrame,\n\tisCloseCode,\n\tmeasureWebSocketFrame,\n\tparseUTF8,\n\tparseWebSocketFrame,\n} from './helpers.js'\nimport {\n\tWEBSOCKET_CLOSE_INVALID,\n\tWEBSOCKET_CLOSE_NORMAL,\n\tWEBSOCKET_CLOSE_PROTOCOL,\n\tWEBSOCKET_CLOSE_TIMEOUT_MS,\n\tWEBSOCKET_CLOSE_TOOBIG,\n\tWEBSOCKET_CLOSE_UNSUPPORTED,\n\tWEBSOCKET_CONTROL_MAXLEN,\n\tWEBSOCKET_FAIL_TIMEOUT_MS,\n\tWEBSOCKET_MAX_PAYLOAD,\n\tWEBSOCKET_OPCODE_BINARY,\n\tWEBSOCKET_OPCODE_CLOSE,\n\tWEBSOCKET_OPCODE_PING,\n\tWEBSOCKET_OPCODE_PONG,\n\tWEBSOCKET_OPCODE_TEXT,\n\tWEBSOCKET_READY_CLOSED,\n\tWEBSOCKET_READY_CLOSING,\n\tWEBSOCKET_READY_CONNECTING,\n\tWEBSOCKET_READY_OPEN,\n} from './constants.js'\n\n/**\n * A server-native WebSocket over a raw upgraded `node:stream` Duplex — the lean\n * wrapper around the RFC 6455 wire protocol.\n *\n * @remarks\n * Created by `createNodeWebSocket`. When given a client `key` it runs in SERVER mode —\n * it writes the `101 Switching Protocols` handshake (`computeWebSocketAccept(key)`) and\n * emits `open`; given no key it runs in CLIENT mode (no handshake, frames masked). It\n * then listens on the socket's `data`, accumulating bytes in `#buffer` and decoding\n * every complete frame with {@link parseWebSocketFrame} (slicing `consumed` and\n * re-parsing the remainder): a TEXT frame — reassembling continuation fragments across\n * `fin: false` frames — decodes to UTF-8 and emits `message`; a PING is auto-answered\n * with a PONG and emits `ping`; a PONG emits `pong`; a CLOSE is echoed and ends the\n * socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close\n * frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that\n * isolates a throwing listener and routes the error to its own `error` handler (the `error`\n * option) — the socket never crashes. The untyped socket `data` is narrowed to a `Buffer`\n * with a guard, never an assertion (AGENTS §14).\n */\nexport class NodeWebSocket implements NodeWebSocketInterface {\n\treadonly #emitter: Emitter<NodeWebSocketEventMap>\n\treadonly #socket: Duplex\n\treadonly #protocol: string | undefined\n\treadonly #masked: boolean\n\treadonly #payload: number\n\treadonly #timeout: number\n\treadonly #requireMask: boolean\n\treadonly #signal: AbortSignal | undefined\n\t#buffer: Buffer = Buffer.alloc(0)\n\t#readyState: WebSocketReadyState = WEBSOCKET_READY_CONNECTING\n\t#code: number | undefined = undefined\n\t#reason: string | undefined = undefined\n\t#fragments: Buffer[] = []\n\t#messageOpcode: number | undefined = undefined\n\t#fragmentBytes = 0\n\t#closeTimer: ReturnType<typeof setTimeout> | undefined = undefined\n\t#destroyed = false\n\n\t// The socket listeners are bound fields so `destroy` can detach exactly these.\n\treadonly #onData = (chunk: unknown): void => {\n\t\tif (this.#readyState === WEBSOCKET_READY_CLOSED) return\n\t\tconst bytes = this.#bytes(chunk)\n\t\tif (bytes === undefined) return\n\t\tthis.#ingest(bytes)\n\t}\n\n\treadonly #onClose = (): void => {\n\t\tthis.#finish()\n\t}\n\n\treadonly #onError = (error: unknown): void => {\n\t\tthis.#emitter.emit('error', error)\n\t}\n\n\t// Bound so `#finish` / `destroy` can detach exactly this listener from `#signal`.\n\treadonly #onAbort = (): void => {\n\t\tthis.destroy()\n\t}\n\n\tconstructor(options: NodeWebSocketOptions) {\n\t\tthis.#emitter = new Emitter({ on: options.on, error: options.error })\n\t\tthis.#socket = options.socket\n\t\tthis.#protocol = options.protocol\n\t\t// Server mode is identified by a client key (it writes the handshake + sends\n\t\t// unmasked frames); without one this is a client (no handshake, masked frames).\n\t\tthis.#masked = options.key === undefined\n\t\tthis.#payload = options.payload ?? WEBSOCKET_MAX_PAYLOAD\n\t\tthis.#timeout = options.timeout ?? WEBSOCKET_CLOSE_TIMEOUT_MS\n\t\t// A server (masked === false, i.e. this instance is unmasked outbound) requires\n\t\t// masked inbound frames from the client (RFC 6455 §5.1); a client instance accepts\n\t\t// unmasked frames from the server.\n\t\tthis.#requireMask = !this.#masked\n\t\tthis.#signal = options.signal\n\n\t\tif (options.key !== undefined) {\n\t\t\tconst protocol =\n\t\t\t\tthis.#protocol === undefined ? '' : `Sec-WebSocket-Protocol: ${this.#protocol}\\r\\n`\n\t\t\tthis.#socket.write(\n\t\t\t\t'HTTP/1.1 101 Switching Protocols\\r\\n' +\n\t\t\t\t\t'Upgrade: websocket\\r\\n' +\n\t\t\t\t\t'Connection: Upgrade\\r\\n' +\n\t\t\t\t\t`Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\\r\\n` +\n\t\t\t\t\tprotocol +\n\t\t\t\t\t'\\r\\n',\n\t\t\t)\n\t\t}\n\n\t\tthis.#readyState = WEBSOCKET_READY_OPEN\n\t\tthis.#socket.on('data', this.#onData)\n\t\tthis.#socket.on('close', this.#onClose)\n\t\tthis.#socket.on('error', this.#onError)\n\t\tthis.#emitter.emit('open')\n\n\t\t// Replay any bytes buffered after the upgrade headers through the same ingest path\n\t\t// as `#onData`, so the pre-buffer cap check applies uniformly (AGENTS §5 dedup).\n\t\tconst head = options.head\n\t\tif (head !== undefined && head.length > 0) {\n\t\t\tthis.#ingest(head)\n\t\t}\n\n\t\t// The external cancellation seam (composes with `@orkestrel/abort` /\n\t\t// `@orkestrel/timeout`'s native AbortSignals) — wired last so an already-aborted\n\t\t// signal tears the socket down only after the rest of construction has run. The\n\t\t// head-replay above can itself synchronously terminate the socket (a complete\n\t\t// CLOSE frame or an RFC violation routes through `#fail`/`#close` -> `#finish`),\n\t\t// which flushes the close frame GRACEFULLY via `#socket.end()`. In that case skip\n\t\t// the seam entirely: forcing `destroy()` would discard that flushing frame (the\n\t\t// loss `#fail` is engineered to avoid), and there is no live socket to attach to.\n\t\tif (this.#readyState !== WEBSOCKET_READY_CLOSED) {\n\t\t\tif (this.#signal?.aborted === true) {\n\t\t\t\tthis.destroy()\n\t\t\t} else {\n\t\t\t\tthis.#signal?.addEventListener('abort', this.#onAbort, { once: true })\n\t\t\t}\n\t\t}\n\t}\n\n\tget emitter(): EmitterInterface<NodeWebSocketEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget readyState(): WebSocketReadyState {\n\t\treturn this.#readyState\n\t}\n\n\tsend(data: string): void {\n\t\tif (this.#readyState !== WEBSOCKET_READY_OPEN) return\n\t\tthis.#write(WEBSOCKET_OPCODE_TEXT, Buffer.from(data, 'utf-8'))\n\t}\n\n\tping(data?: string): void {\n\t\tif (this.#readyState !== WEBSOCKET_READY_OPEN) return\n\t\tif (data !== undefined && Buffer.byteLength(data, 'utf-8') > WEBSOCKET_CONTROL_MAXLEN) {\n\t\t\tthrow new RangeError('ping payload exceeds 125 bytes')\n\t\t}\n\t\tthis.#write(\n\t\t\tWEBSOCKET_OPCODE_PING,\n\t\t\tdata === undefined ? Buffer.alloc(0) : Buffer.from(data, 'utf-8'),\n\t\t)\n\t}\n\n\tclose(code?: number, reason?: string): void {\n\t\tif (\n\t\t\tthis.#readyState === WEBSOCKET_READY_CLOSING ||\n\t\t\tthis.#readyState === WEBSOCKET_READY_CLOSED\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (code !== undefined && !isCloseCode(code)) throw new RangeError('invalid close code')\n\t\tif (reason !== undefined && Buffer.byteLength(reason, 'utf-8') > 123) {\n\t\t\tthrow new RangeError('close reason exceeds 123 bytes')\n\t\t}\n\t\tthis.#readyState = WEBSOCKET_READY_CLOSING\n\t\tthis.#code = code ?? WEBSOCKET_CLOSE_NORMAL\n\t\tthis.#reason = reason === undefined || reason.length === 0 ? undefined : reason\n\t\tthis.#write(WEBSOCKET_OPCODE_CLOSE, this.#encodeClose(this.#code, this.#reason))\n\t\t// End the writable side after the close frame; the peer's echo (or the socket\n\t\t// `close`) drives the final state transition through `#finish`.\n\t\tthis.#socket.end()\n\t\tthis.#closeTimer = setTimeout(() => this.destroy(), this.#timeout)\n\t\tthis.#closeTimer.unref()\n\t}\n\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tthis.#destroyed = true\n\t\tthis.#socket.off('data', this.#onData)\n\t\tthis.#socket.off('close', this.#onClose)\n\t\tthis.#socket.off('error', this.#onError)\n\t\tthis.#signal?.removeEventListener('abort', this.#onAbort)\n\t\t// `#finish` no-ops once already CLOSED (e.g. after `#fail` armed the hard-teardown\n\t\t// fallback), so the timer is cleared here unconditionally rather than relying on it.\n\t\tclearTimeout(this.#closeTimer)\n\t\tthis.#closeTimer = undefined\n\t\tif (!this.#socket.destroyed) this.#socket.destroy()\n\t\tthis.#finish()\n\t\tthis.#emitter.destroy()\n\t}\n\n\t// Decode every complete frame currently in the buffer, dispatching each and slicing\n\t// it off; stops when a partial frame remains (parse returns `undefined`).\n\t#drain(): void {\n\t\tfor (;;) {\n\t\t\tconst frame = parseWebSocketFrame(this.#buffer)\n\t\t\tif (frame === undefined) return\n\t\t\tthis.#buffer = this.#buffer.subarray(frame.consumed)\n\t\t\tthis.#dispatch(frame.fin, frame.opcode, frame.payload, frame.masked, frame.rsv)\n\t\t\tif (this.#readyState === WEBSOCKET_READY_CLOSED) return\n\t\t}\n\t}\n\n\t// Route one decoded frame through the RFC 6455 validation gauntlet, then the\n\t// fragmentation state machine. Any validity breach funnels through `#fail`, which\n\t// closes with the specified code and tears the socket down.\n\t#dispatch(fin: boolean, opcode: number, payload: Buffer, masked: boolean, rsv: number): void {\n\t\tif (rsv !== 0) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\treturn\n\t\t}\n\t\tif (masked !== this.#requireMask) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\treturn\n\t\t}\n\n\t\tif (\n\t\t\topcode === WEBSOCKET_OPCODE_CLOSE ||\n\t\t\topcode === WEBSOCKET_OPCODE_PING ||\n\t\t\topcode === WEBSOCKET_OPCODE_PONG\n\t\t) {\n\t\t\tif (!fin || payload.length > WEBSOCKET_CONTROL_MAXLEN) {\n\t\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (opcode === WEBSOCKET_OPCODE_PING) {\n\t\t\t\tthis.#write(WEBSOCKET_OPCODE_PONG, payload)\n\t\t\t\tthis.#emitter.emit('ping')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (opcode === WEBSOCKET_OPCODE_PONG) {\n\t\t\t\tthis.#emitter.emit('pong')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.#close(payload)\n\t\t\treturn\n\t\t}\n\n\t\tif (opcode === WEBSOCKET_OPCODE_TEXT || opcode === WEBSOCKET_OPCODE_BINARY) {\n\t\t\tif (this.#messageOpcode !== undefined) {\n\t\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.#messageOpcode = opcode\n\t\t} else if (opcode === 0x00) {\n\t\t\tif (this.#messageOpcode === undefined) {\n\t\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\t// Reserved data (0x3–0x7) or reserved control (0xB–0xF) opcodes.\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\treturn\n\t\t}\n\n\t\tthis.#fragments.push(payload)\n\t\tthis.#fragmentBytes += payload.length\n\t\tif (this.#fragmentBytes > this.#payload) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_TOOBIG)\n\t\t\treturn\n\t\t}\n\t\tif (!fin) return\n\n\t\tif (this.#messageOpcode === WEBSOCKET_OPCODE_BINARY) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_UNSUPPORTED)\n\t\t\treturn\n\t\t}\n\t\tconst text = parseUTF8(Buffer.concat(this.#fragments))\n\t\tif (text === undefined) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_INVALID)\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', text)\n\t\tthis.#messageOpcode = undefined\n\t\tthis.#fragments = []\n\t\tthis.#fragmentBytes = 0\n\t}\n\n\t// Handle a validated CLOSE frame: decode it (which itself may `#fail` on an invalid\n\t// code/reason), then — if still OPEN — echo the peer's payload verbatim and end.\n\t#close(payload: Buffer): void {\n\t\tconst valid = this.#decodeClose(payload)\n\t\tif (!valid) return\n\t\tif (this.#readyState === WEBSOCKET_READY_OPEN) {\n\t\t\t// Echo the peer's close frame before ending, per RFC 6455 §5.5.1.\n\t\t\tthis.#readyState = WEBSOCKET_READY_CLOSING\n\t\t\tthis.#write(WEBSOCKET_OPCODE_CLOSE, payload)\n\t\t}\n\t\tthis.#socket.end()\n\t\t// Detach the socket listeners here, mirroring `#fail` — the echo write above has\n\t\t// already gone out, so this cannot drop it, but it prevents a socket `error` fired\n\t\t// during the post-close `end()` flush from surfacing AFTER the terminal `close`\n\t\t// (the same asymmetry `#fail` guards against).\n\t\tthis.#socket.off('data', this.#onData)\n\t\tthis.#socket.off('close', this.#onClose)\n\t\tthis.#socket.off('error', this.#onError)\n\t\tthis.#finish()\n\t}\n\n\t// The single funnel for every RFC 6455 validation breach: close with `code`, DETACH\n\t// the socket listeners (the connection is protocol-dead — RFC 6455 permits discarding\n\t// further input after sending close, and this also stops a post-fail socket `error`\n\t// emitting AFTER the terminal `close` event), write the close frame, then flush + half\n\t// -close via `end()` (never a synchronous `destroy()`, which can discard the buffered\n\t// close frame and leave the peer seeing 1006 instead of the intended code) before\n\t// finishing. The hard-teardown fallback is armed AFTER `#finish` so `#finish`'s\n\t// `clearTimeout` cannot kill it; the normal path destroys the moment the write buffer\n\t// flushes (the `end()` callback), the unref'd timer is only the malicious-peer backstop.\n\t#fail(code: number, reason?: string): void {\n\t\tif (\n\t\t\tthis.#readyState === WEBSOCKET_READY_CLOSING ||\n\t\t\tthis.#readyState === WEBSOCKET_READY_CLOSED\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tthis.#code = code\n\t\tthis.#reason = reason\n\t\tthis.#readyState = WEBSOCKET_READY_CLOSING\n\t\tthis.#socket.off('data', this.#onData)\n\t\tthis.#socket.off('close', this.#onClose)\n\t\tthis.#socket.off('error', this.#onError)\n\t\tthis.#write(WEBSOCKET_OPCODE_CLOSE, this.#encodeClose(code, reason))\n\t\tthis.#socket.end(() => {\n\t\t\tif (!this.#socket.destroyed) this.#socket.destroy()\n\t\t\t// The normal flush path destroyed the socket already — clear the unref'd\n\t\t\t// fallback timer below so it doesn't linger `WEBSOCKET_FAIL_TIMEOUT_MS` holding\n\t\t\t// its closure alive for no reason.\n\t\t\tclearTimeout(this.#closeTimer)\n\t\t\tthis.#closeTimer = undefined\n\t\t})\n\t\tthis.#messageOpcode = undefined\n\t\tthis.#fragments = []\n\t\tthis.#fragmentBytes = 0\n\t\tthis.#finish()\n\t\tthis.#closeTimer = setTimeout(() => {\n\t\t\tif (!this.#socket.destroyed) this.#socket.destroy()\n\t\t}, WEBSOCKET_FAIL_TIMEOUT_MS)\n\t\tthis.#closeTimer.unref()\n\t}\n\n\t// Write one frame to the socket — masked in client mode, unmasked in server mode.\n\t// A destroyed socket silently drops the write (the lifecycle is already ending).\n\t#write(opcode: number, payload: Buffer): void {\n\t\tif (this.#socket.destroyed) return\n\t\tthis.#socket.write(encodeWebSocketFrame(opcode, payload, { masked: this.#masked }))\n\t}\n\n\t// Build a close-frame payload: the 2-byte big-endian code, then the optional UTF-8\n\t// reason. An undefined code yields an empty payload (a bare close).\n\t#encodeClose(code: number | undefined, reason: string | undefined): Buffer {\n\t\tif (code === undefined) return Buffer.alloc(0)\n\t\tconst text = reason === undefined ? Buffer.alloc(0) : Buffer.from(reason, 'utf-8')\n\t\tconst payload = Buffer.alloc(2 + text.length)\n\t\tpayload.writeUInt16BE(code, 0)\n\t\ttext.copy(payload, 2)\n\t\treturn payload\n\t}\n\n\t// Validate and read a peer close-frame payload into `#code` / `#reason` (RFC 6455\n\t// §7.4.1). A bare close (0 bytes) is valid with no code/reason. A single stray byte\n\t// is a protocol error. 2+ bytes carry a code (must be a receivable close code) and\n\t// an optional UTF-8 reason. Returns `false` when a breach routed through `#fail`\n\t// (the caller must not also echo).\n\t#decodeClose(payload: Buffer): boolean {\n\t\tif (payload.length === 0) {\n\t\t\tthis.#code = undefined\n\t\t\tthis.#reason = undefined\n\t\t\treturn true\n\t\t}\n\t\tif (payload.length === 1) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\treturn false\n\t\t}\n\t\tconst code = payload.readUInt16BE(0)\n\t\tif (!isCloseCode(code)) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_PROTOCOL)\n\t\t\treturn false\n\t\t}\n\t\tconst reason = payload.length > 2 ? parseUTF8(payload.subarray(2)) : ''\n\t\tif (reason === undefined) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_INVALID)\n\t\t\treturn false\n\t\t}\n\t\tthis.#code = code\n\t\tthis.#reason = reason.length === 0 ? undefined : reason\n\t\treturn true\n\t}\n\n\t// Transition to CLOSED once (idempotent), clear the close-handshake timer, and emit\n\t// the final `close` with the last known code/reason.\n\t#finish(): void {\n\t\tif (this.#readyState === WEBSOCKET_READY_CLOSED) return\n\t\tclearTimeout(this.#closeTimer)\n\t\tthis.#closeTimer = undefined\n\t\tthis.#signal?.removeEventListener('abort', this.#onAbort)\n\t\tthis.#readyState = WEBSOCKET_READY_CLOSED\n\t\tthis.#emitter.emit('close', this.#code, this.#reason)\n\t}\n\n\t// Append `bytes` to the accumulation buffer, then reject an over-cap frame the moment\n\t// its declared length is known — before its payload is even buffered — else drain\n\t// every complete frame. Shared by `#onData` AND the constructor's head-replay so the\n\t// pre-buffer cap check applies uniformly on both ingest paths (AGENTS §5 dedup).\n\t#ingest(bytes: Buffer): void {\n\t\tthis.#buffer = Buffer.concat([this.#buffer, bytes])\n\t\tconst declared = measureWebSocketFrame(this.#buffer)\n\t\tif (declared !== undefined && declared > this.#payload) {\n\t\t\tthis.#fail(WEBSOCKET_CLOSE_TOOBIG)\n\t\t\treturn\n\t\t}\n\t\tthis.#drain()\n\t}\n\n\t// Narrow an untyped socket `data` chunk to a `Buffer` (AGENTS §14) — a `node:net`\n\t// socket without an explicit encoding yields Buffers, but the listener parameter is\n\t// `unknown`, so it crosses through this guard, never an assertion. A non-Buffer\n\t// chunk (a string from a mis-encoded socket) is normalized; anything else is dropped.\n\t#bytes(chunk: unknown): Buffer | undefined {\n\t\tif (Buffer.isBuffer(chunk)) return chunk\n\t\tif (typeof chunk === 'string') return Buffer.from(chunk, 'utf-8')\n\t\treturn undefined\n\t}\n}\n","import type { NodeWebSocketInterface, NodeWebSocketOptions } from './types.js'\nimport { NodeWebSocket } from './NodeWebSocket.js'\n\n/**\n * Create a server-native WebSocket over a raw upgraded `node:stream` Duplex socket.\n *\n * @remarks\n * The construction entry point for the {@link NodeWebSocketInterface} (AGENTS §8). Pass\n * the upgraded `socket` plus the client's `Sec-WebSocket-Key` as `key` to run in SERVER\n * mode — the wrapper writes the `101 Switching Protocols` handshake and sends unmasked\n * frames; omit `key` for CLIENT mode (no handshake, masked frames). This is the\n * lean-native handle; it speaks only the WebSocket wire protocol — an MCP transport (the\n * later chunk) is built ON it. It is the WebSocket counterpart to\n * `createSQLiteDatabase` / `createIndexedDBDatabase`.\n *\n * @param options - The {@link NodeWebSocketOptions} (`socket`, optional `key` / `head` /\n * `protocol` / `on`)\n * @returns A typed {@link NodeWebSocketInterface}\n *\n * @example\n * ```ts\n * import { createNodeWebSocket } from '@src/server'\n *\n * // In a node:http 'upgrade' handler — server mode, identified by the client key:\n * server.on('upgrade', (request, socket, head) => {\n * \tconst ws = createNodeWebSocket({\n * \t\tsocket,\n * \t\tkey: request.headers['sec-websocket-key'],\n * \t\thead,\n * \t\ton: { message: (text) => ws.send(`echo: ${text}`) },\n * \t})\n * })\n * ```\n */\nexport function createNodeWebSocket(options: NodeWebSocketOptions): NodeWebSocketInterface {\n\treturn new NodeWebSocket(options)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { Duplex } from 'node:stream';
|
|
2
|
+
import { EmitterErrorHandler } from '@orkestrel/emitter';
|
|
3
|
+
import { EmitterHooks } from '@orkestrel/emitter';
|
|
4
|
+
import { EmitterInterface } from '@orkestrel/emitter';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compute the `Sec-WebSocket-Accept` response value for an RFC 6455 upgrade.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The base64-encoded SHA-1 of the client's `Sec-WebSocket-Key` concatenated with the
|
|
11
|
+
* fixed {@link WEBSOCKET_GUID} (RFC 6455 §4.2.2) — the proof the server understood the
|
|
12
|
+
* handshake. Pure and deterministic.
|
|
13
|
+
*
|
|
14
|
+
* @param key - The client's `Sec-WebSocket-Key` header value
|
|
15
|
+
* @returns The base64 accept token to send back as `Sec-WebSocket-Accept`
|
|
16
|
+
*/
|
|
17
|
+
export declare function computeWebSocketAccept(key: string): string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a server-native WebSocket over a raw upgraded `node:stream` Duplex socket.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* The construction entry point for the {@link NodeWebSocketInterface} (AGENTS §8). Pass
|
|
24
|
+
* the upgraded `socket` plus the client's `Sec-WebSocket-Key` as `key` to run in SERVER
|
|
25
|
+
* mode — the wrapper writes the `101 Switching Protocols` handshake and sends unmasked
|
|
26
|
+
* frames; omit `key` for CLIENT mode (no handshake, masked frames). This is the
|
|
27
|
+
* lean-native handle; it speaks only the WebSocket wire protocol — an MCP transport (the
|
|
28
|
+
* later chunk) is built ON it. It is the WebSocket counterpart to
|
|
29
|
+
* `createSQLiteDatabase` / `createIndexedDBDatabase`.
|
|
30
|
+
*
|
|
31
|
+
* @param options - The {@link NodeWebSocketOptions} (`socket`, optional `key` / `head` /
|
|
32
|
+
* `protocol` / `on`)
|
|
33
|
+
* @returns A typed {@link NodeWebSocketInterface}
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { createNodeWebSocket } from '@src/server'
|
|
38
|
+
*
|
|
39
|
+
* // In a node:http 'upgrade' handler — server mode, identified by the client key:
|
|
40
|
+
* server.on('upgrade', (request, socket, head) => {
|
|
41
|
+
* const ws = createNodeWebSocket({
|
|
42
|
+
* socket,
|
|
43
|
+
* key: request.headers['sec-websocket-key'],
|
|
44
|
+
* head,
|
|
45
|
+
* on: { message: (text) => ws.send(`echo: ${text}`) },
|
|
46
|
+
* })
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function createNodeWebSocket(options: NodeWebSocketOptions): NodeWebSocketInterface;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Encode a single RFC 6455 frame to its wire bytes — the inverse of
|
|
54
|
+
* {@link parseWebSocketFrame}.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* Builds a final (FIN-set) frame: byte 0 is `0x80 | opcode`; the payload length uses
|
|
58
|
+
* the 7-bit form below 126, the `126` + 16-bit form below 65 536, or the `127` +
|
|
59
|
+
* 64-bit form beyond; when `masked` is set the mask bit is set, a 4-byte key (supplied
|
|
60
|
+
* via `options.mask`, else random) is written, and the payload is XOR-masked. Server→
|
|
61
|
+
* client frames are unmasked (the default); pass `masked: true` to encode a CLIENT
|
|
62
|
+
* frame (e.g. to feed the parser in a test). A `string` payload is encoded as UTF-8.
|
|
63
|
+
* Returns one contiguous `Buffer` (header + payload), so the wrapper writes it with a
|
|
64
|
+
* single `socket.write`. Pure.
|
|
65
|
+
*
|
|
66
|
+
* @param opcode - The frame opcode (a `WEBSOCKET_OPCODE_*` value)
|
|
67
|
+
* @param payload - The payload, a `Buffer` or a UTF-8 `string`
|
|
68
|
+
* @param options - Masking control ({@link WebSocketEncodeOptions}); defaults to unmasked
|
|
69
|
+
* @returns The complete frame as wire bytes
|
|
70
|
+
*/
|
|
71
|
+
export declare function encodeWebSocketFrame(opcode: number, payload: Buffer | string, options?: WebSocketEncodeOptions): Buffer;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether a numeric value is a valid RFC 6455 close status code to RECEIVE (§7.4.1).
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* True for `1000`–`1003`, `1007`–`1014`, and the application range `3000`–`4999`; false
|
|
78
|
+
* for anything below `1000`, the reserved-for-local-use-only codes `1004`–`1006` and
|
|
79
|
+
* `1015`, and the unassigned `1016`–`2999` range. The `1012`–`1014` extension of the
|
|
80
|
+
* strict RFC 6455 receivable set is a deliberate IANA-interop choice: those three codes
|
|
81
|
+
* (Service Restart, Try Again Later, Bad Gateway) are IANA-registered in the WebSocket
|
|
82
|
+
* Close Code Number Registry and accepted by the `ws` ecosystem and modern conformance
|
|
83
|
+
* suites, so a peer sending one is not treated as a protocol violation. Pure predicate,
|
|
84
|
+
* never throws.
|
|
85
|
+
*
|
|
86
|
+
* @param code - The close status code to validate
|
|
87
|
+
* @returns `true` when `code` is a valid RFC 6455 close code
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* if (!isCloseCode(code)) fail(WEBSOCKET_CLOSE_PROTOCOL)
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare function isCloseCode(code: number): boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read the declared payload length off the front of a buffer, without buffering or
|
|
98
|
+
* reading the payload itself.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Decodes only byte 1's 7-bit length field, extended by the 16-bit (`126`) or 64-bit
|
|
102
|
+
* (`127`) form exactly like {@link parseWebSocketFrame} — but stops there, so a caller
|
|
103
|
+
* can reject an over-cap frame the moment its length is known, before the payload
|
|
104
|
+
* bytes have even arrived. Returns `undefined` until the length field itself is fully
|
|
105
|
+
* buffered (mirrors the parser's incomplete-buffer contract). Pure; never throws.
|
|
106
|
+
*
|
|
107
|
+
* @param buffer - The accumulation buffer to read the next frame's length from
|
|
108
|
+
* @returns The declared payload length, or `undefined` when the buffer is too short to know it yet
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const declared = measureWebSocketFrame(buffer)
|
|
113
|
+
* if (declared !== undefined && declared > limit) fail(WEBSOCKET_CLOSE_TOOBIG)
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function measureWebSocketFrame(buffer: Buffer): number | undefined;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A server-native WebSocket over a raw upgraded `node:stream` Duplex — the lean
|
|
120
|
+
* wrapper around the RFC 6455 wire protocol.
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* Created by `createNodeWebSocket`. When given a client `key` it runs in SERVER mode —
|
|
124
|
+
* it writes the `101 Switching Protocols` handshake (`computeWebSocketAccept(key)`) and
|
|
125
|
+
* emits `open`; given no key it runs in CLIENT mode (no handshake, frames masked). It
|
|
126
|
+
* then listens on the socket's `data`, accumulating bytes in `#buffer` and decoding
|
|
127
|
+
* every complete frame with {@link parseWebSocketFrame} (slicing `consumed` and
|
|
128
|
+
* re-parsing the remainder): a TEXT frame — reassembling continuation fragments across
|
|
129
|
+
* `fin: false` frames — decodes to UTF-8 and emits `message`; a PING is auto-answered
|
|
130
|
+
* with a PONG and emits `ping`; a PONG emits `pong`; a CLOSE is echoed and ends the
|
|
131
|
+
* socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
|
|
132
|
+
* frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that
|
|
133
|
+
* isolates a throwing listener and routes the error to its own `error` handler (the `error`
|
|
134
|
+
* option) — the socket never crashes. The untyped socket `data` is narrowed to a `Buffer`
|
|
135
|
+
* with a guard, never an assertion (AGENTS §14).
|
|
136
|
+
*/
|
|
137
|
+
export declare class NodeWebSocket implements NodeWebSocketInterface {
|
|
138
|
+
#private;
|
|
139
|
+
constructor(options: NodeWebSocketOptions);
|
|
140
|
+
get emitter(): EmitterInterface<NodeWebSocketEventMap>;
|
|
141
|
+
get readyState(): WebSocketReadyState;
|
|
142
|
+
send(data: string): void;
|
|
143
|
+
ping(data?: string): void;
|
|
144
|
+
close(code?: number, reason?: string): void;
|
|
145
|
+
destroy(): void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The event map of a {@link NodeWebSocketInterface} (AGENTS §13).
|
|
150
|
+
*
|
|
151
|
+
* @remarks
|
|
152
|
+
* `open` — the handshake completed and the socket is ready. `message` — a text frame
|
|
153
|
+
* arrived (its decoded UTF-8 string). `close` — the connection ended (its
|
|
154
|
+
* {@link WebSocketClose} metadata). `error` — the underlying socket faulted (a DOMAIN
|
|
155
|
+
* event). `ping` / `pong` — a control frame arrived (a ping is auto-answered with a pong).
|
|
156
|
+
* Listener isolation is the emitter's (AGENTS §13): a listener throw is routed to the
|
|
157
|
+
* emitter's `error` handler (the `error` option), never onto this map, so a buggy observer
|
|
158
|
+
* never breaks the socket.
|
|
159
|
+
*/
|
|
160
|
+
export declare type NodeWebSocketEventMap = {
|
|
161
|
+
open: [];
|
|
162
|
+
message: [message: string];
|
|
163
|
+
close: [code: number | undefined, reason: string | undefined];
|
|
164
|
+
error: [error: unknown];
|
|
165
|
+
ping: [];
|
|
166
|
+
pong: [];
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A server-native WebSocket over a raw upgraded socket — the behavioral contract.
|
|
171
|
+
*
|
|
172
|
+
* @remarks
|
|
173
|
+
* Created by `createNodeWebSocket`. In server mode it writes the RFC 6455 handshake
|
|
174
|
+
* and emits `open`; thereafter it buffers incoming `data`, decodes each frame, and
|
|
175
|
+
* dispatches: a text frame (reassembling continuation fragments) emits `message`; a
|
|
176
|
+
* ping is auto-answered with a pong and emits `ping`; a pong emits `pong`; a close
|
|
177
|
+
* frame is echoed and ends the socket, emitting `close`. `send` writes a text frame;
|
|
178
|
+
* `ping` writes a ping; `close` writes a close frame (the 2-byte code + optional
|
|
179
|
+
* reason); `destroy` tears the socket down immediately. `readyState` tracks the
|
|
180
|
+
* lifecycle. It owns a typed `emitter` (AGENTS §13) and never throws on a faulty
|
|
181
|
+
* listener — the emitter routes it to its `error` handler (the `error` option).
|
|
182
|
+
*/
|
|
183
|
+
export declare interface NodeWebSocketInterface {
|
|
184
|
+
readonly emitter: EmitterInterface<NodeWebSocketEventMap>;
|
|
185
|
+
readonly readyState: WebSocketReadyState;
|
|
186
|
+
send(data: string): void;
|
|
187
|
+
ping(data?: string): void;
|
|
188
|
+
close(code?: number, reason?: string): void;
|
|
189
|
+
destroy(): void;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Options for `createNodeWebSocket`.
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* `socket` is the upgraded `node:stream` Duplex (the raw TCP stream after the HTTP
|
|
197
|
+
* upgrade). `key` is the client's `Sec-WebSocket-Key`: present it to run in SERVER
|
|
198
|
+
* mode — the wrapper writes the `101 Switching Protocols` handshake and sends UNMASKED
|
|
199
|
+
* frames; omit it for CLIENT mode — no handshake is written and frames are MASKED (RFC
|
|
200
|
+
* 6455 §5.3). `head` is any bytes buffered after the upgrade headers (replayed through
|
|
201
|
+
* the parser). `protocol` is a negotiated subprotocol to echo in the handshake. `on`
|
|
202
|
+
* wires initial listeners at construction (AGENTS §8 reserved option); `error` is the
|
|
203
|
+
* emitter's listener-error handler (§13 — a listener throw routes here). `payload` caps
|
|
204
|
+
* both a single inbound frame's declared length AND the total bytes of a reassembled
|
|
205
|
+
* fragmented message (default `WEBSOCKET_MAX_PAYLOAD`) — a breach closes 1009. `timeout`
|
|
206
|
+
* is how long the wrapper waits, after sending a close frame, for the peer's echo before
|
|
207
|
+
* it gives up and tears the socket down (default `WEBSOCKET_CLOSE_TIMEOUT_MS`). `signal`
|
|
208
|
+
* is the external cancellation seam — on abort the socket destroys; composes with the
|
|
209
|
+
* line's `@orkestrel/abort` and `@orkestrel/timeout` primitives, which expose native
|
|
210
|
+
* `AbortSignal`s. An already-aborted signal tears the socket down immediately after
|
|
211
|
+
* construction.
|
|
212
|
+
*/
|
|
213
|
+
export declare interface NodeWebSocketOptions {
|
|
214
|
+
readonly socket: Duplex;
|
|
215
|
+
readonly key?: string;
|
|
216
|
+
readonly head?: Buffer;
|
|
217
|
+
readonly protocol?: string;
|
|
218
|
+
readonly on?: EmitterHooks<NodeWebSocketEventMap>;
|
|
219
|
+
/** The emitter's listener-error handler (AGENTS §13) — a listener throw routes here, not to a domain event. */
|
|
220
|
+
readonly error?: EmitterErrorHandler;
|
|
221
|
+
readonly payload?: number;
|
|
222
|
+
readonly timeout?: number;
|
|
223
|
+
readonly signal?: AbortSignal;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Decode a byte sequence as strict UTF-8, or signal it is malformed.
|
|
228
|
+
*
|
|
229
|
+
* @remarks
|
|
230
|
+
* Wraps `TextDecoder('utf-8', { fatal: true })` in a try/catch so a malformed sequence
|
|
231
|
+
* returns `undefined` instead of throwing (AGENTS §14 — a guard-adjacent coercer never
|
|
232
|
+
* throws on bad input). Pure.
|
|
233
|
+
*
|
|
234
|
+
* @param bytes - The raw bytes to decode
|
|
235
|
+
* @returns The decoded string, or `undefined` when `bytes` is not valid UTF-8
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* const text = parseUTF8(payload)
|
|
240
|
+
* if (text === undefined) fail(WEBSOCKET_CLOSE_INVALID)
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
export declare function parseUTF8(bytes: Buffer): string | undefined;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Decode a single RFC 6455 frame from the front of a buffer.
|
|
247
|
+
*
|
|
248
|
+
* @remarks
|
|
249
|
+
* Reads the FIN bit and opcode (byte 0), the mask bit and 7-bit payload length (byte
|
|
250
|
+
* 1) — extended to a 16-bit length when the 7-bit field is `126`, or a 64-bit length
|
|
251
|
+
* when it is `127` — the optional 4-byte mask key, then the payload, XOR-unmasking it
|
|
252
|
+
* against the key when the mask bit is set (client→server frames MUST be masked, RFC
|
|
253
|
+
* 6455 §5.3; an unmasked frame still decodes, leaving the payload as-is, so the caller
|
|
254
|
+
* can enforce policy). Returns `undefined` the moment the buffer is too short for the
|
|
255
|
+
* part it is up to (the length prefix, the mask, or the full payload) — the signal to
|
|
256
|
+
* the caller to read more bytes and retry, exactly like {@link SSEParser} on a partial
|
|
257
|
+
* line. `consumed` is the total bytes the frame occupied, so the caller slices the
|
|
258
|
+
* remainder. Pure; never throws on a short buffer.
|
|
259
|
+
*
|
|
260
|
+
* @param buffer - The accumulation buffer to decode the next frame from
|
|
261
|
+
* @returns The parsed {@link WebSocketFrame}, or `undefined` when the buffer is incomplete
|
|
262
|
+
*/
|
|
263
|
+
export declare function parseWebSocketFrame(buffer: Buffer): WebSocketFrame | undefined;
|
|
264
|
+
|
|
265
|
+
/** Invalid-frame-payload-data status code (RFC 6455 §7.4.1) — e.g. non-UTF-8 text or an unparseable close reason. */
|
|
266
|
+
export declare const WEBSOCKET_CLOSE_INVALID = 1007;
|
|
267
|
+
|
|
268
|
+
/** Normal-closure status code (RFC 6455 §7.4.1) — the default `close` code. */
|
|
269
|
+
export declare const WEBSOCKET_CLOSE_NORMAL = 1000;
|
|
270
|
+
|
|
271
|
+
/** Protocol-error status code (RFC 6455 §7.4.1) — a framing/state rule was violated. */
|
|
272
|
+
export declare const WEBSOCKET_CLOSE_PROTOCOL = 1002;
|
|
273
|
+
|
|
274
|
+
/** The default close-handshake timeout in milliseconds — how long `close()` waits for the peer's echo before tearing the socket down. */
|
|
275
|
+
export declare const WEBSOCKET_CLOSE_TIMEOUT_MS = 30000;
|
|
276
|
+
|
|
277
|
+
/** Message-too-big status code (RFC 6455 §7.4.1) — a reassembled message exceeded the payload cap. */
|
|
278
|
+
export declare const WEBSOCKET_CLOSE_TOOBIG = 1009;
|
|
279
|
+
|
|
280
|
+
/** Unsupported-data status code (RFC 6455 §7.4.1) — the endpoint received a data type it cannot accept (e.g. binary on a text-only endpoint). */
|
|
281
|
+
export declare const WEBSOCKET_CLOSE_UNSUPPORTED = 1003;
|
|
282
|
+
|
|
283
|
+
/** The maximum control-frame payload length in bytes (RFC 6455 §5.5). */
|
|
284
|
+
export declare const WEBSOCKET_CONTROL_MAXLEN = 125;
|
|
285
|
+
|
|
286
|
+
/** The post-`#fail` flush grace in milliseconds — how long a validation-breach close frame is given to flush through the socket's write buffer before the hard `destroy()` fallback fires (the normal path destroys sooner, on the `end()` flush callback). */
|
|
287
|
+
export declare const WEBSOCKET_FAIL_TIMEOUT_MS = 1000;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The RFC 6455 GUID concatenated to a client's `Sec-WebSocket-Key` before the SHA-1
|
|
291
|
+
* hash that yields the `Sec-WebSocket-Accept` response value.
|
|
292
|
+
*
|
|
293
|
+
* @remarks
|
|
294
|
+
* A fixed, spec-mandated constant (RFC 6455 §4.2.2) — read only by
|
|
295
|
+
* {@link computeWebSocketAccept}.
|
|
296
|
+
*/
|
|
297
|
+
export declare const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
298
|
+
|
|
299
|
+
/** The default maximum inbound single-frame length AND reassembled-message total byte count (100 MiB — the `ws` package default). */
|
|
300
|
+
export declare const WEBSOCKET_MAX_PAYLOAD = 104857600;
|
|
301
|
+
|
|
302
|
+
/** Binary frame opcode — a raw byte payload (RFC 6455 §5.6). */
|
|
303
|
+
export declare const WEBSOCKET_OPCODE_BINARY = 2;
|
|
304
|
+
|
|
305
|
+
/** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
|
|
306
|
+
export declare const WEBSOCKET_OPCODE_CLOSE = 8;
|
|
307
|
+
|
|
308
|
+
/** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
|
|
309
|
+
export declare const WEBSOCKET_OPCODE_PING = 9;
|
|
310
|
+
|
|
311
|
+
/** Pong frame opcode — a control frame answering a ping (RFC 6455 §5.5.3). */
|
|
312
|
+
export declare const WEBSOCKET_OPCODE_PONG = 10;
|
|
313
|
+
|
|
314
|
+
/** Text frame opcode — a UTF-8 payload (RFC 6455 §5.6). */
|
|
315
|
+
export declare const WEBSOCKET_OPCODE_TEXT = 1;
|
|
316
|
+
|
|
317
|
+
/** Ready state for a closed WebSocket (the socket ended). */
|
|
318
|
+
export declare const WEBSOCKET_READY_CLOSED: WebSocketReadyState;
|
|
319
|
+
|
|
320
|
+
/** Ready state for a closing WebSocket (a close frame was sent or received). */
|
|
321
|
+
export declare const WEBSOCKET_READY_CLOSING: WebSocketReadyState;
|
|
322
|
+
|
|
323
|
+
/** Ready state for a connecting WebSocket (before the handshake completes). */
|
|
324
|
+
export declare const WEBSOCKET_READY_CONNECTING: WebSocketReadyState;
|
|
325
|
+
|
|
326
|
+
/** Ready state for an open WebSocket (the handshake completed; frames flow). */
|
|
327
|
+
export declare const WEBSOCKET_READY_OPEN: WebSocketReadyState;
|
|
328
|
+
|
|
329
|
+
/** The WebSocket protocol version this wrapper speaks (`Sec-WebSocket-Version: 13`). */
|
|
330
|
+
export declare const WEBSOCKET_VERSION = "13";
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The metadata of a closed WebSocket — why the connection ended.
|
|
334
|
+
*
|
|
335
|
+
* @remarks
|
|
336
|
+
* `code` is the RFC 6455 close status code (undefined when the peer closed with no
|
|
337
|
+
* payload); `reason` is the optional UTF-8 reason text (undefined when empty).
|
|
338
|
+
*/
|
|
339
|
+
export declare interface WebSocketClose {
|
|
340
|
+
readonly code: number | undefined;
|
|
341
|
+
readonly reason: string | undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** A WebSocket close status code (RFC 6455 §7.4) — e.g. `WEBSOCKET_CLOSE_NORMAL` (1000). */
|
|
345
|
+
export declare type WebSocketCloseCode = number;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Options for {@link encodeWebSocketFrame} — how a frame is masked on the wire.
|
|
349
|
+
*
|
|
350
|
+
* @remarks
|
|
351
|
+
* `masked` toggles the mask bit (server→client frames are NOT masked, the default;
|
|
352
|
+
* client→server frames MUST be, RFC 6455 §5.3). `mask` supplies an explicit 4-byte
|
|
353
|
+
* mask key (deterministic, for tests); when `masked` is true and `mask` is omitted a
|
|
354
|
+
* random key is generated.
|
|
355
|
+
*/
|
|
356
|
+
export declare interface WebSocketEncodeOptions {
|
|
357
|
+
readonly masked?: boolean;
|
|
358
|
+
readonly mask?: Buffer;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A parsed RFC 6455 frame — the structured result of decoding one frame off the wire.
|
|
363
|
+
*
|
|
364
|
+
* @remarks
|
|
365
|
+
* `fin` is the final-fragment bit (false for a continued fragment); `opcode`
|
|
366
|
+
* identifies the frame kind (one of the `WEBSOCKET_OPCODE_*` values); `payload` is
|
|
367
|
+
* the already-unmasked application data; `consumed` is the total byte count the frame
|
|
368
|
+
* occupied (header + mask + payload), so the caller slices it off the front of its
|
|
369
|
+
* accumulation buffer and re-parses the remainder. `masked` is the mask bit off byte 1
|
|
370
|
+
* (client→server frames MUST be masked, RFC 6455 §5.1); `rsv` is the three reserved
|
|
371
|
+
* bits off byte 0 packed into a single 0–7 value (RFC 6455 §5.2) — non-zero means an
|
|
372
|
+
* extension the wrapper does not negotiate, so the caller rejects it. Produced by
|
|
373
|
+
* {@link parseWebSocketFrame}.
|
|
374
|
+
*/
|
|
375
|
+
export declare interface WebSocketFrame {
|
|
376
|
+
readonly fin: boolean;
|
|
377
|
+
readonly opcode: number;
|
|
378
|
+
readonly payload: Buffer;
|
|
379
|
+
readonly consumed: number;
|
|
380
|
+
readonly masked: boolean;
|
|
381
|
+
readonly rsv: number;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** A decoded text message received from, or to send to, a WebSocket peer. */
|
|
385
|
+
export declare interface WebSocketMessage {
|
|
386
|
+
readonly data: string;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A WebSocket ready state — the four browser-compatible lifecycle values.
|
|
391
|
+
*
|
|
392
|
+
* @remarks
|
|
393
|
+
* `0` connecting, `1` open, `2` closing, `3` closed — the same numbering the DOM
|
|
394
|
+
* `WebSocket.readyState` uses, so the wrapper reads like the platform API. The named
|
|
395
|
+
* `WEBSOCKET_READY_*` constants spell each value.
|
|
396
|
+
*/
|
|
397
|
+
export declare type WebSocketReadyState = 0 | 1 | 2 | 3;
|
|
398
|
+
|
|
399
|
+
export { }
|