@parall/daemon 1.33.0 → 1.35.0

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.
@@ -1,3 +1,4 @@
1
+ import { type ViewerTurnConfig } from './browser-viewer-streamer.js';
1
2
  type BrowserProfileStatus = 'running' | 'stopped' | 'error';
2
3
  interface BrowserProfileManagerOptions {
3
4
  homeDir: string;
@@ -22,6 +23,7 @@ export declare class BrowserProfileManager {
22
23
  private readonly ensuredAccounts;
23
24
  private readonly ensuringAccounts;
24
25
  private readonly reportedStatuses;
26
+ private readonly viewer;
25
27
  constructor(opts: BrowserProfileManagerOptions);
26
28
  invoke({ profileId, account, command, input }: BrowserInvokeRequest): Promise<unknown>;
27
29
  ensureRuntime(profileId: string): Promise<void>;
@@ -29,6 +31,14 @@ export declare class BrowserProfileManager {
29
31
  stopProfile(profileId: string): Promise<void>;
30
32
  resetProfile(profileId: string): Promise<void>;
31
33
  stop(): Promise<void>;
34
+ /**
35
+ * Viewer control command entrypoint — delegates to BrowserViewerStreamer
36
+ * (browser-viewer-streamer.ts), which owns the per-profile bb-viewer
37
+ * subprocesses and viewer tab navigation.
38
+ */
39
+ handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
40
+ /** Read cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
41
+ private cdpEndpoint;
32
42
  private prepareOpenProfile;
33
43
  private openProfileTabOnce;
34
44
  private resetProfileOnce;
@@ -54,6 +64,9 @@ export declare class BrowserProfileManager {
54
64
  private findAccountTabOnHost;
55
65
  private ensureAccount;
56
66
  private ensureAccountOwnedTab;
67
+ private profileAlreadyHasOpenTab;
68
+ private hasAccountOwnedTab;
69
+ private findAccountTabMatchingUrl;
57
70
  private reportStatus;
58
71
  private sendCommand;
59
72
  private ensureDaemon;
@@ -1 +1 @@
1
- {"version":3,"file":"browser-profile-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-manager.ts"],"names":[],"mappings":"AAQA,KAAK,oBAAoB,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5D,UAAU,4BAA4B;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7F;AASD,UAAU,oBAAoB;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AA2BD,qBAAa,qBAAqB;IAQpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,QAAQ,CAA4C;IAC5D,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAuC;IACxE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;gBAEjC,IAAI,EAAE,4BAA4B;IAIzD,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IA0CtF,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/C,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmDhE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ7C,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBb,kBAAkB;YAelB,kBAAkB;YAgBlB,gBAAgB;YAOhB,oBAAoB;YAKpB,yBAAyB;YAezB,aAAa;YAWb,mBAAmB;IAqBjC;;;;;;;;;;;;OAYG;YACW,uBAAuB;IAiCrC,oFAAoF;YACtE,oBAAoB;YAuBpB,aAAa;YAiCb,qBAAqB;IASnC,OAAO,CAAC,YAAY;YAON,WAAW;YAaX,YAAY;YAmBZ,kBAAkB;YAelB,aAAa;YAUb,iBAAiB;YAsBjB,WAAW;YAmDX,IAAI;CA2BnB"}
1
+ {"version":3,"file":"browser-profile-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-manager.ts"],"names":[],"mappings":"AAMA,OAAO,EAAyB,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAG5F,KAAK,oBAAoB,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5D,UAAU,4BAA4B;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7F;AAeD,UAAU,oBAAoB;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AA0BD,qBAAa,qBAAqB;IAYpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,QAAQ,CAA4C;IAC5D,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAuC;IACxE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;IAI9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAElB,IAAI,EAAE,4BAA4B;IAWzD,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IA0CtF,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/C,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBhE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU7C,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB3B;;;;OAIG;IACG,mBAAmB,CACvB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAInC,qEAAqE;YACvD,WAAW;YAWX,kBAAkB;YAYlB,kBAAkB;YAUlB,gBAAgB;YAOhB,oBAAoB;YAKpB,yBAAyB;YAezB,aAAa;YAWb,mBAAmB;IAqBjC;;;;;;;;;;;;OAYG;YACW,uBAAuB;IAiCrC,oFAAoF;YACtE,oBAAoB;YAuBpB,aAAa;YAiCb,qBAAqB;YAKrB,wBAAwB;YAQxB,kBAAkB;YAQlB,yBAAyB;IAqBvC,OAAO,CAAC,YAAY;YAON,WAAW;YAaX,YAAY;YAmBZ,kBAAkB;YAUlB,aAAa;YAUb,iBAAiB;YAsBjB,WAAW;YAmDX,IAAI;CA2BnB"}
@@ -2,9 +2,10 @@ import { spawn } from 'node:child_process';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import { existsSync, mkdirSync, readFileSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
- import * as net from 'node:net';
6
5
  import * as path from 'node:path';
7
6
  import { fileURLToPath } from 'node:url';
7
+ import { BrowserViewerStreamer } from './browser-viewer-streamer.js';
8
+ import { findFreePort, formatErrorForLog, sleep } from './subprocess.js';
8
9
  const BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15_000;
9
10
  class BrowserCommandError extends Error {
10
11
  method;
@@ -24,9 +25,20 @@ export class BrowserProfileManager {
24
25
  ensuredAccounts = new Set();
25
26
  ensuringAccounts = new Map();
26
27
  reportedStatuses = new Map();
28
+ // Live viewer orchestration (bb-viewer streamer subprocesses + viewer nav)
29
+ // lives in BrowserViewerStreamer; the manager hands it the account-scoped
30
+ // bb-browser command path via the host interface.
31
+ viewer;
27
32
  constructor(opts) {
28
33
  this.opts = opts;
29
34
  mkdirSync(this.opts.homeDir, { recursive: true });
35
+ this.viewer = new BrowserViewerStreamer({
36
+ log: this.opts.log,
37
+ sendBrowserCommand: (request) => this.sendCommand(request),
38
+ ensureAccount: (account) => this.ensureAccount(account),
39
+ withDaemonRecovery: (operation, label) => this.withDaemonRecovery(operation, label),
40
+ cdpEndpoint: () => this.cdpEndpoint(),
41
+ });
30
42
  }
31
43
  async invoke({ profileId, account, command, input }) {
32
44
  if (!profileId)
@@ -88,32 +100,14 @@ export class BrowserProfileManager {
88
100
  // chatty invoke path while making lifecycle outcomes authoritative.
89
101
  this.reportedStatuses.delete(profileId);
90
102
  const url = normalizeStartUrl(startUrl);
91
- let prepareResult = {
92
- accountCreated: false,
93
- accountExistedBeforePrepare: true,
94
- };
95
- let firstAccountExistedBeforePrepare;
96
- let recoveredDuringPrepare = false;
97
- try {
98
- prepareResult = await this.withDaemonRecovery(() => this.prepareOpenProfile(profileId, url, (exists) => {
99
- firstAccountExistedBeforePrepare ??= exists;
100
- }), `prepare open profile ${profileId}`, () => {
101
- recoveredDuringPrepare = true;
102
- });
103
- }
104
- catch (err) {
105
- const message = err instanceof Error ? err.message : String(err);
106
- this.reportStatus(profileId, 'error', message);
107
- throw err;
108
- }
109
103
  try {
110
- await this.openProfileTabOnce(profileId, url, prepareResult.accountCreated, recoveredDuringPrepare, firstAccountExistedBeforePrepare ?? prepareResult.accountExistedBeforePrepare);
104
+ await this.withDaemonRecovery(async () => {
105
+ const prepareResult = await this.prepareOpenProfile(profileId, url);
106
+ await this.openProfileTabOnce(profileId, url, prepareResult.accountCreated);
107
+ }, `open profile ${profileId}`);
111
108
  this.reportStatus(profileId, 'running');
112
109
  }
113
110
  catch (err) {
114
- if (isRecoverableBrowserDaemonError(err)) {
115
- await this.restartDaemon(`open profile ${profileId}`, err);
116
- }
117
111
  const message = err instanceof Error ? err.message : String(err);
118
112
  this.reportStatus(profileId, 'error', message);
119
113
  throw err;
@@ -123,6 +117,8 @@ export class BrowserProfileManager {
123
117
  if (!profileId)
124
118
  throw new Error('browser profile id is required');
125
119
  this.reportedStatuses.delete(profileId); // see openProfile — lifecycle outcomes always report
120
+ // A stopped profile can have no live viewer — tear down its streamer.
121
+ this.viewer.killProfileStreamer(profileId);
126
122
  // bb-browser-pro does not expose a single-account "dispose context" command.
127
123
  // Server-side stopped status is the authority that gates future invokes.
128
124
  this.reportStatus(profileId, 'stopped');
@@ -131,6 +127,8 @@ export class BrowserProfileManager {
131
127
  if (!profileId)
132
128
  throw new Error('browser profile id is required');
133
129
  this.reportedStatuses.delete(profileId); // see openProfile — lifecycle outcomes always report
130
+ // Reset destroys the account context the viewer is connected to — kill it.
131
+ this.viewer.killProfileStreamer(profileId);
134
132
  try {
135
133
  await this.withDaemonRecovery(() => this.resetProfileOnce(profileId), `reset profile ${profileId}`);
136
134
  this.reportStatus(profileId, 'stopped');
@@ -142,6 +140,8 @@ export class BrowserProfileManager {
142
140
  }
143
141
  }
144
142
  async stop() {
143
+ // Tear down all per-profile bb-viewer streamers before the bb-browser daemon.
144
+ this.viewer.shutdown();
145
145
  const daemon = this.daemon;
146
146
  this.daemon = null;
147
147
  this.starting = null;
@@ -160,23 +160,35 @@ export class BrowserProfileManager {
160
160
  daemon.child.kill('SIGTERM');
161
161
  }
162
162
  }
163
- async prepareOpenProfile(profileId, url, recordAccountExists) {
164
- const accountExistedBeforePrepare = await this.accountExists(profileId);
165
- recordAccountExists?.(accountExistedBeforePrepare);
163
+ /**
164
+ * Viewer control command entrypoint — delegates to BrowserViewerStreamer
165
+ * (browser-viewer-streamer.ts), which owns the per-profile bb-viewer
166
+ * subprocesses and viewer tab navigation.
167
+ */
168
+ async handleViewerCommand(profileId, sessionId, command, input, turn) {
169
+ return this.viewer.handleViewerCommand(profileId, sessionId, command, input, turn);
170
+ }
171
+ /** Read cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
172
+ async cdpEndpoint() {
173
+ const daemon = await this.ensureDaemon();
174
+ const status = await this.post('/status', daemon, undefined, 5_000);
175
+ const host = status.cdpHost?.trim();
176
+ const port = status.cdpPort;
177
+ if (!host || typeof port !== 'number' || !Number.isFinite(port)) {
178
+ throw new Error('bb-browser /status did not report a CDP endpoint');
179
+ }
180
+ return { host, port };
181
+ }
182
+ async prepareOpenProfile(profileId, url) {
166
183
  const accountCreated = await this.ensureAccount(profileId, url !== 'about:blank' ? url : undefined);
167
184
  await this.sendCommand({ method: 'tab_list', account: profileId });
168
- return { accountCreated, accountExistedBeforePrepare };
185
+ return { accountCreated };
169
186
  }
170
- async openProfileTabOnce(profileId, url, accountCreated, recoveredDuringPrepare, accountExistedBeforePrepare) {
187
+ async openProfileTabOnce(profileId, url, accountCreated) {
171
188
  if (accountCreated)
172
189
  return;
173
- if (recoveredDuringPrepare && !accountExistedBeforePrepare) {
174
- if (url === 'about:blank')
175
- return;
176
- const host = safeUrlHost(url);
177
- if (host && (await this.findAccountTabOnHost(profileId, host)) !== undefined)
178
- return;
179
- }
190
+ if (await this.profileAlreadyHasOpenTab(profileId, url))
191
+ return;
180
192
  await this.sendCommand({ method: 'tab_new', account: profileId, url });
181
193
  }
182
194
  async resetProfileOnce(profileId) {
@@ -332,13 +344,46 @@ export class BrowserProfileManager {
332
344
  }
333
345
  }
334
346
  async ensureAccountOwnedTab(account) {
347
+ if (await this.hasAccountOwnedTab(account))
348
+ return;
349
+ await this.sendCommand({ method: 'tab_new', account, url: 'about:blank' });
350
+ }
351
+ async profileAlreadyHasOpenTab(profileId, url) {
352
+ // Lifecycle `open` can be delivered more than once (WS + pending reconcile,
353
+ // retry, or duplicate user action). Treat only the requested target URL as
354
+ // already-open; a same-host different path is still a distinct user intent.
355
+ if (url === 'about:blank')
356
+ return this.hasAccountOwnedTab(profileId);
357
+ return this.findAccountTabMatchingUrl(profileId, url);
358
+ }
359
+ async hasAccountOwnedTab(account) {
335
360
  const list = await this.sendCommand({ method: 'tab_list', account });
336
361
  const tabs = Array.isArray(list.tabs)
337
362
  ? list.tabs
338
363
  : [];
339
- if (tabs.some((tab) => tab?.account === account))
340
- return;
341
- await this.sendCommand({ method: 'tab_new', account, url: 'about:blank' });
364
+ return tabs.some((tab) => tab?.account === account);
365
+ }
366
+ async findAccountTabMatchingUrl(account, url) {
367
+ const expected = comparableUrl(url);
368
+ if (!expected)
369
+ return false;
370
+ const list = await this.sendCommand({ method: 'tab_list', account });
371
+ const tabs = Array.isArray(list.tabs)
372
+ ? list.tabs
373
+ : [];
374
+ for (const tab of tabs) {
375
+ if (tab?.account !== account || typeof tab.url !== 'string')
376
+ continue;
377
+ const actual = comparableUrl(tab.url);
378
+ if (!actual)
379
+ continue;
380
+ const sameResource = hostsMatch(actual.host, expected.host) && actual.target === expected.target;
381
+ const sameProtocol = actual.protocol === expected.protocol;
382
+ const redirectedUpgrade = expected.protocol === 'http:' && actual.protocol === 'https:';
383
+ if (sameResource && (sameProtocol || redirectedUpgrade))
384
+ return true;
385
+ }
386
+ return false;
342
387
  }
343
388
  reportStatus(profileId, status, errorMsg) {
344
389
  const marker = `${status}\u0000${errorMsg ?? ''}`;
@@ -374,7 +419,7 @@ export class BrowserProfileManager {
374
419
  this.starting = null;
375
420
  }
376
421
  }
377
- async withDaemonRecovery(operation, label, onRecovered) {
422
+ async withDaemonRecovery(operation, label) {
378
423
  try {
379
424
  return await operation();
380
425
  }
@@ -382,7 +427,6 @@ export class BrowserProfileManager {
382
427
  if (!isRecoverableBrowserDaemonError(err))
383
428
  throw err;
384
429
  await this.restartDaemon(label, err);
385
- onRecovered?.();
386
430
  return operation();
387
431
  }
388
432
  }
@@ -403,7 +447,7 @@ export class BrowserProfileManager {
403
447
  this.starting = null;
404
448
  this.ensuredAccounts.clear();
405
449
  this.ensuringAccounts.clear();
406
- this.opts.log.warn(`[bb-browser] ${label} hit a disconnected Chrome/CDP session; restarting bb-browser-daemon (${formatErrorForLog(err)})`);
450
+ this.opts.log.warn(`[bb-browser] ${label} hit an unavailable Chrome/CDP page target; restarting bb-browser-daemon (${formatErrorForLog(err)})`);
407
451
  if (!daemon)
408
452
  return;
409
453
  try {
@@ -534,21 +578,23 @@ function isRecoverableBrowserDaemonError(err) {
534
578
  const lower = message.toLowerCase();
535
579
  return (lower.includes('chrome not connected') ||
536
580
  lower.includes('cdp at 127.0.0.1') ||
581
+ lower.includes('no page target found') ||
537
582
  (lower.includes('bb-browser /command returned 503') &&
538
583
  (lower.includes('chrome') || lower.includes('cdp'))));
539
584
  }
540
- function safeUrlHost(url) {
585
+ function comparableUrl(url) {
541
586
  try {
542
- return new URL(url).host;
587
+ const parsed = new URL(url);
588
+ return {
589
+ protocol: parsed.protocol,
590
+ host: parsed.host,
591
+ target: `${parsed.pathname}${parsed.search}${parsed.hash}`,
592
+ };
543
593
  }
544
594
  catch {
545
- return '';
595
+ return null;
546
596
  }
547
597
  }
548
- function formatErrorForLog(err) {
549
- const message = err instanceof Error ? err.message : String(err);
550
- return message.replace(/\s+/g, ' ').slice(0, 300);
551
- }
552
598
  function resolveBbBrowserDaemonPath() {
553
599
  // Bundle channels (CDN self-update, desktop, npm bin — all run the flat
554
600
  // esbuild artifact with NO node_modules) ship bb-browser as a sibling flat
@@ -569,27 +615,6 @@ function resolveBbBrowserDaemonPath() {
569
615
  }
570
616
  return full;
571
617
  }
572
- async function findFreePort() {
573
- return new Promise((resolve, reject) => {
574
- const server = net.createServer();
575
- server.unref();
576
- server.on('error', reject);
577
- server.listen(0, '127.0.0.1', () => {
578
- const address = server.address();
579
- server.close(() => {
580
- if (address && typeof address === 'object') {
581
- resolve(address.port);
582
- }
583
- else {
584
- reject(new Error('failed to allocate free port'));
585
- }
586
- });
587
- });
588
- });
589
- }
590
618
  function randomToken() {
591
619
  return randomBytes(16).toString('hex');
592
620
  }
593
- function sleep(ms) {
594
- return new Promise((resolve) => setTimeout(resolve, ms));
595
- }
@@ -0,0 +1,188 @@
1
+ /** Optional managed TURN relay config carried on a `stream.start` command. */
2
+ export interface ViewerTurnConfig {
3
+ url: string;
4
+ username?: string;
5
+ credential?: string;
6
+ }
7
+ /**
8
+ * The slice of BrowserProfileManager the viewer streamer depends on: the
9
+ * account-scoped bb-browser /command path and the daemon lifecycle helpers.
10
+ * One-directional — the manager constructs the streamer; the streamer never
11
+ * imports the manager.
12
+ */
13
+ export interface ViewerStreamerHost {
14
+ log: {
15
+ info(msg: string): void;
16
+ warn(msg: string): void;
17
+ error(msg: string): void;
18
+ };
19
+ /** Account-scoped bb-browser `/command` (BrowserProfileManager.sendCommand). */
20
+ sendBrowserCommand(request: Record<string, unknown> & {
21
+ method: string;
22
+ account?: string;
23
+ }): Promise<Record<string, unknown>>;
24
+ ensureAccount(account: string): Promise<boolean>;
25
+ withDaemonRecovery<T>(operation: () => Promise<T>, label: string): Promise<T>;
26
+ /** cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
27
+ cdpEndpoint(): Promise<{
28
+ host: string;
29
+ port: number;
30
+ }>;
31
+ }
32
+ export declare class BrowserViewerStreamer {
33
+ private readonly host;
34
+ private readonly streamers;
35
+ private stopping;
36
+ constructor(host: ViewerStreamerHost);
37
+ /**
38
+ * Handle a viewer control command for a profile. Throws on any failure (the
39
+ * supervisor maps a throw to a `{error:{message}}` reply); never returns a
40
+ * partial result. `data.input` / `data.turn` / `data.session_id` come from
41
+ * MachineBrowserProfileViewerData.
42
+ */
43
+ handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
44
+ /**
45
+ * stream.start — spawn a FRESH bb-viewer for this profile (killing any prior
46
+ * one), resolve the profile's account-scoped page-target CDP ws URL, and run
47
+ * the streamer's `connect` command. Records sessionId on the streamer entry.
48
+ */
49
+ private viewerStreamStart;
50
+ /**
51
+ * stream.answer — apply the client's WebRTC answer to the profile's streamer.
52
+ * Rejects if the streamer's sessionId no longer matches (a stale viewer
53
+ * session from a superseded stream.start).
54
+ */
55
+ private viewerStreamAnswer;
56
+ /**
57
+ * stream.close — best-effort stop the streamer, then kill + remove it. A
58
+ * stale session's close (independent HTTP legs can arrive out of order after
59
+ * a retry) must NOT tear down a successor session's live streamer, so a
60
+ * mismatched sessionId is ignored. An EMPTY sessionId gets the same stale
61
+ * treatment — a client closed mid-handshake never received a session id, and
62
+ * its unscoped close racing the next session's stream.start would kill that
63
+ * session's live streamer (React StrictMode's dev double-mount exercises
64
+ * exactly this ordering). The only unscoped close is an explicit
65
+ * `{force:true}` input, reserved for a server-initiated admin disconnect.
66
+ */
67
+ private viewerStreamClose;
68
+ /**
69
+ * stream.switch — point the existing streamer at a different account-scoped
70
+ * tab. input {tab} is the bb-browser short tab id / target id. Guarded by
71
+ * sessionId like stream.answer: a superseded session must not steer the
72
+ * current streamer, and an EMPTY sessionId is rejected the same way — every
73
+ * real caller has its session id by the time it can switch (it arrives with
74
+ * the stream.start response), so a session-less switch is by definition not
75
+ * the current viewer.
76
+ */
77
+ private viewerStreamSwitch;
78
+ /**
79
+ * close — close an account tab via bb-browser, then, if the closed tab was
80
+ * the one being streamed, re-point the streamer at the profile's next active
81
+ * tab. Without the re-switch the streamer stays bound to a destroyed CDP
82
+ * target and the viewer goes black. Returns `streamed_tab_id` when a
83
+ * re-switch happened so the client can adopt the new active tab.
84
+ */
85
+ private viewerCloseTab;
86
+ /**
87
+ * Bring a CDP page target to the foreground via Chrome's DevTools HTTP
88
+ * endpoint (`/json/activate/{targetId}`, same host:port as the CDP ws). Chrome
89
+ * does not composite backgrounded tabs, so `Page.startScreencast` on a
90
+ * background tab yields zero frames (black viewer). Best-effort: a failure
91
+ * must not block the stream. host:port + targetId are parsed from the cdpUrl
92
+ * (`ws://host:port/devtools/page/<targetId>`) to avoid an extra /status call.
93
+ */
94
+ private bringTargetToFront;
95
+ /**
96
+ * Forward a navigation command (tab_list/tab_new/open/reload/back/forward/
97
+ * close) to bb-browser scoped to the profile's account. This lets the human
98
+ * in the live viewer navigate and handle OAuth popups directly. Reuses the
99
+ * same account-scoped sendCommand path the agent invokes use.
100
+ *
101
+ * SECURITY — the `account` field alone does NOT scope tab addressing:
102
+ * bb-browser's ensurePageTarget resolves a client-supplied tab/tabId against
103
+ * EVERY page target in the shared Chrome (all accounts), and tab_list
104
+ * returns every tab with `account` as a mere annotation (verified in
105
+ * @pinixai/bb-browser-pro@0.15.0 dist/daemon.js ensurePageTarget/tab_list).
106
+ * On a multi-profile host that would let one profile's viewer list,
107
+ * navigate, and close other profiles' logged-in tabs. So before forwarding:
108
+ * (a) any tab ref must resolve through accountTabTargetId, which throws on
109
+ * tabs the profile's account does not own; (b) ref-less tab-addressed
110
+ * commands are pinned to the profile's own active tab instead of
111
+ * bb-browser's account-blind global current tab; (c) tab_list output is
112
+ * filtered to the profile's own rows; and (d) open/tab_new URLs must be
113
+ * http(s)/about:blank — file:// or chrome:// would read the host
114
+ * filesystem / browser internals straight into the video stream.
115
+ */
116
+ private viewerForwardNav;
117
+ /**
118
+ * Resolve `ws://<cdpHost>:<cdpPort>/devtools/page/<targetId>` for a PAGE
119
+ * target owned by THIS profile's account — NOT bb-browser's global current
120
+ * tab (which `getCurrentTabCdpUrl`/`getTabCdpUrl` in the bundled daemon use
121
+ * via `cdp.currentTargetId`, an account-blind global; see daemon.js
122
+ * line ~10389/10410). When `tabRef` is given (stream.switch) we resolve that
123
+ * specific account tab; otherwise the profile's active page tab.
124
+ *
125
+ * Evidence from the installed @pinixai/bb-browser-pro@0.15.0 dist/daemon.js:
126
+ * - GET /status (handleStatus, line ~1659-1668) returns `cdpHost` + `cdpPort`
127
+ * — the CDP host/port the daemon's Chrome listens on.
128
+ * - The CDP page ws URL format is `ws://<cdp.host>:<cdp.port>/devtools/page/<page.id>`
129
+ * (getCurrentTabCdpUrl, line ~10392), where `page.id` is the full CDP
130
+ * targetId (getTargets maps `t.targetId` → `.id`, line ~2461-2466).
131
+ * - tab_list (command handler, line ~1041-1059) returns per-tab
132
+ * `{ tabId: t.id, tab: <shortId>, account: <accountName>, url, ... }`, so
133
+ * `tabId` is exactly the full targetId for the devtools URL and `account`
134
+ * is the per-tab attribution we filter on (reused by findAccountTabOnHost).
135
+ */
136
+ private resolveAccountPageCdpUrl;
137
+ /**
138
+ * Full CDP targetId of the profile account's active page tab (the owned tab
139
+ * Chrome marks active, else the first owned tab). If the account owns no
140
+ * page tab yet, open `about:blank` for it first (mirrors the agent-invoke
141
+ * path's "always give a fresh account an owned tab" rule).
142
+ */
143
+ private accountActivePageTargetId;
144
+ /**
145
+ * Full CDP targetId (`tabId`) of the account's active page tab: the owned tab
146
+ * Chrome marks `active` when it belongs to this account, else the first owned
147
+ * tab (the global active flag is account-blind — another profile's tab may
148
+ * hold it, which must not leak here).
149
+ */
150
+ private firstAccountPageTargetId;
151
+ /**
152
+ * Full CDP targetId for a specific account tab, addressed by the bb-browser
153
+ * short tab id (`tab`) or full target id (`tabId`). Used by stream.switch.
154
+ */
155
+ private accountTabTargetId;
156
+ /** `tab_list {account}` rows, typed to the fields we read (tabId/tab/account/active). */
157
+ private accountTabs;
158
+ /**
159
+ * Spawn a bb-viewer streamer for the profile and wait until healthy. Binary
160
+ * is PRLL_BB_VIEWER_BIN (our image sets it to /usr/local/bin/bb-viewer),
161
+ * defaulting to `bb-viewer` on PATH — we NEVER download it. A free port is
162
+ * allocated per streamer. Stores the entry in `this.streamers` once healthy.
163
+ */
164
+ private spawnStreamer;
165
+ /**
166
+ * SIGTERM with a SIGKILL escalation: a bb-viewer that hangs or ignores TERM
167
+ * would otherwise outlive its teardown holding the port + CDP session (the
168
+ * shell-level pattern-kill only exists on the hosted pod, not BYOC). The
169
+ * escalation timer is unref'd so it never holds the daemon open; it is
170
+ * cleared on exit. Caveat: a daemon process that exits immediately after
171
+ * shutdown() abandons the timer — acceptable, the TERM was still sent.
172
+ */
173
+ private killChild;
174
+ /**
175
+ * POST a command to the profile's bb-viewer `/command` endpoint. Parses the
176
+ * `{result}` / `{error:{message}}` envelope (bb-viewer api.go) and returns
177
+ * the inner result, throwing the error message on failure.
178
+ */
179
+ private streamerCommand;
180
+ /** Kill + remove the profile's bb-viewer streamer, if any. Idempotent. */
181
+ killProfileStreamer(profileId: string): void;
182
+ /**
183
+ * Tear down every profile streamer and refuse new spawns (daemon stop).
184
+ * Idempotent.
185
+ */
186
+ shutdown(): void;
187
+ }
188
+ //# sourceMappingURL=browser-viewer-streamer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-viewer-streamer.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-viewer-streamer.ts"],"names":[],"mappings":"AAsCA,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAcD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF,gFAAgF;IAChF,kBAAkB,CAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GACtE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpC,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,kBAAkB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9E,gEAAgE;IAChE,WAAW,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,qBAAa,qBAAqB;IAUpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;IAI9D,OAAO,CAAC,QAAQ,CAAS;gBAEI,IAAI,EAAE,kBAAkB;IAErD;;;;;OAKG;IACG,mBAAmB,CACvB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAyBnC;;;;OAIG;YACW,iBAAiB;IA+C/B;;;;OAIG;YACW,kBAAkB;IAgBhC;;;;;;;;;;OAUG;YACW,iBAAiB;IAuB/B;;;;;;;;OAQG;YACW,kBAAkB;IAuBhC;;;;;;OAMG;YACW,cAAc;IAiC5B;;;;;;;OAOG;YACW,kBAAkB;IAqBhC;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,gBAAgB;IA6C9B;;;;;;;;;;;;;;;;;;OAkBG;YACW,wBAAwB;IAetC;;;;;OAKG;YACW,yBAAyB;IAevC;;;;;OAKG;YACW,wBAAwB;IAatC;;;OAGG;YACW,kBAAkB;IAehC,yFAAyF;YAC3E,WAAW;IAgBzB;;;;;OAKG;YACW,aAAa;IA2F3B;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;IAqBjB;;;;OAIG;YACW,eAAe;IA+B7B,0EAA0E;IAC1E,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAO5C;;;OAGG;IACH,QAAQ,IAAI,IAAI;CAMjB"}