@browserbridge/bbx 1.11.0 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +14 -2
- package/package.json +2 -2
- package/packages/agent-client/src/client.js +110 -37
- package/packages/agent-client/src/runtime.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +1 -1
- package/packages/mcp-server/src/handlers-navigation.js +14 -21
- package/packages/mcp-server/src/handlers-page.js +16 -20
- package/packages/mcp-server/src/handlers-utils.js +188 -44
- package/packages/mcp-server/src/server.js +2 -1
- package/packages/native-host/bin/postinstall.js +7 -5
- package/packages/native-host/src/auth-token.js +138 -14
- package/packages/native-host/src/daemon-process.js +5 -2
- package/packages/native-host/src/daemon.js +258 -63
- package/packages/native-host/src/framing.js +107 -17
- package/packages/native-host/src/install-manifest.js +3 -9
- package/packages/native-host/src/native-host.js +7 -3
- package/packages/protocol/src/defaults.js +6 -0
- package/packages/protocol/src/protocol.js +62 -3
- package/packages/protocol/src/types.ts +2 -0
|
@@ -1,9 +1,111 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { once } from 'node:events';
|
|
4
|
-
|
|
5
3
|
import { MAX_JSON_LINE_BYTES, MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
|
|
6
4
|
|
|
5
|
+
export const MAX_OUTPUT_QUEUE_BYTES = 4 * MAX_JSON_LINE_BYTES;
|
|
6
|
+
export const MAX_OUTPUT_QUEUE_MESSAGES = 128;
|
|
7
|
+
export const OUTPUT_DRAIN_TIMEOUT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
/** @typedef {NodeJS.WritableStream & { destroyed?: boolean, writableEnded?: boolean, destroy?: () => unknown }} OutputStream */
|
|
10
|
+
/** @typedef {{ parts: Array<string | Buffer>, bytes: number, resolve: () => void, reject: (error: Error) => void }} QueuedWrite */
|
|
11
|
+
|
|
12
|
+
/** @param {OutputStream} stream @returns {(parts: Array<string | Buffer>) => Promise<void>} */
|
|
13
|
+
function createBoundedWriter(stream) {
|
|
14
|
+
/** @type {QueuedWrite[]} */
|
|
15
|
+
const queue = [];
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
let running = false;
|
|
18
|
+
/** @type {Error | null} */
|
|
19
|
+
let failure = null;
|
|
20
|
+
/** @type {((error: Error) => void) | null} */
|
|
21
|
+
let cancelDrain = null;
|
|
22
|
+
|
|
23
|
+
/** @param {Error} error */
|
|
24
|
+
function fail(error) {
|
|
25
|
+
failure ??= error;
|
|
26
|
+
cancelDrain?.(error);
|
|
27
|
+
for (const entry of queue.splice(0)) entry.reject(error);
|
|
28
|
+
bytes = 0;
|
|
29
|
+
stream.destroy?.();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function pump() {
|
|
33
|
+
if (running) return;
|
|
34
|
+
running = true;
|
|
35
|
+
try {
|
|
36
|
+
while (queue.length && !failure) {
|
|
37
|
+
const entry = queue[0];
|
|
38
|
+
for (const part of entry.parts) {
|
|
39
|
+
if (failure) throw failure;
|
|
40
|
+
if (stream.destroyed || stream.writableEnded) throw new Error('Output stream closed.');
|
|
41
|
+
if (!stream.write(part)) {
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
const timer = setTimeout(
|
|
44
|
+
() => finish(new Error('Output drain timed out.')),
|
|
45
|
+
OUTPUT_DRAIN_TIMEOUT_MS
|
|
46
|
+
);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
/** @param {Error} [error] */
|
|
49
|
+
function finish(error) {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
stream.removeListener('drain', drained);
|
|
52
|
+
stream.removeListener('close', closed);
|
|
53
|
+
stream.removeListener('error', finish);
|
|
54
|
+
cancelDrain = null;
|
|
55
|
+
if (error) reject(error);
|
|
56
|
+
else resolve(undefined);
|
|
57
|
+
}
|
|
58
|
+
const drained = () => finish();
|
|
59
|
+
const closed = () => finish(new Error('Output stream closed while writing.'));
|
|
60
|
+
cancelDrain = finish;
|
|
61
|
+
stream.once('drain', drained);
|
|
62
|
+
stream.once('close', closed);
|
|
63
|
+
stream.once('error', finish);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (failure) break;
|
|
68
|
+
queue.shift();
|
|
69
|
+
bytes -= entry.bytes;
|
|
70
|
+
entry.resolve();
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
74
|
+
} finally {
|
|
75
|
+
running = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (parts) => {
|
|
80
|
+
if (failure) return Promise.reject(failure);
|
|
81
|
+
const size = parts.reduce((total, part) => total + Buffer.byteLength(part), 0);
|
|
82
|
+
if (queue.length >= MAX_OUTPUT_QUEUE_MESSAGES || bytes + size > MAX_OUTPUT_QUEUE_BYTES) {
|
|
83
|
+
const error = new Error('Output queue limit exceeded.');
|
|
84
|
+
fail(error);
|
|
85
|
+
return Promise.reject(error);
|
|
86
|
+
}
|
|
87
|
+
const result = new Promise((resolve, reject) => {
|
|
88
|
+
queue.push({ parts, bytes: size, resolve: () => resolve(undefined), reject });
|
|
89
|
+
bytes += size;
|
|
90
|
+
});
|
|
91
|
+
void pump();
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @type {WeakMap<OutputStream, ReturnType<typeof createBoundedWriter>>} */
|
|
97
|
+
const outputWriters = new WeakMap();
|
|
98
|
+
|
|
99
|
+
/** @param {OutputStream} stream @param {Array<string | Buffer>} parts */
|
|
100
|
+
function writeParts(stream, parts) {
|
|
101
|
+
let writer = outputWriters.get(stream);
|
|
102
|
+
if (!writer) {
|
|
103
|
+
writer = createBoundedWriter(stream);
|
|
104
|
+
outputWriters.set(stream, writer);
|
|
105
|
+
}
|
|
106
|
+
return writer(parts);
|
|
107
|
+
}
|
|
108
|
+
|
|
7
109
|
/**
|
|
8
110
|
* @param {NodeJS.WritableStream} stream
|
|
9
111
|
* @param {unknown} message
|
|
@@ -16,12 +118,7 @@ export async function writeNativeMessage(stream, message) {
|
|
|
16
118
|
}
|
|
17
119
|
const header = Buffer.alloc(4);
|
|
18
120
|
header.writeUInt32LE(payload.length, 0);
|
|
19
|
-
|
|
20
|
-
await once(stream, 'drain');
|
|
21
|
-
}
|
|
22
|
-
if (!stream.write(payload)) {
|
|
23
|
-
await once(stream, 'drain');
|
|
24
|
-
}
|
|
121
|
+
await writeParts(stream, [header, payload]);
|
|
25
122
|
}
|
|
26
123
|
|
|
27
124
|
/**
|
|
@@ -29,12 +126,7 @@ export async function writeNativeMessage(stream, message) {
|
|
|
29
126
|
* @returns {(message: unknown) => Promise<void>}
|
|
30
127
|
*/
|
|
31
128
|
export function createNativeMessageWriter(stream) {
|
|
32
|
-
|
|
33
|
-
return (message) => {
|
|
34
|
-
const writePromise = queue.then(() => writeNativeMessage(stream, message));
|
|
35
|
-
queue = writePromise.catch(() => {});
|
|
36
|
-
return writePromise;
|
|
37
|
-
};
|
|
129
|
+
return (message) => writeNativeMessage(stream, message);
|
|
38
130
|
}
|
|
39
131
|
|
|
40
132
|
/**
|
|
@@ -198,7 +290,5 @@ export async function writeJsonLine(socket, message) {
|
|
|
198
290
|
if (byteLength > MAX_JSON_LINE_BYTES) {
|
|
199
291
|
throw new Error(`JSON line exceeds ${MAX_JSON_LINE_BYTES} bytes: ${byteLength}`);
|
|
200
292
|
}
|
|
201
|
-
|
|
202
|
-
await once(socket, 'drain');
|
|
203
|
-
}
|
|
293
|
+
await writeParts(socket, [line]);
|
|
204
294
|
}
|
|
@@ -184,7 +184,8 @@ export function getDefaultExtensionId(env = process.env) {
|
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Build the allowed_origins list.
|
|
187
|
-
* If an extension ID was provided,
|
|
187
|
+
* If an extension ID was provided, it is the complete allowlist so stale
|
|
188
|
+
* origins from an earlier installation do not retain native-host access.
|
|
188
189
|
* Otherwise falls back to existing origins or a placeholder.
|
|
189
190
|
*
|
|
190
191
|
* @param {{allowed_origins?: string[]} | null} existingManifest
|
|
@@ -199,14 +200,7 @@ export function getAllowedOrigins(existingManifest, extensionId) {
|
|
|
199
200
|
|
|
200
201
|
if (extensionId) {
|
|
201
202
|
const origin = `chrome-extension://${extensionId}/`;
|
|
202
|
-
|
|
203
|
-
merged.add(origin);
|
|
204
|
-
for (const item of [...merged]) {
|
|
205
|
-
if (item.includes('__REPLACE_WITH_EXTENSION_ID__')) {
|
|
206
|
-
merged.delete(item);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return [...merged];
|
|
203
|
+
return [origin];
|
|
210
204
|
}
|
|
211
205
|
|
|
212
206
|
if (existing.length > 0) return existing;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
parseJsonLines,
|
|
9
9
|
sanitizeIncidentalText,
|
|
10
10
|
} from '../../protocol/src/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { readBridgeExtensionAuthToken } from './auth-token.js';
|
|
12
12
|
import { createSocketBridgeTransport, getBridgeTransport } from './config.js';
|
|
13
13
|
import { spawnBridgeDaemonProcess } from './daemon-process.js';
|
|
14
14
|
import { createNativeMessageReader, createNativeMessageWriter, writeJsonLine } from './framing.js';
|
|
@@ -163,16 +163,17 @@ export async function runNativeHost({
|
|
|
163
163
|
socket.once('close', cleanupStdinEndListener);
|
|
164
164
|
socket.once('end', cleanupStdinEndListener);
|
|
165
165
|
socket.once('error', cleanupStdinEndListener);
|
|
166
|
-
const authToken =
|
|
166
|
+
const authToken = await readBridgeExtensionAuthToken();
|
|
167
167
|
await writeJsonLine(socket, {
|
|
168
168
|
type: 'register',
|
|
169
169
|
role: 'extension',
|
|
170
|
-
|
|
170
|
+
authToken,
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
parseJsonLines(
|
|
174
174
|
socket,
|
|
175
175
|
(raw) => {
|
|
176
|
+
if (socket.destroyed) return;
|
|
176
177
|
const message = /** @type {Record<string, unknown>} */ (raw);
|
|
177
178
|
void (async () => {
|
|
178
179
|
if (message.type === 'extension.request') {
|
|
@@ -201,6 +202,7 @@ export async function runNativeHost({
|
|
|
201
202
|
});
|
|
202
203
|
}
|
|
203
204
|
})().catch((err) => {
|
|
205
|
+
socket.destroy();
|
|
204
206
|
console.error(
|
|
205
207
|
'native-host: socket message handler failed:',
|
|
206
208
|
err instanceof Error ? err.message : err
|
|
@@ -220,6 +222,7 @@ export async function runNativeHost({
|
|
|
220
222
|
createNativeMessageReader(
|
|
221
223
|
process.stdin,
|
|
222
224
|
(message) => {
|
|
225
|
+
if (socket.destroyed) return;
|
|
223
226
|
void (async () => {
|
|
224
227
|
if (isHostBridgeRequest(message)) {
|
|
225
228
|
await writeJsonLine(socket, {
|
|
@@ -271,6 +274,7 @@ export async function runNativeHost({
|
|
|
271
274
|
response: message,
|
|
272
275
|
});
|
|
273
276
|
})().catch((err) => {
|
|
277
|
+
socket.destroy();
|
|
274
278
|
console.error(
|
|
275
279
|
'native-host: stdin message handler failed:',
|
|
276
280
|
err instanceof Error ? err.message : err
|
|
@@ -45,6 +45,12 @@ export const MAX_BATCH_CALLS = 20;
|
|
|
45
45
|
|
|
46
46
|
/** Maximum read-only batch calls executing concurrently. */
|
|
47
47
|
export const MAX_BATCH_CONCURRENCY = 5;
|
|
48
|
+
export const MAX_INTERCEPT_RULES_PER_TAB = 32;
|
|
49
|
+
export const MAX_INTERCEPT_URL_PATTERN_LENGTH = 2_048;
|
|
50
|
+
export const MAX_INTERCEPT_BODY_BYTES = 262_144;
|
|
51
|
+
export const MAX_INTERCEPT_HEADERS = 64;
|
|
52
|
+
export const MAX_INTERCEPT_HEADER_VALUE_BYTES = 8_192;
|
|
53
|
+
export const MAX_INTERCEPT_HEADER_BYTES = 65_536;
|
|
48
54
|
export const SCREENSHOT_AUTO_INLINE_BYTES = 262_144;
|
|
49
55
|
export const SCREENSHOT_MAX_INLINE_BYTES = 524_288;
|
|
50
56
|
export const ARTIFACT_CHUNK_BYTES = 196_608;
|
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
MAX_EXTRACT_SETTLE_TIMEOUT_MS,
|
|
27
27
|
MAX_ARTIFACT_BYTES,
|
|
28
28
|
MAX_HAR_ENTRIES,
|
|
29
|
+
MAX_INTERCEPT_BODY_BYTES,
|
|
30
|
+
MAX_INTERCEPT_HEADER_BYTES,
|
|
31
|
+
MAX_INTERCEPT_HEADERS,
|
|
32
|
+
MAX_INTERCEPT_HEADER_VALUE_BYTES,
|
|
33
|
+
MAX_INTERCEPT_URL_PATTERN_LENGTH,
|
|
29
34
|
MAX_SENSITIVE_VALUE_BYTES,
|
|
30
35
|
} from './defaults.js';
|
|
31
36
|
import { BridgeError, ERROR_CODES, getErrorRecovery } from './errors.js';
|
|
@@ -300,8 +305,16 @@ export function validateBridgeRequest(request) {
|
|
|
300
305
|
|
|
301
306
|
const candidate = /** @type {Record<string, unknown>} */ (request);
|
|
302
307
|
|
|
303
|
-
if (
|
|
304
|
-
|
|
308
|
+
if (
|
|
309
|
+
typeof candidate.id !== 'string' ||
|
|
310
|
+
!candidate.id.trim() ||
|
|
311
|
+
candidate.id.length > 128 ||
|
|
312
|
+
hasAsciiControlCharacters(candidate.id)
|
|
313
|
+
) {
|
|
314
|
+
throw new BridgeError(
|
|
315
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
316
|
+
'Request id must be a non-empty string of at most 128 characters without controls.'
|
|
317
|
+
);
|
|
305
318
|
}
|
|
306
319
|
|
|
307
320
|
if (
|
|
@@ -378,6 +391,15 @@ export function validateBridgeRequest(request) {
|
|
|
378
391
|
};
|
|
379
392
|
}
|
|
380
393
|
|
|
394
|
+
/** @param {string} value @returns {boolean} */
|
|
395
|
+
function hasAsciiControlCharacters(value) {
|
|
396
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
397
|
+
const code = value.charCodeAt(index);
|
|
398
|
+
if (code < 32 || code === 127) return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
381
403
|
/**
|
|
382
404
|
* @param {unknown} source
|
|
383
405
|
* @param {unknown} value
|
|
@@ -1503,6 +1525,12 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1503
1525
|
if (typeof params.urlPattern !== 'string' || !params.urlPattern) {
|
|
1504
1526
|
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'urlPattern is required.');
|
|
1505
1527
|
}
|
|
1528
|
+
if (params.urlPattern.length > MAX_INTERCEPT_URL_PATTERN_LENGTH) {
|
|
1529
|
+
throw new BridgeError(
|
|
1530
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1531
|
+
`urlPattern must not exceed ${MAX_INTERCEPT_URL_PATTERN_LENGTH} characters.`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1506
1534
|
|
|
1507
1535
|
const action = params.action ?? DEFAULT_NETWORK_INTERCEPT_ACTION;
|
|
1508
1536
|
if (action !== 'fulfill' && action !== 'continue' && action !== 'block') {
|
|
@@ -1523,6 +1551,15 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1523
1551
|
if (params.body !== undefined && typeof params.body !== 'string') {
|
|
1524
1552
|
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'body must be a string.');
|
|
1525
1553
|
}
|
|
1554
|
+
if (
|
|
1555
|
+
typeof params.body === 'string' &&
|
|
1556
|
+
new TextEncoder().encode(params.body).byteLength > MAX_INTERCEPT_BODY_BYTES
|
|
1557
|
+
) {
|
|
1558
|
+
throw new BridgeError(
|
|
1559
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1560
|
+
`body must not exceed ${MAX_INTERCEPT_BODY_BYTES} UTF-8 bytes.`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1526
1563
|
|
|
1527
1564
|
/** @type {Record<string, string> | undefined} */
|
|
1528
1565
|
let headers;
|
|
@@ -1533,8 +1570,16 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1533
1570
|
'headers must be an object containing string values.'
|
|
1534
1571
|
);
|
|
1535
1572
|
}
|
|
1573
|
+
const entries = Object.entries(params.headers);
|
|
1574
|
+
if (entries.length > MAX_INTERCEPT_HEADERS) {
|
|
1575
|
+
throw new BridgeError(
|
|
1576
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1577
|
+
`headers must contain at most ${MAX_INTERCEPT_HEADERS} entries.`
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1536
1580
|
headers = {};
|
|
1537
|
-
|
|
1581
|
+
let headerBytes = 0;
|
|
1582
|
+
for (const [name, value] of entries) {
|
|
1538
1583
|
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
1539
1584
|
throw new BridgeError(
|
|
1540
1585
|
ERROR_CODES.INVALID_REQUEST,
|
|
@@ -1547,6 +1592,20 @@ export function normalizeNetworkInterceptAddParams(params = {}) {
|
|
|
1547
1592
|
`Header ${name} must have a string value without newlines.`
|
|
1548
1593
|
);
|
|
1549
1594
|
}
|
|
1595
|
+
const valueBytes = new TextEncoder().encode(value).byteLength;
|
|
1596
|
+
if (valueBytes > MAX_INTERCEPT_HEADER_VALUE_BYTES) {
|
|
1597
|
+
throw new BridgeError(
|
|
1598
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1599
|
+
`Header ${name} must not exceed ${MAX_INTERCEPT_HEADER_VALUE_BYTES} UTF-8 bytes.`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
headerBytes += new TextEncoder().encode(name).byteLength + valueBytes;
|
|
1603
|
+
if (headerBytes > MAX_INTERCEPT_HEADER_BYTES) {
|
|
1604
|
+
throw new BridgeError(
|
|
1605
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1606
|
+
`headers must not exceed ${MAX_INTERCEPT_HEADER_BYTES} UTF-8 bytes in total.`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1550
1609
|
headers[name] = value;
|
|
1551
1610
|
}
|
|
1552
1611
|
}
|
|
@@ -1053,6 +1053,8 @@ export interface NetworkResult {
|
|
|
1053
1053
|
dropped: number;
|
|
1054
1054
|
abandoned: number;
|
|
1055
1055
|
source: NetworkSource;
|
|
1056
|
+
provenance: 'page_main_world' | 'chrome_devtools_protocol';
|
|
1057
|
+
integrity: 'untrusted' | 'browser_observed';
|
|
1056
1058
|
capture: NetworkCaptureAction | null;
|
|
1057
1059
|
armed: boolean;
|
|
1058
1060
|
armedDuringCapture: boolean;
|