@rynx-ai/server 0.1.0 → 0.1.9
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/dist/browser-surface-control-queue.d.ts +42 -0
- package/dist/browser-surface-control-queue.js +187 -0
- package/dist/browser-surface-ws.d.ts +14 -0
- package/dist/browser-surface-ws.js +355 -0
- package/dist/control-api.d.ts +59 -4
- package/dist/control-api.js +2213 -132
- package/dist/control-web/assets/highlighted-body-OFNGDK62-e-w61YLy.js +1 -0
- package/dist/control-web/assets/index-BQ7XVBKO.css +32 -0
- package/dist/control-web/assets/index-ClpzuBJx.js +606 -0
- package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/control-web/assets/mermaid-GHXKKRXX-DZn7Hbr2.js +131 -0
- package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
- package/dist/control-web/favicon.png +0 -0
- package/dist/control-web/favicon.svg +38 -0
- package/dist/control-web/index.html +16 -0
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +19 -14
- package/dist/desktop-browser-host.d.ts +153 -0
- package/dist/desktop-browser-host.js +936 -0
- package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
- package/dist/direct-runtime-browser-surface-server.js +821 -0
- package/dist/direct-runtime-server.d.ts +137 -0
- package/dist/direct-runtime-server.js +1931 -0
- package/dist/emulator-surface-ws.d.ts +6 -0
- package/dist/emulator-surface-ws.js +197 -0
- package/dist/machine-session-service.d.ts +182 -0
- package/dist/machine-session-service.js +559 -0
- package/dist/provider-cli-host.d.ts +10 -0
- package/dist/provider-cli-host.js +1 -0
- package/dist/remote-runtime-dispatcher.d.ts +94 -0
- package/dist/remote-runtime-dispatcher.js +380 -0
- package/dist/remote-runtime-session-projection.d.ts +19 -0
- package/dist/remote-runtime-session-projection.js +556 -0
- package/dist/remote-runtime.d.ts +22 -0
- package/dist/remote-runtime.js +32 -0
- package/dist/runtime-web-auth.d.ts +21 -0
- package/dist/runtime-web-auth.js +92 -0
- package/dist/server.d.ts +292 -10
- package/dist/server.js +314 -98
- package/dist/session-browser-service.d.ts +218 -0
- package/dist/session-browser-service.js +686 -0
- package/dist/session-browser-surface-coordinator.d.ts +23 -0
- package/dist/session-browser-surface-coordinator.js +563 -0
- package/dist/session-emulator-service.d.ts +167 -0
- package/dist/session-emulator-service.js +464 -0
- package/dist/session-runtime-index.d.ts +17 -0
- package/dist/session-runtime-index.js +86 -0
- package/dist/session-terminal-host.d.ts +46 -0
- package/dist/session-terminal-host.js +252 -0
- package/dist/terminal-ws.d.ts +9 -20
- package/dist/terminal-ws.js +420 -97
- package/package.json +9 -7
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { decodeDirectRuntimeFrame, DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL, DIRECT_RUNTIME_BROWSER_SURFACE_PATH, DIRECT_RUNTIME_CLOSE_CODE, DIRECT_RUNTIME_MAX_HANDSHAKE_FRAME_BYTES, encodeDirectRuntimeFrame, parseServerAccepted, parseServerHello, } from "@rynx-ai/protocol/direct-runtime";
|
|
2
|
+
import { encodeRuntimeBrowserSurfaceImageFrame, parseRuntimeBrowserSurfaceClientFrame, parseRuntimeBrowserSurfaceServerFrame, RUNTIME_BROWSER_SURFACE_HEARTBEAT_INTERVAL_MS, RUNTIME_BROWSER_SURFACE_MAX_CONTROL_FRAME_BYTES, RUNTIME_BROWSER_SURFACE_MAX_RELIABLE_INPUT_QUEUE, RUNTIME_BROWSER_SURFACE_PEER_LIVENESS_TIMEOUT_MS, } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
3
|
+
import { createServerE2EEChannel, createServerSurfaceE2EEChannel, DIRECT_RUNTIME_E2EE_MAX_ENVELOPE_BYTES, DIRECT_RUNTIME_SURFACE_E2EE_MAX_BINARY_ENVELOPE_BYTES, DIRECT_RUNTIME_SURFACE_E2EE_MAX_TEXT_ENVELOPE_BYTES, inspectDirectRuntimeSurfaceTextRecordContext, } from "@rynx-ai/remote-runtime-e2ee";
|
|
4
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
5
|
+
import { BrowserSurfaceControlQueue, BrowserSurfaceControlQueueCapacityError, } from "./browser-surface-control-queue.js";
|
|
6
|
+
const CLOSE_BAD_PROTOCOL = DIRECT_RUNTIME_CLOSE_CODE.badProtocol;
|
|
7
|
+
const CLOSE_UNAUTHORIZED = DIRECT_RUNTIME_CLOSE_CODE.unauthorized;
|
|
8
|
+
const CLOSE_AUTH_TIMEOUT = DIRECT_RUNTIME_CLOSE_CODE.authenticationTimeout;
|
|
9
|
+
const CLOSE_CAPACITY = DIRECT_RUNTIME_CLOSE_CODE.capacity;
|
|
10
|
+
const CLOSE_INTERNAL = DIRECT_RUNTIME_CLOSE_CODE.internal;
|
|
11
|
+
const DEFAULT_MAX_CONNECTIONS = 64;
|
|
12
|
+
const DEFAULT_MAX_UNAUTHENTICATED_CONNECTIONS_PER_SOURCE = 4;
|
|
13
|
+
const DEFAULT_MAX_AUTHENTICATION_ATTEMPTS_PER_SOURCE = 20;
|
|
14
|
+
const DEFAULT_AUTHENTICATION_ATTEMPT_WINDOW_MS = 60_000;
|
|
15
|
+
const DEFAULT_MAX_TRACKED_AUTHENTICATION_SOURCES = 1_024;
|
|
16
|
+
const DEFAULT_MAX_CONNECTIONS_PER_GRANT = 8;
|
|
17
|
+
const DEFAULT_MAX_BUFFERED_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const DEFAULT_SHUTDOWN_DEADLINE_MS = 5_000;
|
|
19
|
+
const CONNECTION_CLOSE_DEADLINE_MS = 1_000;
|
|
20
|
+
export class DirectRuntimeBrowserSurfaceHostError extends Error {
|
|
21
|
+
reason;
|
|
22
|
+
constructor(reason, message, options) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
this.reason = reason;
|
|
25
|
+
this.name = "DirectRuntimeBrowserSurfaceHostError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Attach only the V3 Browser Surface route to an existing Direct HTTP server. */
|
|
29
|
+
export function attachDirectRuntimeBrowserSurfaceServer(server, options) {
|
|
30
|
+
const limits = resolveLimits(options.limits);
|
|
31
|
+
const wss = new WebSocketServer({
|
|
32
|
+
noServer: true,
|
|
33
|
+
clientTracking: true,
|
|
34
|
+
perMessageDeflate: false,
|
|
35
|
+
maxPayload: Math.max(DIRECT_RUNTIME_SURFACE_E2EE_MAX_BINARY_ENVELOPE_BYTES, DIRECT_RUNTIME_E2EE_MAX_ENVELOPE_BYTES),
|
|
36
|
+
});
|
|
37
|
+
const connections = new Set();
|
|
38
|
+
const pendingUpgradesBySource = new Map();
|
|
39
|
+
const unauthenticatedConnectionsBySource = new Map();
|
|
40
|
+
const authenticationAttempts = new SourceAuthenticationAttemptLimiter({
|
|
41
|
+
maxAttempts: limits.maxAuthenticationAttemptsPerSource,
|
|
42
|
+
windowMs: limits.authenticationAttemptWindowMs,
|
|
43
|
+
maxSources: limits.maxTrackedAuthenticationSources,
|
|
44
|
+
});
|
|
45
|
+
let pendingUpgrades = 0;
|
|
46
|
+
let stopping = false;
|
|
47
|
+
let heartbeatTimer;
|
|
48
|
+
let shutdownPromise;
|
|
49
|
+
const handleUpgrade = (request, socket, head) => {
|
|
50
|
+
if (request.url !== DIRECT_RUNTIME_BROWSER_SURFACE_PATH)
|
|
51
|
+
return;
|
|
52
|
+
const sourceAddress = socketSourceAddress(request);
|
|
53
|
+
if (stopping) {
|
|
54
|
+
rejectUpgrade(socket, 503, "Browser Surface is stopping");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (connections.size + pendingUpgrades >= limits.maxConnections) {
|
|
58
|
+
rejectUpgrade(socket, 503, "Browser Surface connection capacity reached");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if ((unauthenticatedConnectionsBySource.get(sourceAddress) ?? 0) +
|
|
62
|
+
(pendingUpgradesBySource.get(sourceAddress) ?? 0) >=
|
|
63
|
+
limits.maxUnauthenticatedConnectionsPerSource) {
|
|
64
|
+
rejectUpgrade(socket, 503, "Browser Surface authentication capacity reached");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
pendingUpgrades += 1;
|
|
68
|
+
incrementCount(pendingUpgradesBySource, sourceAddress);
|
|
69
|
+
let pending = true;
|
|
70
|
+
const releasePending = () => {
|
|
71
|
+
if (!pending)
|
|
72
|
+
return;
|
|
73
|
+
pending = false;
|
|
74
|
+
pendingUpgrades -= 1;
|
|
75
|
+
decrementCount(pendingUpgradesBySource, sourceAddress);
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
79
|
+
releasePending();
|
|
80
|
+
connections.add(acceptConnection(ws, sourceAddress));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
releasePending();
|
|
85
|
+
options.onError?.(error, "Browser Surface websocket upgrade");
|
|
86
|
+
rejectUpgrade(socket, 400, "invalid Browser Surface WebSocket upgrade");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
server.on("upgrade", handleUpgrade);
|
|
90
|
+
function acceptConnection(ws, sourceAddress) {
|
|
91
|
+
const abortController = new AbortController();
|
|
92
|
+
let connection;
|
|
93
|
+
const authenticationTimer = setTimeout(() => {
|
|
94
|
+
abortController.abort(new Error("Browser Surface authentication timed out"));
|
|
95
|
+
connection.close(CLOSE_AUTH_TIMEOUT, "Browser Surface authentication timed out");
|
|
96
|
+
}, limits.authenticationTimeoutMs);
|
|
97
|
+
authenticationTimer.unref?.();
|
|
98
|
+
connection = {
|
|
99
|
+
ws,
|
|
100
|
+
sourceAddress,
|
|
101
|
+
countedAsUnauthenticated: true,
|
|
102
|
+
state: "awaiting_e2ee_hello",
|
|
103
|
+
attachmentClosed: false,
|
|
104
|
+
abortController,
|
|
105
|
+
authenticationTimer,
|
|
106
|
+
receiveTail: Promise.resolve(),
|
|
107
|
+
queuedFrames: 0,
|
|
108
|
+
authenticationFrameQueued: false,
|
|
109
|
+
lastInboundActivityAt: Date.now(),
|
|
110
|
+
close(code, reason) {
|
|
111
|
+
closeConnection(connection, code, reason);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
incrementCount(unauthenticatedConnectionsBySource, sourceAddress);
|
|
115
|
+
ws.on("message", (data, isBinary) => {
|
|
116
|
+
connection.lastInboundActivityAt = Date.now();
|
|
117
|
+
if (isClosed(connection))
|
|
118
|
+
return;
|
|
119
|
+
if (connection.state !== "active" && connection.authenticationFrameQueued) {
|
|
120
|
+
connection.close(CLOSE_BAD_PROTOCOL, "Browser Surface traffic must remain ordered");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (connection.state !== "active")
|
|
124
|
+
connection.authenticationFrameQueued = true;
|
|
125
|
+
connection.queuedFrames += 1;
|
|
126
|
+
if (connection.queuedFrames > limits.maxQueuedFrames) {
|
|
127
|
+
connection.queuedFrames -= 1;
|
|
128
|
+
connection.close(CLOSE_CAPACITY, "Browser Surface input queue is full");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
connection.receiveTail = connection.receiveTail
|
|
132
|
+
.then(() => handleFrame(connection, data, isBinary))
|
|
133
|
+
.catch((error) => {
|
|
134
|
+
if (isClosed(connection))
|
|
135
|
+
return;
|
|
136
|
+
options.onError?.(error, "Browser Surface frame");
|
|
137
|
+
connection.close(CLOSE_INTERNAL, "Browser Surface frame handling failed");
|
|
138
|
+
})
|
|
139
|
+
.finally(() => {
|
|
140
|
+
connection.queuedFrames -= 1;
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
ws.on("ping", () => {
|
|
144
|
+
connection.lastInboundActivityAt = Date.now();
|
|
145
|
+
});
|
|
146
|
+
ws.on("pong", () => {
|
|
147
|
+
connection.lastInboundActivityAt = Date.now();
|
|
148
|
+
});
|
|
149
|
+
ws.on("close", () => void cleanupConnection(connection, "client_closed"));
|
|
150
|
+
ws.on("error", (error) => {
|
|
151
|
+
options.onError?.(error, "Browser Surface socket");
|
|
152
|
+
void cleanupConnection(connection, "transport_error");
|
|
153
|
+
});
|
|
154
|
+
return connection;
|
|
155
|
+
}
|
|
156
|
+
async function handleFrame(connection, data, isBinary) {
|
|
157
|
+
if (isClosed(connection))
|
|
158
|
+
return;
|
|
159
|
+
if (isBinary) {
|
|
160
|
+
connection.close(CLOSE_BAD_PROTOCOL, "Browser Surface client records must be text");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const wireBytes = rawDataBytes(data);
|
|
164
|
+
if (connection.state === "awaiting_e2ee_hello") {
|
|
165
|
+
await establishE2EE(connection, wireBytes);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (wireBytes.byteLength > DIRECT_RUNTIME_SURFACE_E2EE_MAX_TEXT_ENVELOPE_BYTES) {
|
|
169
|
+
connection.close(CLOSE_BAD_PROTOCOL, "Browser Surface text record is too large");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (connection.state === "awaiting_authentication") {
|
|
173
|
+
await authenticateConnection(connection, wireBytes);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (connection.state === "authenticating") {
|
|
177
|
+
connection.close(CLOSE_BAD_PROTOCOL, "Browser Surface authentication is already in progress");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await handleSurfaceControl(connection, decodeText(wireBytes));
|
|
181
|
+
}
|
|
182
|
+
async function establishE2EE(connection, bytes) {
|
|
183
|
+
if (bytes.byteLength > DIRECT_RUNTIME_MAX_HANDSHAKE_FRAME_BYTES) {
|
|
184
|
+
connection.close(CLOSE_BAD_PROTOCOL, "Browser Surface E2EE hello is too large");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const frame = decodeDirectRuntimeFrame(bytes);
|
|
189
|
+
if (frame.type !== "e2ee.client-hello") {
|
|
190
|
+
throw new Error("first Browser Surface frame must be e2ee.client-hello");
|
|
191
|
+
}
|
|
192
|
+
const sharedSecret = options.e2ee.deriveSharedSecret(frame.clientEphemeralPublicKey);
|
|
193
|
+
const input = {
|
|
194
|
+
sharedSecret,
|
|
195
|
+
serverEncryptionPublicKey: options.e2ee.serverPublicKey,
|
|
196
|
+
clientHello: frame,
|
|
197
|
+
};
|
|
198
|
+
connection.surfaceChannelInput = input;
|
|
199
|
+
connection.authenticationChannel = createServerE2EEChannel(input);
|
|
200
|
+
await sendWireText(connection, encodeDirectRuntimeFrame({ type: "e2ee.ready" }));
|
|
201
|
+
await initializeServerHello(connection);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
connection.close(CLOSE_BAD_PROTOCOL, "invalid Browser Surface E2EE client hello");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function initializeServerHello(connection) {
|
|
208
|
+
try {
|
|
209
|
+
const channel = connection.authenticationChannel;
|
|
210
|
+
if (!channel)
|
|
211
|
+
throw new Error("Browser Surface authentication channel is unavailable");
|
|
212
|
+
const hello = parseServerHello(await options.authenticator.createServerHello({
|
|
213
|
+
channelBinding: channel.channelBinding,
|
|
214
|
+
role: "browser-surface",
|
|
215
|
+
signal: connection.abortController.signal,
|
|
216
|
+
}));
|
|
217
|
+
if (hello.role !== "browser-surface") {
|
|
218
|
+
throw new Error("Browser Surface authenticator returned the wrong role");
|
|
219
|
+
}
|
|
220
|
+
if (hello.bindingProtocolVersion !==
|
|
221
|
+
DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL.currentVersion ||
|
|
222
|
+
hello.minimumCompatibleBindingVersion !==
|
|
223
|
+
DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL.minimumCompatibleVersion) {
|
|
224
|
+
throw new Error("Browser Surface authenticator returned an incompatible binding");
|
|
225
|
+
}
|
|
226
|
+
const encoded = encodeDirectRuntimeFrame(hello);
|
|
227
|
+
connection.serverHello = hello;
|
|
228
|
+
connection.serverHelloBytes = Uint8Array.from(Buffer.from(encoded, "utf8"));
|
|
229
|
+
connection.authenticationFrameQueued = false;
|
|
230
|
+
connection.state = "awaiting_authentication";
|
|
231
|
+
await sendAuthenticationText(connection, encoded);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (isClosed(connection))
|
|
235
|
+
return;
|
|
236
|
+
options.onError?.(error, "Browser Surface server hello");
|
|
237
|
+
connection.close(CLOSE_INTERNAL, "Browser Surface authentication setup failed");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function authenticateConnection(connection, wireBytes) {
|
|
241
|
+
if (!authenticationAttempts.tryConsume(connection.sourceAddress)) {
|
|
242
|
+
connection.close(CLOSE_CAPACITY, "Browser Surface authentication limit reached");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const channel = connection.authenticationChannel;
|
|
246
|
+
const serverHello = connection.serverHello;
|
|
247
|
+
const serverHelloBytes = connection.serverHelloBytes;
|
|
248
|
+
if (!channel || !serverHello || !serverHelloBytes) {
|
|
249
|
+
connection.close(CLOSE_INTERNAL, "Browser Surface server hello is unavailable");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
let plaintext;
|
|
253
|
+
let authenticate;
|
|
254
|
+
try {
|
|
255
|
+
plaintext = channel.open(decodeText(wireBytes));
|
|
256
|
+
const bytes = Uint8Array.from(Buffer.from(plaintext, "utf8"));
|
|
257
|
+
if (bytes.byteLength > DIRECT_RUNTIME_MAX_HANDSHAKE_FRAME_BYTES) {
|
|
258
|
+
throw new Error("Browser Surface authentication frame is too large");
|
|
259
|
+
}
|
|
260
|
+
const frame = decodeDirectRuntimeFrame(bytes);
|
|
261
|
+
if (frame.type !== "client.authenticate" || frame.role !== "browser-surface") {
|
|
262
|
+
throw new Error("Browser Surface client role is invalid");
|
|
263
|
+
}
|
|
264
|
+
authenticate = frame;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
connection.close(CLOSE_BAD_PROTOCOL, "invalid Browser Surface authentication frame");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
connection.state = "authenticating";
|
|
271
|
+
try {
|
|
272
|
+
const authenticationBytes = Uint8Array.from(Buffer.from(plaintext, "utf8"));
|
|
273
|
+
const result = await options.authenticator.authenticate({
|
|
274
|
+
channelBinding: channel.channelBinding,
|
|
275
|
+
role: "browser-surface",
|
|
276
|
+
serverHello,
|
|
277
|
+
serverHelloBytes: new Uint8Array(serverHelloBytes),
|
|
278
|
+
clientAuthenticate: authenticate,
|
|
279
|
+
clientAuthenticateBytes: authenticationBytes,
|
|
280
|
+
signal: connection.abortController.signal,
|
|
281
|
+
});
|
|
282
|
+
if (connection.state !== "authenticating")
|
|
283
|
+
return;
|
|
284
|
+
const accepted = parseServerAccepted(result.accepted);
|
|
285
|
+
assertAuthenticationResult(serverHello, authenticate, result.peer, accepted);
|
|
286
|
+
connection.peer = result.peer;
|
|
287
|
+
const registration = options.connectionHub.registerWithinLimit(result.peer.grantId, connection, limits.maxConnectionsPerGrant);
|
|
288
|
+
if (registration === "revoked") {
|
|
289
|
+
connection.close(CLOSE_UNAUTHORIZED, "Remote Runtime grant is not active");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (registration === "capacity") {
|
|
293
|
+
connection.close(CLOSE_CAPACITY, "Remote Runtime grant connection capacity reached");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const acceptedSend = sendAuthenticationText(connection, encodeDirectRuntimeFrame(accepted));
|
|
297
|
+
const surfaceInput = connection.surfaceChannelInput;
|
|
298
|
+
if (!surfaceInput)
|
|
299
|
+
throw new Error("Browser Surface key exchange is unavailable");
|
|
300
|
+
connection.accepted = accepted;
|
|
301
|
+
connection.surfaceChannel = createServerSurfaceE2EEChannel(surfaceInput);
|
|
302
|
+
connection.surfaceChannelInput = undefined;
|
|
303
|
+
connection.authenticationChannel = undefined;
|
|
304
|
+
connection.state = "awaiting_open";
|
|
305
|
+
connection.authenticationFrameQueued = false;
|
|
306
|
+
releaseUnauthenticated(connection);
|
|
307
|
+
clearTimeout(connection.authenticationTimer);
|
|
308
|
+
await acceptedSend;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (isClosed(connection))
|
|
312
|
+
return;
|
|
313
|
+
options.onError?.(error, "Browser Surface client authentication");
|
|
314
|
+
connection.close(CLOSE_UNAUTHORIZED, "Browser Surface authentication failed");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function handleSurfaceControl(connection, encoded) {
|
|
318
|
+
const channel = connection.surfaceChannel;
|
|
319
|
+
const accepted = connection.accepted;
|
|
320
|
+
const peer = connection.peer;
|
|
321
|
+
if (!channel || !accepted || !peer) {
|
|
322
|
+
connection.close(CLOSE_BAD_PROTOCOL, "Browser Surface channel is unavailable");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let context = connection.recordContext;
|
|
326
|
+
try {
|
|
327
|
+
if (!context) {
|
|
328
|
+
context = inspectDirectRuntimeSurfaceTextRecordContext(encoded, "client-to-server");
|
|
329
|
+
if (context.channelId !== accepted.channelId) {
|
|
330
|
+
throw new Error("Browser Surface channel id does not match authentication");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const plaintext = channel.openText(encoded, context);
|
|
334
|
+
if (Buffer.byteLength(plaintext, "utf8") > RUNTIME_BROWSER_SURFACE_MAX_CONTROL_FRAME_BYTES) {
|
|
335
|
+
throw new Error("Browser Surface control frame is too large");
|
|
336
|
+
}
|
|
337
|
+
const frame = parseRuntimeBrowserSurfaceClientFrame(JSON.parse(plaintext));
|
|
338
|
+
if (frame.subscriptionId !== context.subscriptionId) {
|
|
339
|
+
throw new Error("Browser Surface subscription id does not match its record context");
|
|
340
|
+
}
|
|
341
|
+
if (!connection.recordContext) {
|
|
342
|
+
if (frame.type !== "browser.surface.open") {
|
|
343
|
+
throw new Error("first Browser Surface control frame must be open");
|
|
344
|
+
}
|
|
345
|
+
connection.recordContext = context;
|
|
346
|
+
await openSurface(connection, frame);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (frame.type === "browser.surface.open") {
|
|
350
|
+
throw new Error("Browser Surface admits one subscription per connection");
|
|
351
|
+
}
|
|
352
|
+
if (connection.state !== "active" || !connection.attachment) {
|
|
353
|
+
throw new Error("Browser Surface is not active");
|
|
354
|
+
}
|
|
355
|
+
if (frame.type !== "browser.surface.frame.ack" &&
|
|
356
|
+
frame.browserGeneration !== connection.attachment.ready.browserGeneration) {
|
|
357
|
+
await sendSurfaceClosed(connection, "generation_changed", "Browser generation changed");
|
|
358
|
+
connection.close(1000, "Browser generation changed");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (connection.attachment.ready.grantedRole === "view" &&
|
|
362
|
+
(frame.type === "browser.surface.driver.take" || frame.type === "browser.surface.input")) {
|
|
363
|
+
await sendSurfaceClosed(connection, "unauthorized", "Browser Surface is view-only");
|
|
364
|
+
connection.close(CLOSE_UNAUTHORIZED, "Browser Surface is view-only");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
let active;
|
|
368
|
+
try {
|
|
369
|
+
active = await options.isPeerActive(peer);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
options.onError?.(error, "Browser Surface peer activity check");
|
|
373
|
+
await sendSurfaceClosed(connection, "internal_error", "Could not verify Runtime grant")
|
|
374
|
+
.catch(() => undefined);
|
|
375
|
+
connection.close(CLOSE_INTERNAL, "Browser Surface grant check failed");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!active) {
|
|
379
|
+
await sendSurfaceClosed(connection, "unauthorized", "Remote Runtime grant is not active");
|
|
380
|
+
connection.close(CLOSE_UNAUTHORIZED, "Remote Runtime grant is not active");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const controlQueue = connection.controlQueue;
|
|
384
|
+
if (!controlQueue)
|
|
385
|
+
throw new Error("Browser Surface control queue is unavailable");
|
|
386
|
+
try {
|
|
387
|
+
controlQueue.enqueue(frame);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
await failSurfaceControl(connection, error);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (isClosed(connection))
|
|
395
|
+
return;
|
|
396
|
+
options.onError?.(error, "Browser Surface client control");
|
|
397
|
+
if (connection.recordContext) {
|
|
398
|
+
await sendSurfaceClosed(connection, "protocol_error", "invalid Browser Surface control frame")
|
|
399
|
+
.catch(() => undefined);
|
|
400
|
+
}
|
|
401
|
+
connection.close(CLOSE_BAD_PROTOCOL, "invalid Browser Surface control frame");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function openSurface(connection, frame) {
|
|
405
|
+
const peer = connection.peer;
|
|
406
|
+
if (!peer)
|
|
407
|
+
throw new Error("Browser Surface peer is unavailable");
|
|
408
|
+
connection.state = "opening";
|
|
409
|
+
try {
|
|
410
|
+
if (!(await options.isPeerActive(peer))) {
|
|
411
|
+
throw new DirectRuntimeBrowserSurfaceHostError("unauthorized", "Remote Runtime grant is not active");
|
|
412
|
+
}
|
|
413
|
+
const attachment = await options.host.open(frame, {
|
|
414
|
+
peer,
|
|
415
|
+
signal: connection.abortController.signal,
|
|
416
|
+
});
|
|
417
|
+
if (isClosed(connection)) {
|
|
418
|
+
await attachment.close("client_closed");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const ready = parseRuntimeBrowserSurfaceServerFrame(attachment.ready);
|
|
422
|
+
if (ready.type !== "browser.surface.ready" ||
|
|
423
|
+
ready.subscriptionId !== frame.subscriptionId ||
|
|
424
|
+
ready.browserGeneration !== frame.browserGeneration ||
|
|
425
|
+
ready.format !== frame.format ||
|
|
426
|
+
(frame.requestedRole === "view" && ready.grantedRole !== "view")) {
|
|
427
|
+
await attachment.close("transport_error");
|
|
428
|
+
throw new Error("Browser Surface host returned an inconsistent ready frame");
|
|
429
|
+
}
|
|
430
|
+
connection.attachment = attachment;
|
|
431
|
+
connection.controlQueue = new BrowserSurfaceControlQueue({
|
|
432
|
+
maxPendingFrames: limits.maxQueuedFrames,
|
|
433
|
+
dispatch: (control) => attachment.handle(control),
|
|
434
|
+
onError: (error) => void failSurfaceControl(connection, error),
|
|
435
|
+
});
|
|
436
|
+
connection.state = "active";
|
|
437
|
+
await sendSurfaceText(connection, ready);
|
|
438
|
+
void pumpAttachmentEvents(connection, attachment);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (isClosed(connection))
|
|
442
|
+
return;
|
|
443
|
+
const reason = error instanceof DirectRuntimeBrowserSurfaceHostError
|
|
444
|
+
? error.reason
|
|
445
|
+
: "internal_error";
|
|
446
|
+
const message = error instanceof Error ? error.message : "Browser Surface could not open";
|
|
447
|
+
await sendSurfaceClosed(connection, reason, message).catch(() => undefined);
|
|
448
|
+
connection.close(reason === "unauthorized" ? CLOSE_UNAUTHORIZED : CLOSE_INTERNAL, message);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async function pumpAttachmentEvents(connection, attachment) {
|
|
452
|
+
try {
|
|
453
|
+
for await (const event of attachment.events) {
|
|
454
|
+
if (isClosed(connection) || connection.attachment !== attachment)
|
|
455
|
+
return;
|
|
456
|
+
if (isImageFrame(event)) {
|
|
457
|
+
if (event.subscriptionId !== connection.recordContext?.subscriptionId) {
|
|
458
|
+
throw new Error("Browser Surface host emitted an image for another subscription");
|
|
459
|
+
}
|
|
460
|
+
await sendSurfaceImage(connection, event);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const frame = parseRuntimeBrowserSurfaceServerFrame(event);
|
|
464
|
+
if (frame.type === "browser.surface.ready" ||
|
|
465
|
+
frame.subscriptionId !== connection.recordContext?.subscriptionId) {
|
|
466
|
+
throw new Error("Browser Surface host emitted an inconsistent control frame");
|
|
467
|
+
}
|
|
468
|
+
await sendSurfaceText(connection, frame);
|
|
469
|
+
if (frame.type === "browser.surface.closed") {
|
|
470
|
+
connection.close(1000, "Browser Surface closed");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (!isClosed(connection)) {
|
|
475
|
+
await sendSurfaceClosed(connection, "internal_error", "Browser Surface event stream ended unexpectedly").catch(() => undefined);
|
|
476
|
+
connection.close(CLOSE_INTERNAL, "Browser Surface event stream ended");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
if (isClosed(connection))
|
|
481
|
+
return;
|
|
482
|
+
options.onError?.(error, "Browser Surface host events");
|
|
483
|
+
await sendSurfaceClosed(connection, "internal_error", "Browser Surface event stream failed")
|
|
484
|
+
.catch(() => undefined);
|
|
485
|
+
connection.close(CLOSE_INTERNAL, "Browser Surface event stream failed");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async function sendSurfaceClosed(connection, reason, message) {
|
|
489
|
+
const subscriptionId = connection.recordContext?.subscriptionId;
|
|
490
|
+
if (!subscriptionId)
|
|
491
|
+
return;
|
|
492
|
+
await sendSurfaceText(connection, {
|
|
493
|
+
type: "browser.surface.closed",
|
|
494
|
+
subscriptionId,
|
|
495
|
+
reason,
|
|
496
|
+
...(message === undefined ? {} : { message: message.slice(0, 1_024) }),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function failSurfaceControl(connection, error) {
|
|
500
|
+
if (isClosed(connection))
|
|
501
|
+
return;
|
|
502
|
+
const reason = error instanceof DirectRuntimeBrowserSurfaceHostError
|
|
503
|
+
? error.reason
|
|
504
|
+
: error instanceof BrowserSurfaceControlQueueCapacityError
|
|
505
|
+
? "capacity"
|
|
506
|
+
: "internal_error";
|
|
507
|
+
const message = error instanceof Error ? error.message : "Browser Surface input failed";
|
|
508
|
+
options.onError?.(error, "Browser Surface host input");
|
|
509
|
+
await sendSurfaceClosed(connection, reason, message).catch(() => undefined);
|
|
510
|
+
connection.close(reason === "unauthorized"
|
|
511
|
+
? CLOSE_UNAUTHORIZED
|
|
512
|
+
: reason === "capacity"
|
|
513
|
+
? CLOSE_CAPACITY
|
|
514
|
+
: CLOSE_INTERNAL, message);
|
|
515
|
+
}
|
|
516
|
+
async function sendAuthenticationText(connection, plaintext) {
|
|
517
|
+
const channel = connection.authenticationChannel;
|
|
518
|
+
if (!channel)
|
|
519
|
+
throw new Error("Browser Surface authentication channel is unavailable");
|
|
520
|
+
await sendWireText(connection, channel.seal(plaintext));
|
|
521
|
+
}
|
|
522
|
+
async function sendSurfaceText(connection, untrustedFrame) {
|
|
523
|
+
const channel = connection.surfaceChannel;
|
|
524
|
+
const context = connection.recordContext;
|
|
525
|
+
if (!channel || !context)
|
|
526
|
+
throw new Error("Browser Surface record channel is unavailable");
|
|
527
|
+
const frame = parseRuntimeBrowserSurfaceServerFrame(untrustedFrame);
|
|
528
|
+
const encrypted = channel.sealText(JSON.stringify(frame), context);
|
|
529
|
+
await sendWireText(connection, encrypted);
|
|
530
|
+
}
|
|
531
|
+
async function sendSurfaceImage(connection, frame) {
|
|
532
|
+
const channel = connection.surfaceChannel;
|
|
533
|
+
const context = connection.recordContext;
|
|
534
|
+
if (!channel || !context)
|
|
535
|
+
throw new Error("Browser Surface record channel is unavailable");
|
|
536
|
+
const encrypted = channel.sealBytes(encodeRuntimeBrowserSurfaceImageFrame(frame), context);
|
|
537
|
+
await sendWireBytes(connection, encrypted);
|
|
538
|
+
}
|
|
539
|
+
async function sendWireText(connection, frame) {
|
|
540
|
+
await sendWire(connection, frame, false, Buffer.byteLength(frame, "utf8"));
|
|
541
|
+
}
|
|
542
|
+
async function sendWireBytes(connection, frame) {
|
|
543
|
+
await sendWire(connection, frame, true, frame.byteLength);
|
|
544
|
+
}
|
|
545
|
+
async function sendWire(connection, frame, binary, byteLength) {
|
|
546
|
+
if (connection.ws.readyState !== WebSocket.OPEN) {
|
|
547
|
+
throw new Error("Browser Surface WebSocket is not open");
|
|
548
|
+
}
|
|
549
|
+
if (byteLength > limits.maxBufferedBytes ||
|
|
550
|
+
connection.ws.bufferedAmount + byteLength > limits.maxBufferedBytes) {
|
|
551
|
+
connection.close(CLOSE_CAPACITY, "Browser Surface output buffer is full");
|
|
552
|
+
throw new Error("Browser Surface output buffer is full");
|
|
553
|
+
}
|
|
554
|
+
await new Promise((resolve, reject) => {
|
|
555
|
+
connection.ws.send(frame, { binary, compress: false }, (error) => {
|
|
556
|
+
if (error)
|
|
557
|
+
reject(error);
|
|
558
|
+
else
|
|
559
|
+
resolve();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function closeConnection(connection, code, reason) {
|
|
564
|
+
if (isClosed(connection))
|
|
565
|
+
return;
|
|
566
|
+
const closeReason = code === CLOSE_UNAUTHORIZED ? "grant_revoked" : "transport_error";
|
|
567
|
+
connection.state = "closing";
|
|
568
|
+
connection.controlQueue?.abort();
|
|
569
|
+
connection.abortController.abort(new Error(reason));
|
|
570
|
+
if (connection.ws.readyState === WebSocket.OPEN) {
|
|
571
|
+
connection.ws.close(code, boundedCloseReason(reason));
|
|
572
|
+
connection.closeTimer = setTimeout(() => {
|
|
573
|
+
if (connection.ws.readyState !== WebSocket.CLOSED)
|
|
574
|
+
connection.ws.terminate();
|
|
575
|
+
}, CONNECTION_CLOSE_DEADLINE_MS);
|
|
576
|
+
connection.closeTimer.unref?.();
|
|
577
|
+
}
|
|
578
|
+
else if (connection.ws.readyState !== WebSocket.CLOSED) {
|
|
579
|
+
connection.ws.terminate();
|
|
580
|
+
}
|
|
581
|
+
void closeAttachment(connection, closeReason);
|
|
582
|
+
}
|
|
583
|
+
async function cleanupConnection(connection, reason) {
|
|
584
|
+
if (connection.state === "closed")
|
|
585
|
+
return;
|
|
586
|
+
connection.state = "closed";
|
|
587
|
+
connection.controlQueue?.abort();
|
|
588
|
+
clearTimeout(connection.authenticationTimer);
|
|
589
|
+
if (connection.closeTimer)
|
|
590
|
+
clearTimeout(connection.closeTimer);
|
|
591
|
+
connection.abortController.abort(new Error("Browser Surface connection closed"));
|
|
592
|
+
releaseUnauthenticated(connection);
|
|
593
|
+
options.connectionHub.unregister(connection);
|
|
594
|
+
connections.delete(connection);
|
|
595
|
+
await closeAttachment(connection, reason);
|
|
596
|
+
}
|
|
597
|
+
async function closeAttachment(connection, reason) {
|
|
598
|
+
if (connection.attachmentClosed)
|
|
599
|
+
return;
|
|
600
|
+
connection.attachmentClosed = true;
|
|
601
|
+
try {
|
|
602
|
+
await connection.attachment?.close(reason);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
options.onError?.(error, "Browser Surface attachment cleanup");
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function releaseUnauthenticated(connection) {
|
|
609
|
+
if (!connection.countedAsUnauthenticated)
|
|
610
|
+
return;
|
|
611
|
+
connection.countedAsUnauthenticated = false;
|
|
612
|
+
decrementCount(unauthenticatedConnectionsBySource, connection.sourceAddress);
|
|
613
|
+
}
|
|
614
|
+
heartbeatTimer = setInterval(() => {
|
|
615
|
+
const now = Date.now();
|
|
616
|
+
for (const connection of connections) {
|
|
617
|
+
if (connection.ws.readyState !== WebSocket.OPEN)
|
|
618
|
+
continue;
|
|
619
|
+
if (now - connection.lastInboundActivityAt > limits.peerLivenessTimeoutMs) {
|
|
620
|
+
connection.ws.terminate();
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
connection.ws.ping();
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
connection.ws.terminate();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}, limits.heartbeatIntervalMs);
|
|
631
|
+
heartbeatTimer.unref?.();
|
|
632
|
+
return {
|
|
633
|
+
path: DIRECT_RUNTIME_BROWSER_SURFACE_PATH,
|
|
634
|
+
get activeConnectionCount() {
|
|
635
|
+
return connections.size;
|
|
636
|
+
},
|
|
637
|
+
shutdown() {
|
|
638
|
+
if (shutdownPromise)
|
|
639
|
+
return shutdownPromise;
|
|
640
|
+
shutdownPromise = (async () => {
|
|
641
|
+
stopping = true;
|
|
642
|
+
server.removeListener("upgrade", handleUpgrade);
|
|
643
|
+
if (heartbeatTimer)
|
|
644
|
+
clearInterval(heartbeatTimer);
|
|
645
|
+
heartbeatTimer = undefined;
|
|
646
|
+
for (const connection of [...connections]) {
|
|
647
|
+
connection.state = "closing";
|
|
648
|
+
connection.abortController.abort(new Error("Browser Surface server is shutting down"));
|
|
649
|
+
void closeAttachment(connection, "server_shutdown");
|
|
650
|
+
if (connection.ws.readyState === WebSocket.OPEN) {
|
|
651
|
+
connection.ws.close(1001, "Browser Surface server is shutting down");
|
|
652
|
+
}
|
|
653
|
+
else if (connection.ws.readyState !== WebSocket.CLOSED) {
|
|
654
|
+
connection.ws.terminate();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const deadline = Date.now() + limits.shutdownDeadlineMs;
|
|
658
|
+
while (connections.size > 0 && Date.now() < deadline) {
|
|
659
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
660
|
+
}
|
|
661
|
+
for (const connection of [...connections])
|
|
662
|
+
connection.ws.terminate();
|
|
663
|
+
await closeWebSocketServer(wss);
|
|
664
|
+
})();
|
|
665
|
+
return shutdownPromise;
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function assertAuthenticationResult(hello, authenticate, peer, accepted) {
|
|
670
|
+
if (hello.role !== "browser-surface" ||
|
|
671
|
+
authenticate.role !== "browser-surface" ||
|
|
672
|
+
accepted.role !== "browser-surface" ||
|
|
673
|
+
authenticate.mode !== "connect" ||
|
|
674
|
+
accepted.daemonId !== hello.daemonId ||
|
|
675
|
+
accepted.daemonInstanceId !== hello.daemonInstanceId ||
|
|
676
|
+
authenticate.daemonId !== hello.daemonId ||
|
|
677
|
+
authenticate.daemonInstanceId !== hello.daemonInstanceId ||
|
|
678
|
+
authenticate.bindingProtocolVersion !==
|
|
679
|
+
DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL.currentVersion ||
|
|
680
|
+
accepted.bindingProtocolVersion !== authenticate.bindingProtocolVersion ||
|
|
681
|
+
accepted.coreProtocolVersion !== authenticate.coreProtocolVersion ||
|
|
682
|
+
accepted.clientId !== authenticate.clientId ||
|
|
683
|
+
accepted.clientId !== peer.clientId ||
|
|
684
|
+
accepted.grantId !== authenticate.grantId ||
|
|
685
|
+
accepted.grantId !== peer.grantId ||
|
|
686
|
+
accepted.authorities.length !== peer.subject.effectiveAuthorities.length ||
|
|
687
|
+
accepted.authorities.some((authority, index) => authority !== peer.subject.effectiveAuthorities[index])) {
|
|
688
|
+
throw new Error("Browser Surface authenticator returned an inconsistent identity");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function isImageFrame(event) {
|
|
692
|
+
return !("type" in event);
|
|
693
|
+
}
|
|
694
|
+
function isClosed(connection) {
|
|
695
|
+
return connection.state === "closing" || connection.state === "closed";
|
|
696
|
+
}
|
|
697
|
+
function rawDataBytes(data) {
|
|
698
|
+
if (data instanceof ArrayBuffer)
|
|
699
|
+
return new Uint8Array(data);
|
|
700
|
+
if (Array.isArray(data))
|
|
701
|
+
return Uint8Array.from(Buffer.concat(data));
|
|
702
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
703
|
+
}
|
|
704
|
+
function decodeText(bytes) {
|
|
705
|
+
try {
|
|
706
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
throw new Error("Browser Surface frame must be valid UTF-8", { cause: error });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function socketSourceAddress(request) {
|
|
713
|
+
const address = request.socket.remoteAddress;
|
|
714
|
+
if (!address)
|
|
715
|
+
return "unknown";
|
|
716
|
+
return address.startsWith("::ffff:") ? address.slice("::ffff:".length) : address;
|
|
717
|
+
}
|
|
718
|
+
function rejectUpgrade(socket, status, message) {
|
|
719
|
+
if (socket.destroyed)
|
|
720
|
+
return;
|
|
721
|
+
const body = `${message}\n`;
|
|
722
|
+
socket.end(`HTTP/1.1 ${status} ${statusText(status)}\r\n` +
|
|
723
|
+
"Connection: close\r\n" +
|
|
724
|
+
"Cache-Control: no-store\r\n" +
|
|
725
|
+
"Content-Type: text/plain; charset=utf-8\r\n" +
|
|
726
|
+
`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
727
|
+
}
|
|
728
|
+
function statusText(status) {
|
|
729
|
+
if (status === 400)
|
|
730
|
+
return "Bad Request";
|
|
731
|
+
if (status === 404)
|
|
732
|
+
return "Not Found";
|
|
733
|
+
if (status === 503)
|
|
734
|
+
return "Service Unavailable";
|
|
735
|
+
return "Error";
|
|
736
|
+
}
|
|
737
|
+
function boundedCloseReason(reason) {
|
|
738
|
+
const bytes = Buffer.from(reason, "utf8");
|
|
739
|
+
if (bytes.byteLength <= 123)
|
|
740
|
+
return reason;
|
|
741
|
+
return bytes.subarray(0, 123).toString("utf8").replace(/\uFFFD+$/u, "");
|
|
742
|
+
}
|
|
743
|
+
function incrementCount(counts, key) {
|
|
744
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
745
|
+
}
|
|
746
|
+
function decrementCount(counts, key) {
|
|
747
|
+
const current = counts.get(key);
|
|
748
|
+
if (current === undefined || current <= 1)
|
|
749
|
+
counts.delete(key);
|
|
750
|
+
else
|
|
751
|
+
counts.set(key, current - 1);
|
|
752
|
+
}
|
|
753
|
+
function resolveLimits(limits) {
|
|
754
|
+
const resolved = {
|
|
755
|
+
authenticationTimeoutMs: positiveInteger(limits?.authenticationTimeoutMs, 10_000),
|
|
756
|
+
maxConnections: positiveInteger(limits?.maxConnections, DEFAULT_MAX_CONNECTIONS),
|
|
757
|
+
maxUnauthenticatedConnectionsPerSource: positiveInteger(limits?.maxUnauthenticatedConnectionsPerSource, DEFAULT_MAX_UNAUTHENTICATED_CONNECTIONS_PER_SOURCE),
|
|
758
|
+
maxAuthenticationAttemptsPerSource: positiveInteger(limits?.maxAuthenticationAttemptsPerSource, DEFAULT_MAX_AUTHENTICATION_ATTEMPTS_PER_SOURCE),
|
|
759
|
+
authenticationAttemptWindowMs: positiveInteger(limits?.authenticationAttemptWindowMs, DEFAULT_AUTHENTICATION_ATTEMPT_WINDOW_MS),
|
|
760
|
+
maxTrackedAuthenticationSources: positiveInteger(limits?.maxTrackedAuthenticationSources, DEFAULT_MAX_TRACKED_AUTHENTICATION_SOURCES),
|
|
761
|
+
maxConnectionsPerGrant: positiveInteger(limits?.maxConnectionsPerGrant, DEFAULT_MAX_CONNECTIONS_PER_GRANT),
|
|
762
|
+
maxQueuedFrames: positiveInteger(limits?.maxQueuedFrames, RUNTIME_BROWSER_SURFACE_MAX_RELIABLE_INPUT_QUEUE),
|
|
763
|
+
maxBufferedBytes: positiveInteger(limits?.maxBufferedBytes, DEFAULT_MAX_BUFFERED_BYTES),
|
|
764
|
+
heartbeatIntervalMs: positiveInteger(limits?.heartbeatIntervalMs, RUNTIME_BROWSER_SURFACE_HEARTBEAT_INTERVAL_MS),
|
|
765
|
+
peerLivenessTimeoutMs: positiveInteger(limits?.peerLivenessTimeoutMs, RUNTIME_BROWSER_SURFACE_PEER_LIVENESS_TIMEOUT_MS),
|
|
766
|
+
shutdownDeadlineMs: positiveInteger(limits?.shutdownDeadlineMs, DEFAULT_SHUTDOWN_DEADLINE_MS),
|
|
767
|
+
};
|
|
768
|
+
if (resolved.maxConnectionsPerGrant > resolved.maxConnections) {
|
|
769
|
+
throw new Error("Browser Surface per-grant limit must not exceed maxConnections");
|
|
770
|
+
}
|
|
771
|
+
if (resolved.peerLivenessTimeoutMs <= resolved.heartbeatIntervalMs) {
|
|
772
|
+
throw new Error("Browser Surface liveness timeout must exceed its heartbeat interval");
|
|
773
|
+
}
|
|
774
|
+
if (resolved.maxBufferedBytes < DIRECT_RUNTIME_SURFACE_E2EE_MAX_BINARY_ENVELOPE_BYTES) {
|
|
775
|
+
throw new Error("Browser Surface output limit must admit one maximum binary record");
|
|
776
|
+
}
|
|
777
|
+
return resolved;
|
|
778
|
+
}
|
|
779
|
+
function positiveInteger(value, fallback) {
|
|
780
|
+
const resolved = value ?? fallback;
|
|
781
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
782
|
+
throw new Error("Browser Surface limits must be positive safe integers");
|
|
783
|
+
}
|
|
784
|
+
return resolved;
|
|
785
|
+
}
|
|
786
|
+
class SourceAuthenticationAttemptLimiter {
|
|
787
|
+
options;
|
|
788
|
+
entries = new Map();
|
|
789
|
+
constructor(options) {
|
|
790
|
+
this.options = options;
|
|
791
|
+
}
|
|
792
|
+
tryConsume(sourceAddress, now = Date.now()) {
|
|
793
|
+
const existing = this.entries.get(sourceAddress);
|
|
794
|
+
if (existing && now - existing.startedAt < this.options.windowMs) {
|
|
795
|
+
existing.attempts += 1;
|
|
796
|
+
this.entries.delete(sourceAddress);
|
|
797
|
+
this.entries.set(sourceAddress, existing);
|
|
798
|
+
return existing.attempts <= this.options.maxAttempts;
|
|
799
|
+
}
|
|
800
|
+
if (existing)
|
|
801
|
+
this.entries.delete(sourceAddress);
|
|
802
|
+
while (this.entries.size >= this.options.maxSources) {
|
|
803
|
+
const oldest = this.entries.keys().next().value;
|
|
804
|
+
if (oldest === undefined)
|
|
805
|
+
break;
|
|
806
|
+
this.entries.delete(oldest);
|
|
807
|
+
}
|
|
808
|
+
this.entries.set(sourceAddress, { startedAt: now, attempts: 1 });
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function closeWebSocketServer(server) {
|
|
813
|
+
return new Promise((resolve) => {
|
|
814
|
+
try {
|
|
815
|
+
server.close(() => resolve());
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
resolve();
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|