@rynx-ai/remote-runtime-client 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-client.d.ts +31 -0
- package/dist/browser-surface-client.js +785 -0
- package/dist/client.d.ts +86 -0
- package/dist/client.js +1915 -0
- package/dist/credential.d.ts +35 -0
- package/dist/credential.js +163 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.js +112 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/package.json +32 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import { createHash, createPublicKey, randomBytes, sign, verify, } from "node:crypto";
|
|
2
|
+
import { decodeDirectRuntimeFrame, DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL, DIRECT_RUNTIME_BROWSER_SURFACE_PATH, DIRECT_RUNTIME_CLOSE_CODE, DIRECT_RUNTIME_MAX_HANDSHAKE_FRAME_BYTES, encodeClientAuthenticateCanonical, encodeClientAuthenticateProofPayload, encodeDirectRuntimeFrame, encodeServerAcceptedProofPayload, encodeServerHelloCanonical, encodeServerHelloProofPayload, } from "@rynx-ai/protocol/direct-runtime";
|
|
3
|
+
import { parseRuntimeBrowserSurfaceClientFrame, parseRuntimeBrowserSurfaceImageFrame, parseRuntimeBrowserSurfaceServerFrame, RUNTIME_BROWSER_SURFACE_MAX_CONTROL_FRAME_BYTES, } from "@rynx-ai/protocol/runtime-browser-surface";
|
|
4
|
+
import { REMOTE_RUNTIME_CORE_PROTOCOL } from "@rynx-ai/protocol/remote-runtime";
|
|
5
|
+
import { createClientSurfaceE2EEChannel, DIRECT_RUNTIME_SURFACE_E2EE_MAX_BINARY_ENVELOPE_BYTES, generateClientKeyExchange, } from "@rynx-ai/remote-runtime-e2ee";
|
|
6
|
+
import WebSocket from "ws";
|
|
7
|
+
import { parseDirectRuntimeClientIdentity, parseDirectRuntimeCredential, } from "./credential.js";
|
|
8
|
+
import { clientError, DirectRuntimeClientError, } from "./errors.js";
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
10
|
+
const MIN_TIMEOUT_MS = 100;
|
|
11
|
+
const MAX_TIMEOUT_MS = 120_000;
|
|
12
|
+
const DEFAULT_PING_INTERVAL_MS = 10_000;
|
|
13
|
+
const DEFAULT_INACTIVITY_TIMEOUT_MS = 25_000;
|
|
14
|
+
const MAX_LIVENESS_INTERVAL_MS = 5 * 60_000;
|
|
15
|
+
const MAX_PRE_ACTIVATION_FRAMES = 4;
|
|
16
|
+
const MAX_PRE_ACTIVATION_BYTES = 128 * 1024;
|
|
17
|
+
const MAX_QUEUED_EVENTS = 64;
|
|
18
|
+
const MAX_QUEUED_EVENT_BYTES = 8 * 1024 * 1024;
|
|
19
|
+
const MAX_PENDING_OUTBOUND_BYTES = 8 * 1024 * 1024;
|
|
20
|
+
const MAX_PENDING_OUTBOUND_FRAMES = 16;
|
|
21
|
+
export class DirectRuntimeBrowserSurfaceError extends DirectRuntimeClientError {
|
|
22
|
+
reason;
|
|
23
|
+
subscriptionId;
|
|
24
|
+
constructor(reason, subscriptionId, message) {
|
|
25
|
+
super(surfaceCloseErrorCode(reason), message);
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
this.subscriptionId = subscriptionId;
|
|
28
|
+
this.name = "DirectRuntimeBrowserSurfaceError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Authenticate a fresh V3 channel and open exactly one Browser Surface. */
|
|
32
|
+
export async function openDirectRuntimeBrowserSurface(untrustedCredential, untrustedOpenFrame, options = {}) {
|
|
33
|
+
if (options.signal?.aborted) {
|
|
34
|
+
throw clientError("closed", "Browser Surface opening was cancelled");
|
|
35
|
+
}
|
|
36
|
+
const credential = parseDirectRuntimeCredential(untrustedCredential);
|
|
37
|
+
const identity = parseDirectRuntimeClientIdentity(credential, "authentication_failed");
|
|
38
|
+
const openFrame = parseRuntimeBrowserSurfaceClientFrame(untrustedOpenFrame);
|
|
39
|
+
if (openFrame.type !== "browser.surface.open") {
|
|
40
|
+
throw clientError("protocol_error", "Browser Surface connection requires an open frame");
|
|
41
|
+
}
|
|
42
|
+
const timeoutMs = parseTimeout(options.timeoutMs);
|
|
43
|
+
const liveness = parseLiveness(options.liveness);
|
|
44
|
+
const endpoint = browserSurfaceEndpoint(credential.directEndpoint);
|
|
45
|
+
const socket = new BrowserSurfaceSocket(endpoint, timeoutMs, liveness);
|
|
46
|
+
const abortOpening = () => {
|
|
47
|
+
socket.abort(clientError("closed", "Browser Surface opening was cancelled"));
|
|
48
|
+
};
|
|
49
|
+
options.signal?.addEventListener("abort", abortOpening, { once: true });
|
|
50
|
+
const deadline = Date.now() + timeoutMs;
|
|
51
|
+
try {
|
|
52
|
+
await socket.open(remaining(deadline));
|
|
53
|
+
const keyExchange = generateClientKeyExchange(credential.serverEncryptionPublicKey);
|
|
54
|
+
await socket.startAuthentication(keyExchange.channel, encodeDirectRuntimeFrame(keyExchange.clientHello));
|
|
55
|
+
const ready = decodeHandshake(await socket.readAuthenticationFrame(remaining(deadline)), "e2ee.ready");
|
|
56
|
+
if (ready.type !== "e2ee.ready") {
|
|
57
|
+
throw clientError("protocol_error", "e2ee.ready must follow e2ee.client-hello");
|
|
58
|
+
}
|
|
59
|
+
const hello = decodeHandshake(await socket.readAuthenticationFrame(remaining(deadline)), "server.hello");
|
|
60
|
+
if (hello.type !== "server.hello") {
|
|
61
|
+
throw clientError("protocol_error", "Browser Surface server.hello is missing");
|
|
62
|
+
}
|
|
63
|
+
assertSurfaceHello(hello, credential);
|
|
64
|
+
const helloFields = withoutSignature(hello);
|
|
65
|
+
const daemonPublicKey = createPublicKey({
|
|
66
|
+
key: Buffer.from(credential.identityPublicKey, "base64url"),
|
|
67
|
+
format: "der",
|
|
68
|
+
type: "spki",
|
|
69
|
+
});
|
|
70
|
+
if (!verify(null, encodeServerHelloProofPayload(keyExchange.channelBinding, helloFields), daemonPublicKey, Buffer.from(hello.signature, "base64url"))) {
|
|
71
|
+
throw clientError("authentication_failed", "Browser Surface server.hello signature is invalid");
|
|
72
|
+
}
|
|
73
|
+
const selectedCoreVersion = selectCoreProtocol(hello);
|
|
74
|
+
const authenticateFields = {
|
|
75
|
+
type: "client.authenticate",
|
|
76
|
+
mode: "connect",
|
|
77
|
+
role: "browser-surface",
|
|
78
|
+
daemonId: hello.daemonId,
|
|
79
|
+
daemonInstanceId: hello.daemonInstanceId,
|
|
80
|
+
bindingProtocolVersion: DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL.currentVersion,
|
|
81
|
+
coreProtocolVersion: selectedCoreVersion,
|
|
82
|
+
clientId: credential.clientId,
|
|
83
|
+
clientPublicKey: credential.clientPublicKey,
|
|
84
|
+
nonce: randomBytes(32).toString("base64url"),
|
|
85
|
+
grantId: credential.grantId,
|
|
86
|
+
};
|
|
87
|
+
const helloHash = sha256(encodeServerHelloCanonical(helloFields));
|
|
88
|
+
const authenticate = {
|
|
89
|
+
...authenticateFields,
|
|
90
|
+
signature: sign(null, encodeClientAuthenticateProofPayload(keyExchange.channelBinding, helloHash, authenticateFields), identity.privateKey).toString("base64url"),
|
|
91
|
+
};
|
|
92
|
+
const authenticateHash = sha256(encodeClientAuthenticateCanonical(authenticateFields));
|
|
93
|
+
await socket.sendAuthenticationText(encodeDirectRuntimeFrame(authenticate));
|
|
94
|
+
const accepted = decodeHandshake(await socket.readAuthenticationFrame(remaining(deadline)), "server.accepted");
|
|
95
|
+
if (accepted.type !== "server.accepted") {
|
|
96
|
+
throw clientError("protocol_error", "Browser Surface server.accepted is missing");
|
|
97
|
+
}
|
|
98
|
+
assertSurfaceAccepted(accepted, hello, authenticate, credential);
|
|
99
|
+
const acceptedFields = withoutSignature(accepted);
|
|
100
|
+
if (!verify(null, encodeServerAcceptedProofPayload(keyExchange.channelBinding, helloHash, authenticateHash, acceptedFields), daemonPublicKey, Buffer.from(accepted.signature, "base64url"))) {
|
|
101
|
+
throw clientError("authentication_failed", "Browser Surface server.accepted signature is invalid");
|
|
102
|
+
}
|
|
103
|
+
const recordContext = {
|
|
104
|
+
channelId: accepted.channelId,
|
|
105
|
+
subscriptionId: openFrame.subscriptionId,
|
|
106
|
+
};
|
|
107
|
+
socket.activateSurface(createClientSurfaceE2EEChannel({
|
|
108
|
+
sharedSecret: keyExchange.sharedSecret,
|
|
109
|
+
serverEncryptionPublicKey: credential.serverEncryptionPublicKey,
|
|
110
|
+
clientHello: keyExchange.clientHello,
|
|
111
|
+
}), recordContext);
|
|
112
|
+
socket.markAuthenticated();
|
|
113
|
+
const connection = new BrowserSurfaceConnectionImpl(socket, openFrame, hello.daemonInstanceId);
|
|
114
|
+
await socket.sendSurfaceText(JSON.stringify(openFrame));
|
|
115
|
+
await connection.waitUntilReady(remaining(deadline));
|
|
116
|
+
if (options.signal?.aborted) {
|
|
117
|
+
throw clientError("closed", "Browser Surface opening was cancelled");
|
|
118
|
+
}
|
|
119
|
+
return connection;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
const normalized = normalizeError(error, "Browser Surface handshake failed");
|
|
123
|
+
socket.abort(normalized);
|
|
124
|
+
await socket.close().catch(() => undefined);
|
|
125
|
+
throw normalized;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
options.signal?.removeEventListener("abort", abortOpening);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
class BrowserSurfaceConnectionImpl {
|
|
132
|
+
socket;
|
|
133
|
+
openFrame;
|
|
134
|
+
subscriptionId;
|
|
135
|
+
sessionId;
|
|
136
|
+
pageId;
|
|
137
|
+
daemonInstanceId;
|
|
138
|
+
closed;
|
|
139
|
+
readyFrame;
|
|
140
|
+
readyResolve;
|
|
141
|
+
readyReject;
|
|
142
|
+
readyPromise;
|
|
143
|
+
closedResolve;
|
|
144
|
+
items = [];
|
|
145
|
+
queuedBytes = 0;
|
|
146
|
+
waiter;
|
|
147
|
+
terminal;
|
|
148
|
+
finished = false;
|
|
149
|
+
localClose = false;
|
|
150
|
+
constructor(socket, openFrame, daemonInstanceId) {
|
|
151
|
+
this.socket = socket;
|
|
152
|
+
this.openFrame = openFrame;
|
|
153
|
+
this.subscriptionId = openFrame.subscriptionId;
|
|
154
|
+
this.sessionId = openFrame.sessionId;
|
|
155
|
+
this.pageId = openFrame.pageId;
|
|
156
|
+
this.daemonInstanceId = daemonInstanceId;
|
|
157
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
158
|
+
this.readyResolve = resolve;
|
|
159
|
+
this.readyReject = reject;
|
|
160
|
+
});
|
|
161
|
+
this.closed = new Promise((resolve) => {
|
|
162
|
+
this.closedResolve = resolve;
|
|
163
|
+
});
|
|
164
|
+
socket.setSurfaceHandlers((record) => this.handleRecord(record), (error) => this.handleTerminal(error));
|
|
165
|
+
}
|
|
166
|
+
get ready() {
|
|
167
|
+
if (!this.readyFrame) {
|
|
168
|
+
throw clientError("protocol_error", "Browser Surface ready barrier has not completed");
|
|
169
|
+
}
|
|
170
|
+
return this.readyFrame;
|
|
171
|
+
}
|
|
172
|
+
async waitUntilReady(timeoutMs) {
|
|
173
|
+
await withTimeout(this.readyPromise, timeoutMs, () => clientError("unreachable", "Browser Surface ready barrier timed out"));
|
|
174
|
+
}
|
|
175
|
+
async send(untrustedFrame) {
|
|
176
|
+
if (this.finished)
|
|
177
|
+
throw this.terminal ?? clientError("closed", "Browser Surface is closed");
|
|
178
|
+
const frame = parseRuntimeBrowserSurfaceClientFrame(untrustedFrame);
|
|
179
|
+
if (frame.type === "browser.surface.open") {
|
|
180
|
+
throw clientError("protocol_error", "Browser Surface cannot be opened twice");
|
|
181
|
+
}
|
|
182
|
+
if (frame.subscriptionId !== this.subscriptionId) {
|
|
183
|
+
throw clientError("protocol_error", "Browser Surface control targets another subscription");
|
|
184
|
+
}
|
|
185
|
+
await this.socket.sendSurfaceText(JSON.stringify(frame));
|
|
186
|
+
}
|
|
187
|
+
async close() {
|
|
188
|
+
if (this.localClose)
|
|
189
|
+
return;
|
|
190
|
+
this.localClose = true;
|
|
191
|
+
if (!this.finished) {
|
|
192
|
+
this.finish({
|
|
193
|
+
type: "browser.surface.closed",
|
|
194
|
+
subscriptionId: this.subscriptionId,
|
|
195
|
+
reason: "cancelled",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
await this.socket.close();
|
|
199
|
+
}
|
|
200
|
+
[Symbol.asyncIterator]() {
|
|
201
|
+
return {
|
|
202
|
+
next: () => this.nextEvent(),
|
|
203
|
+
return: async () => {
|
|
204
|
+
await this.close();
|
|
205
|
+
return { done: true, value: undefined };
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
nextEvent() {
|
|
210
|
+
const item = this.items.shift();
|
|
211
|
+
if (item) {
|
|
212
|
+
this.queuedBytes -= item.bytes;
|
|
213
|
+
return Promise.resolve({ done: false, value: item.event });
|
|
214
|
+
}
|
|
215
|
+
if (this.terminal)
|
|
216
|
+
return Promise.reject(this.terminal);
|
|
217
|
+
if (this.finished)
|
|
218
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
219
|
+
if (this.waiter) {
|
|
220
|
+
return Promise.reject(clientError("protocol_error", "concurrent Surface iterator reads are not allowed"));
|
|
221
|
+
}
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
this.waiter = { resolve, reject };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
handleRecord(record) {
|
|
227
|
+
try {
|
|
228
|
+
if (record.binary) {
|
|
229
|
+
const image = parseRuntimeBrowserSurfaceImageFrame(record.bytes);
|
|
230
|
+
if (image.subscriptionId !== this.subscriptionId) {
|
|
231
|
+
throw new Error("Browser Surface server targeted another subscription");
|
|
232
|
+
}
|
|
233
|
+
if (!this.readyFrame)
|
|
234
|
+
throw new Error("Browser Surface data preceded ready");
|
|
235
|
+
this.enqueue(image, record.bytes.byteLength);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (record.bytes.byteLength > RUNTIME_BROWSER_SURFACE_MAX_CONTROL_FRAME_BYTES) {
|
|
239
|
+
throw new Error("Browser Surface control frame is too large");
|
|
240
|
+
}
|
|
241
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(record.bytes));
|
|
242
|
+
const event = parseRuntimeBrowserSurfaceServerFrame(value);
|
|
243
|
+
if (event.subscriptionId !== this.subscriptionId) {
|
|
244
|
+
throw new Error("Browser Surface server targeted another subscription");
|
|
245
|
+
}
|
|
246
|
+
if (event.type === "browser.surface.ready") {
|
|
247
|
+
if (this.readyFrame)
|
|
248
|
+
throw new Error("Browser Surface sent ready twice");
|
|
249
|
+
if (event.browserGeneration !== this.openFrame.browserGeneration ||
|
|
250
|
+
event.format !== this.openFrame.format) {
|
|
251
|
+
throw new Error("Browser Surface ready frame does not match the open request");
|
|
252
|
+
}
|
|
253
|
+
this.readyFrame = event;
|
|
254
|
+
this.readyResolve(event);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (event.type === "browser.surface.closed") {
|
|
258
|
+
if (this.readyFrame)
|
|
259
|
+
this.enqueue(event, record.bytes.byteLength);
|
|
260
|
+
this.finish(event);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (!this.readyFrame)
|
|
264
|
+
throw new Error("Browser Surface data preceded ready");
|
|
265
|
+
this.enqueue(event, record.bytes.byteLength);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const normalized = clientError("protocol_error", "invalid Browser Surface server record", error);
|
|
269
|
+
this.socket.abort(normalized);
|
|
270
|
+
this.handleTerminal(normalized);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
enqueue(event, bytes) {
|
|
274
|
+
if (this.finished)
|
|
275
|
+
return;
|
|
276
|
+
if (this.waiter) {
|
|
277
|
+
const waiter = this.waiter;
|
|
278
|
+
this.waiter = undefined;
|
|
279
|
+
waiter.resolve({ done: false, value: event });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (this.items.length >= MAX_QUEUED_EVENTS ||
|
|
283
|
+
this.queuedBytes + bytes > MAX_QUEUED_EVENT_BYTES) {
|
|
284
|
+
throw new Error("Browser Surface client event queue is full");
|
|
285
|
+
}
|
|
286
|
+
this.items.push({ event, bytes });
|
|
287
|
+
this.queuedBytes += bytes;
|
|
288
|
+
}
|
|
289
|
+
finish(frame) {
|
|
290
|
+
if (this.finished)
|
|
291
|
+
return;
|
|
292
|
+
this.finished = true;
|
|
293
|
+
this.closedResolve(frame);
|
|
294
|
+
if (!this.readyFrame) {
|
|
295
|
+
this.readyReject(new DirectRuntimeBrowserSurfaceError(frame.reason, frame.subscriptionId, frame.message ?? `Browser Surface closed: ${frame.reason}`));
|
|
296
|
+
}
|
|
297
|
+
if (this.waiter) {
|
|
298
|
+
const waiter = this.waiter;
|
|
299
|
+
this.waiter = undefined;
|
|
300
|
+
waiter.resolve({ done: true, value: undefined });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
handleTerminal(error) {
|
|
304
|
+
if (this.finished)
|
|
305
|
+
return;
|
|
306
|
+
this.terminal = error;
|
|
307
|
+
this.readyReject(error);
|
|
308
|
+
if (!this.finished) {
|
|
309
|
+
this.finished = true;
|
|
310
|
+
this.closedResolve({
|
|
311
|
+
type: "browser.surface.closed",
|
|
312
|
+
subscriptionId: this.subscriptionId,
|
|
313
|
+
reason: error.code === "authentication_failed" ? "unauthorized" : "internal_error",
|
|
314
|
+
message: error.message.slice(0, 1_024),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (this.waiter) {
|
|
318
|
+
const waiter = this.waiter;
|
|
319
|
+
this.waiter = undefined;
|
|
320
|
+
waiter.reject(error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
class BrowserSurfaceSocket {
|
|
325
|
+
liveness;
|
|
326
|
+
websocket;
|
|
327
|
+
opened;
|
|
328
|
+
openResolve;
|
|
329
|
+
openReject;
|
|
330
|
+
authenticationChannel;
|
|
331
|
+
surfaceChannel;
|
|
332
|
+
surfaceContext;
|
|
333
|
+
awaitingPlaintextReady = false;
|
|
334
|
+
authenticationQueue = [];
|
|
335
|
+
authenticationQueuedBytes = 0;
|
|
336
|
+
authenticationWaiter;
|
|
337
|
+
recordHandler;
|
|
338
|
+
terminalHandler;
|
|
339
|
+
terminal;
|
|
340
|
+
authenticated = false;
|
|
341
|
+
localClose = false;
|
|
342
|
+
livenessTimer;
|
|
343
|
+
lastInboundActivityAt = 0;
|
|
344
|
+
pendingOutboundFrames = 0;
|
|
345
|
+
pendingOutboundBytes = 0;
|
|
346
|
+
pendingSurfaceSendFrames = 0;
|
|
347
|
+
pendingSurfaceSendBytes = 0;
|
|
348
|
+
surfaceSendOperations = Promise.resolve();
|
|
349
|
+
constructor(endpoint, timeoutMs, liveness) {
|
|
350
|
+
this.liveness = liveness;
|
|
351
|
+
this.opened = new Promise((resolve, reject) => {
|
|
352
|
+
this.openResolve = resolve;
|
|
353
|
+
this.openReject = reject;
|
|
354
|
+
});
|
|
355
|
+
this.websocket = new WebSocket(endpoint, {
|
|
356
|
+
rejectUnauthorized: true,
|
|
357
|
+
followRedirects: false,
|
|
358
|
+
handshakeTimeout: timeoutMs,
|
|
359
|
+
perMessageDeflate: false,
|
|
360
|
+
maxPayload: DIRECT_RUNTIME_SURFACE_E2EE_MAX_BINARY_ENVELOPE_BYTES,
|
|
361
|
+
});
|
|
362
|
+
this.websocket.on("open", () => {
|
|
363
|
+
this.noteActivity();
|
|
364
|
+
this.startLiveness();
|
|
365
|
+
this.openResolve();
|
|
366
|
+
});
|
|
367
|
+
this.websocket.on("message", (data, isBinary) => {
|
|
368
|
+
this.noteActivity();
|
|
369
|
+
try {
|
|
370
|
+
this.handleWireRecord(rawDataBytes(data), isBinary);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
this.abort(clientError("protocol_error", "invalid Browser Surface E2EE record", error));
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
this.websocket.on("ping", () => this.noteActivity());
|
|
377
|
+
this.websocket.on("pong", () => this.noteActivity());
|
|
378
|
+
this.websocket.on("unexpected-response", (_request, response) => {
|
|
379
|
+
response.resume();
|
|
380
|
+
this.abort(clientError(response.statusCode === 401 || response.statusCode === 403
|
|
381
|
+
? "authentication_failed"
|
|
382
|
+
: response.statusCode === 429 || response.statusCode === 503
|
|
383
|
+
? "unreachable"
|
|
384
|
+
: "protocol_error", `Browser Surface WebSocket upgrade failed with HTTP ${response.statusCode ?? "unknown"}`));
|
|
385
|
+
});
|
|
386
|
+
this.websocket.on("error", (error) => {
|
|
387
|
+
this.fail(this.authenticated ? closedError(error) : mapWebSocketError(error));
|
|
388
|
+
});
|
|
389
|
+
this.websocket.on("close", (code, reason) => {
|
|
390
|
+
const suffix = reason.length === 0 ? "" : `: ${reason.toString("utf8").slice(0, 256)}`;
|
|
391
|
+
this.fail(webSocketCloseError(code, suffix, this.localClose));
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async open(timeoutMs) {
|
|
395
|
+
await withTimeout(this.opened, timeoutMs, () => clientError("unreachable", "Browser Surface WebSocket connection timed out"));
|
|
396
|
+
}
|
|
397
|
+
async startAuthentication(channel, clientHello) {
|
|
398
|
+
if (this.authenticationChannel)
|
|
399
|
+
throw clientError("protocol_error", "E2EE is already initialized");
|
|
400
|
+
this.authenticationChannel = channel;
|
|
401
|
+
this.awaitingPlaintextReady = true;
|
|
402
|
+
await this.sendWire(clientHello, false);
|
|
403
|
+
}
|
|
404
|
+
async readAuthenticationFrame(timeoutMs) {
|
|
405
|
+
if (this.authenticationWaiter) {
|
|
406
|
+
throw clientError("protocol_error", "concurrent Browser Surface handshake reads are not allowed");
|
|
407
|
+
}
|
|
408
|
+
const queued = this.authenticationQueue.shift();
|
|
409
|
+
if (queued) {
|
|
410
|
+
this.authenticationQueuedBytes -= queued.byteLength;
|
|
411
|
+
return queued;
|
|
412
|
+
}
|
|
413
|
+
if (this.terminal)
|
|
414
|
+
throw this.terminal;
|
|
415
|
+
return await new Promise((resolve, reject) => {
|
|
416
|
+
const timer = setTimeout(() => {
|
|
417
|
+
if (this.authenticationWaiter?.timer !== timer)
|
|
418
|
+
return;
|
|
419
|
+
this.authenticationWaiter = undefined;
|
|
420
|
+
reject(clientError("unreachable", "Browser Surface handshake timed out"));
|
|
421
|
+
}, timeoutMs);
|
|
422
|
+
timer.unref?.();
|
|
423
|
+
this.authenticationWaiter = { resolve, reject, timer };
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
async sendAuthenticationText(plaintext) {
|
|
427
|
+
const channel = this.authenticationChannel;
|
|
428
|
+
if (!channel)
|
|
429
|
+
throw clientError("protocol_error", "authentication E2EE is unavailable");
|
|
430
|
+
await this.sendWire(channel.seal(plaintext), false);
|
|
431
|
+
}
|
|
432
|
+
activateSurface(channel, context) {
|
|
433
|
+
if (this.authenticationWaiter || this.authenticationQueue.length > 0) {
|
|
434
|
+
throw clientError("protocol_error", "Browser Surface handshake has unread frames");
|
|
435
|
+
}
|
|
436
|
+
this.surfaceChannel = channel;
|
|
437
|
+
this.surfaceContext = context;
|
|
438
|
+
this.authenticationChannel = undefined;
|
|
439
|
+
}
|
|
440
|
+
markAuthenticated() {
|
|
441
|
+
this.authenticated = true;
|
|
442
|
+
}
|
|
443
|
+
setSurfaceHandlers(onRecord, onTerminal) {
|
|
444
|
+
if (this.recordHandler)
|
|
445
|
+
throw clientError("protocol_error", "Browser Surface socket is active");
|
|
446
|
+
this.recordHandler = onRecord;
|
|
447
|
+
this.terminalHandler = onTerminal;
|
|
448
|
+
if (this.terminal)
|
|
449
|
+
onTerminal(this.terminal);
|
|
450
|
+
}
|
|
451
|
+
async sendSurfaceText(plaintext) {
|
|
452
|
+
const plaintextBytes = Buffer.byteLength(plaintext, "utf8");
|
|
453
|
+
if (this.pendingSurfaceSendFrames >= MAX_PENDING_OUTBOUND_FRAMES ||
|
|
454
|
+
this.pendingSurfaceSendBytes + plaintextBytes > MAX_PENDING_OUTBOUND_BYTES) {
|
|
455
|
+
throw clientError("unreachable", "Browser Surface outbound queue is full");
|
|
456
|
+
}
|
|
457
|
+
this.pendingSurfaceSendFrames += 1;
|
|
458
|
+
this.pendingSurfaceSendBytes += plaintextBytes;
|
|
459
|
+
const send = this.surfaceSendOperations.then(async () => {
|
|
460
|
+
if (this.terminal || this.websocket.readyState !== WebSocket.OPEN) {
|
|
461
|
+
throw this.terminal ?? clientError("closed", "Browser Surface connection is not open");
|
|
462
|
+
}
|
|
463
|
+
const channel = this.surfaceChannel;
|
|
464
|
+
const context = this.surfaceContext;
|
|
465
|
+
if (!channel || !context)
|
|
466
|
+
throw clientError("protocol_error", "Surface E2EE is unavailable");
|
|
467
|
+
let sequenceConsumed = false;
|
|
468
|
+
try {
|
|
469
|
+
// Admission and serialization happen before sealing because sealing
|
|
470
|
+
// advances the strict E2EE record sequence.
|
|
471
|
+
sequenceConsumed = true;
|
|
472
|
+
const sealed = channel.sealText(plaintext, context);
|
|
473
|
+
await this.sendWire(sealed, false);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
const failure = normalizeError(error, "Browser Surface send failed");
|
|
477
|
+
// Once a sequence was consumed, continuing would send a permanent gap.
|
|
478
|
+
if (sequenceConsumed)
|
|
479
|
+
this.abort(failure);
|
|
480
|
+
throw failure;
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
this.surfaceSendOperations = send.then(() => undefined, () => undefined);
|
|
484
|
+
try {
|
|
485
|
+
await send;
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
this.pendingSurfaceSendFrames -= 1;
|
|
489
|
+
this.pendingSurfaceSendBytes -= plaintextBytes;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
abort(error) {
|
|
493
|
+
this.fail(error);
|
|
494
|
+
if (this.websocket.readyState !== WebSocket.CLOSED)
|
|
495
|
+
this.websocket.terminate();
|
|
496
|
+
}
|
|
497
|
+
async close() {
|
|
498
|
+
this.stopLiveness();
|
|
499
|
+
if (this.websocket.readyState === WebSocket.CLOSED)
|
|
500
|
+
return;
|
|
501
|
+
this.localClose = true;
|
|
502
|
+
await new Promise((resolve) => {
|
|
503
|
+
const timer = setTimeout(() => {
|
|
504
|
+
if (this.websocket.readyState !== WebSocket.CLOSED)
|
|
505
|
+
this.websocket.terminate();
|
|
506
|
+
resolve();
|
|
507
|
+
}, 1_000);
|
|
508
|
+
timer.unref?.();
|
|
509
|
+
this.websocket.once("close", () => {
|
|
510
|
+
clearTimeout(timer);
|
|
511
|
+
resolve();
|
|
512
|
+
});
|
|
513
|
+
if (this.websocket.readyState === WebSocket.CONNECTING)
|
|
514
|
+
this.websocket.terminate();
|
|
515
|
+
else
|
|
516
|
+
this.websocket.close(1000, "client closed");
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
handleWireRecord(bytes, binary) {
|
|
520
|
+
const surfaceChannel = this.surfaceChannel;
|
|
521
|
+
const context = this.surfaceContext;
|
|
522
|
+
if (surfaceChannel && context) {
|
|
523
|
+
const plaintext = binary
|
|
524
|
+
? surfaceChannel.openBytes(bytes, context)
|
|
525
|
+
: Uint8Array.from(Buffer.from(surfaceChannel.openText(decodeText(bytes), context), "utf8"));
|
|
526
|
+
this.recordHandler?.({ bytes: plaintext, binary });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (binary)
|
|
530
|
+
throw clientError("protocol_error", "binary traffic preceded Surface activation");
|
|
531
|
+
const authenticationChannel = this.authenticationChannel;
|
|
532
|
+
if (!authenticationChannel) {
|
|
533
|
+
throw clientError("protocol_error", "server traffic preceded E2EE setup");
|
|
534
|
+
}
|
|
535
|
+
const plaintext = this.awaitingPlaintextReady
|
|
536
|
+
? (this.awaitingPlaintextReady = false, bytes)
|
|
537
|
+
: Uint8Array.from(Buffer.from(authenticationChannel.open(decodeText(bytes)), "utf8"));
|
|
538
|
+
if (plaintext.byteLength > DIRECT_RUNTIME_MAX_HANDSHAKE_FRAME_BYTES) {
|
|
539
|
+
throw clientError("protocol_error", "Browser Surface handshake frame is too large");
|
|
540
|
+
}
|
|
541
|
+
this.deliverAuthentication(plaintext);
|
|
542
|
+
}
|
|
543
|
+
deliverAuthentication(frame) {
|
|
544
|
+
if (this.authenticationWaiter) {
|
|
545
|
+
const waiter = this.authenticationWaiter;
|
|
546
|
+
this.authenticationWaiter = undefined;
|
|
547
|
+
clearTimeout(waiter.timer);
|
|
548
|
+
waiter.resolve(frame);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (this.authenticationQueue.length >= MAX_PRE_ACTIVATION_FRAMES ||
|
|
552
|
+
this.authenticationQueuedBytes + frame.byteLength > MAX_PRE_ACTIVATION_BYTES) {
|
|
553
|
+
throw clientError("protocol_error", "too many Browser Surface handshake frames arrived");
|
|
554
|
+
}
|
|
555
|
+
this.authenticationQueue.push(frame);
|
|
556
|
+
this.authenticationQueuedBytes += frame.byteLength;
|
|
557
|
+
}
|
|
558
|
+
async sendWire(frame, binary) {
|
|
559
|
+
if (this.terminal || this.websocket.readyState !== WebSocket.OPEN) {
|
|
560
|
+
throw this.terminal ?? clientError("closed", "Browser Surface connection is not open");
|
|
561
|
+
}
|
|
562
|
+
const bytes = typeof frame === "string" ? Buffer.byteLength(frame, "utf8") : frame.byteLength;
|
|
563
|
+
if (this.pendingOutboundFrames >= MAX_PENDING_OUTBOUND_FRAMES ||
|
|
564
|
+
this.pendingOutboundBytes + bytes > MAX_PENDING_OUTBOUND_BYTES ||
|
|
565
|
+
this.websocket.bufferedAmount + bytes > MAX_PENDING_OUTBOUND_BYTES) {
|
|
566
|
+
throw clientError("unreachable", "Browser Surface outbound queue is full");
|
|
567
|
+
}
|
|
568
|
+
this.pendingOutboundFrames += 1;
|
|
569
|
+
this.pendingOutboundBytes += bytes;
|
|
570
|
+
await new Promise((resolve, reject) => {
|
|
571
|
+
const complete = (error) => {
|
|
572
|
+
this.pendingOutboundFrames -= 1;
|
|
573
|
+
this.pendingOutboundBytes -= bytes;
|
|
574
|
+
if (error)
|
|
575
|
+
reject(this.authenticated ? closedError(error) : mapWebSocketError(error));
|
|
576
|
+
else
|
|
577
|
+
resolve();
|
|
578
|
+
};
|
|
579
|
+
try {
|
|
580
|
+
this.websocket.send(frame, { binary, compress: false }, complete);
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
this.pendingOutboundFrames -= 1;
|
|
584
|
+
this.pendingOutboundBytes -= bytes;
|
|
585
|
+
reject(this.authenticated ? closedError(error) : mapWebSocketError(error));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
fail(error) {
|
|
590
|
+
if (this.terminal)
|
|
591
|
+
return;
|
|
592
|
+
this.terminal = error;
|
|
593
|
+
this.stopLiveness();
|
|
594
|
+
this.openReject(error);
|
|
595
|
+
if (this.authenticationWaiter) {
|
|
596
|
+
clearTimeout(this.authenticationWaiter.timer);
|
|
597
|
+
this.authenticationWaiter.reject(error);
|
|
598
|
+
this.authenticationWaiter = undefined;
|
|
599
|
+
}
|
|
600
|
+
this.terminalHandler?.(error);
|
|
601
|
+
}
|
|
602
|
+
noteActivity() {
|
|
603
|
+
this.lastInboundActivityAt = Date.now();
|
|
604
|
+
}
|
|
605
|
+
startLiveness() {
|
|
606
|
+
this.livenessTimer = setInterval(() => {
|
|
607
|
+
if (this.terminal)
|
|
608
|
+
return;
|
|
609
|
+
if (Date.now() - this.lastInboundActivityAt > this.liveness.inactivityTimeoutMs) {
|
|
610
|
+
this.abort(clientError(this.authenticated ? "closed" : "unreachable", "Browser Surface peer stopped responding"));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (this.websocket.readyState === WebSocket.OPEN)
|
|
614
|
+
this.websocket.ping();
|
|
615
|
+
}, this.liveness.pingIntervalMs);
|
|
616
|
+
this.livenessTimer.unref?.();
|
|
617
|
+
}
|
|
618
|
+
stopLiveness() {
|
|
619
|
+
if (this.livenessTimer)
|
|
620
|
+
clearInterval(this.livenessTimer);
|
|
621
|
+
this.livenessTimer = undefined;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function assertSurfaceHello(hello, credential) {
|
|
625
|
+
if (hello.role !== "browser-surface") {
|
|
626
|
+
throw clientError("protocol_error", "Browser Surface server.hello has the wrong role");
|
|
627
|
+
}
|
|
628
|
+
if (hello.daemonId !== credential.daemonId) {
|
|
629
|
+
throw clientError("identity_mismatch", "Browser Surface daemonId differs from the pin");
|
|
630
|
+
}
|
|
631
|
+
if (hello.identityPublicKey !== credential.identityPublicKey) {
|
|
632
|
+
throw clientError("identity_mismatch", "Browser Surface identity key differs from the pin");
|
|
633
|
+
}
|
|
634
|
+
if (hello.bindingProtocolVersion !== DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL.currentVersion ||
|
|
635
|
+
hello.minimumCompatibleBindingVersion !==
|
|
636
|
+
DIRECT_RUNTIME_BROWSER_SURFACE_BINDING_PROTOCOL.minimumCompatibleVersion) {
|
|
637
|
+
throw clientError("incompatible", "Browser Surface binding protocol is incompatible");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function assertSurfaceAccepted(accepted, hello, authenticate, credential) {
|
|
641
|
+
if (accepted.role !== "browser-surface" ||
|
|
642
|
+
authenticate.role !== "browser-surface" ||
|
|
643
|
+
accepted.daemonId !== hello.daemonId ||
|
|
644
|
+
accepted.daemonId !== credential.daemonId ||
|
|
645
|
+
accepted.daemonInstanceId !== hello.daemonInstanceId ||
|
|
646
|
+
accepted.bindingProtocolVersion !== authenticate.bindingProtocolVersion ||
|
|
647
|
+
accepted.coreProtocolVersion !== authenticate.coreProtocolVersion ||
|
|
648
|
+
accepted.clientId !== authenticate.clientId ||
|
|
649
|
+
accepted.grantId !== credential.grantId ||
|
|
650
|
+
accepted.authorities.length !== 1 ||
|
|
651
|
+
accepted.authorities[0] !== "daemon.full") {
|
|
652
|
+
throw clientError("authentication_failed", "Browser Surface acceptance is inconsistent");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function selectCoreProtocol(hello) {
|
|
656
|
+
if (hello.minimumCompatibleCoreVersion > REMOTE_RUNTIME_CORE_PROTOCOL.currentVersion ||
|
|
657
|
+
REMOTE_RUNTIME_CORE_PROTOCOL.minimumCompatibleVersion > hello.coreProtocolVersion) {
|
|
658
|
+
throw clientError("incompatible", "Remote Runtime Core protocol ranges do not overlap");
|
|
659
|
+
}
|
|
660
|
+
return Math.min(hello.coreProtocolVersion, REMOTE_RUNTIME_CORE_PROTOCOL.currentVersion);
|
|
661
|
+
}
|
|
662
|
+
function decodeHandshake(frame, expected) {
|
|
663
|
+
try {
|
|
664
|
+
return decodeDirectRuntimeFrame(frame);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
throw clientError("protocol_error", `invalid ${expected} frame`, error);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function browserSurfaceEndpoint(directEndpoint) {
|
|
671
|
+
const url = new URL(directEndpoint);
|
|
672
|
+
url.pathname = DIRECT_RUNTIME_BROWSER_SURFACE_PATH;
|
|
673
|
+
url.search = "";
|
|
674
|
+
url.hash = "";
|
|
675
|
+
return url.toString();
|
|
676
|
+
}
|
|
677
|
+
function withoutSignature(value) {
|
|
678
|
+
const { signature: _signature, ...fields } = value;
|
|
679
|
+
return fields;
|
|
680
|
+
}
|
|
681
|
+
function sha256(value) {
|
|
682
|
+
return Uint8Array.from(createHash("sha256").update(value).digest());
|
|
683
|
+
}
|
|
684
|
+
function parseTimeout(value) {
|
|
685
|
+
const timeout = value ?? DEFAULT_TIMEOUT_MS;
|
|
686
|
+
if (!Number.isSafeInteger(timeout) || timeout < MIN_TIMEOUT_MS || timeout > MAX_TIMEOUT_MS) {
|
|
687
|
+
throw clientError("protocol_error", `timeoutMs must be an integer between ${MIN_TIMEOUT_MS} and ${MAX_TIMEOUT_MS}`);
|
|
688
|
+
}
|
|
689
|
+
return timeout;
|
|
690
|
+
}
|
|
691
|
+
function parseLiveness(value) {
|
|
692
|
+
const pingIntervalMs = parseLivenessInterval(value?.pingIntervalMs, DEFAULT_PING_INTERVAL_MS, "liveness.pingIntervalMs");
|
|
693
|
+
const inactivityTimeoutMs = parseLivenessInterval(value?.inactivityTimeoutMs, DEFAULT_INACTIVITY_TIMEOUT_MS, "liveness.inactivityTimeoutMs");
|
|
694
|
+
if (inactivityTimeoutMs <= pingIntervalMs) {
|
|
695
|
+
throw clientError("protocol_error", "liveness timeout must exceed ping interval");
|
|
696
|
+
}
|
|
697
|
+
return { pingIntervalMs, inactivityTimeoutMs };
|
|
698
|
+
}
|
|
699
|
+
function parseLivenessInterval(value, fallback, field) {
|
|
700
|
+
const interval = value ?? fallback;
|
|
701
|
+
if (!Number.isSafeInteger(interval) || interval <= 0 || interval > MAX_LIVENESS_INTERVAL_MS) {
|
|
702
|
+
throw clientError("protocol_error", `${field} must be a bounded positive integer`);
|
|
703
|
+
}
|
|
704
|
+
return interval;
|
|
705
|
+
}
|
|
706
|
+
function remaining(deadline) {
|
|
707
|
+
return Math.max(1, deadline - Date.now());
|
|
708
|
+
}
|
|
709
|
+
function rawDataBytes(data) {
|
|
710
|
+
if (Array.isArray(data))
|
|
711
|
+
return Uint8Array.from(Buffer.concat(data));
|
|
712
|
+
if (data instanceof ArrayBuffer)
|
|
713
|
+
return new Uint8Array(data.slice(0));
|
|
714
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
715
|
+
}
|
|
716
|
+
function decodeText(bytes) {
|
|
717
|
+
try {
|
|
718
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
throw clientError("protocol_error", "Browser Surface frame must be valid UTF-8", error);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function normalizeError(error, message) {
|
|
725
|
+
return error instanceof DirectRuntimeClientError
|
|
726
|
+
? error
|
|
727
|
+
: clientError("protocol_error", message, error);
|
|
728
|
+
}
|
|
729
|
+
function closedError(cause) {
|
|
730
|
+
return clientError("closed", "Browser Surface connection was interrupted", cause);
|
|
731
|
+
}
|
|
732
|
+
function webSocketCloseError(closeCode, reasonSuffix, localClose) {
|
|
733
|
+
if (localClose)
|
|
734
|
+
return clientError("closed", "Browser Surface connection was closed", undefined, closeCode);
|
|
735
|
+
const code = closeCode === DIRECT_RUNTIME_CLOSE_CODE.unauthorized
|
|
736
|
+
? "authentication_failed"
|
|
737
|
+
: closeCode === DIRECT_RUNTIME_CLOSE_CODE.badProtocol
|
|
738
|
+
? "protocol_error"
|
|
739
|
+
: closeCode === DIRECT_RUNTIME_CLOSE_CODE.authenticationTimeout ||
|
|
740
|
+
closeCode === DIRECT_RUNTIME_CLOSE_CODE.capacity
|
|
741
|
+
? "unreachable"
|
|
742
|
+
: "closed";
|
|
743
|
+
return clientError(code, `Browser Surface connection closed (${closeCode})${reasonSuffix}`, undefined, closeCode);
|
|
744
|
+
}
|
|
745
|
+
function mapWebSocketError(error) {
|
|
746
|
+
const code = typeof error === "object" && error !== null && "code" in error &&
|
|
747
|
+
typeof error.code === "string" ? error.code : "";
|
|
748
|
+
if (code.startsWith("ERR_TLS_") ||
|
|
749
|
+
code.startsWith("ERR_SSL_") ||
|
|
750
|
+
code.startsWith("CERT_") ||
|
|
751
|
+
[
|
|
752
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
753
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
754
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
755
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
756
|
+
].includes(code)) {
|
|
757
|
+
return clientError("tls_error", "Browser Surface TLS verification failed", error);
|
|
758
|
+
}
|
|
759
|
+
if (code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH") {
|
|
760
|
+
return clientError("protocol_error", "Browser Surface WebSocket frame is too large", error);
|
|
761
|
+
}
|
|
762
|
+
return clientError("unreachable", "Browser Surface endpoint is unreachable", error);
|
|
763
|
+
}
|
|
764
|
+
function surfaceCloseErrorCode(reason) {
|
|
765
|
+
if (reason === "unauthorized")
|
|
766
|
+
return "authentication_failed";
|
|
767
|
+
if (reason === "capacity")
|
|
768
|
+
return "unreachable";
|
|
769
|
+
if (reason === "protocol_error")
|
|
770
|
+
return "protocol_error";
|
|
771
|
+
return "closed";
|
|
772
|
+
}
|
|
773
|
+
async function withTimeout(promise, timeoutMs, timeoutError) {
|
|
774
|
+
return await new Promise((resolve, reject) => {
|
|
775
|
+
const timer = setTimeout(() => reject(timeoutError()), timeoutMs);
|
|
776
|
+
timer.unref?.();
|
|
777
|
+
promise.then((value) => {
|
|
778
|
+
clearTimeout(timer);
|
|
779
|
+
resolve(value);
|
|
780
|
+
}, (error) => {
|
|
781
|
+
clearTimeout(timer);
|
|
782
|
+
reject(error);
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
}
|