@orkestrel/websocket 0.0.10 → 0.0.12
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/README.md +18 -8
- package/dist/src/server/index.cjs +458 -226
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +404 -147
- package/dist/src/server/index.d.ts +404 -147
- package/dist/src/server/index.js +453 -223
- package/dist/src/server/index.js.map +1 -1
- package/package.json +19 -16
|
@@ -3,180 +3,259 @@ let node_crypto = require("node:crypto");
|
|
|
3
3
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
4
4
|
//#region src/server/constants.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* hash
|
|
6
|
+
* Names the accept GUID concatenated to a client's `Sec-WebSocket-Key` before the accept
|
|
7
|
+
* hash, '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'.
|
|
8
8
|
*
|
|
9
9
|
* @remarks
|
|
10
|
-
*
|
|
10
|
+
* The base64-encoded SHA-1 of that concatenation is the `Sec-WebSocket-Accept` response
|
|
11
|
+
* value. A fixed, spec-mandated constant (RFC 6455 §4.2.2) — read only by
|
|
11
12
|
* {@link computeWebSocketAccept}.
|
|
12
13
|
*/
|
|
13
14
|
var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Names the supported protocol version, '13'.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* The value this wrapper speaks, carried by the `Sec-WebSocket-Version` handshake header.
|
|
20
|
+
*/
|
|
15
21
|
var WEBSOCKET_VERSION = "13";
|
|
16
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Names the text frame opcode, 0x01.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* A UTF-8 payload (RFC 6455 §5.6).
|
|
27
|
+
*/
|
|
17
28
|
var WEBSOCKET_OPCODE_TEXT = 1;
|
|
18
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Names the binary frame opcode, 0x02.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* A raw byte payload (RFC 6455 §5.6).
|
|
34
|
+
*/
|
|
19
35
|
var WEBSOCKET_OPCODE_BINARY = 2;
|
|
20
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Names the continuation frame opcode, 0x00.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* The next fragment of an open data message (RFC 6455 §5.4).
|
|
41
|
+
*/
|
|
21
42
|
var WEBSOCKET_OPCODE_CONTINUATION = 0;
|
|
22
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Names the close frame opcode, 0x08.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* A control frame ending the connection (RFC 6455 §5.5.1).
|
|
48
|
+
*/
|
|
23
49
|
var WEBSOCKET_OPCODE_CLOSE = 8;
|
|
24
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Names the ping frame opcode, 0x09.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* A control frame the peer must answer with a pong (RFC 6455 §5.5.2).
|
|
55
|
+
*/
|
|
25
56
|
var WEBSOCKET_OPCODE_PING = 9;
|
|
26
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Names the pong frame opcode, 0x0a.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* A control frame answering a ping (RFC 6455 §5.5.3).
|
|
62
|
+
*/
|
|
27
63
|
var WEBSOCKET_OPCODE_PONG = 10;
|
|
28
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Names the connecting ready state, 0.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* The state a WebSocket holds before its handshake completes.
|
|
69
|
+
*/
|
|
29
70
|
var WEBSOCKET_READY_CONNECTING = 0;
|
|
30
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Names the open ready state, 1.
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* The state a WebSocket holds after the handshake completes and while frames flow.
|
|
76
|
+
*/
|
|
31
77
|
var WEBSOCKET_READY_OPEN = 1;
|
|
32
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Names the closing ready state, 2.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* The state a WebSocket holds after a close frame is sent or received.
|
|
83
|
+
*/
|
|
33
84
|
var WEBSOCKET_READY_CLOSING = 2;
|
|
34
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Names the closed ready state, 3.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* The state a WebSocket holds after the socket ends.
|
|
90
|
+
*/
|
|
35
91
|
var WEBSOCKET_READY_CLOSED = 3;
|
|
36
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Names the normal-closure status code, 1000.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* The default `close` code (RFC 6455 §7.4.1).
|
|
97
|
+
*/
|
|
37
98
|
var WEBSOCKET_CLOSE_NORMAL = 1e3;
|
|
38
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Names the protocol-error status code, 1002.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* Sent when a framing or state rule was violated (RFC 6455 §7.4.1).
|
|
104
|
+
*/
|
|
39
105
|
var WEBSOCKET_CLOSE_PROTOCOL = 1002;
|
|
40
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Names the unsupported-data status code, 1003.
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* Sent when the endpoint received a data type it cannot accept (RFC 6455 §7.4.1), for
|
|
111
|
+
* example binary on a text-only endpoint.
|
|
112
|
+
*/
|
|
41
113
|
var WEBSOCKET_CLOSE_UNSUPPORTED = 1003;
|
|
42
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Names the invalid-frame-payload-data status code, 1007.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* Sent for non-UTF-8 text or an unparseable close reason (RFC 6455 §7.4.1).
|
|
119
|
+
*/
|
|
43
120
|
var WEBSOCKET_CLOSE_INVALID = 1007;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Names the message-too-big status code, 1009.
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* Sent when a reassembled message exceeded the payload cap (RFC 6455 §7.4.1).
|
|
126
|
+
*/
|
|
127
|
+
var WEBSOCKET_CLOSE_TOO_BIG = 1009;
|
|
128
|
+
/**
|
|
129
|
+
* Names the default cap on both an inbound frame's declared length and a reassembled
|
|
130
|
+
* message's total byte count, 104,857,600 bytes (100 MiB).
|
|
131
|
+
*
|
|
132
|
+
* @remarks
|
|
133
|
+
* The same value the `ws` package defaults to. Either breach closes
|
|
134
|
+
* {@link WEBSOCKET_CLOSE_TOO_BIG}.
|
|
135
|
+
*/
|
|
47
136
|
var WEBSOCKET_MAX_PAYLOAD = 104857600;
|
|
48
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Names the default close-handshake timeout, 30,000 milliseconds — how long `close` waits
|
|
139
|
+
* for the peer's echo.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* After it expires the wrapper tears the socket down, so a silent peer cannot leak the
|
|
143
|
+
* handle open.
|
|
144
|
+
*/
|
|
49
145
|
var WEBSOCKET_CLOSE_TIMEOUT_MS = 3e4;
|
|
50
|
-
/** 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). */
|
|
51
|
-
var WEBSOCKET_FAIL_TIMEOUT_MS = 1e3;
|
|
52
|
-
/** The maximum control-frame payload length in bytes (RFC 6455 §5.5). */
|
|
53
|
-
var WEBSOCKET_CONTROL_MAXLEN = 125;
|
|
54
|
-
/** The maximum UTF-8 close-reason length after the two-byte status code. */
|
|
55
|
-
var WEBSOCKET_CLOSE_REASON_MAXLEN = 123;
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/server/helpers.ts
|
|
58
146
|
/**
|
|
59
|
-
*
|
|
147
|
+
* Names the flush grace, 1,000 milliseconds, a validation-breach close frame is given
|
|
148
|
+
* before the hard teardown fallback destroys the socket.
|
|
60
149
|
*
|
|
61
150
|
* @remarks
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
151
|
+
* Armed after `#fail` writes the close frame, so the frame drains through the socket's
|
|
152
|
+
* write buffer rather than being discarded. The normal path destroys sooner, on the
|
|
153
|
+
* `end()` flush callback.
|
|
154
|
+
*/
|
|
155
|
+
var WEBSOCKET_FAIL_TIMEOUT_MS = 1e3;
|
|
156
|
+
/**
|
|
157
|
+
* Names the maximum control-frame payload length, 125 bytes.
|
|
65
158
|
*
|
|
66
|
-
* @
|
|
67
|
-
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* The cap RFC 6455 §5.5 sets on every control frame's payload.
|
|
68
161
|
*/
|
|
69
|
-
|
|
70
|
-
return (0, node_crypto.createHash)("sha1").update(key + WEBSOCKET_GUID).digest("base64");
|
|
71
|
-
}
|
|
162
|
+
var WEBSOCKET_CONTROL_MAX_LENGTH = 125;
|
|
72
163
|
/**
|
|
73
|
-
*
|
|
164
|
+
* Names the maximum UTF-8 close-reason length after the two-byte status code, 123.
|
|
74
165
|
*
|
|
75
166
|
* @remarks
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
167
|
+
* What is left of {@link WEBSOCKET_CONTROL_MAX_LENGTH} after the close frame's status
|
|
168
|
+
* code.
|
|
169
|
+
*/
|
|
170
|
+
var WEBSOCKET_CLOSE_REASON_MAX_LENGTH = 123;
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/server/errors.ts
|
|
173
|
+
/**
|
|
174
|
+
* Represents an error the WebSocket wrapper throws for a refused caller-supplied value,
|
|
175
|
+
* carrying a machine-readable `code` and an optional `context`.
|
|
79
176
|
*
|
|
80
|
-
* @
|
|
81
|
-
*
|
|
177
|
+
* @remarks
|
|
178
|
+
* The `code` is a {@link WebSocketErrorCode}; the `context` record holds the refused
|
|
179
|
+
* value under a key naming it: an `'OPTION'` carries the offending option (`payload`,
|
|
180
|
+
* `timeout`, `key`, or `protocol`), a `'LIMIT'` carries `size` and the `limit` it
|
|
181
|
+
* exceeded, a `'CLOSE'` carries the refused close `code`, and a `'FRAME'` carries
|
|
182
|
+
* `opcode` or the mask's `size`. Narrow a caught value with {@link isWebSocketError}.
|
|
82
183
|
*
|
|
83
184
|
* @example
|
|
84
185
|
* ```ts
|
|
85
|
-
*
|
|
86
|
-
*
|
|
186
|
+
* import { createNodeWebSocket, isWebSocketError } from '@src/server'
|
|
187
|
+
*
|
|
188
|
+
* try {
|
|
189
|
+
* createNodeWebSocket({ socket, key: 'not-base64' })
|
|
190
|
+
* } catch (error) {
|
|
191
|
+
* if (isWebSocketError(error) && error.code === 'OPTION') socket.destroy()
|
|
192
|
+
* }
|
|
87
193
|
* ```
|
|
88
194
|
*/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
195
|
+
var WebSocketError = class extends Error {
|
|
196
|
+
code;
|
|
197
|
+
context;
|
|
198
|
+
/**
|
|
199
|
+
* Creates a WebSocket error carrying a machine-readable code.
|
|
200
|
+
*
|
|
201
|
+
* @param code - The machine-readable {@link WebSocketErrorCode} a `catch` branches on
|
|
202
|
+
* @param message - The human-readable description, carried as the `Error` message
|
|
203
|
+
* @param context - The refused value keyed by name; omitted leaves `context` `undefined`
|
|
204
|
+
*/
|
|
205
|
+
constructor(code, message, context) {
|
|
206
|
+
super(message);
|
|
207
|
+
this.name = "WebSocketError";
|
|
208
|
+
this.code = code;
|
|
209
|
+
if (context !== void 0) this.context = context;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
93
212
|
/**
|
|
94
|
-
*
|
|
213
|
+
* Checks whether a caught value is a {@link WebSocketError}, narrowing it so a `catch` can
|
|
214
|
+
* branch on `error.code`.
|
|
95
215
|
*
|
|
96
|
-
* @
|
|
97
|
-
*
|
|
98
|
-
* control characters are rejected, preventing an untrusted value from injecting a
|
|
99
|
-
* second handshake header.
|
|
100
|
-
*
|
|
101
|
-
* @param protocol - The negotiated subprotocol to validate
|
|
102
|
-
* @returns `true` when `protocol` is one non-empty HTTP token
|
|
216
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
217
|
+
* @returns True if `value` is a `WebSocketError`; false otherwise
|
|
103
218
|
*
|
|
104
219
|
* @example
|
|
105
220
|
* ```ts
|
|
106
|
-
*
|
|
221
|
+
* import { isWebSocketError } from '@src/server'
|
|
222
|
+
*
|
|
223
|
+
* try {
|
|
224
|
+
* ws.close(1000.5)
|
|
225
|
+
* } catch (error) {
|
|
226
|
+
* if (isWebSocketError(error) && error.code === 'CLOSE') ws.close()
|
|
227
|
+
* }
|
|
107
228
|
* ```
|
|
108
229
|
*/
|
|
109
|
-
function
|
|
110
|
-
return
|
|
230
|
+
function isWebSocketError(value) {
|
|
231
|
+
return value instanceof WebSocketError;
|
|
111
232
|
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/server/helpers.ts
|
|
112
235
|
/**
|
|
113
|
-
*
|
|
236
|
+
* Computes the `Sec-WebSocket-Accept` response value for an RFC 6455 upgrade.
|
|
114
237
|
*
|
|
115
238
|
* @remarks
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* against the key when the mask bit is set (client→server frames MUST be masked, RFC
|
|
120
|
-
* 6455 §5.3; an unmasked frame still decodes, leaving the payload as-is, so the caller
|
|
121
|
-
* can enforce policy). Returns `undefined` the moment the buffer is too short for the
|
|
122
|
-
* part it is up to (the length prefix, the mask, or the full payload) — the signal to
|
|
123
|
-
* the caller to read more bytes and retry, exactly like {@link SSEParser} on a partial
|
|
124
|
-
* line. `consumed` is the total bytes the frame occupied, so the caller slices the
|
|
125
|
-
* remainder. Pure; never throws on a short buffer.
|
|
239
|
+
* The base64-encoded SHA-1 of the client's `Sec-WebSocket-Key` concatenated with the
|
|
240
|
+
* fixed {@link WEBSOCKET_GUID} (RFC 6455 §4.2.2) — the proof the server understood the
|
|
241
|
+
* handshake. Pure and deterministic.
|
|
126
242
|
*
|
|
127
|
-
* @param
|
|
128
|
-
* @returns The
|
|
243
|
+
* @param key - The client's `Sec-WebSocket-Key` header value
|
|
244
|
+
* @returns The base64 accept token to send back as `Sec-WebSocket-Accept`
|
|
129
245
|
*/
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
const firstByte = buffer.readUInt8(0);
|
|
133
|
-
const secondByte = buffer.readUInt8(1);
|
|
134
|
-
const fin = (firstByte & 128) !== 0;
|
|
135
|
-
const rsv = (firstByte & 112) >> 4;
|
|
136
|
-
const opcode = firstByte & 15;
|
|
137
|
-
const masked = (secondByte & 128) !== 0;
|
|
138
|
-
let length = secondByte & 127;
|
|
139
|
-
let offset = 2;
|
|
140
|
-
if (length === 126) {
|
|
141
|
-
if (buffer.length < offset + 2) return void 0;
|
|
142
|
-
length = buffer.readUInt16BE(offset);
|
|
143
|
-
offset += 2;
|
|
144
|
-
} else if (length === 127) {
|
|
145
|
-
if (buffer.length < offset + 8) return void 0;
|
|
146
|
-
const high = buffer.readUInt32BE(offset);
|
|
147
|
-
const low = buffer.readUInt32BE(offset + 4);
|
|
148
|
-
length = high * 4294967296 + low;
|
|
149
|
-
offset += 8;
|
|
150
|
-
}
|
|
151
|
-
let mask;
|
|
152
|
-
if (masked) {
|
|
153
|
-
if (buffer.length < offset + 4) return void 0;
|
|
154
|
-
mask = buffer.subarray(offset, offset + 4);
|
|
155
|
-
offset += 4;
|
|
156
|
-
}
|
|
157
|
-
if (buffer.length < offset + length) return void 0;
|
|
158
|
-
const payload = Buffer.alloc(length);
|
|
159
|
-
buffer.copy(payload, 0, offset, offset + length);
|
|
160
|
-
if (mask !== void 0) for (let index = 0; index < length; index += 1) payload[index] = payload.readUInt8(index) ^ mask.readUInt8(index % 4);
|
|
161
|
-
return {
|
|
162
|
-
fin,
|
|
163
|
-
opcode,
|
|
164
|
-
payload,
|
|
165
|
-
consumed: offset + length,
|
|
166
|
-
masked,
|
|
167
|
-
rsv
|
|
168
|
-
};
|
|
246
|
+
function computeWebSocketAccept(key) {
|
|
247
|
+
return (0, node_crypto.createHash)("sha1").update(key + WEBSOCKET_GUID).digest("base64");
|
|
169
248
|
}
|
|
170
249
|
/**
|
|
171
|
-
*
|
|
172
|
-
* reading the payload itself.
|
|
250
|
+
* Reads the declared payload length off the front of a buffer without buffering or
|
|
251
|
+
* reading the payload itself, answering `undefined` until the length field is complete.
|
|
173
252
|
*
|
|
174
253
|
* @remarks
|
|
175
254
|
* Decodes only byte 1's 7-bit length field, extended by the 16-bit (`126`) or 64-bit
|
|
176
|
-
* (`127`) form exactly like
|
|
255
|
+
* (`127`) form exactly like `parseWebSocketFrame` — but stops there, so a caller
|
|
177
256
|
* can reject an over-cap frame the moment its length is known, before the payload
|
|
178
|
-
* bytes have even arrived.
|
|
179
|
-
*
|
|
257
|
+
* bytes have even arrived. The incomplete-buffer contract mirrors the parser's. Pure;
|
|
258
|
+
* never throws.
|
|
180
259
|
*
|
|
181
260
|
* @param buffer - The accumulation buffer to read the next frame's length from
|
|
182
261
|
* @returns The declared payload length, or `undefined` when the buffer is too short to know it yet
|
|
@@ -184,7 +263,7 @@ function parseWebSocketFrame(buffer) {
|
|
|
184
263
|
* @example
|
|
185
264
|
* ```ts
|
|
186
265
|
* const declared = measureWebSocketFrame(buffer)
|
|
187
|
-
* if (declared !== undefined && declared > limit) fail(
|
|
266
|
+
* if (declared !== undefined && declared > limit) fail(WEBSOCKET_CLOSE_TOO_BIG)
|
|
188
267
|
* ```
|
|
189
268
|
*/
|
|
190
269
|
function measureWebSocketFrame(buffer) {
|
|
@@ -203,22 +282,24 @@ function measureWebSocketFrame(buffer) {
|
|
|
203
282
|
return length;
|
|
204
283
|
}
|
|
205
284
|
/**
|
|
206
|
-
*
|
|
285
|
+
* Checks whether the next frame uses the shortest valid RFC 6455 payload-length
|
|
286
|
+
* encoding, answering `undefined` until its length prefix is complete.
|
|
207
287
|
*
|
|
208
288
|
* @remarks
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
289
|
+
* The 16-bit form is canonical only for lengths at least 126; the 64-bit form only for
|
|
290
|
+
* lengths at least 65,536 and with its most-significant bit clear (RFC 6455 §5.2). Reads
|
|
291
|
+
* the same length prefix as {@link measureWebSocketFrame}, under the same
|
|
292
|
+
* incomplete-buffer contract. Pure; never throws.
|
|
212
293
|
*
|
|
213
294
|
* @param buffer - The accumulation buffer containing the next frame header
|
|
214
295
|
* @returns Its canonicality, or `undefined` while the length prefix is incomplete
|
|
215
296
|
*
|
|
216
297
|
* @example
|
|
217
298
|
* ```ts
|
|
218
|
-
* if (
|
|
299
|
+
* if (matchesWebSocketCanonical(buffer) === false) fail(WEBSOCKET_CLOSE_PROTOCOL)
|
|
219
300
|
* ```
|
|
220
301
|
*/
|
|
221
|
-
function
|
|
302
|
+
function matchesWebSocketCanonical(buffer) {
|
|
222
303
|
if (buffer.length < 2) return void 0;
|
|
223
304
|
const lengthCode = buffer.readUInt8(1) & 127;
|
|
224
305
|
if (lengthCode < 126) return true;
|
|
@@ -233,31 +314,95 @@ function isWebSocketFrameCanonical(buffer) {
|
|
|
233
314
|
return high > 0 || low >= 65536;
|
|
234
315
|
}
|
|
235
316
|
/**
|
|
236
|
-
*
|
|
317
|
+
* Encodes a single RFC 6455 frame to its wire bytes — the inverse of
|
|
318
|
+
* `parseWebSocketFrame`.
|
|
237
319
|
*
|
|
238
320
|
* @remarks
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
321
|
+
* Builds a final (FIN-set) frame: byte 0 is `0x80 | opcode`; the payload length uses
|
|
322
|
+
* the 7-bit form below 126, the `126` + 16-bit form below 65 536, or the `127` +
|
|
323
|
+
* 64-bit form beyond; when `masked` is set the mask bit is set, a 4-byte key (supplied
|
|
324
|
+
* through `options.mask`, else random) is written, and the payload is XOR-masked. Server→
|
|
325
|
+
* client frames are unmasked (the default); pass `masked: true` to encode a client
|
|
326
|
+
* frame (for example to feed the parser in a test). A `string` payload is encoded as
|
|
327
|
+
* UTF-8. Returns one contiguous `Buffer` (header + payload), so the wrapper writes it
|
|
328
|
+
* with a single `socket.write`. Pure.
|
|
242
329
|
*
|
|
243
|
-
* @param
|
|
244
|
-
* @
|
|
330
|
+
* @param opcode - The frame opcode (a `WEBSOCKET_OPCODE_*` value)
|
|
331
|
+
* @param payload - The payload, a `Buffer` or a UTF-8 `string`
|
|
332
|
+
* @param options - Masking control ({@link WebSocketEncodeOptions}); defaults to unmasked
|
|
333
|
+
* @returns The complete frame as wire bytes
|
|
334
|
+
* @throws A {@link WebSocketError} coded `FRAME` when `opcode` is outside the four-bit wire field, when `options.mask` is not 4 bytes, or when `options.mask` is supplied without `masked: true`
|
|
335
|
+
*/
|
|
336
|
+
function encodeWebSocketFrame(opcode, payload, options) {
|
|
337
|
+
if (!Number.isInteger(opcode) || opcode < 0 || opcode > 15) throw new WebSocketError("FRAME", "opcode must be an integer between 0 and 15", { opcode });
|
|
338
|
+
if (options?.mask !== void 0 && options.mask.length !== 4) throw new WebSocketError("FRAME", "mask must contain exactly 4 bytes", { size: options.mask.length });
|
|
339
|
+
if (options?.mask !== void 0 && options.masked !== true) throw new WebSocketError("FRAME", "mask requires masked: true");
|
|
340
|
+
const body = typeof payload === "string" ? Buffer.from(payload, "utf-8") : payload;
|
|
341
|
+
const length = body.length;
|
|
342
|
+
const masked = options?.masked === true;
|
|
343
|
+
const mask = masked ? options?.mask ?? (0, node_crypto.randomBytes)(4) : void 0;
|
|
344
|
+
const maskBit = masked ? 128 : 0;
|
|
345
|
+
const extended = length < 126 ? 0 : length < 65536 ? 2 : 8;
|
|
346
|
+
const header = Buffer.alloc(2 + extended + (mask !== void 0 ? 4 : 0));
|
|
347
|
+
header[0] = 128 | opcode;
|
|
348
|
+
if (length < 126) header[1] = maskBit | length;
|
|
349
|
+
else if (length < 65536) {
|
|
350
|
+
header[1] = maskBit | 126;
|
|
351
|
+
header.writeUInt16BE(length, 2);
|
|
352
|
+
} else {
|
|
353
|
+
header[1] = maskBit | 127;
|
|
354
|
+
header.writeUInt32BE(Math.floor(length / 4294967296), 2);
|
|
355
|
+
header.writeUInt32BE(length % 4294967296, 6);
|
|
356
|
+
}
|
|
357
|
+
if (mask === void 0) return Buffer.concat([header, body]);
|
|
358
|
+
mask.copy(header, header.length - 4);
|
|
359
|
+
const maskedBody = Buffer.alloc(length);
|
|
360
|
+
for (let index = 0; index < length; index += 1) maskedBody[index] = body.readUInt8(index) ^ mask.readUInt8(index % 4);
|
|
361
|
+
return Buffer.concat([header, maskedBody]);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Checks whether a value is a canonical RFC 6455 `Sec-WebSocket-Key`.
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
367
|
+
* A valid key is exactly 16 random bytes encoded as 24 characters of base64, ending
|
|
368
|
+
* in `==` (RFC 6455 §4.1). This predicate is suitable at an HTTP upgrade boundary:
|
|
369
|
+
* malformed or non-canonical encodings return `false`; nothing is thrown.
|
|
370
|
+
*
|
|
371
|
+
* @param key - The proposed `Sec-WebSocket-Key` header value
|
|
372
|
+
* @returns True if `key` is the canonical base64 encoding of 16 bytes; false otherwise
|
|
245
373
|
*
|
|
246
374
|
* @example
|
|
247
375
|
* ```ts
|
|
248
|
-
* const
|
|
249
|
-
* if (
|
|
376
|
+
* const key = request.headers['sec-websocket-key']
|
|
377
|
+
* if (typeof key !== 'string' || !isWebSocketKey(key)) socket.destroy()
|
|
250
378
|
* ```
|
|
251
379
|
*/
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
380
|
+
function isWebSocketKey(key) {
|
|
381
|
+
if (!/^[A-Za-z0-9+/]{22}==$/.test(key)) return false;
|
|
382
|
+
return Buffer.from(key, "base64").length === 16;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Checks whether a value is one valid WebSocket subprotocol token.
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* Subprotocols use the HTTP `token` grammar. Whitespace, separators, commas, and
|
|
389
|
+
* control characters are rejected, preventing an untrusted value from injecting a
|
|
390
|
+
* second handshake header.
|
|
391
|
+
*
|
|
392
|
+
* @param protocol - The negotiated subprotocol to validate
|
|
393
|
+
* @returns True if `protocol` is one non-empty HTTP token; false otherwise
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* if (!isWebSocketProtocol(protocol)) socket.destroy()
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
function isWebSocketProtocol(protocol) {
|
|
401
|
+
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(protocol);
|
|
258
402
|
}
|
|
259
403
|
/**
|
|
260
|
-
*
|
|
404
|
+
* Checks whether a numeric value is a close status code an RFC 6455 endpoint may
|
|
405
|
+
* receive (§7.4.1).
|
|
261
406
|
*
|
|
262
407
|
* @remarks
|
|
263
408
|
* True for `1000`–`1003`, `1007`–`1014`, and the application range `3000`–`4999`; false
|
|
@@ -270,7 +415,7 @@ function parseUTF8(bytes) {
|
|
|
270
415
|
* never throws.
|
|
271
416
|
*
|
|
272
417
|
* @param code - The close status code to validate
|
|
273
|
-
* @returns
|
|
418
|
+
* @returns True if `code` is a valid RFC 6455 close code; false otherwise
|
|
274
419
|
*
|
|
275
420
|
* @example
|
|
276
421
|
* ```ts
|
|
@@ -284,73 +429,133 @@ function isCloseCode(code) {
|
|
|
284
429
|
if (code >= 3e3 && code <= 4999) return true;
|
|
285
430
|
return false;
|
|
286
431
|
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/server/parsers.ts
|
|
287
434
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
435
|
+
* Decodes a single RFC 6455 frame from the front of a buffer, answering `undefined`
|
|
436
|
+
* while the buffer is incomplete so the caller accumulates and retries.
|
|
290
437
|
*
|
|
291
438
|
* @remarks
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
439
|
+
* Reads the FIN bit and opcode (byte 0), the mask bit and 7-bit payload length (byte
|
|
440
|
+
* 1) — extended to a 16-bit length when the 7-bit field is `126`, or a 64-bit length
|
|
441
|
+
* when it is `127` — the optional 4-byte mask key, then the payload, XOR-unmasking it
|
|
442
|
+
* against the key when the mask bit is set (client→server frames must be masked, RFC
|
|
443
|
+
* 6455 §5.3; an unmasked frame still decodes, leaving the payload as-is, so the caller
|
|
444
|
+
* can enforce policy). The incomplete answer comes the moment the buffer is too short
|
|
445
|
+
* for the part it is up to: the length prefix, the mask, or the full payload.
|
|
446
|
+
* `consumed` is the total bytes the frame occupied, so the caller slices the remainder.
|
|
447
|
+
* Pure; never throws on a short buffer.
|
|
300
448
|
*
|
|
301
|
-
* @param
|
|
302
|
-
* @
|
|
303
|
-
*
|
|
304
|
-
* @
|
|
449
|
+
* @param buffer - The accumulation buffer to decode the next frame from
|
|
450
|
+
* @returns The parsed {@link WebSocketFrame}, or `undefined` when the buffer is incomplete
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```ts
|
|
454
|
+
* const frame = parseWebSocketFrame(buffer)
|
|
455
|
+
* if (frame === undefined) return // incomplete — wait for more bytes
|
|
456
|
+
* ```
|
|
305
457
|
*/
|
|
306
|
-
function
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
458
|
+
function parseWebSocketFrame(buffer) {
|
|
459
|
+
if (buffer.length < 2) return void 0;
|
|
460
|
+
const firstByte = buffer.readUInt8(0);
|
|
461
|
+
const secondByte = buffer.readUInt8(1);
|
|
462
|
+
const fin = (firstByte & 128) !== 0;
|
|
463
|
+
const rsv = (firstByte & 112) >> 4;
|
|
464
|
+
const opcode = firstByte & 15;
|
|
465
|
+
const masked = (secondByte & 128) !== 0;
|
|
466
|
+
let length = secondByte & 127;
|
|
467
|
+
let offset = 2;
|
|
468
|
+
if (length === 126) {
|
|
469
|
+
if (buffer.length < offset + 2) return void 0;
|
|
470
|
+
length = buffer.readUInt16BE(offset);
|
|
471
|
+
offset += 2;
|
|
472
|
+
} else if (length === 127) {
|
|
473
|
+
if (buffer.length < offset + 8) return void 0;
|
|
474
|
+
const high = buffer.readUInt32BE(offset);
|
|
475
|
+
const low = buffer.readUInt32BE(offset + 4);
|
|
476
|
+
length = high * 4294967296 + low;
|
|
477
|
+
offset += 8;
|
|
478
|
+
}
|
|
479
|
+
let mask;
|
|
480
|
+
if (masked) {
|
|
481
|
+
if (buffer.length < offset + 4) return void 0;
|
|
482
|
+
mask = buffer.subarray(offset, offset + 4);
|
|
483
|
+
offset += 4;
|
|
484
|
+
}
|
|
485
|
+
if (buffer.length < offset + length) return void 0;
|
|
486
|
+
const payload = Buffer.alloc(length);
|
|
487
|
+
buffer.copy(payload, 0, offset, offset + length);
|
|
488
|
+
if (mask !== void 0) for (let index = 0; index < length; index += 1) payload[index] = payload.readUInt8(index) ^ mask.readUInt8(index % 4);
|
|
489
|
+
return {
|
|
490
|
+
fin,
|
|
491
|
+
opcode,
|
|
492
|
+
payload,
|
|
493
|
+
consumed: offset + length,
|
|
494
|
+
masked,
|
|
495
|
+
rsv
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Decodes a byte sequence as strict UTF-8, answering `undefined` when the sequence is
|
|
500
|
+
* malformed.
|
|
501
|
+
*
|
|
502
|
+
* @remarks
|
|
503
|
+
* Wraps `TextDecoder('utf-8', { fatal: true })` in a try/catch, so a malformed sequence
|
|
504
|
+
* returns rather than throwing — a guard-adjacent coercer never throws on bad input.
|
|
505
|
+
* Pure.
|
|
506
|
+
*
|
|
507
|
+
* @param bytes - The raw bytes to decode
|
|
508
|
+
* @returns The decoded string, or `undefined` when `bytes` is not valid UTF-8
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* ```ts
|
|
512
|
+
* const text = parseUTF8(payload)
|
|
513
|
+
* if (text === undefined) fail(WEBSOCKET_CLOSE_INVALID)
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
function parseUTF8(bytes) {
|
|
517
|
+
try {
|
|
518
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
519
|
+
} catch {
|
|
520
|
+
return;
|
|
326
521
|
}
|
|
327
|
-
if (mask === void 0) return Buffer.concat([header, body]);
|
|
328
|
-
mask.copy(header, header.length - 4);
|
|
329
|
-
const maskedBody = Buffer.alloc(length);
|
|
330
|
-
for (let index = 0; index < length; index += 1) maskedBody[index] = body.readUInt8(index) ^ mask.readUInt8(index % 4);
|
|
331
|
-
return Buffer.concat([header, maskedBody]);
|
|
332
522
|
}
|
|
333
523
|
//#endregion
|
|
334
524
|
//#region src/server/NodeWebSocket.ts
|
|
335
525
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
526
|
+
* Implements the wrapper contract over a raw upgraded `node:stream` Duplex socket,
|
|
527
|
+
* driving the RFC 6455 handshake, the frame codec, auto-pong, and the close handshake,
|
|
528
|
+
* and surfacing every event on an owned `emitter`.
|
|
338
529
|
*
|
|
339
530
|
* @remarks
|
|
340
|
-
* Created by `createNodeWebSocket`. When given a client `key` it runs in
|
|
531
|
+
* Created by `createNodeWebSocket`. When given a client `key` it runs in server mode —
|
|
341
532
|
* it writes the `101 Switching Protocols` handshake (`computeWebSocketAccept(key)`) and
|
|
342
|
-
* emits `open`; given no key it runs in
|
|
533
|
+
* emits `open`; given no key it runs in client mode (no handshake, frames masked). It
|
|
343
534
|
* then listens on the socket's `data`, accumulating bytes in `#buffer` and decoding
|
|
344
535
|
* every complete frame with {@link parseWebSocketFrame} (slicing `consumed` and
|
|
345
|
-
* re-parsing the remainder): a
|
|
346
|
-
* `fin: false` frames — decodes to UTF-8 and emits `message`; a
|
|
347
|
-
* with a
|
|
536
|
+
* re-parsing the remainder): a text frame — reassembling continuation fragments across
|
|
537
|
+
* `fin: false` frames — decodes to UTF-8 and emits `message`; a ping is auto-answered
|
|
538
|
+
* with a pong and emits `ping`; a pong emits `pong`; a close frame is echoed and ends the
|
|
348
539
|
* socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
|
|
349
|
-
* frame; `destroy` tears down immediately. It owns a typed `#emitter`
|
|
350
|
-
* isolates a throwing listener and routes the error to its own `error` handler
|
|
351
|
-
* option) — the socket never crashes. An underlying socket error emits the
|
|
352
|
-
* `error` event and terminates the wrapper. The untyped socket `data` is narrowed
|
|
353
|
-
* `Buffer` with a guard, never an assertion
|
|
540
|
+
* frame; `destroy` tears down immediately. It owns a typed `#emitter` by composition, and
|
|
541
|
+
* the emitter isolates a throwing listener and routes the error to its own `error` handler
|
|
542
|
+
* (the `error` option) — the socket never crashes. An underlying socket error emits the
|
|
543
|
+
* domain `error` event and terminates the wrapper. The untyped socket `data` is narrowed
|
|
544
|
+
* to a `Buffer` with a guard, never an assertion.
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```ts
|
|
548
|
+
* import { NodeWebSocket } from '@src/server'
|
|
549
|
+
*
|
|
550
|
+
* // In a node:http 'upgrade' handler, over the socket the server already handed over:
|
|
551
|
+
* const key = request.headers['sec-websocket-key']
|
|
552
|
+
* if (typeof key !== 'string') {
|
|
553
|
+
* socket.destroy()
|
|
554
|
+
* return
|
|
555
|
+
* }
|
|
556
|
+
* const ws = new NodeWebSocket({ socket, key, head })
|
|
557
|
+
* ws.emitter.on('message', (text) => ws.send(`echo: ${text}`))
|
|
558
|
+
* ```
|
|
354
559
|
*/
|
|
355
560
|
var NodeWebSocket = class {
|
|
356
561
|
#emitter;
|
|
@@ -373,14 +578,25 @@ var NodeWebSocket = class {
|
|
|
373
578
|
#closeTimer;
|
|
374
579
|
#destroyed = false;
|
|
375
580
|
#detached = false;
|
|
581
|
+
/**
|
|
582
|
+
* Creates a WebSocket wrapper over an already-upgraded Duplex socket.
|
|
583
|
+
*
|
|
584
|
+
* @remarks
|
|
585
|
+
* `key` selects the mode: present runs server mode and writes the `101 Switching
|
|
586
|
+
* Protocols` handshake, omitted runs client mode and masks every outgoing frame.
|
|
587
|
+
* {@link NodeWebSocketOptions} describes every member.
|
|
588
|
+
*
|
|
589
|
+
* @param options - The {@link NodeWebSocketOptions} the wrapper is built from
|
|
590
|
+
* @throws A {@link WebSocketError} coded `OPTION` when `payload`, `timeout`, `key`, or `protocol` is refused, or when `protocol` is supplied without a server `key`, thrown before the wrapper writes to or assumes ownership of the `socket`
|
|
591
|
+
*/
|
|
376
592
|
constructor(options) {
|
|
377
593
|
const payload = options.payload ?? 104857600;
|
|
378
|
-
if (!Number.isSafeInteger(payload) || payload < 0) throw new
|
|
594
|
+
if (!Number.isSafeInteger(payload) || payload < 0) throw new WebSocketError("OPTION", "payload must be a non-negative safe integer", { payload });
|
|
379
595
|
const timeout = options.timeout ?? 3e4;
|
|
380
|
-
if (!Number.isSafeInteger(timeout) || timeout < 0) throw new
|
|
381
|
-
if (options.key !== void 0 && !isWebSocketKey(options.key)) throw new
|
|
382
|
-
if (options.protocol !== void 0 && !isWebSocketProtocol(options.protocol)) throw new
|
|
383
|
-
if (options.protocol !== void 0 && options.key === void 0) throw new
|
|
596
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0) throw new WebSocketError("OPTION", "timeout must be a non-negative safe integer", { timeout });
|
|
597
|
+
if (options.key !== void 0 && !isWebSocketKey(options.key)) throw new WebSocketError("OPTION", "key must be the canonical base64 encoding of 16 bytes", { key: options.key });
|
|
598
|
+
if (options.protocol !== void 0 && !isWebSocketProtocol(options.protocol)) throw new WebSocketError("OPTION", "protocol must be a valid WebSocket subprotocol token", { protocol: options.protocol });
|
|
599
|
+
if (options.protocol !== void 0 && options.key === void 0) throw new WebSocketError("OPTION", "protocol requires a server key", { protocol: options.protocol });
|
|
384
600
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
385
601
|
...options.on === void 0 ? {} : { on: options.on },
|
|
386
602
|
...options.error === void 0 ? {} : { error: options.error }
|
|
@@ -422,19 +638,27 @@ var NodeWebSocket = class {
|
|
|
422
638
|
get readyState() {
|
|
423
639
|
return this.#readyState;
|
|
424
640
|
}
|
|
425
|
-
send(
|
|
641
|
+
send(message) {
|
|
426
642
|
if (this.#readyState !== 1) return;
|
|
427
|
-
this.#write(1, Buffer.from(
|
|
643
|
+
this.#write(1, Buffer.from(message, "utf-8"));
|
|
428
644
|
}
|
|
429
|
-
ping(
|
|
645
|
+
ping(payload) {
|
|
430
646
|
if (this.#readyState !== 1) return;
|
|
431
|
-
|
|
432
|
-
|
|
647
|
+
const size = payload === void 0 ? 0 : Buffer.byteLength(payload, "utf-8");
|
|
648
|
+
if (size > 125) throw new WebSocketError("LIMIT", `ping payload exceeds 125 bytes`, {
|
|
649
|
+
size,
|
|
650
|
+
limit: 125
|
|
651
|
+
});
|
|
652
|
+
this.#write(9, payload === void 0 ? Buffer.alloc(0) : Buffer.from(payload, "utf-8"));
|
|
433
653
|
}
|
|
434
654
|
close(code, reason) {
|
|
435
655
|
if (this.#readyState === 2 || this.#readyState === 3) return;
|
|
436
|
-
if (code !== void 0 && !isCloseCode(code)) throw new
|
|
437
|
-
|
|
656
|
+
if (code !== void 0 && !isCloseCode(code)) throw new WebSocketError("CLOSE", "invalid close code", { code });
|
|
657
|
+
const size = reason === void 0 ? 0 : Buffer.byteLength(reason, "utf-8");
|
|
658
|
+
if (size > 123) throw new WebSocketError("LIMIT", `close reason exceeds 123 bytes`, {
|
|
659
|
+
size,
|
|
660
|
+
limit: 123
|
|
661
|
+
});
|
|
438
662
|
this.#readyState = 2;
|
|
439
663
|
this.#code = code ?? 1e3;
|
|
440
664
|
this.#reason = reason === void 0 || reason.length === 0 ? void 0 : reason;
|
|
@@ -456,13 +680,13 @@ var NodeWebSocket = class {
|
|
|
456
680
|
}
|
|
457
681
|
#drain() {
|
|
458
682
|
for (;;) {
|
|
459
|
-
if (
|
|
683
|
+
if (matchesWebSocketCanonical(this.#buffer) === false) {
|
|
460
684
|
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
461
685
|
return;
|
|
462
686
|
}
|
|
463
687
|
const declared = measureWebSocketFrame(this.#buffer);
|
|
464
688
|
if (declared !== void 0 && declared > this.#payload) {
|
|
465
|
-
this.#fail(
|
|
689
|
+
this.#fail(WEBSOCKET_CLOSE_TOO_BIG);
|
|
466
690
|
return;
|
|
467
691
|
}
|
|
468
692
|
const frame = parseWebSocketFrame(this.#buffer);
|
|
@@ -516,7 +740,7 @@ var NodeWebSocket = class {
|
|
|
516
740
|
this.#fragments.push(payload);
|
|
517
741
|
this.#fragmentBytes += payload.length;
|
|
518
742
|
if (this.#fragmentBytes > this.#payload) {
|
|
519
|
-
this.#fail(
|
|
743
|
+
this.#fail(WEBSOCKET_CLOSE_TOO_BIG);
|
|
520
744
|
return;
|
|
521
745
|
}
|
|
522
746
|
if (!fin) return;
|
|
@@ -645,33 +869,39 @@ var NodeWebSocket = class {
|
|
|
645
869
|
//#endregion
|
|
646
870
|
//#region src/server/factories.ts
|
|
647
871
|
/**
|
|
648
|
-
*
|
|
872
|
+
* Creates a server-native WebSocket over a raw upgraded `node:stream` Duplex socket —
|
|
873
|
+
* server mode when a `key` is given, client mode otherwise.
|
|
649
874
|
*
|
|
650
875
|
* @remarks
|
|
651
|
-
* The construction entry point for the {@link NodeWebSocketInterface}
|
|
652
|
-
* the
|
|
653
|
-
* mode
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
* later chunk) is built ON it. It is the WebSocket counterpart to
|
|
657
|
-
* `createSQLiteDatabase` / `createIndexedDBDatabase`.
|
|
876
|
+
* The construction entry point for the {@link NodeWebSocketInterface}. In server mode the
|
|
877
|
+
* wrapper writes the `101 Switching Protocols` handshake and sends unmasked frames; in
|
|
878
|
+
* client mode it writes no handshake and masks every outgoing frame. This is the
|
|
879
|
+
* lean-native handle: it speaks the WebSocket wire protocol and nothing above it, so a
|
|
880
|
+
* message transport is built on it rather than into it.
|
|
658
881
|
*
|
|
659
882
|
* @param options - The {@link NodeWebSocketOptions} (`socket`, optional `key` / `head` /
|
|
660
883
|
* `protocol` / `on`)
|
|
661
884
|
* @returns A typed {@link NodeWebSocketInterface}
|
|
885
|
+
* @throws A `WebSocketError` coded `OPTION` when `payload`, `timeout`, `key`, or `protocol` is refused, thrown before the wrapper writes to or assumes ownership of the `socket`
|
|
662
886
|
*
|
|
663
|
-
* @example
|
|
887
|
+
* @example Accept an upgrade and echo messages (server mode)
|
|
664
888
|
* ```ts
|
|
665
|
-
* import { createNodeWebSocket } from '@
|
|
889
|
+
* import { createNodeWebSocket } from '@orkestrel/websocket'
|
|
666
890
|
*
|
|
667
|
-
* // In a node:http 'upgrade' handler — server mode, identified by the client key:
|
|
668
891
|
* server.on('upgrade', (request, socket, head) => {
|
|
892
|
+
* const key = request.headers['sec-websocket-key']
|
|
893
|
+
* if (typeof key !== 'string') {
|
|
894
|
+
* socket.destroy()
|
|
895
|
+
* return
|
|
896
|
+
* }
|
|
669
897
|
* const ws = createNodeWebSocket({
|
|
670
898
|
* socket,
|
|
671
|
-
* key
|
|
672
|
-
* head,
|
|
673
|
-
* on: { message: (text) => ws.send(`echo: ${text}`) },
|
|
899
|
+
* key,
|
|
900
|
+
* head, // any bytes already buffered after the upgrade headers
|
|
901
|
+
* on: { message: (text) => ws.send(`echo: ${text}`) }, // wired before the first frame arrives
|
|
674
902
|
* })
|
|
903
|
+
* ws.emitter.on('message', (text) => log('echoed', text)) // a second observer of the same event
|
|
904
|
+
* ws.emitter.on('close', (code, reason) => log('closed', code, reason))
|
|
675
905
|
* })
|
|
676
906
|
* ```
|
|
677
907
|
*/
|
|
@@ -683,11 +913,11 @@ exports.NodeWebSocket = NodeWebSocket;
|
|
|
683
913
|
exports.WEBSOCKET_CLOSE_INVALID = WEBSOCKET_CLOSE_INVALID;
|
|
684
914
|
exports.WEBSOCKET_CLOSE_NORMAL = WEBSOCKET_CLOSE_NORMAL;
|
|
685
915
|
exports.WEBSOCKET_CLOSE_PROTOCOL = WEBSOCKET_CLOSE_PROTOCOL;
|
|
686
|
-
exports.
|
|
916
|
+
exports.WEBSOCKET_CLOSE_REASON_MAX_LENGTH = WEBSOCKET_CLOSE_REASON_MAX_LENGTH;
|
|
687
917
|
exports.WEBSOCKET_CLOSE_TIMEOUT_MS = WEBSOCKET_CLOSE_TIMEOUT_MS;
|
|
688
|
-
exports.
|
|
918
|
+
exports.WEBSOCKET_CLOSE_TOO_BIG = WEBSOCKET_CLOSE_TOO_BIG;
|
|
689
919
|
exports.WEBSOCKET_CLOSE_UNSUPPORTED = WEBSOCKET_CLOSE_UNSUPPORTED;
|
|
690
|
-
exports.
|
|
920
|
+
exports.WEBSOCKET_CONTROL_MAX_LENGTH = WEBSOCKET_CONTROL_MAX_LENGTH;
|
|
691
921
|
exports.WEBSOCKET_FAIL_TIMEOUT_MS = WEBSOCKET_FAIL_TIMEOUT_MS;
|
|
692
922
|
exports.WEBSOCKET_GUID = WEBSOCKET_GUID;
|
|
693
923
|
exports.WEBSOCKET_MAX_PAYLOAD = WEBSOCKET_MAX_PAYLOAD;
|
|
@@ -702,13 +932,15 @@ exports.WEBSOCKET_READY_CLOSING = WEBSOCKET_READY_CLOSING;
|
|
|
702
932
|
exports.WEBSOCKET_READY_CONNECTING = WEBSOCKET_READY_CONNECTING;
|
|
703
933
|
exports.WEBSOCKET_READY_OPEN = WEBSOCKET_READY_OPEN;
|
|
704
934
|
exports.WEBSOCKET_VERSION = WEBSOCKET_VERSION;
|
|
935
|
+
exports.WebSocketError = WebSocketError;
|
|
705
936
|
exports.computeWebSocketAccept = computeWebSocketAccept;
|
|
706
937
|
exports.createNodeWebSocket = createNodeWebSocket;
|
|
707
938
|
exports.encodeWebSocketFrame = encodeWebSocketFrame;
|
|
708
939
|
exports.isCloseCode = isCloseCode;
|
|
709
|
-
exports.
|
|
940
|
+
exports.isWebSocketError = isWebSocketError;
|
|
710
941
|
exports.isWebSocketKey = isWebSocketKey;
|
|
711
942
|
exports.isWebSocketProtocol = isWebSocketProtocol;
|
|
943
|
+
exports.matchesWebSocketCanonical = matchesWebSocketCanonical;
|
|
712
944
|
exports.measureWebSocketFrame = measureWebSocketFrame;
|
|
713
945
|
exports.parseUTF8 = parseUTF8;
|
|
714
946
|
exports.parseWebSocketFrame = parseWebSocketFrame;
|