@rynx-ai/server 0.1.9 → 0.1.10-beta.2
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-ws.d.ts +2 -0
- package/dist/browser-surface-ws.js +1 -0
- package/dist/control-api.d.ts +17 -0
- package/dist/control-api.js +809 -9
- package/dist/control-web/assets/{highlighted-body-OFNGDK62-e-w61YLy.js → highlighted-body-OFNGDK62-DHu2g-rk.js} +1 -1
- package/dist/control-web/assets/index-B6Pb5j9M.js +666 -0
- package/dist/control-web/assets/index-BU-_Qud_.css +32 -0
- package/dist/control-web/assets/{mermaid-GHXKKRXX-DZn7Hbr2.js → mermaid-GHXKKRXX-CSYzOmBR.js} +3 -3
- package/dist/control-web/index.html +2 -2
- package/dist/desktop-browser-host.d.ts +20 -3
- package/dist/desktop-browser-host.js +126 -44
- package/dist/direct-runtime-browser-inspect-server.d.ts +72 -0
- package/dist/direct-runtime-browser-inspect-server.js +749 -0
- package/dist/direct-runtime-server.d.ts +6 -0
- package/dist/direct-runtime-server.js +29 -1
- package/dist/emulator-surface-ws.d.ts +2 -0
- package/dist/emulator-surface-ws.js +1 -0
- package/dist/machine-session-service.d.ts +88 -5
- package/dist/machine-session-service.js +283 -20
- package/dist/remote-runtime-dispatcher.d.ts +1 -1
- package/dist/remote-runtime-dispatcher.js +25 -2
- package/dist/remote-runtime-session-projection.js +10 -0
- package/dist/runtime-web-auth.d.ts +5 -0
- package/dist/runtime-web-auth.js +43 -3
- package/dist/server.d.ts +78 -8
- package/dist/server.js +42 -17
- package/dist/session-browser-service.d.ts +30 -0
- package/dist/session-browser-service.js +89 -3
- package/dist/session-browser-surface-coordinator.d.ts +4 -3
- package/dist/session-browser-surface-coordinator.js +374 -268
- package/dist/session-runtime-index.d.ts +6 -0
- package/dist/session-runtime-index.js +10 -0
- package/dist/session-terminal-host.d.ts +6 -1
- package/dist/session-terminal-host.js +18 -0
- package/dist/terminal-ws.d.ts +3 -0
- package/dist/terminal-ws.js +1 -0
- package/package.json +12 -7
- package/dist/control-web/assets/index-BQ7XVBKO.css +0 -32
- package/dist/control-web/assets/index-ClpzuBJx.js +0 -606
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
8
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
9
9
|
<title>rynx · control</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-B6Pb5j9M.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BU-_Qud_.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Server as HttpServer } from "node:http";
|
|
2
|
-
import { parseDesktopBrowserHostSurfaceOpenResult, type DesktopBrowserHostClientFrame, type DesktopBrowserHostCommand, type DesktopBrowserHostHelloFrame, type DesktopBrowserHostLeaseFrame, type DesktopBrowserHostSurfaceCapability, type DesktopBrowserHostSurfaceFrame } from "@rynx-ai/protocol/desktop-browser-host";
|
|
2
|
+
import { parseDesktopBrowserHostSurfaceOpenResult, type DesktopBrowserHostClientFrame, type DesktopBrowserHostCommand, type DesktopBrowserHostHelloFrame, type DesktopBrowserHostLeaseFrame, type DesktopBrowserHostPresentation, type DesktopBrowserHostSurfaceCapability, type DesktopBrowserHostSurfaceFrame } from "@rynx-ai/protocol/desktop-browser-host";
|
|
3
3
|
import { WebSocket, WebSocketServer } from "ws";
|
|
4
4
|
import { SessionBrowserHostError, type SessionBrowserHost, type SessionBrowserHostCreateInput, type SessionBrowserHostEvent, type SessionBrowserHostHandle, type SessionBrowserHostSnapshot, type SessionBrowserHostSurfaceFrame, type SessionBrowserHostSurfaceOpenInput, type SessionBrowserHostSurfaceSource, type SessionBrowserHostSurfaceTrustedInput, type SessionBrowserHostSurfaceViewport } from "./session-browser-service.js";
|
|
5
5
|
export interface DesktopBrowserHostRegistryOptions {
|
|
@@ -26,6 +26,7 @@ interface DesktopBrowserLease {
|
|
|
26
26
|
leaseId: string;
|
|
27
27
|
daemonInstanceId: string;
|
|
28
28
|
hostInstanceId: string;
|
|
29
|
+
presentation: DesktopBrowserHostPresentation;
|
|
29
30
|
semanticPageBinding: boolean;
|
|
30
31
|
surface: DesktopBrowserHostSurfaceCapability;
|
|
31
32
|
socket: WebSocket;
|
|
@@ -43,16 +44,19 @@ export declare class DesktopBrowserHostRegistry implements SessionBrowserHost {
|
|
|
43
44
|
private readonly idFactory;
|
|
44
45
|
private readonly handles;
|
|
45
46
|
private readonly connectedListeners;
|
|
46
|
-
private
|
|
47
|
+
private readonly leases;
|
|
47
48
|
private idSequence;
|
|
48
49
|
constructor(options?: DesktopBrowserHostRegistryOptions);
|
|
49
50
|
get connected(): boolean;
|
|
51
|
+
get directConnected(): boolean;
|
|
50
52
|
/** Runs after a fresh lease frame has been queued to the Host. Listener
|
|
51
53
|
* failures are isolated from the authenticated Host handshake. */
|
|
52
54
|
onConnected(listener: () => void | Promise<void>): () => void;
|
|
53
55
|
createBrowser(input: SessionBrowserHostCreateInput): Promise<SessionBrowserHostHandle>;
|
|
54
56
|
deleteSessionData(sessionId: string): Promise<void>;
|
|
55
|
-
/** Attachment-only:
|
|
57
|
+
/** Attachment-only: installs one lease per presentation role. Existing
|
|
58
|
+
* generations stay pinned to their Host while new generations prefer the
|
|
59
|
+
* direct App-window Host. */
|
|
56
60
|
acceptConnection(socket: WebSocket, hello: DesktopBrowserHostHelloFrame, daemonInstanceId: string, heartbeatIntervalMilliseconds: number): DesktopBrowserHostLeaseFrame;
|
|
57
61
|
/** Attachment-only: validates lease scope before routing a client frame. */
|
|
58
62
|
receive(leaseId: string, frame: Exclude<DesktopBrowserHostClientFrame, DesktopBrowserHostHelloFrame>): void;
|
|
@@ -72,6 +76,14 @@ export declare class DesktopBrowserHostRegistry implements SessionBrowserHost {
|
|
|
72
76
|
private rejectCommandsFor;
|
|
73
77
|
private failLease;
|
|
74
78
|
private detach;
|
|
79
|
+
private currentLease;
|
|
80
|
+
private currentLeases;
|
|
81
|
+
private leaseById;
|
|
82
|
+
private installedLease;
|
|
83
|
+
private installedLeaseById;
|
|
84
|
+
private isInstalledLease;
|
|
85
|
+
private isCurrentLease;
|
|
86
|
+
private retireOffscreenIfIdle;
|
|
75
87
|
}
|
|
76
88
|
export declare class DesktopBrowserHostProtocolError extends Error {
|
|
77
89
|
readonly closeCode: number;
|
|
@@ -101,6 +113,11 @@ declare class DesktopBrowserHandle implements SessionBrowserHostHandle {
|
|
|
101
113
|
getCdpEndpoint(): Promise<{
|
|
102
114
|
endpoint: string;
|
|
103
115
|
engineVersion: string;
|
|
116
|
+
pageTargetId?: string;
|
|
117
|
+
}>;
|
|
118
|
+
getPageCdpEndpoint(hostPageId: string): Promise<{
|
|
119
|
+
endpoint: string;
|
|
120
|
+
engineVersion: string;
|
|
104
121
|
}>;
|
|
105
122
|
openSurface(input: SessionBrowserHostSurfaceOpenInput): Promise<SessionBrowserHostSurfaceSource>;
|
|
106
123
|
snapshot(): Promise<SessionBrowserHostSnapshot>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS, DESKTOP_BROWSER_HOST_MAX_QUEUED_EVENTS, DESKTOP_BROWSER_HOST_PATH, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION, parseDesktopBrowserHostCdpEndpointResult, parseDesktopBrowserHostClientFrame, parseDesktopBrowserHostCompletedResult, parseDesktopBrowserHostCreateResult, parseDesktopBrowserHostSurfaceFrame, parseDesktopBrowserHostSurfaceOpenResult, parseDesktopBrowserHostSnapshot, } from "@rynx-ai/protocol/desktop-browser-host";
|
|
2
|
+
import { DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS, DESKTOP_BROWSER_HOST_MAX_QUEUED_EVENTS, DESKTOP_BROWSER_HOST_PATH, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION, parseDesktopBrowserHostCdpEndpointResult, parseDesktopBrowserHostClientFrame, parseDesktopBrowserHostCompletedResult, parseDesktopBrowserHostCreateResult, parseDesktopBrowserHostPageCdpEndpointResult, parseDesktopBrowserHostSurfaceFrame, parseDesktopBrowserHostSurfaceOpenResult, parseDesktopBrowserHostSnapshot, } from "@rynx-ai/protocol/desktop-browser-host";
|
|
3
3
|
import { WebSocket, WebSocketServer } from "ws";
|
|
4
4
|
import { SessionBrowserHostError, SessionBrowserSurfaceUnsupportedError, } from "./session-browser-service.js";
|
|
5
5
|
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
@@ -20,7 +20,7 @@ export class DesktopBrowserHostRegistry {
|
|
|
20
20
|
idFactory;
|
|
21
21
|
handles = new Map();
|
|
22
22
|
connectedListeners = new Set();
|
|
23
|
-
|
|
23
|
+
leases = new Map();
|
|
24
24
|
idSequence = 0;
|
|
25
25
|
constructor(options = {}) {
|
|
26
26
|
this.commandTimeoutMilliseconds = boundedDuration(options.commandTimeoutMilliseconds ?? DEFAULT_COMMAND_TIMEOUT_MS, "commandTimeoutMilliseconds");
|
|
@@ -28,7 +28,10 @@ export class DesktopBrowserHostRegistry {
|
|
|
28
28
|
this.idFactory = options.idFactory ?? randomUUID;
|
|
29
29
|
}
|
|
30
30
|
get connected() {
|
|
31
|
-
return this.
|
|
31
|
+
return this.currentLeases().length > 0;
|
|
32
|
+
}
|
|
33
|
+
get directConnected() {
|
|
34
|
+
return this.currentLease("direct") !== undefined;
|
|
32
35
|
}
|
|
33
36
|
/** Runs after a fresh lease frame has been queued to the Host. Listener
|
|
34
37
|
* failures are isolated from the authenticated Host handshake. */
|
|
@@ -38,32 +41,68 @@ export class DesktopBrowserHostRegistry {
|
|
|
38
41
|
}
|
|
39
42
|
async createBrowser(input) {
|
|
40
43
|
const lease = this.requireLease();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
try {
|
|
45
|
+
const result = await this.request(lease, {
|
|
46
|
+
method: "browser.create",
|
|
47
|
+
sessionId: input.sessionId,
|
|
48
|
+
browserGeneration: input.browserGeneration,
|
|
49
|
+
...(input.url === undefined ? {} : { url: input.url }),
|
|
50
|
+
}, parseDesktopBrowserHostCreateResult);
|
|
51
|
+
if (!this.isCurrentLease(lease)) {
|
|
52
|
+
throw commandOutcomeError({ method: "browser.create", sessionId: input.sessionId, browserGeneration: input.browserGeneration }, "Desktop Browser Host create outcome is unknown after lease loss");
|
|
53
|
+
}
|
|
54
|
+
const key = browserKey(input.sessionId, input.browserGeneration);
|
|
55
|
+
const existing = this.handles.get(key);
|
|
56
|
+
if (existing) {
|
|
57
|
+
await this.request(lease, { method: "browser.close", sessionId: input.sessionId, browserGeneration: input.browserGeneration }, parseDesktopBrowserHostCompletedResult).catch(() => undefined);
|
|
58
|
+
throw new Error("Desktop Browser Host returned a duplicate Browser generation");
|
|
59
|
+
}
|
|
60
|
+
const handle = new DesktopBrowserHandle(this, lease, input, result.capabilities);
|
|
61
|
+
this.handles.set(key, handle);
|
|
62
|
+
return handle;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.retireOffscreenIfIdle();
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
async deleteSessionData(sessionId) {
|
|
61
|
-
const
|
|
62
|
-
|
|
69
|
+
const attemptedPresentations = new Set();
|
|
70
|
+
const failures = [];
|
|
71
|
+
let attempted = false;
|
|
72
|
+
while (true) {
|
|
73
|
+
const leases = this.currentLeases()
|
|
74
|
+
.filter((lease) => !attemptedPresentations.has(lease.presentation));
|
|
75
|
+
if (leases.length === 0)
|
|
76
|
+
break;
|
|
77
|
+
attempted = true;
|
|
78
|
+
for (const lease of leases) {
|
|
79
|
+
attemptedPresentations.add(lease.presentation);
|
|
80
|
+
}
|
|
81
|
+
const results = await Promise.allSettled(leases.map((lease) => this.request(lease, { method: "browser.delete-data", sessionId }, parseDesktopBrowserHostCompletedResult)));
|
|
82
|
+
failures.push(...results
|
|
83
|
+
.filter((result) => result.status === "rejected")
|
|
84
|
+
.map((result) => result.reason));
|
|
85
|
+
}
|
|
86
|
+
this.retireOffscreenIfIdle();
|
|
87
|
+
if (!attempted)
|
|
88
|
+
throw new DesktopBrowserHostAbsentError();
|
|
89
|
+
if (failures.length === 1)
|
|
90
|
+
throw failures[0];
|
|
91
|
+
if (failures.length > 0) {
|
|
92
|
+
throw new AggregateError(failures, "Desktop Browser Host profile cleanup failed");
|
|
93
|
+
}
|
|
63
94
|
}
|
|
64
|
-
/** Attachment-only:
|
|
95
|
+
/** Attachment-only: installs one lease per presentation role. Existing
|
|
96
|
+
* generations stay pinned to their Host while new generations prefer the
|
|
97
|
+
* direct App-window Host. */
|
|
65
98
|
acceptConnection(socket, hello, daemonInstanceId, heartbeatIntervalMilliseconds) {
|
|
66
|
-
const
|
|
99
|
+
const presentation = hello.capabilities.presentation;
|
|
100
|
+
const direct = this.currentLease("direct");
|
|
101
|
+
if (presentation === "offscreen" &&
|
|
102
|
+
direct) {
|
|
103
|
+
throw new DesktopBrowserHostProtocolError("an offscreen Desktop Browser Host is unnecessary while the direct Host is active", CLOSE_REPLACED_OR_STALE);
|
|
104
|
+
}
|
|
105
|
+
const previous = this.installedLease(presentation);
|
|
67
106
|
if (previous) {
|
|
68
107
|
this.detach(previous, hostUnavailable("Desktop Browser Host lease was replaced"));
|
|
69
108
|
closeSocket(previous.socket, CLOSE_REPLACED_OR_STALE, "Desktop Browser Host lease replaced");
|
|
@@ -72,6 +111,7 @@ export class DesktopBrowserHostRegistry {
|
|
|
72
111
|
leaseId: this.nextId("lease"),
|
|
73
112
|
daemonInstanceId,
|
|
74
113
|
hostInstanceId: hello.hostInstanceId,
|
|
114
|
+
presentation: hello.capabilities.presentation,
|
|
75
115
|
semanticPageBinding: hello.capabilities.semanticPageBinding,
|
|
76
116
|
surface: hello.capabilities.surface,
|
|
77
117
|
socket,
|
|
@@ -79,9 +119,11 @@ export class DesktopBrowserHostRegistry {
|
|
|
79
119
|
nextEventSequence: 1,
|
|
80
120
|
detached: false,
|
|
81
121
|
};
|
|
82
|
-
this.
|
|
122
|
+
this.leases.set(presentation, lease);
|
|
123
|
+
if (presentation === "direct")
|
|
124
|
+
this.retireOffscreenIfIdle();
|
|
83
125
|
queueMicrotask(() => {
|
|
84
|
-
if (this.lease
|
|
126
|
+
if (!this.isCurrentLease(lease))
|
|
85
127
|
return;
|
|
86
128
|
for (const listener of this.connectedListeners) {
|
|
87
129
|
void Promise.resolve().then(listener).catch(() => undefined);
|
|
@@ -102,8 +144,8 @@ export class DesktopBrowserHostRegistry {
|
|
|
102
144
|
}
|
|
103
145
|
/** Attachment-only: validates lease scope before routing a client frame. */
|
|
104
146
|
receive(leaseId, frame) {
|
|
105
|
-
const lease = this.
|
|
106
|
-
if (!lease ||
|
|
147
|
+
const lease = this.leaseById(leaseId);
|
|
148
|
+
if (!lease || frame.leaseId !== leaseId) {
|
|
107
149
|
throw new DesktopBrowserHostProtocolError("frame belongs to a stale lease", CLOSE_REPLACED_OR_STALE);
|
|
108
150
|
}
|
|
109
151
|
if (frame.type === "desktop.browser.host.reply") {
|
|
@@ -114,9 +156,8 @@ export class DesktopBrowserHostRegistry {
|
|
|
114
156
|
}
|
|
115
157
|
/** Attachment-only: routes a fully identity-bound image envelope to one active Surface. */
|
|
116
158
|
receiveBinary(leaseId, frame) {
|
|
117
|
-
const lease = this.
|
|
118
|
-
if (!lease || lease.
|
|
119
|
-
lease.leaseId !== leaseId || frame.leaseId !== leaseId) {
|
|
159
|
+
const lease = this.leaseById(leaseId);
|
|
160
|
+
if (!lease || lease.surface !== "binary-v1" || frame.leaseId !== leaseId) {
|
|
120
161
|
throw new DesktopBrowserHostProtocolError("binary frame belongs to a stale or unsupported lease", CLOSE_REPLACED_OR_STALE);
|
|
121
162
|
}
|
|
122
163
|
const handle = this.handles.get(browserKey(frame.sessionId, frame.browserGeneration));
|
|
@@ -127,8 +168,8 @@ export class DesktopBrowserHostRegistry {
|
|
|
127
168
|
}
|
|
128
169
|
/** Attachment-only and identity-fenced: an old close cannot clear a new lease. */
|
|
129
170
|
disconnect(leaseId, reason = "Desktop Browser Host disconnected") {
|
|
130
|
-
const lease = this.
|
|
131
|
-
if (!lease
|
|
171
|
+
const lease = this.installedLeaseById(leaseId);
|
|
172
|
+
if (!lease)
|
|
132
173
|
return;
|
|
133
174
|
this.detach(lease, hostUnavailable(reason));
|
|
134
175
|
}
|
|
@@ -140,27 +181,27 @@ export class DesktopBrowserHostRegistry {
|
|
|
140
181
|
const key = browserKey(handle.sessionId, handle.browserGeneration);
|
|
141
182
|
if (this.handles.get(key) === handle)
|
|
142
183
|
this.handles.delete(key);
|
|
184
|
+
this.retireOffscreenIfIdle();
|
|
143
185
|
}
|
|
144
186
|
createId(label) {
|
|
145
187
|
return this.nextId(label);
|
|
146
188
|
}
|
|
147
189
|
requireLease() {
|
|
148
|
-
const lease = this.
|
|
149
|
-
if (!lease
|
|
190
|
+
const lease = this.currentLease("direct") ?? this.currentLease("offscreen");
|
|
191
|
+
if (!lease)
|
|
150
192
|
throw new DesktopBrowserHostAbsentError();
|
|
151
|
-
}
|
|
152
193
|
return lease;
|
|
153
194
|
}
|
|
154
195
|
requireCurrentHandle(handle) {
|
|
155
|
-
const lease = this.
|
|
156
|
-
if (
|
|
196
|
+
const lease = this.leaseById(handle.leaseId);
|
|
197
|
+
if (!lease ||
|
|
157
198
|
this.handles.get(browserKey(handle.sessionId, handle.browserGeneration)) !== handle ||
|
|
158
199
|
!handle.available)
|
|
159
200
|
throw hostUnavailable("Desktop Browser Host generation is unavailable");
|
|
160
201
|
return lease;
|
|
161
202
|
}
|
|
162
203
|
request(lease, command, parseResult) {
|
|
163
|
-
if (this.lease
|
|
204
|
+
if (!this.isCurrentLease(lease)) {
|
|
164
205
|
return Promise.reject(command.method === "browser.create"
|
|
165
206
|
? new DesktopBrowserHostAbsentError()
|
|
166
207
|
: hostUnavailable("Desktop Browser Host lease ended"));
|
|
@@ -288,7 +329,7 @@ export class DesktopBrowserHostRegistry {
|
|
|
288
329
|
}
|
|
289
330
|
}
|
|
290
331
|
failLease(lease, code, reason) {
|
|
291
|
-
if (this.lease
|
|
332
|
+
if (this.isInstalledLease(lease))
|
|
292
333
|
this.detach(lease, hostUnavailable(reason));
|
|
293
334
|
closeSocket(lease.socket, code, reason);
|
|
294
335
|
}
|
|
@@ -296,8 +337,9 @@ export class DesktopBrowserHostRegistry {
|
|
|
296
337
|
if (lease.detached)
|
|
297
338
|
return;
|
|
298
339
|
lease.detached = true;
|
|
299
|
-
if (this.lease === lease)
|
|
300
|
-
this.lease
|
|
340
|
+
if (this.leases.get(lease.presentation) === lease) {
|
|
341
|
+
this.leases.delete(lease.presentation);
|
|
342
|
+
}
|
|
301
343
|
for (const pending of lease.pending.values()) {
|
|
302
344
|
clearTimeout(pending.timer);
|
|
303
345
|
pending.reject(commandOutcomeError(pending.command, error.message, error));
|
|
@@ -310,6 +352,41 @@ export class DesktopBrowserHostRegistry {
|
|
|
310
352
|
this.forgetHandle(handle);
|
|
311
353
|
}
|
|
312
354
|
}
|
|
355
|
+
currentLease(presentation) {
|
|
356
|
+
const lease = this.installedLease(presentation);
|
|
357
|
+
return lease && this.isCurrentLease(lease) ? lease : undefined;
|
|
358
|
+
}
|
|
359
|
+
currentLeases() {
|
|
360
|
+
return [...this.leases.values()].filter((lease) => this.isCurrentLease(lease));
|
|
361
|
+
}
|
|
362
|
+
leaseById(leaseId) {
|
|
363
|
+
return this.currentLeases().find((lease) => lease.leaseId === leaseId);
|
|
364
|
+
}
|
|
365
|
+
installedLease(presentation) {
|
|
366
|
+
const lease = this.leases.get(presentation);
|
|
367
|
+
return lease && this.isInstalledLease(lease) ? lease : undefined;
|
|
368
|
+
}
|
|
369
|
+
installedLeaseById(leaseId) {
|
|
370
|
+
return [...this.leases.values()].find((lease) => lease.leaseId === leaseId && this.isInstalledLease(lease));
|
|
371
|
+
}
|
|
372
|
+
isInstalledLease(lease) {
|
|
373
|
+
return !lease.detached && this.leases.get(lease.presentation) === lease;
|
|
374
|
+
}
|
|
375
|
+
isCurrentLease(lease) {
|
|
376
|
+
return this.isInstalledLease(lease) &&
|
|
377
|
+
lease.socket.readyState === WebSocket.OPEN;
|
|
378
|
+
}
|
|
379
|
+
retireOffscreenIfIdle() {
|
|
380
|
+
if (!this.directConnected)
|
|
381
|
+
return;
|
|
382
|
+
const offscreen = this.currentLease("offscreen");
|
|
383
|
+
if (!offscreen || offscreen.pending.size > 0)
|
|
384
|
+
return;
|
|
385
|
+
if ([...this.handles.values()].some((handle) => handle.available && handle.leaseId === offscreen.leaseId))
|
|
386
|
+
return;
|
|
387
|
+
this.detach(offscreen, hostUnavailable("Desktop Browser offscreen Host became idle"));
|
|
388
|
+
closeSocket(offscreen.socket, CLOSE_REPLACED_OR_STALE, "Desktop Browser offscreen Host became idle");
|
|
389
|
+
}
|
|
313
390
|
}
|
|
314
391
|
export class DesktopBrowserHostProtocolError extends Error {
|
|
315
392
|
closeCode;
|
|
@@ -436,6 +513,9 @@ class DesktopBrowserHandle {
|
|
|
436
513
|
getCdpEndpoint() {
|
|
437
514
|
return this.command({ method: "browser.cdp-endpoint", ...this.generationScope() }, parseDesktopBrowserHostCdpEndpointResult);
|
|
438
515
|
}
|
|
516
|
+
getPageCdpEndpoint(hostPageId) {
|
|
517
|
+
return this.command({ method: "page.cdp-endpoint", ...this.pageScope(hostPageId) }, parseDesktopBrowserHostPageCdpEndpointResult);
|
|
518
|
+
}
|
|
439
519
|
async openSurface(input) {
|
|
440
520
|
if (this.lease.surface !== "binary-v1") {
|
|
441
521
|
throw new SessionBrowserSurfaceUnsupportedError();
|
|
@@ -828,7 +908,9 @@ function commandOutcomeError(command, message, cause) {
|
|
|
828
908
|
return new SessionBrowserHostError(readOnly ? "unavailable" : "outcome_unknown", message, cause === undefined ? undefined : { cause });
|
|
829
909
|
}
|
|
830
910
|
function isReadOnlyCommand(command) {
|
|
831
|
-
return command.method === "browser.snapshot" ||
|
|
911
|
+
return command.method === "browser.snapshot" ||
|
|
912
|
+
command.method === "browser.cdp-endpoint" ||
|
|
913
|
+
command.method === "page.cdp-endpoint";
|
|
832
914
|
}
|
|
833
915
|
class BoundedAsyncQueue {
|
|
834
916
|
capacity;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Server as HttpServer } from "node:http";
|
|
2
|
+
import { DIRECT_RUNTIME_BROWSER_INSPECT_PATH } from "@rynx-ai/protocol/direct-runtime";
|
|
3
|
+
import { type RuntimeBrowserInspectClosedReason, type RuntimeBrowserInspectMessage, type RuntimeBrowserInspectTarget } from "@rynx-ai/protocol/runtime-browser-inspect";
|
|
4
|
+
import { WebSocket } from "ws";
|
|
5
|
+
import type { DirectRuntimeAuthenticator, DirectRuntimePeer } from "./direct-runtime-server.js";
|
|
6
|
+
export type DirectRuntimeBrowserInspectTransportCloseReason = "client_closed" | "transport_error" | "grant_revoked" | "server_shutdown";
|
|
7
|
+
/**
|
|
8
|
+
* A resolved local page stream. Implementations connect only after the host has
|
|
9
|
+
* resolved the semantic tuple to its own loopback CDP endpoint.
|
|
10
|
+
*/
|
|
11
|
+
export interface DirectRuntimeBrowserInspectAttachment {
|
|
12
|
+
readonly messages: AsyncIterable<RuntimeBrowserInspectMessage>;
|
|
13
|
+
send(message: RuntimeBrowserInspectMessage): void | Promise<void>;
|
|
14
|
+
close(reason: DirectRuntimeBrowserInspectTransportCloseReason): void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Deliberately exposes no URL/host/port input: callers can request only an
|
|
18
|
+
* exact Session generation page owned by this runtime.
|
|
19
|
+
*/
|
|
20
|
+
export interface DirectRuntimeBrowserInspectHost {
|
|
21
|
+
open(target: RuntimeBrowserInspectTarget, context: {
|
|
22
|
+
peer: DirectRuntimePeer;
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
}): DirectRuntimeBrowserInspectAttachment | Promise<DirectRuntimeBrowserInspectAttachment>;
|
|
25
|
+
}
|
|
26
|
+
export declare class DirectRuntimeBrowserInspectHostError extends Error {
|
|
27
|
+
readonly reason: RuntimeBrowserInspectClosedReason;
|
|
28
|
+
constructor(reason: RuntimeBrowserInspectClosedReason, message: string, options?: ErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
export interface DirectRuntimeBrowserInspectServerLimits {
|
|
31
|
+
authenticationTimeoutMs: number;
|
|
32
|
+
maxConnections: number;
|
|
33
|
+
maxUnauthenticatedConnectionsPerSource: number;
|
|
34
|
+
maxAuthenticationAttemptsPerSource: number;
|
|
35
|
+
authenticationAttemptWindowMs: number;
|
|
36
|
+
maxTrackedAuthenticationSources: number;
|
|
37
|
+
maxConnectionsPerGrant: number;
|
|
38
|
+
maxQueuedBytes: number;
|
|
39
|
+
maxBufferedBytes: number;
|
|
40
|
+
heartbeatIntervalMs: number;
|
|
41
|
+
peerLivenessTimeoutMs: number;
|
|
42
|
+
shutdownDeadlineMs: number;
|
|
43
|
+
}
|
|
44
|
+
export interface DirectRuntimeBrowserInspectConnectionHub {
|
|
45
|
+
registerWithinLimit(grantId: string, connection: {
|
|
46
|
+
ws: WebSocket;
|
|
47
|
+
close(code: number, reason: string): void;
|
|
48
|
+
}, maxConnections: number): "registered" | "revoked" | "capacity";
|
|
49
|
+
unregister(connection: {
|
|
50
|
+
ws: WebSocket;
|
|
51
|
+
close(code: number, reason: string): void;
|
|
52
|
+
}): void;
|
|
53
|
+
}
|
|
54
|
+
export interface AttachDirectRuntimeBrowserInspectServerOptions {
|
|
55
|
+
e2ee: {
|
|
56
|
+
serverPublicKey: string;
|
|
57
|
+
deriveSharedSecret(clientEphemeralPublicKey: string): Uint8Array;
|
|
58
|
+
};
|
|
59
|
+
authenticator: DirectRuntimeAuthenticator;
|
|
60
|
+
host: DirectRuntimeBrowserInspectHost;
|
|
61
|
+
isPeerActive(peer: DirectRuntimePeer): boolean | Promise<boolean>;
|
|
62
|
+
connectionHub: DirectRuntimeBrowserInspectConnectionHub;
|
|
63
|
+
limits?: Partial<DirectRuntimeBrowserInspectServerLimits>;
|
|
64
|
+
onError?: (error: unknown, context: string) => void;
|
|
65
|
+
}
|
|
66
|
+
export interface DirectRuntimeBrowserInspectServer {
|
|
67
|
+
readonly path: typeof DIRECT_RUNTIME_BROWSER_INSPECT_PATH;
|
|
68
|
+
readonly activeConnectionCount: number;
|
|
69
|
+
shutdown(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
/** Attach only the V5 dedicated Browser Inspect route to a Direct listener. */
|
|
72
|
+
export declare function attachDirectRuntimeBrowserInspectServer(server: HttpServer, options: AttachDirectRuntimeBrowserInspectServerOptions): DirectRuntimeBrowserInspectServer;
|