quico 0.3.0 → 0.7.0

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/src/ws.js ADDED
@@ -0,0 +1,624 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import crypto from 'node:crypto';
16
+ import { Emitter } from './http_common.js';
17
+
18
+
19
+ // ============================================================
20
+ // WebSocket framing (RFC 6455 §5).
21
+ //
22
+ // No I/O, same as h1.js: feed bytes to execute(), get events. The socket, the
23
+ // handshake and the browser-facing API all live above this.
24
+ //
25
+ // Everything here refuses rather than repairs. A WebSocket frame is simple
26
+ // enough that any deviation is a bug or an attack, and RFC 6455 §7.4.1 gives
27
+ // a close code for each case — so a violation is answered with the right code
28
+ // and the connection ends, never with a guess about what the peer meant.
29
+ // ============================================================
30
+
31
+ var OPCODE = {
32
+ CONTINUATION: 0x0,
33
+ TEXT: 0x1,
34
+ BINARY: 0x2,
35
+ CLOSE: 0x8,
36
+ PING: 0x9,
37
+ PONG: 0xa
38
+ };
39
+
40
+ // RFC 6455 §7.4.1 plus the WHATWG additions.
41
+ var CLOSE = {
42
+ NORMAL: 1000,
43
+ GOING_AWAY: 1001,
44
+ PROTOCOL_ERROR: 1002,
45
+ UNSUPPORTED_DATA: 1003,
46
+ NO_STATUS: 1005, // never sent on the wire
47
+ ABNORMAL: 1006, // never sent on the wire
48
+ INVALID_PAYLOAD: 1007,
49
+ POLICY_VIOLATION: 1008,
50
+ MESSAGE_TOO_BIG: 1009,
51
+ MISSING_EXTENSION: 1010,
52
+ INTERNAL_ERROR: 1011,
53
+ TLS_HANDSHAKE: 1015 // never sent on the wire
54
+ };
55
+
56
+ // RFC 6455 §1.3. Fixed by the spec; the only reason it exists is to make a
57
+ // WebSocket handshake impossible to produce by tricking a naive HTTP server
58
+ // into echoing a header.
59
+ // Fixed by RFC 6455 §1.3, and worth checking against the RFC's own worked
60
+ // example rather than trusting the transcription: with one character out of
61
+ // place every handshake fails with "Invalid Sec-WebSocket-Accept", which reads
62
+ // like a broken server rather than a typo here. ws.test.mjs asserts that
63
+ // acceptKey('dGhlIHNhbXBsZSBub25jZQ==') is 's3pPLMBiTxaQ9kYGzzhZRbK+xOo='.
64
+ var WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
65
+
66
+ var MAX_CONTROL_PAYLOAD = 125;
67
+ var DEFAULT_MAX_PAYLOAD = 100 * 1024 * 1024;
68
+
69
+ var EMPTY = Buffer.alloc(0);
70
+
71
+
72
+ // ============================================================
73
+ // Handshake helpers
74
+ // ============================================================
75
+
76
+ /** 16 random bytes, base64. RFC 6455 §4.1 requires them to be unpredictable. */
77
+ function generateKey() {
78
+ return crypto.randomBytes(16).toString('base64');
79
+ }
80
+
81
+ /** base64(sha1(key + GUID)) — the value the server must echo (§4.2.2). */
82
+ function acceptKey(key) {
83
+ return crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
84
+ }
85
+
86
+ /**
87
+ * Is a close code allowed to appear on the wire (§7.4)?
88
+ *
89
+ * 1005/1006/1015 are statuses the local endpoint reports to its own
90
+ * application when there was no code, the connection dropped, or TLS failed.
91
+ * A peer sending one is claiming a local condition it cannot know about.
92
+ */
93
+ function isValidCloseCode(code) {
94
+ if (code >= 3000 && code <= 4999) return true; // application / registered
95
+ if (code < 1000 || code > 1015) return false;
96
+ if (code === 1004 || code === 1005 || code === 1006 || code === 1015) return false;
97
+ return true;
98
+ }
99
+
100
+ /** Codes an application may pass to close() (WHATWG: 1000 or 3000-4999). */
101
+ function isValidUserCloseCode(code) {
102
+ return code === 1000 || (code >= 3000 && code <= 4999);
103
+ }
104
+
105
+
106
+ // ============================================================
107
+ // Masking (RFC 6455 §5.3)
108
+ //
109
+ // Every client-to-server frame is XORed with a fresh 32-bit key. It is not
110
+ // confidentiality — the key travels in the clear — it exists so an attacker
111
+ // who controls a page cannot make a browser emit chosen bytes on the wire and
112
+ // poison an intermediary's cache. That only holds if the key is
113
+ // unpredictable, which is why it comes from crypto.randomBytes and not
114
+ // Math.random.
115
+ // ============================================================
116
+
117
+ function applyMask(payload, mask, offset) {
118
+ offset = offset || 0;
119
+ for (var i = 0; i < payload.length; i++) {
120
+ payload[i] ^= mask[(offset + i) & 3];
121
+ }
122
+ return payload;
123
+ }
124
+
125
+
126
+ // ============================================================
127
+ // Frame encoding
128
+ // ============================================================
129
+
130
+ /**
131
+ * Build one frame.
132
+ *
133
+ * `mask` must be true for every frame a CLIENT sends (§5.1) and false for
134
+ * every frame a server sends. A server that masks, or a client that does not,
135
+ * is closed by the peer with 1002.
136
+ */
137
+ function encodeFrame(opcode, payload, options) {
138
+ options = options || {};
139
+ var fin = options.fin !== false;
140
+ var mask = !!options.mask;
141
+
142
+ if (payload == null) payload = EMPTY;
143
+ else if (typeof payload === 'string') payload = Buffer.from(payload, 'utf8');
144
+ else if (!Buffer.isBuffer(payload)) payload = Buffer.from(payload);
145
+
146
+ if (opcode >= 0x8 && payload.length > MAX_CONTROL_PAYLOAD) {
147
+ throw new Error('control frame payload exceeds ' + MAX_CONTROL_PAYLOAD + ' bytes');
148
+ }
149
+
150
+ var len = payload.length;
151
+ var header;
152
+
153
+ if (len < 126) {
154
+ header = Buffer.allocUnsafe(2);
155
+ header[1] = len;
156
+ } else if (len < 65536) {
157
+ header = Buffer.allocUnsafe(4);
158
+ header[1] = 126;
159
+ header.writeUInt16BE(len, 2);
160
+ } else {
161
+ header = Buffer.allocUnsafe(10);
162
+ header[1] = 127;
163
+ // 64-bit length. The high word is always zero here: a frame longer than
164
+ // 4 GiB is not something this library will build.
165
+ header.writeUInt32BE(0, 2);
166
+ header.writeUInt32BE(len, 6);
167
+ }
168
+
169
+ header[0] = (fin ? 0x80 : 0x00) | (opcode & 0x0f);
170
+
171
+ if (!mask) {
172
+ return Buffer.concat([header, payload], header.length + len);
173
+ }
174
+
175
+ header[1] |= 0x80;
176
+ var maskKey = crypto.randomBytes(4);
177
+ // Copy before masking: the caller's buffer must not be mutated. A user who
178
+ // sends the same Buffer twice would otherwise send garbage the second time,
179
+ // and the bug would look like corruption on the network.
180
+ var masked = Buffer.from(payload);
181
+ applyMask(masked, maskKey);
182
+ return Buffer.concat([header, maskKey, masked], header.length + 4 + len);
183
+ }
184
+
185
+
186
+ // ============================================================
187
+ // WSParser
188
+ //
189
+ // Events:
190
+ // 'message' (data, isBinary) — a complete message, fragments reassembled
191
+ // 'ping' (payload)
192
+ // 'pong' (payload)
193
+ // 'close' (code, reason) — peer's close frame
194
+ // 'error' (err) — err.closeCode is the code to answer with
195
+ // ============================================================
196
+
197
+ function WSParser(options) {
198
+ if (!(this instanceof WSParser)) return new WSParser(options);
199
+ options = options || {};
200
+
201
+ var isServer = !!options.isServer;
202
+ var maxPayload = options.maxPayload || DEFAULT_MAX_PAYLOAD;
203
+
204
+ var ev = Emitter();
205
+ var on = ev.on, emit = ev.emit;
206
+
207
+ var buf = EMPTY;
208
+ var dead = false;
209
+
210
+ // A frame whose payload is still arriving. Its length is known from its
211
+ // header, so the bytes it still needs are known too: later chunks wait in
212
+ // `waiting` and are joined once, when there are enough of them. Joining on
213
+ // every chunk instead re-copied the whole partial frame each time — O(n^2):
214
+ // a 5MB message over WebSocket-over-HTTP/3, where each chunk is one QUIC
215
+ // packet (~1.2KB), took over two seconds of CPU to parse.
216
+ var need = 0; // bytes `buf` must hold before parsing resumes; 0 = not waiting
217
+ var waiting = [];
218
+ var waitingLen = 0;
219
+
220
+ // Fragmented message in progress.
221
+ var fragments = [];
222
+ var fragmentsLen = 0;
223
+ var fragmentOpcode = 0;
224
+ var textDecoder = null;
225
+ var textSoFar = '';
226
+
227
+ function fatal(message, closeCode) {
228
+ if (dead) return;
229
+ dead = true;
230
+ var err = new Error(message);
231
+ err.closeCode = closeCode || CLOSE.PROTOCOL_ERROR;
232
+ emit('error', err);
233
+ }
234
+
235
+ /**
236
+ * Feed bytes. Never throws; violations arrive as 'error' with a closeCode.
237
+ */
238
+ function execute(chunk) {
239
+ if (dead) return;
240
+ if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
241
+
242
+ if (need > 0 && buf.length + waitingLen + chunk.length < need) {
243
+ waiting.push(chunk);
244
+ waitingLen += chunk.length;
245
+ return;
246
+ }
247
+ if (waitingLen > 0) {
248
+ waiting.unshift(buf);
249
+ waiting.push(chunk);
250
+ buf = Buffer.concat(waiting, buf.length + waitingLen + chunk.length);
251
+ waiting = [];
252
+ waitingLen = 0;
253
+ } else {
254
+ buf = buf.length === 0 ? chunk : Buffer.concat([buf, chunk]);
255
+ }
256
+ need = 0;
257
+
258
+ while (!dead) {
259
+ if (buf.length < 2) return;
260
+
261
+ var b0 = buf[0], b1 = buf[1];
262
+ var fin = (b0 & 0x80) !== 0;
263
+ var rsv = b0 & 0x70;
264
+ var opcode = b0 & 0x0f;
265
+ var masked = (b1 & 0x80) !== 0;
266
+ var len = b1 & 0x7f;
267
+ var offset = 2;
268
+
269
+ // RSV bits are reserved for extensions. We negotiate none, so a peer
270
+ // setting one is either using an extension we never agreed to or is
271
+ // corrupt (§5.2).
272
+ if (rsv !== 0) { fatal('reserved bits set without a negotiated extension', CLOSE.PROTOCOL_ERROR); return; }
273
+
274
+ // Masking direction is fixed and asymmetric (§5.1): client frames are
275
+ // always masked, server frames never are. Accepting the wrong direction
276
+ // removes the cache-poisoning protection the mask exists for.
277
+ if (isServer && !masked) { fatal('client frame is not masked', CLOSE.PROTOCOL_ERROR); return; }
278
+ if (!isServer && masked) { fatal('server frame must not be masked', CLOSE.PROTOCOL_ERROR); return; }
279
+
280
+ if (len === 126) {
281
+ if (buf.length < offset + 2) return;
282
+ len = buf.readUInt16BE(offset);
283
+ offset += 2;
284
+ } else if (len === 127) {
285
+ if (buf.length < offset + 8) return;
286
+ var hi = buf.readUInt32BE(offset);
287
+ var lo = buf.readUInt32BE(offset + 4);
288
+ // Beyond 2^53 a length cannot be represented exactly, and nothing
289
+ // legitimate is that large. Refuse rather than silently truncate.
290
+ if (hi > 0x1fffff) { fatal('frame length exceeds Number.MAX_SAFE_INTEGER', CLOSE.MESSAGE_TOO_BIG); return; }
291
+ len = hi * 4294967296 + lo;
292
+ offset += 8;
293
+ }
294
+
295
+ if (opcode >= 0x8) {
296
+ // Control frames: never fragmented, never longer than 125 bytes
297
+ // (§5.5). They may be interleaved between the fragments of a data
298
+ // message, which is why they are handled before the fragment state.
299
+ if (!fin) { fatal('control frame must not be fragmented', CLOSE.PROTOCOL_ERROR); return; }
300
+ if (len > MAX_CONTROL_PAYLOAD) { fatal('control frame payload too long', CLOSE.PROTOCOL_ERROR); return; }
301
+ }
302
+
303
+ if (len > maxPayload) { fatal('message exceeds maxPayload', CLOSE.MESSAGE_TOO_BIG); return; }
304
+
305
+ var maskKey = null;
306
+ if (masked) {
307
+ if (buf.length < offset + 4) return;
308
+ maskKey = buf.subarray(offset, offset + 4);
309
+ offset += 4;
310
+ }
311
+
312
+ if (buf.length < offset + len) { need = offset + len; return; } // payload incomplete
313
+
314
+ var payload = Buffer.from(buf.subarray(offset, offset + len));
315
+ buf = buf.subarray(offset + len);
316
+ if (maskKey) applyMask(payload, maskKey);
317
+
318
+ if (!handleFrame(fin, opcode, payload)) return;
319
+ }
320
+ }
321
+
322
+ function handleFrame(fin, opcode, payload) {
323
+ switch (opcode) {
324
+
325
+ case OPCODE.PING:
326
+ emit('ping', payload);
327
+ return true;
328
+
329
+ case OPCODE.PONG:
330
+ emit('pong', payload);
331
+ return true;
332
+
333
+ case OPCODE.CLOSE: {
334
+ if (payload.length === 1) {
335
+ // A close code is two bytes or nothing at all (§5.5.1).
336
+ fatal('close frame with a 1-byte payload', CLOSE.PROTOCOL_ERROR);
337
+ return false;
338
+ }
339
+ var code = CLOSE.NO_STATUS;
340
+ var reason = '';
341
+ if (payload.length >= 2) {
342
+ code = payload.readUInt16BE(0);
343
+ if (!isValidCloseCode(code)) {
344
+ fatal('invalid close code ' + code, CLOSE.PROTOCOL_ERROR);
345
+ return false;
346
+ }
347
+ try {
348
+ reason = new TextDecoder('utf-8', { fatal: true }).decode(payload.subarray(2));
349
+ } catch (e) {
350
+ fatal('close reason is not valid UTF-8', CLOSE.INVALID_PAYLOAD);
351
+ return false;
352
+ }
353
+ }
354
+ dead = true;
355
+ emit('close', code, reason);
356
+ return false;
357
+ }
358
+
359
+ case OPCODE.TEXT:
360
+ case OPCODE.BINARY: {
361
+ if (fragments.length > 0) {
362
+ // A new data frame while a fragmented message is open. §5.4 allows
363
+ // only continuation frames there.
364
+ fatal('data frame received while a fragmented message is open', CLOSE.PROTOCOL_ERROR);
365
+ return false;
366
+ }
367
+ if (fin) return deliver(opcode, payload, true);
368
+
369
+ fragmentOpcode = opcode;
370
+ fragments = [payload];
371
+ fragmentsLen = payload.length;
372
+ if (opcode === OPCODE.TEXT) {
373
+ // Validate UTF-8 as fragments arrive rather than at the end: a peer
374
+ // streaming a huge invalid message should be cut off at the first
375
+ // bad byte, not after we have buffered all of it.
376
+ textDecoder = new TextDecoder('utf-8', { fatal: true });
377
+ textSoFar = '';
378
+ if (!feedText(payload)) return false;
379
+ }
380
+ return true;
381
+ }
382
+
383
+ case OPCODE.CONTINUATION: {
384
+ if (fragments.length === 0) {
385
+ fatal('continuation frame with no message to continue', CLOSE.PROTOCOL_ERROR);
386
+ return false;
387
+ }
388
+ fragmentsLen += payload.length;
389
+ if (fragmentsLen > maxPayload) {
390
+ fatal('fragmented message exceeds maxPayload', CLOSE.MESSAGE_TOO_BIG);
391
+ return false;
392
+ }
393
+ fragments.push(payload);
394
+ if (fragmentOpcode === OPCODE.TEXT && !feedText(payload)) return false;
395
+
396
+ if (!fin) return true;
397
+
398
+ var whole = Buffer.concat(fragments, fragmentsLen);
399
+ var op = fragmentOpcode;
400
+ resetFragments();
401
+ return deliver(op, whole, false);
402
+ }
403
+
404
+ default:
405
+ fatal('unknown opcode 0x' + opcode.toString(16), CLOSE.PROTOCOL_ERROR);
406
+ return false;
407
+ }
408
+ }
409
+
410
+ function feedText(chunk) {
411
+ try {
412
+ textSoFar += textDecoder.decode(chunk, { stream: true });
413
+ return true;
414
+ } catch (e) {
415
+ // §8.1: a text message that is not valid UTF-8 is 1007, not a protocol
416
+ // error — the framing was fine, the content was not.
417
+ fatal('text message is not valid UTF-8', CLOSE.INVALID_PAYLOAD);
418
+ return false;
419
+ }
420
+ }
421
+
422
+ function deliver(opcode, payload, single) {
423
+ if (opcode === OPCODE.BINARY) {
424
+ emit('message', payload, true);
425
+ return true;
426
+ }
427
+
428
+ var text;
429
+ if (single) {
430
+ try {
431
+ text = new TextDecoder('utf-8', { fatal: true }).decode(payload);
432
+ } catch (e) {
433
+ fatal('text message is not valid UTF-8', CLOSE.INVALID_PAYLOAD);
434
+ return false;
435
+ }
436
+ } else {
437
+ try {
438
+ // Flush: catches a message that ends mid-sequence, which the streaming
439
+ // decode above cannot know about until it is told there is no more.
440
+ text = textSoFar + textDecoder.decode();
441
+ } catch (e) {
442
+ fatal('text message ends mid UTF-8 sequence', CLOSE.INVALID_PAYLOAD);
443
+ return false;
444
+ }
445
+ textDecoder = null;
446
+ textSoFar = '';
447
+ }
448
+ emit('message', text, false);
449
+ return true;
450
+ }
451
+
452
+ function resetFragments() {
453
+ fragments = [];
454
+ fragmentsLen = 0;
455
+ fragmentOpcode = 0;
456
+ }
457
+
458
+ this.on = on;
459
+ this.execute = execute;
460
+ Object.defineProperty(this, 'dead', { get: function () { return dead; } });
461
+
462
+ return this;
463
+ }
464
+
465
+
466
+ // ============================================================
467
+ // Two event surfaces on one WebSocket.
468
+ //
469
+ // A WebSocket here answers to both of the interfaces people actually write
470
+ // against, and each one behaves the way its own users expect:
471
+ //
472
+ // WHATWG — onmessage / onclose / addEventListener. Handlers receive an
473
+ // Event: MessageEvent with .data, CloseEvent with .code and
474
+ // .wasClean. This is what browser code is written against, and
475
+ // such code runs here unchanged.
476
+ //
477
+ // ws — .on('message') / .on('close') / .once / .off. Handlers receive
478
+ // the data directly: (data, isBinary), (code, reason), (err).
479
+ // This is what Node code is written against, and such code also
480
+ // runs here unchanged.
481
+ //
482
+ // BOTH FIRE FOR THE SAME EVENT, WITH DIFFERENT ARGUMENTS. That is the
483
+ // deliberate part and the one thing to know before reading anything else:
484
+ //
485
+ // ws.onmessage = e => e.data; // MessageEvent
486
+ // ws.on('message', (data, isBinary) => data); // Buffer or string
487
+ //
488
+ // The alternative was to make .on() an alias for addEventListener, so that
489
+ // everything received an Event. That is more consistent with itself and worse
490
+ // in practice: every `ws` example on the internet would then be subtly wrong
491
+ // here — `data.length` on a MessageEvent is undefined, not an error — and a
492
+ // method that looks familiar while meaning something else is a worse trap
493
+ // than one that does not exist.
494
+ //
495
+ // Two places where the interfaces disagree and both are kept as they are:
496
+ //
497
+ // binaryType governs the WHATWG surface only. `ws` has no such concept and
498
+ // always hands over a Buffer, so .on('message') always gets a Buffer even
499
+ // when onmessage is producing Blobs.
500
+ //
501
+ // close reason is a string in CloseEvent and a Buffer in ws's
502
+ // .on('close', (code, reason)).
503
+ // ============================================================
504
+
505
+
506
+ /**
507
+ * Install the `ws`-style surface on an object that already extends
508
+ * EventTarget, and give it the emit helpers both surfaces go through.
509
+ *
510
+ * The class keeps its own `_onmessage` etc. slots for the WHATWG `on<event>`
511
+ * properties; this only adds the second surface and the dispatch that feeds
512
+ * both, so ws_client and ws_server do not each grow a copy.
513
+ */
514
+ function installEventSurfaces(target) {
515
+ // ws-style listeners, kept apart from EventTarget's own registry: they
516
+ // receive different arguments, so they cannot share a list.
517
+ var nodeListeners = Object.create(null);
518
+
519
+ function add(type, fn, once) {
520
+ if (typeof fn !== 'function') return target;
521
+ (nodeListeners[type] = nodeListeners[type] || []).push({ fn: fn, once: !!once });
522
+ return target;
523
+ }
524
+
525
+ target.on = function (type, fn) { return add(type, fn, false); };
526
+ target.once = function (type, fn) { return add(type, fn, true); };
527
+ target.addListener = target.on;
528
+
529
+ target.off = function (type, fn) {
530
+ var a = nodeListeners[type];
531
+ if (!a) return target;
532
+ for (var i = 0; i < a.length; i++) {
533
+ if (a[i].fn === fn) { a.splice(i, 1); break; }
534
+ }
535
+ return target;
536
+ };
537
+ target.removeListener = target.off;
538
+
539
+ target.removeAllListeners = function (type) {
540
+ if (type === undefined) nodeListeners = Object.create(null);
541
+ else delete nodeListeners[type];
542
+ return target;
543
+ };
544
+
545
+ target.listenerCount = function (type) {
546
+ return (nodeListeners[type] || []).length;
547
+ };
548
+
549
+ target.eventNames = function () { return Object.keys(nodeListeners); };
550
+
551
+ /** Call the ws-style listeners for `type` with the given arguments. */
552
+ target._emitNode = function (type, args) {
553
+ var a = nodeListeners[type];
554
+ if (!a || a.length === 0) return false;
555
+ // Copied before iterating: a listener may remove itself (once) or another,
556
+ // and mutating the array mid-loop silently skips one.
557
+ var list = a.slice();
558
+ for (var i = 0; i < list.length; i++) {
559
+ if (list[i].once) target.off(type, list[i].fn);
560
+ list[i].fn.apply(target, args);
561
+ }
562
+ return true;
563
+ };
564
+
565
+ return target;
566
+ }
567
+
568
+
569
+ /**
570
+ * Fire one event on both surfaces.
571
+ *
572
+ * `event` is dispatched to the WHATWG side; `nodeArgs` is spread into the
573
+ * ws-style listeners. Every emission in ws_client and ws_server goes through
574
+ * here, so the two surfaces cannot drift apart — a new event added to one is
575
+ * added to both or to neither.
576
+ */
577
+ function emitBoth(target, type, event, nodeArgs) {
578
+ if (event) target.dispatchEvent(event);
579
+ target._emitNode(type, nodeArgs || []);
580
+ }
581
+
582
+
583
+ /**
584
+ * The payload `ws` hands to .on('message'), regardless of binaryType.
585
+ *
586
+ * ALWAYS a Buffer, text included. That is not an oversight in `ws`: since v8 it
587
+ * emits a Buffer for every message and passes `isBinary` alongside, leaving the
588
+ * decision to decode to the consumer — which is why `ws` handlers are full of
589
+ * `data.toString()`. Handing a string to a text message here would look more
590
+ * convenient and would break exactly those handlers, silently, because
591
+ * `.toString()` on a string is a no-op but `.length` and `.subarray()` are not.
592
+ *
593
+ * The differential test in ws_compat.test.mjs is what caught this: the same
594
+ * handler recorded `Buffer(text)` against `ws` and `string(text)` against us.
595
+ *
596
+ * binaryType is deliberately not consulted. `ws` has no such concept, so a
597
+ * Blob must never reach this surface even when the WHATWG side is producing
598
+ * them.
599
+ */
600
+ function nodeMessagePayload(data, isBinary) {
601
+ if (typeof data === 'string') return Buffer.from(data, 'utf8');
602
+ if (Buffer.isBuffer(data)) return data;
603
+ if (data instanceof ArrayBuffer) return Buffer.from(data);
604
+ if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
605
+ return data;
606
+ }
607
+
608
+
609
+ export {
610
+ WSParser,
611
+ encodeFrame,
612
+ applyMask,
613
+ generateKey,
614
+ acceptKey,
615
+ isValidCloseCode,
616
+ isValidUserCloseCode,
617
+ OPCODE,
618
+ CLOSE,
619
+ WS_GUID,
620
+ // The two event surfaces, for ws_client and ws_server.
621
+ installEventSurfaces,
622
+ emitBoth,
623
+ nodeMessagePayload
624
+ };