@browserbridge/bbx 1.6.1 → 1.6.3
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/package.json +1 -1
- package/packages/agent-client/src/cli.js +269 -0
- package/packages/agent-client/src/command-registry.js +4 -0
- package/packages/agent-client/src/remotes.js +202 -0
- package/packages/mcp-server/src/handlers-capture.js +201 -198
- package/packages/mcp-server/src/handlers-navigation.js +81 -9
- package/packages/mcp-server/src/handlers-page.js +110 -88
- package/packages/mcp-server/src/handlers-utils.js +55 -27
- package/packages/mcp-server/src/handlers.js +52 -10
- package/packages/mcp-server/src/server.js +22 -3
- package/packages/native-host/src/auth-token.js +56 -1
- package/packages/native-host/src/config.js +125 -5
|
@@ -6,9 +6,13 @@ import path from 'node:path';
|
|
|
6
6
|
|
|
7
7
|
import { getBridgeDir } from './config.js';
|
|
8
8
|
|
|
9
|
+
export const BRIDGE_AUTH_TOKEN_ENV = 'BBX_AUTH_TOKEN';
|
|
10
|
+
export const BRIDGE_AUTH_TOKEN_FILE_ENV = 'BBX_AUTH_TOKEN_FILE';
|
|
9
11
|
const TOKEN_FILENAME = 'daemon.auth';
|
|
10
12
|
const TOKEN_BYTES = 32;
|
|
11
13
|
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/u;
|
|
14
|
+
const UUID_TOKEN_PATTERN =
|
|
15
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
@@ -27,7 +31,32 @@ export function normalizeBridgeAuthToken(value) {
|
|
|
27
31
|
return null;
|
|
28
32
|
}
|
|
29
33
|
const token = value.trim();
|
|
30
|
-
return TOKEN_PATTERN.test(token) ? token : null;
|
|
34
|
+
return TOKEN_PATTERN.test(token) || UUID_TOKEN_PATTERN.test(token) ? token : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{ env?: NodeJS.ProcessEnv, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
39
|
+
* @returns {Promise<string | null>}
|
|
40
|
+
*/
|
|
41
|
+
export async function readBridgeAuthTokenOverride(options = {}) {
|
|
42
|
+
const env = options.env ?? process.env;
|
|
43
|
+
const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
|
|
44
|
+
const explicitToken = normalizeBridgeAuthToken(env[BRIDGE_AUTH_TOKEN_ENV]);
|
|
45
|
+
if (explicitToken) {
|
|
46
|
+
return explicitToken;
|
|
47
|
+
}
|
|
48
|
+
const tokenFile = env[BRIDGE_AUTH_TOKEN_FILE_ENV];
|
|
49
|
+
if (!tokenFile) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return normalizeBridgeAuthToken(await readFile(tokenFile, 'utf8'));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (isMissingFileError(error)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
31
60
|
}
|
|
32
61
|
|
|
33
62
|
/**
|
|
@@ -35,6 +64,10 @@ export function normalizeBridgeAuthToken(value) {
|
|
|
35
64
|
* @returns {Promise<string | null>}
|
|
36
65
|
*/
|
|
37
66
|
export async function readBridgeAuthToken(options = {}) {
|
|
67
|
+
const override = await readBridgeAuthTokenOverride(options);
|
|
68
|
+
if (override) {
|
|
69
|
+
return override;
|
|
70
|
+
}
|
|
38
71
|
const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
|
|
39
72
|
const readFile = options.readFile ?? fs.promises.readFile.bind(fs.promises);
|
|
40
73
|
try {
|
|
@@ -47,6 +80,28 @@ export async function readBridgeAuthToken(options = {}) {
|
|
|
47
80
|
}
|
|
48
81
|
}
|
|
49
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} token
|
|
85
|
+
* @param {{ tokenPath?: string, writeFile?: typeof fs.promises.writeFile, mkdir?: typeof fs.promises.mkdir, chmod?: typeof fs.promises.chmod }} [options={}]
|
|
86
|
+
* @returns {Promise<string>}
|
|
87
|
+
*/
|
|
88
|
+
export async function writeBridgeAuthToken(token, options = {}) {
|
|
89
|
+
const normalized = normalizeBridgeAuthToken(token);
|
|
90
|
+
if (!normalized) {
|
|
91
|
+
throw new Error('Bridge auth token must be a UUID or 32-256 URL-safe characters.');
|
|
92
|
+
}
|
|
93
|
+
const tokenPath = options.tokenPath ?? getBridgeAuthTokenPath();
|
|
94
|
+
const writeFile = options.writeFile ?? fs.promises.writeFile.bind(fs.promises);
|
|
95
|
+
const mkdir = options.mkdir ?? fs.promises.mkdir.bind(fs.promises);
|
|
96
|
+
const chmod = options.chmod ?? fs.promises.chmod.bind(fs.promises);
|
|
97
|
+
await mkdir(path.dirname(tokenPath), { recursive: true });
|
|
98
|
+
await writeFile(tokenPath, `${normalized}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
99
|
+
if (process.platform !== 'win32') {
|
|
100
|
+
await chmod(tokenPath, 0o600).catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
return normalized;
|
|
103
|
+
}
|
|
104
|
+
|
|
50
105
|
/**
|
|
51
106
|
* @param {{
|
|
52
107
|
* tokenPath?: string,
|
|
@@ -7,9 +7,21 @@ import path from 'node:path';
|
|
|
7
7
|
export const APP_NAME = 'com.browserbridge.browser_bridge';
|
|
8
8
|
export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
|
|
9
9
|
export const BRIDGE_TCP_PORT_ENV = 'BBX_TCP_PORT';
|
|
10
|
+
export const BRIDGE_TCP_HOST_ENV = 'BBX_TCP_HOST';
|
|
11
|
+
export const BRIDGE_TCP_BIND_HOST_ENV = 'BBX_TCP_BIND_HOST';
|
|
10
12
|
export const DEFAULT_WINDOWS_TCP_PORT = 9223;
|
|
13
|
+
export const PROXY_CONFIG_FILENAME = 'proxy.json';
|
|
11
14
|
|
|
12
|
-
/** @typedef {{ type: 'socket', socketPath: string, label: string } | { type: 'tcp', host: string, port: number, label: string }} BridgeTransport */
|
|
15
|
+
/** @typedef {{ type: 'socket', socketPath: string, label: string } | { type: 'tcp', host: string, port: number, bindHost?: string, label: string }} BridgeTransport */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* enabled: boolean,
|
|
20
|
+
* port: number,
|
|
21
|
+
* bindHost: string,
|
|
22
|
+
* token?: string,
|
|
23
|
+
* }} BridgeProxyConfig
|
|
24
|
+
*/
|
|
13
25
|
|
|
14
26
|
/**
|
|
15
27
|
* The official Chrome Web Store extension ID used when callers do not provide
|
|
@@ -63,6 +75,72 @@ export function getSocketPath(env = process.env) {
|
|
|
63
75
|
return path.join(getBridgeDir(env), 'bridge.sock');
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function getProxyConfigPath(env = process.env) {
|
|
83
|
+
return path.join(getBridgeDir(env), PROXY_CONFIG_FILENAME);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {unknown} value
|
|
88
|
+
* @returns {string | null}
|
|
89
|
+
*/
|
|
90
|
+
function normalizeHost(value) {
|
|
91
|
+
if (typeof value !== 'string') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const host = value.trim();
|
|
95
|
+
if (!host || /[\s/]/u.test(host)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return host;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {unknown} value
|
|
103
|
+
* @returns {number | null}
|
|
104
|
+
*/
|
|
105
|
+
function normalizePort(value) {
|
|
106
|
+
if (typeof value !== 'number' && typeof value !== 'string') {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const raw = String(value).trim();
|
|
110
|
+
const port = Number.parseInt(raw, 10);
|
|
111
|
+
if (!Number.isInteger(port) || String(port) !== raw || port < 1 || port > 65535) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return port;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
119
|
+
* @returns {BridgeProxyConfig | null}
|
|
120
|
+
*/
|
|
121
|
+
export function readProxyConfig(env = process.env) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(fs.readFileSync(getProxyConfigPath(env), 'utf8'));
|
|
124
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const record = /** @type {Record<string, unknown>} */ (parsed);
|
|
128
|
+
const port = normalizePort(record.port);
|
|
129
|
+
const bindHost = normalizeHost(record.bindHost) ?? '0.0.0.0';
|
|
130
|
+
if (record.enabled !== true || port === null) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
enabled: true,
|
|
135
|
+
port,
|
|
136
|
+
bindHost,
|
|
137
|
+
...(typeof record.token === 'string' ? { token: record.token } : {}),
|
|
138
|
+
};
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
66
144
|
/**
|
|
67
145
|
* @param {string} socketPath
|
|
68
146
|
* @returns {BridgeTransport}
|
|
@@ -80,12 +158,13 @@ export function createSocketBridgeTransport(socketPath) {
|
|
|
80
158
|
* @param {string} [host='127.0.0.1']
|
|
81
159
|
* @returns {BridgeTransport}
|
|
82
160
|
*/
|
|
83
|
-
export function createTcpBridgeTransport(port, host = '127.0.0.1') {
|
|
161
|
+
export function createTcpBridgeTransport(port, host = '127.0.0.1', bindHost = host) {
|
|
84
162
|
return {
|
|
85
163
|
type: 'tcp',
|
|
86
164
|
host,
|
|
87
165
|
port,
|
|
88
|
-
|
|
166
|
+
...(bindHost !== host ? { bindHost } : {}),
|
|
167
|
+
label: bindHost !== host ? `${host}:${port} (bind ${bindHost})` : `${host}:${port}`,
|
|
89
168
|
};
|
|
90
169
|
}
|
|
91
170
|
|
|
@@ -109,6 +188,38 @@ export function getBridgeTcpPort(env = process.env) {
|
|
|
109
188
|
return port;
|
|
110
189
|
}
|
|
111
190
|
|
|
191
|
+
/**
|
|
192
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
193
|
+
* @returns {string}
|
|
194
|
+
*/
|
|
195
|
+
export function getBridgeTcpHost(env = process.env) {
|
|
196
|
+
const raw = env[BRIDGE_TCP_HOST_ENV];
|
|
197
|
+
if (raw == null || raw === '') {
|
|
198
|
+
return '127.0.0.1';
|
|
199
|
+
}
|
|
200
|
+
const host = normalizeHost(raw);
|
|
201
|
+
if (!host) {
|
|
202
|
+
throw new Error(`${BRIDGE_TCP_HOST_ENV} must be a hostname or IP address.`);
|
|
203
|
+
}
|
|
204
|
+
return host;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
209
|
+
* @returns {string | null}
|
|
210
|
+
*/
|
|
211
|
+
export function getBridgeTcpBindHost(env = process.env) {
|
|
212
|
+
const raw = env[BRIDGE_TCP_BIND_HOST_ENV];
|
|
213
|
+
if (raw == null || raw === '') {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const host = normalizeHost(raw);
|
|
217
|
+
if (!host) {
|
|
218
|
+
throw new Error(`${BRIDGE_TCP_BIND_HOST_ENV} must be a hostname or IP address.`);
|
|
219
|
+
}
|
|
220
|
+
return host;
|
|
221
|
+
}
|
|
222
|
+
|
|
112
223
|
/**
|
|
113
224
|
* Align Windows CLI/daemon entrypoints with the installed native-host launcher,
|
|
114
225
|
* which uses TCP by default. Preserve explicit overrides and custom bridge-home
|
|
@@ -124,6 +235,9 @@ export function applyWindowsTcpTransportDefaults(env = process.env) {
|
|
|
124
235
|
if (env[BRIDGE_TCP_PORT_ENV] != null && env[BRIDGE_TCP_PORT_ENV] !== '') {
|
|
125
236
|
return false;
|
|
126
237
|
}
|
|
238
|
+
if (readProxyConfig(env)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
127
241
|
if (env[BRIDGE_HOME_ENV]) {
|
|
128
242
|
return false;
|
|
129
243
|
}
|
|
@@ -139,7 +253,13 @@ export function applyWindowsTcpTransportDefaults(env = process.env) {
|
|
|
139
253
|
export function getBridgeTransport(env = process.env) {
|
|
140
254
|
const tcpPort = getBridgeTcpPort(env);
|
|
141
255
|
if (tcpPort !== null) {
|
|
142
|
-
|
|
256
|
+
const host = getBridgeTcpHost(env);
|
|
257
|
+
return createTcpBridgeTransport(tcpPort, host, getBridgeTcpBindHost(env) ?? host);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const proxyConfig = readProxyConfig(env);
|
|
261
|
+
if (proxyConfig) {
|
|
262
|
+
return createTcpBridgeTransport(proxyConfig.port, '127.0.0.1', proxyConfig.bindHost);
|
|
143
263
|
}
|
|
144
264
|
|
|
145
265
|
return createSocketBridgeTransport(getSocketPath(env));
|
|
@@ -151,7 +271,7 @@ export function getBridgeTransport(env = process.env) {
|
|
|
151
271
|
*/
|
|
152
272
|
export function getBridgeListenTarget(transport = getBridgeTransport()) {
|
|
153
273
|
if (transport.type === 'tcp') {
|
|
154
|
-
return { host: transport.host, port: transport.port };
|
|
274
|
+
return { host: transport.bindHost ?? transport.host, port: transport.port };
|
|
155
275
|
}
|
|
156
276
|
return transport.socketPath;
|
|
157
277
|
}
|