@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.
Files changed (39) hide show
  1. package/dist/browser-surface-ws.d.ts +2 -0
  2. package/dist/browser-surface-ws.js +1 -0
  3. package/dist/control-api.d.ts +17 -0
  4. package/dist/control-api.js +809 -9
  5. package/dist/control-web/assets/{highlighted-body-OFNGDK62-e-w61YLy.js → highlighted-body-OFNGDK62-DHu2g-rk.js} +1 -1
  6. package/dist/control-web/assets/index-B6Pb5j9M.js +666 -0
  7. package/dist/control-web/assets/index-BU-_Qud_.css +32 -0
  8. package/dist/control-web/assets/{mermaid-GHXKKRXX-DZn7Hbr2.js → mermaid-GHXKKRXX-CSYzOmBR.js} +3 -3
  9. package/dist/control-web/index.html +2 -2
  10. package/dist/desktop-browser-host.d.ts +20 -3
  11. package/dist/desktop-browser-host.js +126 -44
  12. package/dist/direct-runtime-browser-inspect-server.d.ts +72 -0
  13. package/dist/direct-runtime-browser-inspect-server.js +749 -0
  14. package/dist/direct-runtime-server.d.ts +6 -0
  15. package/dist/direct-runtime-server.js +29 -1
  16. package/dist/emulator-surface-ws.d.ts +2 -0
  17. package/dist/emulator-surface-ws.js +1 -0
  18. package/dist/machine-session-service.d.ts +88 -5
  19. package/dist/machine-session-service.js +283 -20
  20. package/dist/remote-runtime-dispatcher.d.ts +1 -1
  21. package/dist/remote-runtime-dispatcher.js +25 -2
  22. package/dist/remote-runtime-session-projection.js +10 -0
  23. package/dist/runtime-web-auth.d.ts +5 -0
  24. package/dist/runtime-web-auth.js +43 -3
  25. package/dist/server.d.ts +78 -8
  26. package/dist/server.js +42 -17
  27. package/dist/session-browser-service.d.ts +30 -0
  28. package/dist/session-browser-service.js +89 -3
  29. package/dist/session-browser-surface-coordinator.d.ts +4 -3
  30. package/dist/session-browser-surface-coordinator.js +374 -268
  31. package/dist/session-runtime-index.d.ts +6 -0
  32. package/dist/session-runtime-index.js +10 -0
  33. package/dist/session-terminal-host.d.ts +6 -1
  34. package/dist/session-terminal-host.js +18 -0
  35. package/dist/terminal-ws.d.ts +3 -0
  36. package/dist/terminal-ws.js +1 -0
  37. package/package.json +12 -7
  38. package/dist/control-web/assets/index-BQ7XVBKO.css +0 -32
  39. 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-ClpzuBJx.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-BQ7XVBKO.css">
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 lease?;
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: atomically installs the newest authenticated socket lease. */
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
- lease;
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.lease !== undefined && !this.lease.detached;
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
- const result = await this.request(lease, {
42
- method: "browser.create",
43
- sessionId: input.sessionId,
44
- browserGeneration: input.browserGeneration,
45
- ...(input.url === undefined ? {} : { url: input.url }),
46
- }, parseDesktopBrowserHostCreateResult);
47
- if (this.lease !== lease || lease.detached) {
48
- throw commandOutcomeError({ method: "browser.create", sessionId: input.sessionId, browserGeneration: input.browserGeneration }, "Desktop Browser Host create outcome is unknown after lease loss");
49
- }
50
- const key = browserKey(input.sessionId, input.browserGeneration);
51
- const existing = this.handles.get(key);
52
- if (existing) {
53
- await this.request(lease, { method: "browser.close", sessionId: input.sessionId, browserGeneration: input.browserGeneration }, parseDesktopBrowserHostCompletedResult).catch(() => undefined);
54
- throw new Error("Desktop Browser Host returned a duplicate Browser generation");
55
- }
56
- const handle = new DesktopBrowserHandle(this, lease, input, result.capabilities);
57
- this.handles.set(key, handle);
58
- return handle;
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 lease = this.requireLease();
62
- await this.request(lease, { method: "browser.delete-data", sessionId }, parseDesktopBrowserHostCompletedResult);
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: atomically installs the newest authenticated socket lease. */
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 previous = this.lease;
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.lease = lease;
122
+ this.leases.set(presentation, lease);
123
+ if (presentation === "direct")
124
+ this.retireOffscreenIfIdle();
83
125
  queueMicrotask(() => {
84
- if (this.lease !== lease || lease.detached)
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.lease;
106
- if (!lease || lease.detached || lease.leaseId !== leaseId || frame.leaseId !== leaseId) {
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.lease;
118
- if (!lease || lease.detached || lease.surface !== "binary-v1" ||
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.lease;
131
- if (!lease || lease.leaseId !== leaseId)
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.lease;
149
- if (!lease || lease.detached || lease.socket.readyState !== WebSocket.OPEN) {
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.requireLease();
156
- if (handle.leaseId !== lease.leaseId ||
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 !== lease || lease.detached || lease.socket.readyState !== WebSocket.OPEN) {
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 === 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 = undefined;
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" || command.method === "browser.cdp-endpoint";
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;