@boo-labs/server 0.0.0-stage → 1.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -2
  3. package/index.mjs +23 -0
  4. package/package.json +71 -4
  5. package/source/application/Kernel/index.mjs +1137 -0
  6. package/source/application/Server/index.mjs +1201 -0
  7. package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
  8. package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
  9. package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
  10. package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
  11. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
  12. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
  13. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
  14. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
  15. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
  16. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
  17. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
  18. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
  19. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
  20. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
  21. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
  22. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
  23. package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
  24. package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
  25. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
  26. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
  27. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
  28. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
  29. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
  30. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
  31. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
  32. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
  33. package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
  34. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
  35. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
  36. package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
  37. package/source/application/resolvers/TransportResolver/index.mjs +119 -0
  38. package/source/application/use-cases/CloseServer/index.mjs +85 -0
  39. package/source/application/use-cases/ListenServer/index.mjs +98 -0
  40. package/source/core/EventSystem/index.mjs +110 -0
  41. package/source/core/Exchange/ExchangeContext.mjs +291 -0
  42. package/source/core/Exchange/Telemetry.mjs +193 -0
  43. package/source/core/Exchange/index.mjs +604 -0
  44. package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
  45. package/source/core/IPCMain/IPCMain.mjs +507 -0
  46. package/source/core/IPCMain/IPCProtocol.mjs +441 -0
  47. package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
  48. package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
  49. package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
  50. package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
  51. package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
  52. package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
  53. package/source/core/Request/RequestHeaders.mjs +177 -0
  54. package/source/core/Request/index.mjs +805 -0
  55. package/source/core/Response/ResponseHeaders.mjs +402 -0
  56. package/source/core/Response/index.mjs +1370 -0
  57. package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
  58. package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
  59. package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
  60. package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
  61. package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
  62. package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
  63. package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
  64. package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
  65. package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
  66. package/source/core/routing/ExchangeLayer/index.mjs +254 -0
  67. package/source/core/routing/Pipeline/index.mjs +157 -0
  68. package/source/core/routing/Router/Route.mjs +184 -0
  69. package/source/core/routing/Router/index.mjs +523 -0
  70. package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
  71. package/source/core/routing/RouterRegister/index.mjs +150 -0
  72. package/source/core/routing/RouterSystem/index.mjs +225 -0
  73. package/source/core/value-objects/Address/index.mjs +281 -0
  74. package/source/core/value-objects/Endpoint/index.mjs +293 -0
  75. package/source/index.mjs +62 -0
  76. package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
  77. package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
  78. package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
  79. package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
  80. package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
  81. package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
  82. package/source/shared/Identifier/index.mjs +671 -0
  83. package/source/shared/constants/symbols/brands/index.mjs +45 -0
  84. package/source/shared/domain/UUIDV7/index.mjs +346 -0
  85. package/source/shared/errors/BaseError/index.mjs +157 -0
  86. package/source/shared/errors/IPCError/index.mjs +97 -0
  87. package/source/shared/errors/MiddlewareError/index.mjs +106 -0
  88. package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
  89. package/source/shared/errors/RequestError/index.mjs +116 -0
  90. package/source/shared/errors/ResponseError/index.mjs +140 -0
  91. package/source/shared/errors/RouterError/index.mjs +90 -0
  92. package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
  93. package/source/shared/errors/ServerError/index.mjs +127 -0
  94. package/source/shared/errors/WebSocketError/index.mjs +58 -0
  95. package/source/shared/outcome/ErrorSchema.mjs +361 -0
  96. package/source/shared/outcome/Result/index.mjs +239 -0
  97. package/source/shared/outcome/Validation/index.mjs +208 -0
  98. package/source/shared/validations/Middleware/index.mjs +160 -0
  99. package/source/shared/validations/PathnamePattern/index.mjs +256 -0
  100. package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
  101. package/source/shared/validations/Server/Router/index.mjs +68 -0
  102. package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
  103. package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
  104. package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
  105. package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
  106. package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
  107. package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
  108. package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
  109. package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
  110. package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
  111. package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
  112. package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
  113. package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
  114. package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
  115. package/types/index.d.ts +719 -0
