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