@browserbridge/bbx 1.7.5 → 1.7.7
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/README.md +1 -1
- package/package.json +6 -6
- package/packages/agent-client/src/cli-args.js +152 -0
- package/packages/agent-client/src/cli-batch.js +140 -0
- package/packages/agent-client/src/cli-output.js +57 -0
- package/packages/agent-client/src/cli-proxy-remote.js +350 -0
- package/packages/agent-client/src/cli-setup-commands.js +404 -0
- package/packages/agent-client/src/cli.js +22 -1062
- package/packages/agent-client/src/runtime.js +1 -2
- package/packages/mcp-server/src/guidance.js +4 -232
- package/packages/mcp-server/src/server.js +1 -3
- package/packages/native-host/bin/bridge-daemon.js +2 -1
- package/packages/native-host/src/auth-token.js +23 -2
- package/packages/native-host/src/config.js +59 -19
- package/packages/native-host/src/daemon-process.js +226 -11
- package/packages/native-host/src/daemon.js +2 -6
- package/packages/protocol/src/capabilities.js +33 -110
- package/packages/protocol/src/capability-values.js +45 -0
- package/packages/protocol/src/defaults.js +17 -2
- package/packages/protocol/src/registry.js +299 -86
|
@@ -433,8 +433,7 @@ export async function getDoctorReport(options = {}) {
|
|
|
433
433
|
if (!response.ok) {
|
|
434
434
|
throw new Error(response.error.message);
|
|
435
435
|
}
|
|
436
|
-
const result =
|
|
437
|
-
/** @type {{ daemon?: string, extensionConnected?: boolean, access?: {
|
|
436
|
+
const result = /** @type {{ daemon?: string, extensionConnected?: boolean, access?: {
|
|
438
437
|
enabled?: boolean,
|
|
439
438
|
windowId?: number | null,
|
|
440
439
|
routeTabId?: number | null,
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import * as z from 'zod/v4';
|
|
4
|
-
|
|
5
|
-
/** @typedef {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} McpServer */
|
|
6
|
-
/** @typedef {import('@modelcontextprotocol/sdk/types.js').GetPromptResult} GetPromptResult */
|
|
7
|
-
|
|
8
3
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
9
4
|
"Browser Bridge MCP inspects and interacts with the user's real Chrome tab through typed MCP tools.",
|
|
10
5
|
'In permission-ask hosts, use browser_call as the default Browser Bridge MCP tool so the user can approve one BBX tool instead of separate browser_status, browser_page, browser_dom, browser_input, and other tools.',
|
|
@@ -15,231 +10,8 @@ export const MCP_SERVER_INSTRUCTIONS = [
|
|
|
15
10
|
'Escalate to screenshot.capture_element, screenshot.capture_region, dom.get_accessibility_tree, page.evaluate, viewport.resize, or CDP only when structured reads cannot answer the question.',
|
|
16
11
|
'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
|
|
17
12
|
'Only use the specialized Browser Bridge MCP tools directly when the host has already allowed them or the user explicitly wants typed tool calls.',
|
|
13
|
+
'',
|
|
14
|
+
'Page investigation: read page state, scoped DOM, and relevant page text first, batching independent reads. Find targets by text or role when selectors are unknown. Add styles, console, or network reads only when they directly answer the objective, and use screenshots or evaluation only when structured evidence is insufficient.',
|
|
15
|
+
'Layout debugging: locate the target, read only relevant computed properties and its box model, and inspect matched rules only when the cascade is unclear. Prototype the smallest fix with a reversible style patch, verify the result, check for new console errors, then rollback before editing source unless the user asks to keep the patch.',
|
|
16
|
+
'Flow verification: read initial page state, locate controls semantically, reuse elementRef values, interact through input tools, and wait for navigation or UI state changes. Verify the final DOM or page text plus console and network state when relevant. Do not create a new tab unless requested or required by the flow.',
|
|
18
17
|
].join('\n');
|
|
19
|
-
|
|
20
|
-
export const MCP_GUIDANCE_PROMPT_NAMES = Object.freeze([
|
|
21
|
-
'browser_bridge_guide',
|
|
22
|
-
'browser_bridge_investigate',
|
|
23
|
-
'browser_bridge_debug_layout',
|
|
24
|
-
'browser_bridge_verify_flow',
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Register Browser Bridge MCP prompt templates. These are the MCP-mode equivalent
|
|
29
|
-
* of a lightweight skill: discoverable by clients without requiring filesystem
|
|
30
|
-
* skill installation or shell access.
|
|
31
|
-
*
|
|
32
|
-
* @param {McpServer} server
|
|
33
|
-
* @returns {void}
|
|
34
|
-
*/
|
|
35
|
-
export function registerBridgeMcpGuidance(server) {
|
|
36
|
-
server.registerPrompt(
|
|
37
|
-
'browser_bridge_guide',
|
|
38
|
-
{
|
|
39
|
-
title: 'Use Browser Bridge MCP',
|
|
40
|
-
description:
|
|
41
|
-
'General Browser Bridge MCP workflow guidance. Prefer this over CLI skill setup.',
|
|
42
|
-
},
|
|
43
|
-
createGuidePrompt
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
server.registerPrompt(
|
|
47
|
-
'browser_bridge_investigate',
|
|
48
|
-
{
|
|
49
|
-
title: 'Investigate Current Page',
|
|
50
|
-
description:
|
|
51
|
-
'Inspect the current page with structured reads before screenshots or evaluation.',
|
|
52
|
-
argsSchema: {
|
|
53
|
-
objective: z.string().optional().describe('What to find, verify, or explain'),
|
|
54
|
-
selector: z
|
|
55
|
-
.string()
|
|
56
|
-
.optional()
|
|
57
|
-
.describe('Optional CSS selector to scope the first DOM read'),
|
|
58
|
-
scope: z.enum(['quick', 'normal', 'deep']).optional().describe('Investigation depth'),
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
createInvestigatePrompt
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
server.registerPrompt(
|
|
65
|
-
'browser_bridge_debug_layout',
|
|
66
|
-
{
|
|
67
|
-
title: 'Debug Layout Or Styling',
|
|
68
|
-
description: 'Diagnose a visual, spacing, sizing, visibility, or CSS issue in the live tab.',
|
|
69
|
-
argsSchema: {
|
|
70
|
-
target: z.string().optional().describe('Element, component, text, or selector to inspect'),
|
|
71
|
-
symptom: z.string().optional().describe('Observed layout or styling problem'),
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
createDebugLayoutPrompt
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
server.registerPrompt(
|
|
78
|
-
'browser_bridge_verify_flow',
|
|
79
|
-
{
|
|
80
|
-
title: 'Verify User Flow',
|
|
81
|
-
description:
|
|
82
|
-
'Drive a user flow through MCP input tools and verify page, console, and network state.',
|
|
83
|
-
argsSchema: {
|
|
84
|
-
flow: z.string().optional().describe('User flow to exercise'),
|
|
85
|
-
successCriteria: z.string().optional().describe('Expected successful outcome'),
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
createVerifyFlowPrompt
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @returns {GetPromptResult}
|
|
94
|
-
*/
|
|
95
|
-
function createGuidePrompt() {
|
|
96
|
-
return createUserPrompt(
|
|
97
|
-
'Browser Bridge MCP workflow guide.',
|
|
98
|
-
[
|
|
99
|
-
'Use Browser Bridge MCP for this browser task.',
|
|
100
|
-
'',
|
|
101
|
-
'Rules:',
|
|
102
|
-
'1. Prefer MCP over `bbx`; do not shell out unless setup, doctor, logs, or raw CLI debugging is explicitly needed.',
|
|
103
|
-
'2. In permission-ask hosts, use `browser_call` as the default tool so the user can approve one BBX MCP tool instead of separate tools for status, page, DOM, input, and patches.',
|
|
104
|
-
'3. Start with `browser_call` method `health.ping`. If access is disabled, call `browser_call` method `access.request` once, ask the user to click Enable, then retry once.',
|
|
105
|
-
'4. Start with structured reads via `browser_call`: `page.get_state`, `dom.query`, `dom.find_by_text`, `dom.find_by_role`, and `styles.get_computed`. Use `browser_batch` for parallel reads.',
|
|
106
|
-
'5. `browser_call` accepts `budgetPreset`; explicit method limits override preset values. Keep limits tight and widen only when results are truncated.',
|
|
107
|
-
'6. Reuse `elementRef` values returned by DOM tools instead of rescanning.',
|
|
108
|
-
'7. Escalate to screenshots, accessibility tree, `page.evaluate`, viewport resize, or CDP only when structured reads cannot answer.',
|
|
109
|
-
'8. Use `patch.apply_styles` or `patch.apply_dom` for temporary experiments and rollback before finishing unless the user asks to keep patches.',
|
|
110
|
-
'9. Only use specialized Browser Bridge MCP tools directly when the host has already allowed them or the user explicitly wants typed tool calls.',
|
|
111
|
-
'',
|
|
112
|
-
'Return concise findings with evidence. Edit source code only after the live page behavior is understood.',
|
|
113
|
-
].join('\n')
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* @param {{ objective?: string, selector?: string, scope?: 'quick' | 'normal' | 'deep' }} args
|
|
119
|
-
* @returns {GetPromptResult}
|
|
120
|
-
*/
|
|
121
|
-
function createInvestigatePrompt(args) {
|
|
122
|
-
const objective = normalizeTextArg(
|
|
123
|
-
args.objective,
|
|
124
|
-
'inspect the current page and report findings'
|
|
125
|
-
);
|
|
126
|
-
const selector = normalizeTextArg(
|
|
127
|
-
args.selector,
|
|
128
|
-
'none; start with main, body, or semantic search'
|
|
129
|
-
);
|
|
130
|
-
const scope = normalizeTextArg(args.scope, 'normal');
|
|
131
|
-
|
|
132
|
-
return createUserPrompt(
|
|
133
|
-
'Browser Bridge MCP page investigation workflow.',
|
|
134
|
-
[
|
|
135
|
-
'Investigate the current page with Browser Bridge MCP.',
|
|
136
|
-
'',
|
|
137
|
-
`Objective: ${objective}`,
|
|
138
|
-
`Scope: ${scope}`,
|
|
139
|
-
`Initial selector: ${selector}`,
|
|
140
|
-
'',
|
|
141
|
-
'Workflow:',
|
|
142
|
-
'1. Call `browser_status` to confirm daemon, extension, and access readiness.',
|
|
143
|
-
'2. If access is disabled, call `browser_access` once, ask the user to click Enable, then retry once.',
|
|
144
|
-
'3. Use `browser_batch` for independent structured reads, usually `page.get_state`, a scoped `dom.query`, and `page.get_text` when page copy matters.',
|
|
145
|
-
'4. Use `browser_dom` `find_text` or `find_role` when the target is known by label but not selector.',
|
|
146
|
-
'5. Add `browser_styles_layout`, `browser_page` console, or `browser_page` network only when they directly answer the objective.',
|
|
147
|
-
'6. Escalate to screenshots, accessibility tree, or evaluate only when structured reads are insufficient.',
|
|
148
|
-
'',
|
|
149
|
-
'Return concise findings, relevant evidence, and the next source-code action if a fix is needed.',
|
|
150
|
-
].join('\n')
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* @param {{ target?: string, symptom?: string }} args
|
|
156
|
-
* @returns {GetPromptResult}
|
|
157
|
-
*/
|
|
158
|
-
function createDebugLayoutPrompt(args) {
|
|
159
|
-
const target = normalizeTextArg(args.target, 'the affected element or component');
|
|
160
|
-
const symptom = normalizeTextArg(args.symptom, 'the observed layout or styling problem');
|
|
161
|
-
|
|
162
|
-
return createUserPrompt(
|
|
163
|
-
'Browser Bridge MCP layout debugging workflow.',
|
|
164
|
-
[
|
|
165
|
-
'Debug a layout or styling issue in the live tab with Browser Bridge MCP.',
|
|
166
|
-
'',
|
|
167
|
-
`Target: ${target}`,
|
|
168
|
-
`Symptom: ${symptom}`,
|
|
169
|
-
'',
|
|
170
|
-
'Workflow:',
|
|
171
|
-
'1. Call `browser_status` first and resolve access if needed.',
|
|
172
|
-
'2. Locate the target with `browser_dom` `query`, `find_text`, or `find_role` using a quick budget.',
|
|
173
|
-
'3. Read only relevant computed styles with `browser_styles_layout` action `computed` and specific `properties`.',
|
|
174
|
-
'4. Read dimensions with `browser_styles_layout` action `box_model`; use matched rules only when the cascade is unclear.',
|
|
175
|
-
'5. Prototype the smallest visual fix with `browser_patch` action `apply_styles` and `verify: true` when useful.',
|
|
176
|
-
'6. Check `browser_page` console for new errors after interaction or patching.',
|
|
177
|
-
'7. Roll back temporary patches unless the user explicitly wants them kept, then edit source with the confirmed fix.',
|
|
178
|
-
'',
|
|
179
|
-
'Avoid screenshots until DOM, computed styles, and box model evidence are insufficient.',
|
|
180
|
-
].join('\n')
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* @param {{ flow?: string, successCriteria?: string }} args
|
|
186
|
-
* @returns {GetPromptResult}
|
|
187
|
-
*/
|
|
188
|
-
function createVerifyFlowPrompt(args) {
|
|
189
|
-
const flow = normalizeTextArg(args.flow, 'the requested user flow');
|
|
190
|
-
const successCriteria = normalizeTextArg(
|
|
191
|
-
args.successCriteria,
|
|
192
|
-
'visible success state, no console errors, and expected network behavior'
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
return createUserPrompt(
|
|
196
|
-
'Browser Bridge MCP user-flow verification workflow.',
|
|
197
|
-
[
|
|
198
|
-
'Verify a user flow in the current real browser tab with Browser Bridge MCP.',
|
|
199
|
-
'',
|
|
200
|
-
`Flow: ${flow}`,
|
|
201
|
-
`Success criteria: ${successCriteria}`,
|
|
202
|
-
'',
|
|
203
|
-
'Workflow:',
|
|
204
|
-
'1. Call `browser_status` and resolve access if needed.',
|
|
205
|
-
'2. Read `browser_page` state so you know the current URL and title before interacting.',
|
|
206
|
-
'3. Locate controls semantically with `browser_dom` `find_role` or `find_text`; reuse returned `elementRef` values.',
|
|
207
|
-
'4. Interact with `browser_input` actions such as `click`, `type`, `set_checked`, `select_option`, and `press_key`.',
|
|
208
|
-
'5. After navigation or UI changes, wait with `browser_dom` action `wait` or `browser_page` action `wait_for_load`.',
|
|
209
|
-
'6. Verify final page text/DOM plus `browser_page` console and network if the flow depends on API calls.',
|
|
210
|
-
'7. Do not create new tabs unless the user requested a fresh page or the flow requires one.',
|
|
211
|
-
'',
|
|
212
|
-
'Report the verified result, evidence, and any blocking failures.',
|
|
213
|
-
].join('\n')
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* @param {string} description
|
|
219
|
-
* @param {string} text
|
|
220
|
-
* @returns {GetPromptResult}
|
|
221
|
-
*/
|
|
222
|
-
function createUserPrompt(description, text) {
|
|
223
|
-
return {
|
|
224
|
-
description,
|
|
225
|
-
messages: [
|
|
226
|
-
{
|
|
227
|
-
role: 'user',
|
|
228
|
-
content: {
|
|
229
|
-
type: 'text',
|
|
230
|
-
text,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* @param {string | undefined} value
|
|
239
|
-
* @param {string} fallback
|
|
240
|
-
* @returns {string}
|
|
241
|
-
*/
|
|
242
|
-
function normalizeTextArg(value, fallback) {
|
|
243
|
-
const text = typeof value === 'string' ? value.trim() : '';
|
|
244
|
-
return text || fallback;
|
|
245
|
-
}
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
getMethodsByMaxComplexity,
|
|
45
45
|
} from '../../protocol/src/index.js';
|
|
46
46
|
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
47
|
-
import { MCP_SERVER_INSTRUCTIONS
|
|
47
|
+
import { MCP_SERVER_INSTRUCTIONS } from './guidance.js';
|
|
48
48
|
|
|
49
49
|
export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
|
|
50
50
|
export const TAB_ID_DESCRIPTION =
|
|
@@ -109,8 +109,6 @@ export function createBridgeMcpServer() {
|
|
|
109
109
|
}
|
|
110
110
|
);
|
|
111
111
|
|
|
112
|
-
registerBridgeMcpGuidance(server);
|
|
113
|
-
|
|
114
112
|
server.registerTool(
|
|
115
113
|
'browser_status',
|
|
116
114
|
{
|
|
@@ -44,7 +44,8 @@ try {
|
|
|
44
44
|
await writeDaemonPidFile(process.pid);
|
|
45
45
|
} catch (error) {
|
|
46
46
|
// A stale root-owned pid file (e.g. after a sudo install) must not take down
|
|
47
|
-
// a daemon that is already listening; `bbx` falls back to
|
|
47
|
+
// a daemon that is already listening; `bbx` falls back to endpoint-owner
|
|
48
|
+
// discovery (lsof/ss on the socket, verified TCP listener lookup) for the pid.
|
|
48
49
|
process.stderr.write(
|
|
49
50
|
`Could not write daemon pid file: ${error instanceof Error ? error.message : String(error)}\n`
|
|
50
51
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
@@ -34,6 +34,27 @@ export function normalizeBridgeAuthToken(value) {
|
|
|
34
34
|
return TOKEN_PATTERN.test(token) || UUID_TOKEN_PATTERN.test(token) ? token : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Compare a candidate auth token against the expected token in constant time.
|
|
39
|
+
* Both sides are hashed before comparison so neither token length nor a
|
|
40
|
+
* matching prefix is observable through timing, which matters when the daemon
|
|
41
|
+
* listens on TCP (proxy mode exposes it beyond localhost).
|
|
42
|
+
*
|
|
43
|
+
* @param {unknown} candidate
|
|
44
|
+
* @param {string | null | undefined} expected
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function bridgeAuthTokensEqual(candidate, expected) {
|
|
48
|
+
const normalizedCandidate = normalizeBridgeAuthToken(candidate);
|
|
49
|
+
if (!normalizedCandidate || typeof expected !== 'string' || !expected) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return timingSafeEqual(
|
|
53
|
+
createHash('sha256').update(normalizedCandidate).digest(),
|
|
54
|
+
createHash('sha256').update(expected).digest()
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
/**
|
|
38
59
|
* @param {{ env?: NodeJS.ProcessEnv, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
39
60
|
* @returns {Promise<string | null>}
|
|
@@ -60,7 +81,7 @@ export async function readBridgeAuthTokenOverride(options = {}) {
|
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
/**
|
|
63
|
-
* @param {{ tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
84
|
+
* @param {{ env?: NodeJS.ProcessEnv, tokenPath?: string, readFile?: typeof fs.promises.readFile }} [options={}]
|
|
64
85
|
* @returns {Promise<string | null>}
|
|
65
86
|
*/
|
|
66
87
|
export async function readBridgeAuthToken(options = {}) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
|
|
7
8
|
export const APP_NAME = 'com.browserbridge.browser_bridge';
|
|
8
9
|
export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
|
|
@@ -62,15 +63,19 @@ export function getBridgeDir(env = process.env) {
|
|
|
62
63
|
* connect to. On Windows we use a Named Pipe by default because Node's AF_UNIX
|
|
63
64
|
* support fails with EACCES on listen() under recent Node + Windows 11
|
|
64
65
|
* combinations, while Named Pipes are the historical and reliable Windows IPC
|
|
65
|
-
* mechanism.
|
|
66
|
-
*
|
|
66
|
+
* mechanism. Custom bridge homes use a stable suffix so isolated daemon and
|
|
67
|
+
* test setups do not contend for the default pipe.
|
|
67
68
|
*
|
|
68
69
|
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
69
70
|
* @returns {string}
|
|
70
71
|
*/
|
|
71
72
|
export function getSocketPath(env = process.env) {
|
|
72
|
-
if (os.platform() === 'win32'
|
|
73
|
-
|
|
73
|
+
if (os.platform() === 'win32') {
|
|
74
|
+
const bridgeHome = env[BRIDGE_HOME_ENV];
|
|
75
|
+
const suffix = bridgeHome
|
|
76
|
+
? `-${createHash('sha256').update(bridgeHome.toLowerCase()).digest('hex').slice(0, 16)}`
|
|
77
|
+
: '';
|
|
78
|
+
return `\\\\.\\pipe\\${APP_NAME}${suffix}`;
|
|
74
79
|
}
|
|
75
80
|
return path.join(getBridgeDir(env), 'bridge.sock');
|
|
76
81
|
}
|
|
@@ -115,30 +120,65 @@ function normalizePort(value) {
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
123
|
+
* @param {string} configPath
|
|
124
|
+
* @param {string} reason
|
|
125
|
+
* @returns {void}
|
|
126
|
+
*/
|
|
127
|
+
function warnProxyConfigIgnored(configPath, reason) {
|
|
128
|
+
console.error(`browser-bridge: ignoring proxy config at ${configPath}: ${reason}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read the opt-in TCP proxy config. A missing file and `enabled: false` are
|
|
133
|
+
* the normal quiet cases; anything else that prevents using the config gets a
|
|
134
|
+
* stderr warning so a corrupt file does not silently fall back to the socket
|
|
135
|
+
* transport. An invalid `bindHost` rejects the whole config rather than
|
|
136
|
+
* defaulting to `0.0.0.0`, so a typo in a value meant to restrict binding
|
|
137
|
+
* cannot widen exposure to all interfaces.
|
|
138
|
+
*
|
|
118
139
|
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
119
140
|
* @returns {BridgeProxyConfig | null}
|
|
120
141
|
*/
|
|
121
142
|
export function readProxyConfig(env = process.env) {
|
|
143
|
+
const configPath = getProxyConfigPath(env);
|
|
144
|
+
/** @type {unknown} */
|
|
145
|
+
let parsed;
|
|
122
146
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
147
|
+
parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (/** @type {{ code?: unknown }} */ (error)?.code !== 'ENOENT') {
|
|
150
|
+
warnProxyConfigIgnored(configPath, error instanceof Error ? error.message : String(error));
|
|
126
151
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
155
|
+
warnProxyConfigIgnored(configPath, 'expected a JSON object');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const record = /** @type {Record<string, unknown>} */ (parsed);
|
|
159
|
+
if (record.enabled !== true) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const port = normalizePort(record.port);
|
|
163
|
+
if (port === null) {
|
|
164
|
+
warnProxyConfigIgnored(configPath, 'invalid "port" value');
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
let bindHost = '0.0.0.0';
|
|
168
|
+
if (record.bindHost != null) {
|
|
169
|
+
const normalized = normalizeHost(record.bindHost);
|
|
170
|
+
if (!normalized) {
|
|
171
|
+
warnProxyConfigIgnored(configPath, 'invalid "bindHost" value');
|
|
131
172
|
return null;
|
|
132
173
|
}
|
|
133
|
-
|
|
134
|
-
enabled: true,
|
|
135
|
-
port,
|
|
136
|
-
bindHost,
|
|
137
|
-
...(typeof record.token === 'string' ? { token: record.token } : {}),
|
|
138
|
-
};
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
174
|
+
bindHost = normalized;
|
|
141
175
|
}
|
|
176
|
+
return {
|
|
177
|
+
enabled: true,
|
|
178
|
+
port,
|
|
179
|
+
bindHost,
|
|
180
|
+
...(typeof record.token === 'string' ? { token: record.token } : {}),
|
|
181
|
+
};
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
/**
|