@@ -0,0 +1,843 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Provides WebSocket upgrade and connection utilities for HTTP/1.1
4
+ * transports.
5
+ *
6
+ * @description Implements the RFC 6455 handshake and the low-level WebSocket
7
+ * connection required by the HTTP/1.1 transport. It includes subprotocol
8
+ * negotiation, message size limits, and full frame lifecycle management
9
+ * (fragmentation, ping/pong, and close).
10
+ *
11
+ * @module WebSocketUpgrade
12
+ */
13
+
14
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15
+ // » IMPORT NATIVE NODEJS MODULES
16
+ import crypto from 'node:crypto';
17
+
18
+ // ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19
+ /**
20
+ * Underlying TCP socket used by a WebSocket connection.
21
+ *
22
+ * @typedef {import('node:net').Socket} WebSocketSocket
23
+ */
24
+
25
+ /**
26
+ * HTTP/1.1 request received during a WebSocket upgrade.
27
+ *
28
+ * @typedef {import('node:http').IncomingMessage} WebSocketUpgradeRequest
29
+ */
30
+
31
+ /**
32
+ * Listener for incoming WebSocket messages.
33
+ *
34
+ * @callback WebSocketMessageListener
35
+ * @param {string|Buffer} data - Incoming text or binary message.
36
+ * @returns {void}
37
+ */
38
+
39
+ /**
40
+ * Listener for WebSocket close events.
41
+ *
42
+ * @callback WebSocketCloseListener
43
+ * @returns {void}
44
+ */
45
+
46
+ /**
47
+ * Listener for WebSocket error events.
48
+ *
49
+ * @callback WebSocketErrorListener
50
+ * @param {Error} error - Error reported by the WebSocket connection.
51
+ * @returns {void}
52
+ */
53
+
54
+ /**
55
+ * Registers a WebSocket event listener.
56
+ *
57
+ * @typedef {(
58
+ * ((event: 'message', listener: WebSocketMessageListener) => void) &
59
+ * ((event: 'close', listener: WebSocketCloseListener) => void) &
60
+ * ((event: 'error', listener: WebSocketErrorListener) => void)
61
+ * )} WebSocketOn
62
+ */
63
+
64
+ /**
65
+ * Represents an established WebSocket connection contract.
66
+ *
67
+ * @typedef {object} WebSocketConnection
68
+ * @property {(data: string|Buffer) => void} send - Sends a text or binary WebSocket message.
69
+ * @property {WebSocketOn} on - Registers a listener for WebSocket events.
70
+ * @property {() => void} close - Closes the WebSocket connection.
71
+ * @property {number} readyState - Current transport-defined WebSocket ready state.
72
+ */
73
+
74
+ /**
75
+ * Transport-level request used to accept or reject a WebSocket handshake.
76
+ *
77
+ * @typedef {object} WebSocketOpenRequest
78
+ * @property {string} url - The requested WebSocket URL.
79
+ * @property {Record<string, string|string[]|undefined>} headers - Headers received during the WebSocket handshake.
80
+ * @property {(selectedProtocol?: string) => WebSocketConnection} accept - Accepts the handshake. Optionally selects a subprotocol.
81
+ * @property {(code?: number, message?: string) => void} reject - Rejects the WebSocket handshake.
82
+ */
83
+
84
+ /**
85
+ * Represents a parsed WebSocket frame.
86
+ *
87
+ * @typedef {object} WebSocketFrame
88
+ * @property {boolean} fin - Whether this is the final fragment of the message.
89
+ * @property {number} opcode - Frame opcode.
90
+ * @property {Buffer} payload - Unmasked frame payload.
91
+ * @property {number} length - Total frame length in bytes, including header.
92
+ */
93
+
94
+ /**
95
+ * Options accepted by {@link createWebSocketOpenRequest}.
96
+ *
97
+ * @typedef {object} WebSocketUpgradeOptions
98
+ * @property {number} [maxMessageSize] - Maximum allowed message size in bytes.
99
+ * @property {string[]} [supportedProtocols] - WebSocket subprotocols supported by the server.
100
+ */
101
+
102
+ // ━━ CONSTANTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
103
+ /**
104
+ * Magic string defined by RFC 6455 for the WebSocket handshake.
105
+ *
106
+ * @type {string}
107
+ */
108
+ const WS_MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
109
+
110
+ /**
111
+ * WebSocket protocol version supported by this implementation.
112
+ *
113
+ * @type {number}
114
+ */
115
+ const WS_VERSION = 13;
116
+
117
+ /**
118
+ * WebSocket close code indicating a normal closure.
119
+ *
120
+ * @type {number}
121
+ */
122
+ const WS_CLOSE_NORMAL = 1000;
123
+
124
+ /**
125
+ * WebSocket close code indicating a protocol error.
126
+ *
127
+ * @type {number}
128
+ */
129
+ const WS_CLOSE_PROTOCOL_ERROR = 1002;
130
+
131
+ /**
132
+ * WebSocket close code indicating invalid frame payload data.
133
+ *
134
+ * @type {number}
135
+ */
136
+ const WS_CLOSE_INVALID_PAYLOAD = 1007;
137
+
138
+ /**
139
+ * WebSocket close code indicating a message that is too large to process.
140
+ *
141
+ * @type {number}
142
+ */
143
+ const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
144
+
145
+ /**
146
+ * WebSocket ready state indicating the connection is open.
147
+ *
148
+ * @type {number}
149
+ */
150
+ const WS_OPEN = 1;
151
+
152
+ /**
153
+ * WebSocket ready state indicating the closing handshake has started.
154
+ *
155
+ * @type {number}
156
+ */
157
+ const WS_CLOSING = 2;
158
+
159
+ /**
160
+ * WebSocket ready state indicating the connection is closed.
161
+ *
162
+ * @type {number}
163
+ */
164
+ const WS_CLOSED = 3;
165
+
166
+ /**
167
+ * Default maximum allowed message size in bytes.
168
+ *
169
+ * Prevents memory exhaustion attacks from oversized WebSocket messages,
170
+ * including fragmented messages.
171
+ *
172
+ * @type {number}
173
+ */
174
+ const DEFAULT_MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
175
+
176
+ // ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
177
+ /**
178
+ * Encodes a WebSocket close code as a two-byte buffer.
179
+ *
180
+ * @private
181
+ * @param {number} code - WebSocket close code.
182
+ * @returns {Buffer} Two-byte close code buffer.
183
+ */
184
+ function encodeCloseCode(code) {
185
+ const buffer = Buffer.alloc(2);
186
+
187
+ buffer.writeUInt16BE(code, 0);
188
+
189
+ return buffer;
190
+ }
191
+
192
+ /**
193
+ * Determines whether a WebSocket close code is valid per RFC 6455.
194
+ *
195
+ * Valid ranges:
196
+ *
197
+ * - `1000`–`1003`: normal and endpoint-defined closures.
198
+ * - `1007`–`1014`: RFC-defined closure conditions.
199
+ * - `3000`–`4999`: application- and library-defined closures.
200
+ *
201
+ * @private
202
+ * @param {number} code - WebSocket close code.
203
+ * @returns {boolean} `true` when the code is valid.
204
+ */
205
+ function isValidCloseCode(code) {
206
+ return (
207
+ (code >= 1000 && code <= 1003) ||
208
+ (code >= 1007 && code <= 1014) ||
209
+ (code >= 3000 && code <= 4999)
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Determines whether a buffer contains valid UTF-8 data.
215
+ *
216
+ * @private
217
+ * @param {Buffer} buffer - Buffer to validate.
218
+ * @returns {boolean} `true` when the buffer is valid UTF-8.
219
+ */
220
+ function isValidUTF8(buffer) {
221
+ try {
222
+ new TextDecoder('utf-8', { fatal: true }).decode(buffer);
223
+
224
+ return true;
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Creates a WebSocket frame.
232
+ *
233
+ * Server-to-client frames are never masked.
234
+ *
235
+ * @private
236
+ * @param {number} opcode - WebSocket frame opcode.
237
+ * @param {Buffer} payload - Frame payload.
238
+ * @returns {Buffer} Encoded WebSocket frame.
239
+ */
240
+ function createWebSocketFrame(opcode, payload) {
241
+ const length = payload.length;
242
+
243
+ if (length <= 125) {
244
+ const frame = Buffer.alloc(2 + length);
245
+
246
+ frame[0] = 0x80 | opcode;
247
+ frame[1] = length;
248
+ payload.copy(frame, 2);
249
+
250
+ return frame;
251
+ }
252
+
253
+ if (length <= 0xffff) {
254
+ const frame = Buffer.alloc(4 + length);
255
+
256
+ frame[0] = 0x80 | opcode;
257
+ frame[1] = 126;
258
+ frame.writeUInt16BE(length, 2);
259
+ payload.copy(frame, 4);
260
+
261
+ return frame;
262
+ }
263
+
264
+ const frame = Buffer.alloc(10 + length);
265
+
266
+ frame[0] = 0x80 | opcode;
267
+ frame[1] = 127;
268
+ frame.writeBigUInt64BE(BigInt(length), 2);
269
+ payload.copy(frame, 10);
270
+
271
+ return frame;
272
+ }
273
+
274
+ /**
275
+ * Parses an incoming WebSocket frame.
276
+ *
277
+ * Client-to-server frames must be masked according to RFC 6455. The returned
278
+ * payload is always unmasked.
279
+ *
280
+ * @private
281
+ * @param {Buffer} buffer - Raw WebSocket bytes.
282
+ * @param {number} maxMessageSize - Maximum allowed message size in bytes.
283
+ * @returns {WebSocketFrame|null} Parsed frame, or `null` when the buffer does not yet contain a complete frame.
284
+ * @throws {Error & { code?: number }} Thrown when the frame violates the WebSocket protocol. The error may carry a numeric `code` indicating the appropriate WebSocket close code.
285
+ */
286
+ function parseWebSocketFrame(buffer, maxMessageSize) {
287
+ if (buffer.length < 2) {
288
+ return null;
289
+ }
290
+
291
+ const firstByte = buffer[0];
292
+ const secondByte = buffer[1];
293
+
294
+ const fin = (firstByte & 0x80) !== 0;
295
+ const reserved = firstByte & 0x70;
296
+ const opcode = firstByte & 0x0f;
297
+
298
+ if (reserved !== 0) {
299
+ throw new Error('Reserved WebSocket bits are not supported.');
300
+ }
301
+
302
+ if (
303
+ opcode !== 0x0 &&
304
+ opcode !== 0x1 &&
305
+ opcode !== 0x2 &&
306
+ opcode !== 0x8 &&
307
+ opcode !== 0x9 &&
308
+ opcode !== 0xa
309
+ ) {
310
+ throw new Error(`Unsupported WebSocket opcode: ${opcode}.`);
311
+ }
312
+
313
+ const isMasked = (secondByte & 0x80) !== 0;
314
+
315
+ if (!isMasked) {
316
+ throw new Error('Client WebSocket frames must be masked.');
317
+ }
318
+
319
+ let payloadLength = secondByte & 0x7f;
320
+ let offset = 2;
321
+
322
+ if (payloadLength === 126) {
323
+ if (buffer.length < 4) {
324
+ return null;
325
+ }
326
+
327
+ payloadLength = buffer.readUInt16BE(2);
328
+ offset = 4;
329
+ } else if (payloadLength === 127) {
330
+ if (buffer.length < 10) {
331
+ return null;
332
+ }
333
+
334
+ const length = buffer.readBigUInt64BE(2);
335
+
336
+ if (length > BigInt(Number.MAX_SAFE_INTEGER)) {
337
+ throw new Error('WebSocket payload is too large.');
338
+ }
339
+
340
+ payloadLength = Number(length);
341
+ offset = 10;
342
+ }
343
+
344
+ const isControlFrame = opcode >= 0x8;
345
+
346
+ if (isControlFrame && (!fin || payloadLength > 125)) {
347
+ throw new Error('Invalid WebSocket control frame.');
348
+ }
349
+
350
+ if (!isControlFrame && payloadLength > maxMessageSize) {
351
+ const error = /** @type {Error & { code?: number }} */ (
352
+ new Error(
353
+ `WebSocket payload size (${payloadLength} bytes) exceeds maximum allowed message size (${maxMessageSize} bytes).`,
354
+ )
355
+ );
356
+
357
+ error.code = WS_CLOSE_MESSAGE_TOO_BIG;
358
+
359
+ throw error;
360
+ }
361
+
362
+ if (buffer.length < offset + 4) {
363
+ return null;
364
+ }
365
+
366
+ const mask = buffer.subarray(offset, offset + 4);
367
+ offset += 4;
368
+
369
+ if (buffer.length < offset + payloadLength) {
370
+ return null;
371
+ }
372
+
373
+ const payload = Buffer.from(buffer.subarray(offset, offset + payloadLength));
374
+
375
+ for (let index = 0; index < payload.length; index += 1) {
376
+ payload[index] ^= mask[index % 4];
377
+ }
378
+
379
+ return {
380
+ fin,
381
+ opcode,
382
+ payload,
383
+ length: offset + payloadLength,
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Creates a WebSocket connection over a TCP socket.
389
+ *
390
+ * @param {WebSocketSocket} socket - Underlying TCP socket.
391
+ * @param {Buffer} head - Bytes already read during the HTTP upgrade.
392
+ * @param {number} maxMessageSize - Maximum allowed message size in bytes.
393
+ * @returns {WebSocketConnection} WebSocket connection.
394
+ * @example
395
+ * ```js
396
+ * const connection = createWebSocketConnection(socket, head, 10 * 1024 * 1024);
397
+ * connection.on('message', data => console.log(data));
398
+ * connection.send('hello');
399
+ * connection.close();
400
+ * ```
401
+ */
402
+ function createWebSocketConnection(socket, head, maxMessageSize) {
403
+ /**
404
+ * Registered message listeners.
405
+ *
406
+ * @type {((data: string|Buffer) => void)[]}
407
+ */
408
+ const messageListeners = [];
409
+
410
+ /**
411
+ * Registered close listeners.
412
+ *
413
+ * @type {(() => void)[]}
414
+ */
415
+ const closeListeners = [];
416
+
417
+ /**
418
+ * Registered error listeners.
419
+ *
420
+ * @type {((error: Error) => void)[]}
421
+ */
422
+ const errorListeners = [];
423
+
424
+ /**
425
+ * Buffered bytes pending parsing.
426
+ *
427
+ * @type {Buffer}
428
+ */
429
+ let bufferQueue = Buffer.alloc(0);
430
+
431
+ /**
432
+ * Current connection ready state.
433
+ *
434
+ * @type {number}
435
+ */
436
+ let readyState = WS_OPEN;
437
+
438
+ /**
439
+ * Opcode of the currently in-progress fragmented message, or `null`.
440
+ *
441
+ * @type {number|null}
442
+ */
443
+ let fragmentedOpcode = null;
444
+
445
+ /**
446
+ * Accumulated fragments of the in-progress fragmented message.
447
+ *
448
+ * @type {Buffer[]}
449
+ */
450
+ let fragmentedPayload = [];
451
+
452
+ /**
453
+ * Accumulated byte size of the in-progress fragmented message.
454
+ *
455
+ * @type {number}
456
+ */
457
+ let fragmentedSize = 0;
458
+
459
+ /**
460
+ * Writes a WebSocket frame to the underlying socket.
461
+ *
462
+ * @param {number} opcode - Frame opcode.
463
+ * @param {Buffer} payload - Frame payload.
464
+ * @throws {Error} Thrown when the WebSocket connection is not open.
465
+ */
466
+ const sendFrame = (opcode, payload) => {
467
+ if (readyState !== WS_OPEN) {
468
+ throw new Error('WebSocket connection is not open.');
469
+ }
470
+
471
+ socket.write(createWebSocketFrame(opcode, payload));
472
+ };
473
+
474
+ /**
475
+ * Sends a WebSocket close frame and ends the underlying socket.
476
+ *
477
+ * @param {Buffer} payload - Close frame payload.
478
+ */
479
+ const sendClose = payload => {
480
+ if (readyState === WS_OPEN) {
481
+ readyState = WS_CLOSING;
482
+ socket.write(createWebSocketFrame(0x8, payload));
483
+ }
484
+
485
+ socket.end();
486
+ };
487
+
488
+ /**
489
+ * Emits a complete WebSocket message to all registered listeners.
490
+ *
491
+ * Text frames are validated for UTF-8 correctness. Invalid UTF-8 payloads
492
+ * trigger a `1007` close frame per RFC 6455 §5.6.
493
+ *
494
+ * @param {number} opcode - Original message opcode.
495
+ * @param {Buffer} payload - Complete message payload.
496
+ * @returns {boolean} `true` when the message was emitted, `false` when it was rejected.
497
+ */
498
+ const emitMessage = (opcode, payload) => {
499
+ if (opcode === 0x1 && !isValidUTF8(payload)) {
500
+ sendClose(encodeCloseCode(WS_CLOSE_INVALID_PAYLOAD));
501
+
502
+ return false;
503
+ }
504
+
505
+ const data = opcode === 0x1 ? payload.toString('utf8') : payload;
506
+
507
+ messageListeners.forEach(listener => listener(data));
508
+
509
+ return true;
510
+ };
511
+
512
+ /**
513
+ * Handles a parsed WebSocket frame.
514
+ *
515
+ * @param {WebSocketFrame} frame - Parsed WebSocket frame.
516
+ * @returns {boolean} Whether the frame was valid.
517
+ */
518
+ const handleFrame = frame => {
519
+ if (frame.opcode === 0x8) {
520
+ if (frame.payload.length === 1) {
521
+ return false;
522
+ }
523
+
524
+ if (frame.payload.length >= 2) {
525
+ const code = frame.payload.readUInt16BE(0);
526
+
527
+ if (!isValidCloseCode(code)) {
528
+ return false;
529
+ }
530
+
531
+ if (frame.payload.length > 2 && !isValidUTF8(frame.payload.subarray(2))) {
532
+ sendClose(encodeCloseCode(WS_CLOSE_INVALID_PAYLOAD));
533
+
534
+ return false;
535
+ }
536
+ }
537
+
538
+ if (readyState === WS_OPEN) {
539
+ readyState = WS_CLOSING;
540
+ socket.write(createWebSocketFrame(0x8, frame.payload));
541
+ }
542
+
543
+ socket.end();
544
+
545
+ return true;
546
+ }
547
+
548
+ if (frame.opcode === 0x9) {
549
+ if (readyState === WS_OPEN) {
550
+ sendFrame(0xa, frame.payload);
551
+ }
552
+
553
+ return true;
554
+ }
555
+
556
+ if (frame.opcode === 0xa) {
557
+ return true;
558
+ }
559
+
560
+ if (frame.opcode === 0x0) {
561
+ if (fragmentedOpcode === null) {
562
+ return false;
563
+ }
564
+
565
+ if (fragmentedSize + frame.payload.length > maxMessageSize) {
566
+ sendClose(encodeCloseCode(WS_CLOSE_MESSAGE_TOO_BIG));
567
+
568
+ return false;
569
+ }
570
+
571
+ fragmentedSize += frame.payload.length;
572
+ fragmentedPayload.push(frame.payload);
573
+
574
+ if (frame.fin) {
575
+ const payload = Buffer.concat(fragmentedPayload);
576
+ const opcode = fragmentedOpcode;
577
+
578
+ fragmentedOpcode = null;
579
+ fragmentedPayload = [];
580
+ fragmentedSize = 0;
581
+
582
+ return emitMessage(opcode, payload);
583
+ }
584
+
585
+ return true;
586
+ }
587
+
588
+ if (fragmentedOpcode !== null) {
589
+ return false;
590
+ }
591
+
592
+ if (frame.fin) {
593
+ return emitMessage(frame.opcode, frame.payload);
594
+ }
595
+
596
+ if (frame.payload.length > maxMessageSize) {
597
+ return false;
598
+ }
599
+
600
+ fragmentedOpcode = frame.opcode;
601
+ fragmentedPayload = [frame.payload];
602
+ fragmentedSize = frame.payload.length;
603
+
604
+ return true;
605
+ };
606
+
607
+ /**
608
+ * Parses and dispatches every complete frame currently available in the
609
+ * buffer.
610
+ *
611
+ * Sends the appropriate close frame when a frame violates the WebSocket
612
+ * protocol. Protocol violations default to `1002`; specific violations may
613
+ * carry a different close code through the thrown error.
614
+ */
615
+ const processBuffer = () => {
616
+ /** @type {WebSocketFrame|null} */
617
+ let frame;
618
+
619
+ while (readyState !== WS_CLOSED) {
620
+ try {
621
+ frame = parseWebSocketFrame(bufferQueue, maxMessageSize);
622
+ } catch (error) {
623
+ const closeCode =
624
+ error && typeof error === 'object' && 'code' in error && typeof error.code === 'number'
625
+ ? error.code
626
+ : WS_CLOSE_PROTOCOL_ERROR;
627
+
628
+ sendClose(encodeCloseCode(closeCode));
629
+ break;
630
+ }
631
+
632
+ if (frame === null) {
633
+ break;
634
+ }
635
+
636
+ bufferQueue = bufferQueue.subarray(frame.length);
637
+
638
+ if (!handleFrame(frame)) {
639
+ if (readyState === WS_OPEN) {
640
+ sendClose(encodeCloseCode(WS_CLOSE_PROTOCOL_ERROR));
641
+ }
642
+
643
+ break;
644
+ }
645
+ }
646
+ };
647
+
648
+ socket.on('data', chunk => {
649
+ bufferQueue = Buffer.concat([bufferQueue, Buffer.from(chunk)]);
650
+ processBuffer();
651
+ });
652
+
653
+ socket.on('close', () => {
654
+ readyState = WS_CLOSED;
655
+
656
+ closeListeners.forEach(listener => listener());
657
+ });
658
+
659
+ socket.on('error', error => {
660
+ errorListeners.forEach(listener => listener(error));
661
+ });
662
+
663
+ if (head && head.length > 0) {
664
+ process.nextTick(() => {
665
+ bufferQueue = Buffer.concat([bufferQueue, head]);
666
+ processBuffer();
667
+ });
668
+ }
669
+
670
+ return {
671
+ get readyState() {
672
+ return readyState;
673
+ },
674
+
675
+ send(data) {
676
+ if (typeof data === 'string') {
677
+ sendFrame(0x1, Buffer.from(data));
678
+ } else {
679
+ sendFrame(0x2, Buffer.from(data));
680
+ }
681
+ },
682
+
683
+ /**
684
+ * Registers a listener for incoming WebSocket messages.
685
+ *
686
+ * @overload
687
+ * @param {'message'} event - Event name.
688
+ * @param {WebSocketMessageListener} listener - Listener invoked for incoming messages.
689
+ * @returns {void}
690
+ */
691
+ /**
692
+ * Registers a listener for WebSocket close events.
693
+ *
694
+ * @overload
695
+ * @param {'close'} event - Event name.
696
+ * @param {WebSocketCloseListener} listener - Listener invoked when the connection closes.
697
+ * @returns {void}
698
+ */
699
+ /**
700
+ * Registers a listener for WebSocket error events.
701
+ *
702
+ * @overload
703
+ * @param {'error'} event - Event name.
704
+ * @param {WebSocketErrorListener} listener - Listener invoked when an error occurs.
705
+ * @returns {void}
706
+ */
707
+ /**
708
+ * Registers a listener for a WebSocket event.
709
+ *
710
+ * @param {'message'|'close'|'error'} event - Event name.
711
+ * @param {WebSocketMessageListener|WebSocketCloseListener|WebSocketErrorListener} listener - Listener invoked for the event.
712
+ * @returns {void}
713
+ */
714
+ on(event, listener) {
715
+ if (event === 'message') {
716
+ messageListeners.push(/** @type {WebSocketMessageListener} */ (listener));
717
+ } else if (event === 'close') {
718
+ closeListeners.push(/** @type {WebSocketCloseListener} */ (listener));
719
+ } else if (event === 'error') {
720
+ errorListeners.push(/** @type {WebSocketErrorListener} */ (listener));
721
+ }
722
+ },
723
+
724
+ close() {
725
+ if (readyState === WS_OPEN) {
726
+ sendClose(encodeCloseCode(WS_CLOSE_NORMAL));
727
+ } else {
728
+ socket.end();
729
+ }
730
+ },
731
+ };
732
+ }
733
+
734
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
735
+ /**
736
+ * Creates a WebSocket upgrade request for an HTTP/1.1 upgrade.
737
+ *
738
+ * The returned `accept` method performs the RFC 6455 handshake and resolves to
739
+ * an established WebSocket connection. When the client requests subprotocols,
740
+ * the caller may pass a `selectedProtocol` that must be both supported by the
741
+ * server and requested by the client.
742
+ *
743
+ * @param {WebSocketUpgradeRequest} request - HTTP upgrade request.
744
+ * @param {WebSocketSocket} socket - Underlying TCP socket.
745
+ * @param {Buffer} head - Bytes already read during the upgrade.
746
+ * @param {WebSocketUpgradeOptions} [options] - Upgrade options.
747
+ * @returns {WebSocketOpenRequest} WebSocket upgrade request.
748
+ * @example
749
+ * ```js
750
+ * const openRequest = createWebSocketOpenRequest(request, socket, head, {
751
+ * maxMessageSize: 1024 * 1024,
752
+ * supportedProtocols: ['chat', 'json'],
753
+ * });
754
+ *
755
+ * openRequest.accept('chat');
756
+ * ```
757
+ */
758
+ function createWebSocketOpenRequest(request, socket, head, options = {}) {
759
+ const upgradeHeader = request.headers.upgrade;
760
+ const connectionHeader = request.headers.connection;
761
+ const secWebSocketKey = request.headers['sec-websocket-key'];
762
+ const secWebSocketVersion = request.headers['sec-websocket-version'];
763
+ const secWebSocketProtocol = request.headers['sec-websocket-protocol'];
764
+
765
+ const maxMessageSize = options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE;
766
+ const supportedProtocols = options.supportedProtocols ?? [];
767
+
768
+ return {
769
+ url: request.url || '/',
770
+ headers: request.headers,
771
+
772
+ accept(selectedProtocol) {
773
+ if (typeof upgradeHeader !== 'string' || upgradeHeader.toLowerCase() !== 'websocket') {
774
+ throw new Error("Missing or invalid 'Upgrade' header for WebSocket handshake.");
775
+ }
776
+
777
+ if (typeof connectionHeader !== 'string') {
778
+ throw new Error("Missing 'Connection' header for WebSocket handshake.");
779
+ }
780
+
781
+ const connectionTokens = connectionHeader
782
+ .toLowerCase()
783
+ .split(',')
784
+ .map(token => token.trim());
785
+
786
+ if (!connectionTokens.includes('upgrade')) {
787
+ throw new Error("Missing 'Upgrade' token in 'Connection' header for WebSocket handshake.");
788
+ }
789
+
790
+ if (secWebSocketVersion !== String(WS_VERSION)) {
791
+ throw new Error('Unsupported WebSocket version.');
792
+ }
793
+
794
+ if (typeof secWebSocketKey !== 'string' || secWebSocketKey.length === 0) {
795
+ throw new Error("Missing 'Sec-WebSocket-Key' header for WebSocket handshake.");
796
+ }
797
+
798
+ if (selectedProtocol) {
799
+ if (!supportedProtocols.includes(selectedProtocol)) {
800
+ throw new Error(`Unsupported WebSocket subprotocol: ${selectedProtocol}.`);
801
+ }
802
+
803
+ const clientProtocols =
804
+ typeof secWebSocketProtocol === 'string'
805
+ ? secWebSocketProtocol.split(',').map(protocol => protocol.trim())
806
+ : [];
807
+
808
+ if (!clientProtocols.includes(selectedProtocol)) {
809
+ throw new Error(`Client did not request the selected subprotocol: ${selectedProtocol}.`);
810
+ }
811
+ }
812
+
813
+ const hash = crypto
814
+ .createHash('sha1')
815
+ .update(secWebSocketKey + WS_MAGIC_STRING)
816
+ .digest('base64');
817
+
818
+ const responseHeaders = [
819
+ 'HTTP/1.1 101 Switching Protocols',
820
+ 'Upgrade: websocket',
821
+ 'Connection: Upgrade',
822
+ `Sec-WebSocket-Accept: ${hash}`,
823
+ ];
824
+
825
+ if (selectedProtocol) {
826
+ responseHeaders.push(`Sec-WebSocket-Protocol: ${selectedProtocol}`);
827
+ }
828
+
829
+ responseHeaders.push('\r\n');
830
+ socket.write(responseHeaders.join('\r\n'));
831
+
832
+ return createWebSocketConnection(socket, head, maxMessageSize);
833
+ },
834
+
835
+ reject(code = 400, message = 'Bad Request') {
836
+ socket.write(`HTTP/1.1 ${code} ${message}\r\n\r\n`);
837
+ socket.destroy();
838
+ },
839
+ };
840
+ }
841
+
842
+ // ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
843
+ export default createWebSocketOpenRequest;