@browserbridge/bbx 1.8.0 → 1.10.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.
- package/PRIVACY.md +29 -3
- package/README.md +10 -3
- package/SECURITY.md +12 -0
- package/package.json +7 -1
- package/packages/agent-client/src/cli-args.js +70 -6
- package/packages/agent-client/src/cli-batch.js +127 -59
- package/packages/agent-client/src/cli-helpers.js +8 -0
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli-setup-commands.js +3 -1
- package/packages/agent-client/src/cli.js +254 -26
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +14 -4
- package/packages/agent-client/src/config.js +195 -0
- package/packages/agent-client/src/mcp-config.js +8 -8
- package/packages/agent-client/src/npm-self-update.js +305 -0
- package/packages/agent-client/src/remotes.js +4 -2
- package/packages/agent-client/src/runtime.js +99 -7
- package/packages/agent-client/src/types.ts +38 -0
- package/packages/mcp-server/src/guidance.js +37 -6
- package/packages/mcp-server/src/handlers-capture.js +29 -2
- package/packages/mcp-server/src/handlers-dom.js +54 -3
- package/packages/mcp-server/src/handlers-page.js +333 -60
- package/packages/mcp-server/src/handlers-utils.js +141 -151
- package/packages/mcp-server/src/handlers.js +17 -4
- package/packages/mcp-server/src/server.js +269 -51
- package/packages/mcp-server/src/toolset.js +48 -0
- package/packages/native-host/src/artifact-store.js +353 -0
- package/packages/native-host/src/config.js +9 -1
- package/packages/native-host/src/daemon-logger.js +3 -1
- package/packages/native-host/src/daemon.js +457 -27
- package/packages/native-host/src/dom-baseline-owners.js +210 -0
- package/packages/native-host/src/native-host.js +38 -29
- package/packages/protocol/src/budget-presets.js +182 -0
- package/packages/protocol/src/capability-values.js +2 -0
- package/packages/protocol/src/defaults.js +28 -0
- package/packages/protocol/src/errors.js +48 -5
- package/packages/protocol/src/har.js +34 -0
- package/packages/protocol/src/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +4 -0
- package/packages/protocol/src/json-lines.js +3 -2
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +226 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +490 -4
- package/skills/browser-bridge/SKILL.md +26 -8
- package/skills/browser-bridge/references/protocol.md +125 -15
- package/skills/browser-bridge/references/token-efficiency.md +17 -7
|
@@ -4,11 +4,19 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
7
9
|
import { fileURLToPath } from 'node:url';
|
|
8
10
|
|
|
9
11
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
10
12
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createRuntimeContext,
|
|
15
|
+
describeBridgeMethods,
|
|
16
|
+
isBridgeMethod,
|
|
17
|
+
isHarDocument,
|
|
18
|
+
METHODS,
|
|
19
|
+
} from '../../protocol/src/index.js';
|
|
12
20
|
import {
|
|
13
21
|
restartRegisteredMcpProcesses,
|
|
14
22
|
tryStartMcpProcessControl,
|
|
@@ -16,6 +24,8 @@ import {
|
|
|
16
24
|
import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
17
25
|
import {
|
|
18
26
|
extractRemoteFlag,
|
|
27
|
+
extractHarFlags,
|
|
28
|
+
extractPresetFlag,
|
|
19
29
|
extractScreenshotFlags,
|
|
20
30
|
extractTabFlag,
|
|
21
31
|
parseCallCommand,
|
|
@@ -23,7 +33,7 @@ import {
|
|
|
23
33
|
readStdin,
|
|
24
34
|
} from './cli-args.js';
|
|
25
35
|
import { runBatchCalls } from './cli-batch.js';
|
|
26
|
-
import { parseIntArg } from './cli-helpers.js';
|
|
36
|
+
import { isElementRef, parseIntArg } from './cli-helpers.js';
|
|
27
37
|
import { printCallResponse, printJson, printSummary, printUsage } from './cli-output.js';
|
|
28
38
|
import { handleProxyCommand, handleRemoteCommand } from './cli-proxy-remote.js';
|
|
29
39
|
import {
|
|
@@ -32,12 +42,16 @@ import {
|
|
|
32
42
|
uninstallBrowserBridge,
|
|
33
43
|
} from './cli-setup-commands.js';
|
|
34
44
|
import { SHORTCUT_COMMANDS } from './command-registry.js';
|
|
45
|
+
import { getAutoUpdatePolicy, parseAutoUpdatePolicy, setAutoUpdatePolicy } from './config.js';
|
|
35
46
|
import { formatMcpConfig, isMcpClientName, MCP_CLIENT_NAMES } from './mcp-config.js';
|
|
36
47
|
import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
|
|
37
48
|
import { createBridgeClientForDestination } from './remotes.js';
|
|
49
|
+
import { atomicWriteFile } from './atomic-write.js';
|
|
38
50
|
|
|
39
51
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
40
52
|
/** @typedef {import('./types.js').ScreenshotResult} ScreenshotResult */
|
|
53
|
+
/** @typedef {import('../../protocol/src/types.js').ArtifactDescriptor} ArtifactDescriptor */
|
|
54
|
+
/** @typedef {import('../../protocol/src/types.js').HarExportResult} HarExportResult */
|
|
41
55
|
|
|
42
56
|
const REQUEST_SOURCE = 'cli';
|
|
43
57
|
const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
|
|
@@ -57,6 +71,7 @@ const LOCAL_ONLY_COMMANDS = new Set([
|
|
|
57
71
|
'--version',
|
|
58
72
|
'-v',
|
|
59
73
|
'skill',
|
|
74
|
+
'protocol',
|
|
60
75
|
'install',
|
|
61
76
|
'uninstall',
|
|
62
77
|
'install-skill',
|
|
@@ -64,6 +79,7 @@ const LOCAL_ONLY_COMMANDS = new Set([
|
|
|
64
79
|
'mcp',
|
|
65
80
|
'proxy',
|
|
66
81
|
'remote',
|
|
82
|
+
'config',
|
|
67
83
|
'doctor',
|
|
68
84
|
'restart',
|
|
69
85
|
]);
|
|
@@ -103,6 +119,22 @@ if (command === 'skill') {
|
|
|
103
119
|
process.exit(0);
|
|
104
120
|
}
|
|
105
121
|
|
|
122
|
+
if (command === 'protocol') {
|
|
123
|
+
const [subcommand, target, ...extra] = rest;
|
|
124
|
+
if (subcommand !== 'describe' || extra.length > 0) {
|
|
125
|
+
process.stderr.write('Usage: bbx protocol describe [method|group]\n');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
await runLocalCommand(async () => {
|
|
129
|
+
printJson(
|
|
130
|
+
target
|
|
131
|
+
? describeBridgeMethods(isBridgeMethod(target) ? { method: target } : { group: target })
|
|
132
|
+
: describeBridgeMethods()
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
process.exit();
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
if (command === 'install') {
|
|
107
139
|
const { execFileSync } = await import('node:child_process');
|
|
108
140
|
const installScript = path.resolve(
|
|
@@ -134,7 +166,7 @@ if (command === 'install-mcp') {
|
|
|
134
166
|
}
|
|
135
167
|
|
|
136
168
|
if (command === 'mcp') {
|
|
137
|
-
const [subcommand, clientName] = rest;
|
|
169
|
+
const [subcommand, clientName, ...extra] = rest;
|
|
138
170
|
if (subcommand === 'serve') {
|
|
139
171
|
const control = await tryStartMcpProcessControl();
|
|
140
172
|
try {
|
|
@@ -146,7 +178,7 @@ if (command === 'mcp') {
|
|
|
146
178
|
await new Promise(() => {});
|
|
147
179
|
}
|
|
148
180
|
if (subcommand === 'config') {
|
|
149
|
-
if (!clientName || !isMcpClientName(clientName)) {
|
|
181
|
+
if (!clientName || !isMcpClientName(clientName) || extra.length > 0) {
|
|
150
182
|
process.stderr.write(`Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`);
|
|
151
183
|
process.exit(1);
|
|
152
184
|
}
|
|
@@ -167,6 +199,11 @@ if (command === 'remote') {
|
|
|
167
199
|
process.exit();
|
|
168
200
|
}
|
|
169
201
|
|
|
202
|
+
if (command === 'config') {
|
|
203
|
+
await runLocalCommand(() => handleConfigCommand(rest));
|
|
204
|
+
process.exit();
|
|
205
|
+
}
|
|
206
|
+
|
|
170
207
|
const clientTimeoutMs = getClientTimeoutOverride();
|
|
171
208
|
applyWindowsTcpTransportDefaults();
|
|
172
209
|
const client = await createCliClient();
|
|
@@ -189,6 +226,26 @@ async function runLocalCommand(run) {
|
|
|
189
226
|
}
|
|
190
227
|
}
|
|
191
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @param {string[]} args
|
|
231
|
+
* @returns {Promise<void>}
|
|
232
|
+
*/
|
|
233
|
+
async function handleConfigCommand(args) {
|
|
234
|
+
const [action, key, value] = args;
|
|
235
|
+
if (action === 'get' && key === 'auto-update' && value === undefined) {
|
|
236
|
+
process.stdout.write(`${await getAutoUpdatePolicy()}\n`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (action === 'set' && key === 'auto-update' && value !== undefined && args.length === 3) {
|
|
240
|
+
const policy = parseAutoUpdatePolicy(value);
|
|
241
|
+
const configPath = await setAutoUpdatePolicy(policy);
|
|
242
|
+
process.stdout.write(`Browser Bridge auto-update policy set to "${policy}".\n`);
|
|
243
|
+
process.stdout.write(`Config: ${configPath}\n`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
throw new Error('Usage: bbx config <get auto-update|set auto-update off|compatible>');
|
|
247
|
+
}
|
|
248
|
+
|
|
192
249
|
/**
|
|
193
250
|
* Create the bridge client for this invocation, honoring the --remote flag
|
|
194
251
|
* and BBX_REMOTE env var. Exits with a friendly error when the destination
|
|
@@ -213,6 +270,7 @@ async function createCliClient() {
|
|
|
213
270
|
}
|
|
214
271
|
|
|
215
272
|
async function main() {
|
|
273
|
+
let relaunchAfterUpdate = false;
|
|
216
274
|
try {
|
|
217
275
|
if (command === 'status') {
|
|
218
276
|
const healthResponse = await requestBridge(
|
|
@@ -226,8 +284,10 @@ async function main() {
|
|
|
226
284
|
}
|
|
227
285
|
|
|
228
286
|
if (command === 'access-request') {
|
|
287
|
+
if (rest.length > 1) throw new Error('Usage: bbx access-request [intent]');
|
|
288
|
+
const params = SHORTCUT_COMMANDS['access-request'].build(rest);
|
|
229
289
|
await printSummary(
|
|
230
|
-
await requestBridge(client, 'access.request',
|
|
290
|
+
await requestBridge(client, 'access.request', params, { source: REQUEST_SOURCE })
|
|
231
291
|
);
|
|
232
292
|
return;
|
|
233
293
|
}
|
|
@@ -338,12 +398,13 @@ async function main() {
|
|
|
338
398
|
tabId,
|
|
339
399
|
source: REQUEST_SOURCE,
|
|
340
400
|
});
|
|
341
|
-
printCallResponse(response);
|
|
401
|
+
printCallResponse(response, method);
|
|
342
402
|
return;
|
|
343
403
|
}
|
|
344
404
|
|
|
345
405
|
if (command === 'batch') {
|
|
346
|
-
const
|
|
406
|
+
const { preset, rest: batchArgs } = extractPresetFlag(rest);
|
|
407
|
+
const results = await runBatchCalls(client, batchArgs[0], REQUEST_SOURCE, { preset });
|
|
347
408
|
if (results.some((result) => !result.ok)) {
|
|
348
409
|
process.exitCode = 1;
|
|
349
410
|
}
|
|
@@ -357,7 +418,7 @@ async function main() {
|
|
|
357
418
|
tabId,
|
|
358
419
|
source: REQUEST_SOURCE,
|
|
359
420
|
});
|
|
360
|
-
printCallResponse(response);
|
|
421
|
+
printCallResponse(response, method);
|
|
361
422
|
return;
|
|
362
423
|
}
|
|
363
424
|
|
|
@@ -378,7 +439,7 @@ async function main() {
|
|
|
378
439
|
const [key, refOrSelector] = rest;
|
|
379
440
|
if (!key) throw new Error('Usage: press-key <key> [ref|selector]');
|
|
380
441
|
const target = refOrSelector
|
|
381
|
-
? refOrSelector
|
|
442
|
+
? isElementRef(refOrSelector)
|
|
382
443
|
? { elementRef: refOrSelector }
|
|
383
444
|
: { selector: refOrSelector }
|
|
384
445
|
: undefined;
|
|
@@ -431,6 +492,7 @@ async function main() {
|
|
|
431
492
|
elementRef,
|
|
432
493
|
format: screenshotOptions.format,
|
|
433
494
|
quality: screenshotOptions.quality,
|
|
495
|
+
delivery: 'artifact',
|
|
434
496
|
},
|
|
435
497
|
{ tabId: parsed.tabId, source: REQUEST_SOURCE }
|
|
436
498
|
);
|
|
@@ -453,8 +515,11 @@ async function main() {
|
|
|
453
515
|
) {
|
|
454
516
|
throw new Error(`Screenshot path extension must match ${screenshotOptions.format} format.`);
|
|
455
517
|
}
|
|
456
|
-
const
|
|
457
|
-
|
|
518
|
+
const imageBytes =
|
|
519
|
+
screenshotResult.delivery === 'artifact'
|
|
520
|
+
? await downloadArtifact(client, screenshotResult.artifact, 'screenshot')
|
|
521
|
+
: decodeScreenshotDataUrl(screenshotResult.image);
|
|
522
|
+
await atomicWriteFile(filePath, imageBytes);
|
|
458
523
|
printJson({
|
|
459
524
|
ok: true,
|
|
460
525
|
summary: `Screenshot saved to ${filePath}.`,
|
|
@@ -469,6 +534,50 @@ async function main() {
|
|
|
469
534
|
return;
|
|
470
535
|
}
|
|
471
536
|
|
|
537
|
+
if (command === 'har') {
|
|
538
|
+
const parsed = extractTabFlag(rest);
|
|
539
|
+
const harOptions = extractHarFlags(parsed.rest);
|
|
540
|
+
const [outputPath = 'browser-bridge.har', ...extra] = harOptions.rest;
|
|
541
|
+
if (extra.length > 0) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
'Usage: har [--tab <tabId>] [--limit 1-200] [--url-pattern <pattern>] [--delivery inline|artifact|auto] [outPath]'
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (path.extname(outputPath) !== '.har') {
|
|
547
|
+
throw new Error('HAR output path must use the .har extension.');
|
|
548
|
+
}
|
|
549
|
+
const response = await requestBridge(
|
|
550
|
+
client,
|
|
551
|
+
'network.export_har',
|
|
552
|
+
{
|
|
553
|
+
limit: harOptions.limit,
|
|
554
|
+
urlPattern: harOptions.urlPattern,
|
|
555
|
+
delivery: harOptions.delivery,
|
|
556
|
+
},
|
|
557
|
+
{ tabId: parsed.tabId, source: REQUEST_SOURCE }
|
|
558
|
+
);
|
|
559
|
+
if (!response.ok) {
|
|
560
|
+
await printSummary(response);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const result = /** @type {HarExportResult} */ (response.result);
|
|
564
|
+
let harBytes;
|
|
565
|
+
if (result.delivery === 'artifact') {
|
|
566
|
+
harBytes = await downloadArtifact(client, result.artifact, 'har');
|
|
567
|
+
assertValidHarBytes(harBytes);
|
|
568
|
+
} else {
|
|
569
|
+
assertValidHarDocument(result.har);
|
|
570
|
+
harBytes = Buffer.from(`${JSON.stringify(result.har, null, 2)}\n`, 'utf8');
|
|
571
|
+
}
|
|
572
|
+
await atomicWriteFile(outputPath, harBytes);
|
|
573
|
+
printJson({
|
|
574
|
+
ok: true,
|
|
575
|
+
summary: `HAR saved to ${outputPath}.`,
|
|
576
|
+
evidence: { savedTo: outputPath, delivery: result.delivery },
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
472
581
|
if (command === 'intercept') {
|
|
473
582
|
const { tabId, rest: iArgs } = extractTabFlag(rest);
|
|
474
583
|
const sub = iArgs[0];
|
|
@@ -559,25 +668,144 @@ async function main() {
|
|
|
559
668
|
} catch (error) {
|
|
560
669
|
const message = error instanceof Error ? error.message : String(error);
|
|
561
670
|
const raw = getErrorCode(error);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
671
|
+
if (raw === 'BBX_NPM_UPDATED') {
|
|
672
|
+
relaunchAfterUpdate = true;
|
|
673
|
+
} else {
|
|
674
|
+
let code = 'ERROR';
|
|
675
|
+
if (raw === 'ENOENT' || raw === 'ECONNREFUSED' || raw === 'EINVAL') {
|
|
676
|
+
code = 'DAEMON_OFFLINE';
|
|
677
|
+
} else if (raw === 'BRIDGE_TIMEOUT') {
|
|
678
|
+
code = 'BRIDGE_TIMEOUT';
|
|
679
|
+
} else if (/socket closed/i.test(message)) {
|
|
680
|
+
code = 'CONNECTION_LOST';
|
|
681
|
+
} else if (raw) {
|
|
682
|
+
code = String(raw);
|
|
683
|
+
}
|
|
684
|
+
printJson({
|
|
685
|
+
ok: false,
|
|
686
|
+
summary: `${code}: ${message}`,
|
|
687
|
+
evidence: null,
|
|
688
|
+
});
|
|
689
|
+
process.exitCode = 1;
|
|
571
690
|
}
|
|
572
|
-
printJson({
|
|
573
|
-
ok: false,
|
|
574
|
-
summary: `${code}: ${message}`,
|
|
575
|
-
evidence: null,
|
|
576
|
-
});
|
|
577
|
-
process.exitCode = 1;
|
|
578
691
|
} finally {
|
|
579
692
|
await client.close();
|
|
580
693
|
}
|
|
694
|
+
if (relaunchAfterUpdate) {
|
|
695
|
+
relaunchCli();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** @param {string} value */
|
|
700
|
+
function decodeScreenshotDataUrl(value) {
|
|
701
|
+
const match = /^data:image\/(?:png|jpeg|webp);base64,([A-Za-z0-9+/]*={0,2})$/u.exec(value);
|
|
702
|
+
if (!match) throw new Error('Screenshot response contains invalid base64 image data.');
|
|
703
|
+
const bytes = Buffer.from(match[1], 'base64');
|
|
704
|
+
if (bytes.toString('base64') !== match[1]) {
|
|
705
|
+
throw new Error('Screenshot response contains non-canonical base64 image data.');
|
|
706
|
+
}
|
|
707
|
+
return bytes;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* @param {import('./client.js').BridgeClient} client
|
|
712
|
+
* @param {ArtifactDescriptor} artifact
|
|
713
|
+
* @param {'screenshot' | 'har'} [expectedKind]
|
|
714
|
+
*/
|
|
715
|
+
async function downloadArtifact(client, artifact, expectedKind) {
|
|
716
|
+
const chunks = [];
|
|
717
|
+
let offset = 0;
|
|
718
|
+
try {
|
|
719
|
+
if (expectedKind && artifact.kind !== expectedKind) {
|
|
720
|
+
throw new Error(`Artifact kind does not match the expected ${expectedKind} result.`);
|
|
721
|
+
}
|
|
722
|
+
if (expectedKind === 'har' && artifact.mimeType !== 'application/json') {
|
|
723
|
+
throw new Error('HAR artifact MIME type is invalid.');
|
|
724
|
+
}
|
|
725
|
+
while (offset < artifact.byteLength) {
|
|
726
|
+
const response = await requestBridge(
|
|
727
|
+
client,
|
|
728
|
+
'artifact.read',
|
|
729
|
+
{ artifactId: artifact.artifactId, offset },
|
|
730
|
+
{ source: REQUEST_SOURCE }
|
|
731
|
+
);
|
|
732
|
+
if (!response.ok) throw new Error(response.error.message);
|
|
733
|
+
const result = /** @type {Record<string, unknown>} */ (response.result);
|
|
734
|
+
if (result.artifactId !== artifact.artifactId || result.offset !== offset) {
|
|
735
|
+
throw new Error('Artifact chunk metadata is inconsistent.');
|
|
736
|
+
}
|
|
737
|
+
const data = String(result.data ?? '');
|
|
738
|
+
const bytes = Buffer.from(data, 'base64');
|
|
739
|
+
if (bytes.toString('base64') !== data || bytes.length !== result.byteLength) {
|
|
740
|
+
throw new Error('Artifact chunk contains invalid base64 data.');
|
|
741
|
+
}
|
|
742
|
+
chunks.push(bytes);
|
|
743
|
+
if (result.nextOffset === null) break;
|
|
744
|
+
if (typeof result.nextOffset !== 'number' || result.nextOffset <= offset) {
|
|
745
|
+
throw new Error('Artifact chunk did not advance the read offset.');
|
|
746
|
+
}
|
|
747
|
+
offset = result.nextOffset;
|
|
748
|
+
}
|
|
749
|
+
const output = Buffer.concat(chunks);
|
|
750
|
+
const sha256 = createHash('sha256').update(output).digest('hex');
|
|
751
|
+
if (output.length !== artifact.byteLength || sha256 !== artifact.sha256) {
|
|
752
|
+
throw new Error('Artifact length or checksum does not match the capture metadata.');
|
|
753
|
+
}
|
|
754
|
+
return output;
|
|
755
|
+
} finally {
|
|
756
|
+
await requestBridge(
|
|
757
|
+
client,
|
|
758
|
+
'artifact.delete',
|
|
759
|
+
{ artifactId: artifact.artifactId },
|
|
760
|
+
{ source: REQUEST_SOURCE }
|
|
761
|
+
).catch(() => {});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** @param {Buffer} bytes */
|
|
766
|
+
function assertValidHarBytes(bytes) {
|
|
767
|
+
const text = bytes.toString('utf8');
|
|
768
|
+
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
769
|
+
throw new Error('HAR artifact is not valid UTF-8 JSON.');
|
|
770
|
+
}
|
|
771
|
+
let document;
|
|
772
|
+
try {
|
|
773
|
+
document = JSON.parse(text);
|
|
774
|
+
} catch {
|
|
775
|
+
throw new Error('HAR artifact is not valid JSON.');
|
|
776
|
+
}
|
|
777
|
+
assertValidHarDocument(document);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** @param {unknown} document */
|
|
781
|
+
function assertValidHarDocument(document) {
|
|
782
|
+
if (!document || typeof document !== 'object' || Array.isArray(document)) {
|
|
783
|
+
throw new Error('HAR response does not contain an inline HAR object.');
|
|
784
|
+
}
|
|
785
|
+
if (!isHarDocument(document)) {
|
|
786
|
+
throw new Error('HAR response does not contain a valid HAR 1.2 document.');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Replace this invocation with a fresh Node process after npm has replaced the
|
|
792
|
+
* package on disk. Child stdio is inherited so command output remains stable.
|
|
793
|
+
*
|
|
794
|
+
* @returns {void}
|
|
795
|
+
*/
|
|
796
|
+
function relaunchCli() {
|
|
797
|
+
const result = spawnSync(
|
|
798
|
+
process.execPath,
|
|
799
|
+
[fileURLToPath(import.meta.url), ...process.argv.slice(2)],
|
|
800
|
+
{
|
|
801
|
+
stdio: 'inherit',
|
|
802
|
+
env: process.env,
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
if (result.error) {
|
|
806
|
+
throw result.error;
|
|
807
|
+
}
|
|
808
|
+
process.exitCode = result.status ?? 1;
|
|
581
809
|
}
|
|
582
810
|
|
|
583
811
|
/**
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
|
|
13
13
|
ERROR_CODES,
|
|
14
14
|
getProtocolVersion,
|
|
15
|
+
isBatchSafeBridgeCall,
|
|
16
|
+
isBridgeMethod,
|
|
17
|
+
MAX_BATCH_CALLS,
|
|
18
|
+
MAX_BATCH_CONCURRENCY,
|
|
15
19
|
parseJsonLines,
|
|
16
20
|
setProtocolPackageVersion,
|
|
17
21
|
} from '../../protocol/src/index.js';
|
|
@@ -23,6 +27,12 @@ import {
|
|
|
23
27
|
} from '../../native-host/src/config.js';
|
|
24
28
|
import { normalizeBridgeAuthToken, readBridgeAuthToken } from '../../native-host/src/auth-token.js';
|
|
25
29
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
30
|
+
import {
|
|
31
|
+
comparePackageVersions,
|
|
32
|
+
NpmPackageUpdatedError,
|
|
33
|
+
parseStableVersion,
|
|
34
|
+
updateCompatibleNpmPackage,
|
|
35
|
+
} from './npm-self-update.js';
|
|
26
36
|
|
|
27
37
|
/** @typedef {import('./types.js').BridgeMeta} BridgeMeta */
|
|
28
38
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
@@ -32,7 +42,8 @@ import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
|
32
42
|
/** @typedef {import('./types.js').PendingRequest} PendingRequest */
|
|
33
43
|
/** @typedef {import('./types.js').ProtocolHealthResult} ProtocolHealthResult */
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
const CLIENT_PACKAGE_VERSION = loadPackageVersion();
|
|
46
|
+
setProtocolPackageVersion(CLIENT_PACKAGE_VERSION);
|
|
36
47
|
|
|
37
48
|
/**
|
|
38
49
|
* @returns {string | null}
|
|
@@ -119,6 +130,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
119
130
|
checkProtocolOnConnect = true,
|
|
120
131
|
restartDaemonOnVersionMismatch = shouldAutoRestartDaemonOnVersionMismatch(),
|
|
121
132
|
restartDaemonFn = restartBridgeDaemon,
|
|
133
|
+
updateNpmOnCompatibleVersion = false,
|
|
134
|
+
exitProcessOnNpmUpdate = false,
|
|
135
|
+
updateCompatibleNpmPackageFn = updateCompatibleNpmPackage,
|
|
122
136
|
authToken = undefined,
|
|
123
137
|
} = {}) {
|
|
124
138
|
super();
|
|
@@ -131,6 +145,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
131
145
|
this.checkProtocolOnConnect = checkProtocolOnConnect;
|
|
132
146
|
this.restartDaemonOnVersionMismatch = restartDaemonOnVersionMismatch;
|
|
133
147
|
this.restartDaemonFn = restartDaemonFn;
|
|
148
|
+
this.updateNpmOnCompatibleVersion = updateNpmOnCompatibleVersion;
|
|
149
|
+
this.exitProcessOnNpmUpdate = exitProcessOnNpmUpdate;
|
|
150
|
+
this.updateCompatibleNpmPackageFn = updateCompatibleNpmPackageFn;
|
|
134
151
|
this.authToken = authToken;
|
|
135
152
|
this.socket = null;
|
|
136
153
|
this.connected = false;
|
|
@@ -140,6 +157,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
140
157
|
this.waiting = new Map();
|
|
141
158
|
this._reconnecting = false;
|
|
142
159
|
this._attemptedVersionMismatchRestart = false;
|
|
160
|
+
this._attemptedNpmUpdate = false;
|
|
161
|
+
/** @type {import('./types.js').NpmUpdateResult | null} */
|
|
162
|
+
this.npmUpdateResult = null;
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
/**
|
|
@@ -280,6 +300,9 @@ export class BridgeClient extends EventEmitter {
|
|
|
280
300
|
if (this.protocolCompatibility.compatible) {
|
|
281
301
|
this._attemptedVersionMismatchRestart = false;
|
|
282
302
|
}
|
|
303
|
+
if (await this.tryCompatibleNpmUpdate(healthResult)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
283
306
|
if (this.shouldRestartDaemonForProtocolMismatch(healthResult)) {
|
|
284
307
|
this._attemptedVersionMismatchRestart = true;
|
|
285
308
|
await this.disconnectForDaemonRestart();
|
|
@@ -288,6 +311,95 @@ export class BridgeClient extends EventEmitter {
|
|
|
288
311
|
}
|
|
289
312
|
}
|
|
290
313
|
|
|
314
|
+
/**
|
|
315
|
+
* @param {ProtocolHealthResult} healthResult
|
|
316
|
+
* @returns {Promise<boolean>}
|
|
317
|
+
*/
|
|
318
|
+
async tryCompatibleNpmUpdate(healthResult) {
|
|
319
|
+
const extensionVersion = healthResult.extensionVersion;
|
|
320
|
+
const processVersion = CLIENT_PACKAGE_VERSION;
|
|
321
|
+
const extensionVersions = Array.isArray(healthResult.extension_supported_versions)
|
|
322
|
+
? healthResult.extension_supported_versions
|
|
323
|
+
: healthResult.extensionConnected === true && Array.isArray(healthResult.supported_versions)
|
|
324
|
+
? healthResult.supported_versions
|
|
325
|
+
: [];
|
|
326
|
+
if (
|
|
327
|
+
!this.updateNpmOnCompatibleVersion ||
|
|
328
|
+
this._attemptedNpmUpdate ||
|
|
329
|
+
healthResult.extensionConnected !== true ||
|
|
330
|
+
typeof extensionVersion !== 'string' ||
|
|
331
|
+
!parseStableVersion(extensionVersion) ||
|
|
332
|
+
typeof processVersion !== 'string' ||
|
|
333
|
+
!parseStableVersion(processVersion) ||
|
|
334
|
+
comparePackageVersions(extensionVersion, processVersion) <= 0 ||
|
|
335
|
+
extensionVersions.length === 0
|
|
336
|
+
) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this._attemptedNpmUpdate = true;
|
|
341
|
+
const previousAutoReconnect = this.autoReconnect;
|
|
342
|
+
this.autoReconnect = false;
|
|
343
|
+
try {
|
|
344
|
+
this.npmUpdateResult = await this.updateCompatibleNpmPackageFn({
|
|
345
|
+
extensionVersion,
|
|
346
|
+
supportedVersions: extensionVersions,
|
|
347
|
+
});
|
|
348
|
+
if (!this.npmUpdateResult.updated) {
|
|
349
|
+
if (
|
|
350
|
+
typeof this.npmUpdateResult.previousVersion === 'string' &&
|
|
351
|
+
parseStableVersion(this.npmUpdateResult.previousVersion) &&
|
|
352
|
+
comparePackageVersions(this.npmUpdateResult.previousVersion, processVersion) > 0
|
|
353
|
+
) {
|
|
354
|
+
this.requireFreshProcess(this.npmUpdateResult.previousVersion);
|
|
355
|
+
}
|
|
356
|
+
if (this.npmUpdateResult.reason === 'not_global_install') {
|
|
357
|
+
this.setProtocolWarning(
|
|
358
|
+
'Automatic npm update skipped because this Browser Bridge client is not running from the global npm installation.'
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.requireFreshProcess(this.npmUpdateResult.version || extensionVersion);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (error instanceof NpmPackageUpdatedError || this.npmUpdateResult?.updated) {
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
this.setProtocolWarning(
|
|
370
|
+
`Automatic Browser Bridge npm update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
371
|
+
);
|
|
372
|
+
return false;
|
|
373
|
+
} finally {
|
|
374
|
+
this.autoReconnect = previousAutoReconnect;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {string} version
|
|
380
|
+
* @returns {never}
|
|
381
|
+
*/
|
|
382
|
+
requireFreshProcess(version) {
|
|
383
|
+
if (this.exitProcessOnNpmUpdate) {
|
|
384
|
+
setTimeout(() => process.exit(0), 250).unref();
|
|
385
|
+
}
|
|
386
|
+
throw new NpmPackageUpdatedError(version);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @param {string} warning
|
|
391
|
+
* @returns {void}
|
|
392
|
+
*/
|
|
393
|
+
setProtocolWarning(warning) {
|
|
394
|
+
this.protocolWarning = this.protocolWarning ? `${this.protocolWarning} ${warning}` : warning;
|
|
395
|
+
if (this.protocolCompatibility) {
|
|
396
|
+
this.protocolCompatibility = {
|
|
397
|
+
...this.protocolCompatibility,
|
|
398
|
+
warning: this.protocolWarning,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
291
403
|
/**
|
|
292
404
|
* @param {{
|
|
293
405
|
* method: BridgeMethod,
|
|
@@ -456,7 +568,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
456
568
|
}
|
|
457
569
|
|
|
458
570
|
/**
|
|
459
|
-
* Send
|
|
571
|
+
* Send up to 20 read-only bridge requests with bounded concurrency.
|
|
460
572
|
*
|
|
461
573
|
* @param {Array<{
|
|
462
574
|
* method: BridgeMethod,
|
|
@@ -471,8 +583,27 @@ export class BridgeClient extends EventEmitter {
|
|
|
471
583
|
if (!Array.isArray(calls)) {
|
|
472
584
|
throw new TypeError('BridgeClient.batch expects an array of request objects.');
|
|
473
585
|
}
|
|
586
|
+
if (calls.length === 0 || calls.length > MAX_BATCH_CALLS) {
|
|
587
|
+
throw new RangeError(
|
|
588
|
+
`BridgeClient.batch expects between 1 and ${MAX_BATCH_CALLS} request objects.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
for (const [index, call] of calls.entries()) {
|
|
592
|
+
if (!call || typeof call !== 'object' || !isBridgeMethod(call.method)) {
|
|
593
|
+
throw new TypeError(`BridgeClient.batch calls[${index}] needs a known bridge method.`);
|
|
594
|
+
}
|
|
595
|
+
const params = call.params ?? {};
|
|
596
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
597
|
+
throw new TypeError(`BridgeClient.batch calls[${index}].params must be an object.`);
|
|
598
|
+
}
|
|
599
|
+
if (!isBatchSafeBridgeCall(call.method, params)) {
|
|
600
|
+
throw new TypeError(
|
|
601
|
+
`${call.method} is not safe for BridgeClient.batch. Call it sequentially.`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
474
605
|
|
|
475
|
-
return
|
|
606
|
+
return mapWithConcurrency(calls, MAX_BATCH_CONCURRENCY, (call) => this.request(call));
|
|
476
607
|
}
|
|
477
608
|
|
|
478
609
|
/**
|
|
@@ -642,6 +773,28 @@ export class BridgeClient extends EventEmitter {
|
|
|
642
773
|
}
|
|
643
774
|
}
|
|
644
775
|
|
|
776
|
+
/**
|
|
777
|
+
* @template T, R
|
|
778
|
+
* @param {T[]} values
|
|
779
|
+
* @param {number} concurrency
|
|
780
|
+
* @param {(value: T) => Promise<R>} callback
|
|
781
|
+
* @returns {Promise<R[]>}
|
|
782
|
+
*/
|
|
783
|
+
async function mapWithConcurrency(values, concurrency, callback) {
|
|
784
|
+
/** @type {R[]} */
|
|
785
|
+
const results = new Array(values.length);
|
|
786
|
+
let nextIndex = 0;
|
|
787
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
788
|
+
while (nextIndex < values.length) {
|
|
789
|
+
const index = nextIndex;
|
|
790
|
+
nextIndex += 1;
|
|
791
|
+
results[index] = await callback(values[index]);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
await Promise.all(workers);
|
|
795
|
+
return results;
|
|
796
|
+
}
|
|
797
|
+
|
|
645
798
|
/**
|
|
646
799
|
* @param {unknown} result
|
|
647
800
|
* @returns {{ html: string, truncated: boolean, omitted: number }}
|