@browserbridge/bbx 1.2.0 → 1.3.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.
@@ -527,6 +527,7 @@ export function createBridgeMcpServer() {
527
527
  .optional()
528
528
  .describe('Element reference (for element action, preferred)'),
529
529
  selector: z.string().optional().describe('CSS selector (used if no elementRef)'),
530
+ nodeId: z.number().optional().describe('CDP node id for cdp_box_model/cdp_computed_styles'),
530
531
  rect: z
531
532
  .object({
532
533
  x: z.number().describe('Region left edge (viewport pixels)'),
@@ -16,28 +16,49 @@ import { installNativeManifest } from '../src/install-manifest.js';
16
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
17
  const repoRoot = path.resolve(__dirname, '../../..');
18
18
 
19
- try {
20
- await installNativeManifest({ repoRoot, preserveCustomExtensionId: true });
21
- process.stdout.write('Browser Bridge: native host installed. Run `bbx doctor` to verify.\n');
22
- } catch (err) {
23
- // Non-fatal - user can run `bbx install` manually.
24
- const message = err instanceof Error ? err.message : String(err);
25
- process.stderr.write(
26
- `Browser Bridge: native host auto-install skipped (${message}).\nRun \`bbx install\` manually if needed.\n`
27
- );
28
- process.exit(0);
29
- }
19
+ /**
20
+ * @param {{
21
+ * installNativeManifestFn?: typeof installNativeManifest,
22
+ * restartBridgeDaemonIfRunningFn?: typeof restartBridgeDaemonIfRunning,
23
+ * stdout?: Pick<NodeJS.WriteStream, 'write'>,
24
+ * stderr?: Pick<NodeJS.WriteStream, 'write'>,
25
+ * exit?: (code?: number) => void,
26
+ * }} [deps]
27
+ * @returns {Promise<void>}
28
+ */
29
+ export async function runPostinstall({
30
+ installNativeManifestFn = installNativeManifest,
31
+ restartBridgeDaemonIfRunningFn = restartBridgeDaemonIfRunning,
32
+ stdout = process.stdout,
33
+ stderr = process.stderr,
34
+ exit = (code) => process.exit(code),
35
+ } = {}) {
36
+ try {
37
+ await installNativeManifestFn({ repoRoot, preserveCustomExtensionId: true });
38
+ stdout.write('Browser Bridge: native host installed. Run `bbx doctor` to verify.\n');
39
+ } catch (err) {
40
+ // Non-fatal - user can run `bbx install` manually.
41
+ const message = err instanceof Error ? err.message : String(err);
42
+ stderr.write(
43
+ `Browser Bridge: native host auto-install skipped (${message}).\nRun \`bbx install\` manually if needed.\n`
44
+ );
45
+ exit(0);
46
+ return;
47
+ }
30
48
 
31
- try {
32
- const restartResult = await restartBridgeDaemonIfRunning();
33
- if (restartResult) {
34
- process.stdout.write(
35
- 'Browser Bridge: restarted the local daemon to use the updated install.\n'
49
+ try {
50
+ const restartResult = await restartBridgeDaemonIfRunningFn();
51
+ if (restartResult) {
52
+ stdout.write('Browser Bridge: restarted the local daemon to use the updated install.\n');
53
+ }
54
+ } catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ stderr.write(
57
+ `Browser Bridge: native host installed, but daemon restart failed (${message}).\nRun \`bbx restart\` if needed.\n`
36
58
  );
37
59
  }
38
- } catch (err) {
39
- const message = err instanceof Error ? err.message : String(err);
40
- process.stderr.write(
41
- `Browser Bridge: native host installed, but daemon restart failed (${message}).\nRun \`bbx restart\` if needed.\n`
42
- );
60
+ }
61
+
62
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
63
+ await runPostinstall();
43
64
  }
@@ -10,7 +10,6 @@ import { pingExistingDaemon } from './daemon.js';
10
10
  import {
11
11
  createSocketBridgeTransport,
12
12
  formatBridgeTransport,
13
- getBridgeDir,
14
13
  getBridgeTransport,
15
14
  getDaemonPidPath,
16
15
  } from './config.js';
@@ -74,7 +73,7 @@ export function spawnBridgeDaemonProcess() {
74
73
  * @returns {Promise<void>}
75
74
  */
76
75
  export async function writeDaemonPidFile(pid = process.pid, pidPath = getDaemonPidPath()) {
77
- await fs.promises.mkdir(getBridgeDir(), { recursive: true });
76
+ await fs.promises.mkdir(path.dirname(pidPath), { recursive: true });
78
77
  await fs.promises.writeFile(pidPath, `${pid}\n`, 'utf8');
79
78
  }
80
79
 
@@ -261,7 +261,6 @@ export class BridgeDaemon {
261
261
  return undefined;
262
262
  }
263
263
  this.pendingRequests.delete(requestId);
264
- this.requestStartTimes.delete(requestId);
265
264
  this.removePendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
266
265
  for (const targetSocket of pending.targets) {
267
266
  this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
@@ -541,8 +540,10 @@ export class BridgeDaemon {
541
540
  }
542
541
 
543
542
  if (request.method === 'log.tail') {
543
+ const limit =
544
+ typeof request.params.limit === 'number' ? request.params.limit : DEFAULT_LOG_TAIL_LIMIT;
544
545
  const response = createSuccess(request.id, {
545
- entries: this.recentLog.slice(-DEFAULT_LOG_TAIL_LIMIT),
546
+ entries: this.recentLog.slice(-limit),
546
547
  });
547
548
  await writeJsonLine(socket, { type: 'agent.response', response });
548
549
  return;
@@ -678,7 +679,30 @@ export class BridgeDaemon {
678
679
  targetCount: targets.length,
679
680
  });
680
681
  const broadcastPayload = { type: 'extension.request', request };
681
- await Promise.all(targets.map((extSocket) => writeJsonLine(extSocket, broadcastPayload)));
682
+ await Promise.all(
683
+ targets.map(async (extSocket) => {
684
+ try {
685
+ await writeJsonLine(extSocket, broadcastPayload);
686
+ } catch (error) {
687
+ this.logger.error('request route failed', {
688
+ requestId: request.id,
689
+ method: request.method,
690
+ message: error instanceof Error ? error.message : String(error),
691
+ });
692
+ const pending = this.pendingRequests.get(request.id);
693
+ if (!pending) {
694
+ return;
695
+ }
696
+ this.removePendingTarget(request.id, pending, extSocket);
697
+ if (extSocket.__extensionId) {
698
+ this.extensionSockets.delete(extSocket.__extensionId);
699
+ this.invalidateConnectedExtensionsCache();
700
+ }
701
+ extSocket.destroy(error instanceof Error ? error : undefined);
702
+ await this.finishPendingRequestIfExhausted(request.id, pending);
703
+ }
704
+ })
705
+ );
682
706
  }
683
707
 
684
708
  /**
@@ -902,6 +926,7 @@ export class BridgeDaemon {
902
926
  */
903
927
  recordRequestCompletion(requestId, ok) {
904
928
  const startedAt = this.requestStartTimes.get(requestId);
929
+ this.requestStartTimes.delete(requestId);
905
930
  this.requestsProcessed += 1;
906
931
  if (!ok) {
907
932
  this.requestsFailed += 1;
@@ -24,6 +24,19 @@ export async function writeNativeMessage(stream, message) {
24
24
  }
25
25
  }
26
26
 
27
+ /**
28
+ * @param {NodeJS.WritableStream} stream
29
+ * @returns {(message: unknown) => Promise<void>}
30
+ */
31
+ export function createNativeMessageWriter(stream) {
32
+ let queue = Promise.resolve();
33
+ return (message) => {
34
+ const writePromise = queue.then(() => writeNativeMessage(stream, message));
35
+ queue = writePromise.catch(() => {});
36
+ return writePromise;
37
+ };
38
+ }
39
+
27
40
  /**
28
41
  * @param {NodeJS.ReadableStream} stream
29
42
  * @param {(message: unknown) => void} onMessage
@@ -5,7 +5,7 @@ import net from 'node:net';
5
5
  import { createFailure, ERROR_CODES } from '../../protocol/src/index.js';
6
6
  import { createSocketBridgeTransport, getBridgeTransport } from './config.js';
7
7
  import { spawnBridgeDaemonProcess } from './daemon-process.js';
8
- import { createNativeMessageReader, writeJsonLine, writeNativeMessage } from './framing.js';
8
+ import { createNativeMessageReader, createNativeMessageWriter, writeJsonLine } from './framing.js';
9
9
 
10
10
  /** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
11
11
 
@@ -117,11 +117,13 @@ export async function runNativeHost({
117
117
  socketPath = undefined,
118
118
  } = {}) {
119
119
  const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
120
+ const writeNativeMessageQueued = createNativeMessageWriter(process.stdout);
121
+
120
122
  let socket;
121
123
  try {
122
124
  socket = await connectWithBootstrap(resolvedTransport);
123
125
  } catch (error) {
124
- await writeNativeMessage(process.stdout, {
126
+ await writeNativeMessageQueued({
125
127
  type: 'agent.response',
126
128
  response: createFailure(
127
129
  'native_bootstrap',
@@ -164,11 +166,11 @@ export async function runNativeHost({
164
166
  }
165
167
  void (async () => {
166
168
  if (message.type === 'extension.request') {
167
- await writeNativeMessage(process.stdout, message.request);
169
+ await writeNativeMessageQueued(message.request);
168
170
  return;
169
171
  }
170
172
  if (message.type === 'agent.response') {
171
- await writeNativeMessage(process.stdout, {
173
+ await writeNativeMessageQueued({
172
174
  type: 'host.bridge_response',
173
175
  response: message.response,
174
176
  });
@@ -178,7 +180,7 @@ export async function runNativeHost({
178
180
  message.type === 'extension.setup_status.response' ||
179
181
  message.type === 'extension.setup_status.error'
180
182
  ) {
181
- await writeNativeMessage(process.stdout, {
183
+ await writeNativeMessageQueued({
182
184
  type:
183
185
  message.type === 'extension.setup_status.response'
184
186
  ? 'host.setup_status.response'
@@ -7,6 +7,7 @@ import {
7
7
  DEFAULT_A11Y_MAX_NODES,
8
8
  DEFAULT_CONSOLE_LIMIT,
9
9
  DEFAULT_EVAL_TIMEOUT_MS,
10
+ DEFAULT_LOG_TAIL_LIMIT,
10
11
  DEFAULT_MAX_DEPTH,
11
12
  DEFAULT_MAX_HTML_LENGTH,
12
13
  DEFAULT_MAX_NODES,
@@ -29,6 +30,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
29
30
  /** @typedef {import('./types.js').BridgeSuccessResponse} BridgeSuccessResponse */
30
31
  /** @typedef {import('./types.js').CheckedActionParams} CheckedActionParams */
31
32
  /** @typedef {import('./types.js').CdpDispatchKeyEventParams} CdpDispatchKeyEventParams */
33
+ /** @typedef {import('./types.js').CdpNodeIdParams} CdpNodeIdParams */
32
34
  /** @typedef {import('./types.js').ConsoleParams} ConsoleParams */
33
35
  /** @typedef {import('./types.js').DomQueryParams} DomQueryParams */
34
36
  /** @typedef {import('./types.js').DragParams} DragParams */
@@ -38,11 +40,13 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
38
40
  /** @typedef {import('./types.js').GetHtmlParams} GetHtmlParams */
39
41
  /** @typedef {import('./types.js').HoverParams} HoverParams */
40
42
  /** @typedef {import('./types.js').InputActionParams} InputActionParams */
43
+ /** @typedef {import('./types.js').LogTailParams} LogTailParams */
41
44
  /** @typedef {import('./types.js').NavigationActionParams} NavigationActionParams */
42
45
  /** @typedef {import('./types.js').NetworkParams} NetworkParams */
43
46
  /** @typedef {import('./types.js').NormalizedAccessibilityTreeParams} NormalizedAccessibilityTreeParams */
44
47
  /** @typedef {import('./types.js').NormalizedCheckedAction} NormalizedCheckedAction */
45
48
  /** @typedef {import('./types.js').NormalizedCdpDispatchKeyEventParams} NormalizedCdpDispatchKeyEventParams */
49
+ /** @typedef {import('./types.js').NormalizedCdpNodeIdParams} NormalizedCdpNodeIdParams */
46
50
  /** @typedef {import('./types.js').NormalizedConsoleParams} NormalizedConsoleParams */
47
51
  /** @typedef {import('./types.js').NormalizedDomQuery} NormalizedDomQuery */
48
52
  /** @typedef {import('./types.js').NormalizedDragParams} NormalizedDragParams */
@@ -52,6 +56,7 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
52
56
  /** @typedef {import('./types.js').NormalizedGetHtmlParams} NormalizedGetHtmlParams */
53
57
  /** @typedef {import('./types.js').NormalizedHoverParams} NormalizedHoverParams */
54
58
  /** @typedef {import('./types.js').NormalizedInputAction} NormalizedInputAction */
59
+ /** @typedef {import('./types.js').NormalizedLogTailParams} NormalizedLogTailParams */
55
60
  /** @typedef {import('./types.js').NormalizedNavigationAction} NormalizedNavigationAction */
56
61
  /** @typedef {import('./types.js').NormalizedNetworkParams} NormalizedNetworkParams */
57
62
  /** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
@@ -209,6 +214,12 @@ export function validateBridgeRequest(request) {
209
214
  'session_id is no longer supported. Use tab_id or window-scoped default routing.'
210
215
  );
211
216
  }
217
+ if (
218
+ candidate.params != null &&
219
+ (typeof candidate.params !== 'object' || Array.isArray(candidate.params))
220
+ ) {
221
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'Request params must be an object.');
222
+ }
212
223
  const parsedTabId = Number(candidate.tab_id);
213
224
 
214
225
  const method = /** @type {BridgeMethod} */ (candidate.method);
@@ -242,6 +253,8 @@ export function validateBridgeRequest(request) {
242
253
  */
243
254
  function normalizeRequestParams(method, params) {
244
255
  switch (method) {
256
+ case 'log.tail':
257
+ return normalizeLogTailParams(params);
245
258
  case 'dom.query':
246
259
  return normalizeDomQuery(params);
247
260
  case 'page.evaluate':
@@ -285,6 +298,9 @@ function normalizeRequestParams(method, params) {
285
298
  return normalizeInputAction(params);
286
299
  case 'cdp.dispatch_key_event':
287
300
  return normalizeCdpDispatchKeyEventParams(params);
301
+ case 'cdp.get_box_model':
302
+ case 'cdp.get_computed_styles_for_node':
303
+ return normalizeCdpNodeIdParams(params);
288
304
  case 'input.set_checked':
289
305
  return normalizeCheckedAction(params);
290
306
  case 'input.select_option':
@@ -429,6 +445,20 @@ export function normalizeCdpDispatchKeyEventParams(params = {}) {
429
445
  };
430
446
  }
431
447
 
448
+ /**
449
+ * @param {CdpNodeIdParams} [params={}]
450
+ * @returns {NormalizedCdpNodeIdParams}
451
+ */
452
+ export function normalizeCdpNodeIdParams(params = {}) {
453
+ if (typeof params.nodeId !== 'number' || !Number.isFinite(params.nodeId)) {
454
+ throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'nodeId must be a finite number.');
455
+ }
456
+
457
+ return {
458
+ nodeId: params.nodeId,
459
+ };
460
+ }
461
+
432
462
  /**
433
463
  * @param {CheckedActionParams} [params={}]
434
464
  * @returns {NormalizedCheckedAction}
@@ -763,6 +793,16 @@ export function normalizePageTextParams(params = {}) {
763
793
  };
764
794
  }
765
795
 
796
+ /**
797
+ * @param {LogTailParams} [params={}]
798
+ * @returns {NormalizedLogTailParams}
799
+ */
800
+ export function normalizeLogTailParams(params = {}) {
801
+ return {
802
+ limit: clampInt(params.limit, 1, 200, DEFAULT_LOG_TAIL_LIMIT),
803
+ };
804
+ }
805
+
766
806
  /**
767
807
  * @param {ViewportResizeParams} [params={}]
768
808
  * @returns {NormalizedViewportResizeParams}
@@ -123,7 +123,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
123
123
  ['kind', 'target'],
124
124
  'trivial'
125
125
  ),
126
- 'log.tail': createRegistryEntry('log.tail', 'system', false, [], 'trivial'),
126
+ 'log.tail': createRegistryEntry('log.tail', 'system', false, ['limit'], 'trivial'),
127
127
  'health.ping': createRegistryEntry('health.ping', 'system', false, [], 'trivial'),
128
128
  'daemon.metrics': createRegistryEntry('daemon.metrics', 'system', false, [], 'trivial'),
129
129
  // tabs — trivial
@@ -413,18 +413,12 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
413
413
  // cdp — high (raw protocol, large payloads)
414
414
  'cdp.get_document': createRegistryEntry('cdp.get_document', 'cdp', true, [], 'high'),
415
415
  'cdp.get_dom_snapshot': createRegistryEntry('cdp.get_dom_snapshot', 'cdp', true, [], 'high'),
416
- 'cdp.get_box_model': createRegistryEntry(
417
- 'cdp.get_box_model',
418
- 'cdp',
419
- true,
420
- ['elementRef'],
421
- 'high'
422
- ),
416
+ 'cdp.get_box_model': createRegistryEntry('cdp.get_box_model', 'cdp', true, ['nodeId'], 'high'),
423
417
  'cdp.get_computed_styles_for_node': createRegistryEntry(
424
418
  'cdp.get_computed_styles_for_node',
425
419
  'cdp',
426
420
  true,
427
- ['elementRef'],
421
+ ['nodeId'],
428
422
  'high'
429
423
  ),
430
424
  'cdp.dispatch_key_event': createRegistryEntry(