@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.
@@ -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 { }