@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_STRING_LENGTH = 4_096;
|
|
4
|
+
const DEFAULT_MAX_DEPTH = 8;
|
|
5
|
+
const DEFAULT_MAX_NODES = 1_000;
|
|
6
|
+
const REDACTED = '[redacted]';
|
|
7
|
+
const REDACTED_PATH = '[redacted-path]';
|
|
8
|
+
|
|
9
|
+
const SENSITIVE_KEYS = new Set([
|
|
10
|
+
'authorization',
|
|
11
|
+
'proxyauthorization',
|
|
12
|
+
'cookie',
|
|
13
|
+
'setcookie',
|
|
14
|
+
'password',
|
|
15
|
+
'passphrase',
|
|
16
|
+
'secret',
|
|
17
|
+
'clientsecret',
|
|
18
|
+
'token',
|
|
19
|
+
'accesstoken',
|
|
20
|
+
'refreshtoken',
|
|
21
|
+
'idtoken',
|
|
22
|
+
'authtoken',
|
|
23
|
+
'apikey',
|
|
24
|
+
'credential',
|
|
25
|
+
'credentials',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** @param {string} key */
|
|
29
|
+
export function isSensitiveIncidentalKey(key) {
|
|
30
|
+
return SENSITIVE_KEYS.has(key.toLowerCase().replaceAll(/[^a-z0-9]/gu, ''));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sanitize a URL that appears incidentally in logs, activity, or diagnostics.
|
|
35
|
+
* Query-key context is retained while credentials, values, and fragments are
|
|
36
|
+
* discarded.
|
|
37
|
+
*
|
|
38
|
+
* @param {unknown} value
|
|
39
|
+
* @param {number} [maxLength]
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeIncidentalUrl(value, maxLength = DEFAULT_MAX_STRING_LENGTH) {
|
|
43
|
+
const raw = typeof value === 'string' ? value.slice(0, maxLength) : String(value ?? '');
|
|
44
|
+
if (!raw) return '';
|
|
45
|
+
if (/^data:/iu.test(raw)) {
|
|
46
|
+
const mime = raw
|
|
47
|
+
.slice(5)
|
|
48
|
+
.split(/[;,]/u, 1)[0]
|
|
49
|
+
.replace(/[^a-z0-9./+-]/giu, '')
|
|
50
|
+
.slice(0, 100);
|
|
51
|
+
return mime ? `data:${mime};${REDACTED}` : `data:${REDACTED}`;
|
|
52
|
+
}
|
|
53
|
+
if (/^blob:/iu.test(raw)) {
|
|
54
|
+
try {
|
|
55
|
+
return `blob:${new URL(raw.slice(5)).origin}/${REDACTED}`.slice(0, maxLength);
|
|
56
|
+
} catch {
|
|
57
|
+
return `blob:${REDACTED}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (/^file:/iu.test(raw)) {
|
|
61
|
+
try {
|
|
62
|
+
return `file:${sanitizeIncidentalPath(decodeURIComponent(new URL(raw).pathname))}`;
|
|
63
|
+
} catch {
|
|
64
|
+
return `file:${REDACTED_PATH}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsed = new URL(raw);
|
|
69
|
+
parsed.username = '';
|
|
70
|
+
parsed.password = '';
|
|
71
|
+
parsed.hash = '';
|
|
72
|
+
for (const key of new Set(parsed.searchParams.keys())) {
|
|
73
|
+
parsed.searchParams.set(key, REDACTED);
|
|
74
|
+
}
|
|
75
|
+
return parsed.href.slice(0, maxLength);
|
|
76
|
+
} catch {
|
|
77
|
+
const withoutCredentials = raw.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/iu, '$1');
|
|
78
|
+
const summary = withoutCredentials.split(/[?#]/u, 1)[0];
|
|
79
|
+
return summary ? summary.slice(0, maxLength) : '[redacted-url]';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reduce a local path to a basename-only marker.
|
|
85
|
+
*
|
|
86
|
+
* @param {unknown} value
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function sanitizeIncidentalPath(value) {
|
|
90
|
+
const raw = typeof value === 'string' ? value : String(value ?? '');
|
|
91
|
+
const basename = raw.split(/[\\/]/u).filter(Boolean).at(-1)?.slice(0, 160);
|
|
92
|
+
return basename ? `${REDACTED_PATH}/${basename}` : REDACTED_PATH;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Sanitize secrets, URLs, and paths embedded in an incidental text message.
|
|
97
|
+
*
|
|
98
|
+
* @param {unknown} value
|
|
99
|
+
* @param {number} [maxLength]
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
export function sanitizeIncidentalText(value, maxLength = DEFAULT_MAX_STRING_LENGTH) {
|
|
103
|
+
let text = typeof value === 'string' ? value : String(value ?? '');
|
|
104
|
+
text = text.replace(
|
|
105
|
+
/\b(authorization|proxy-authorization)\s*:\s*[^\r\n,;]+/giu,
|
|
106
|
+
(_match, name) => `${name}: ${REDACTED}`
|
|
107
|
+
);
|
|
108
|
+
text = text.replace(/\b(set-cookie|cookie)\s*:\s*[^\r\n]+/giu, (_match, name) => {
|
|
109
|
+
return `${name}: ${REDACTED}`;
|
|
110
|
+
});
|
|
111
|
+
text = text.replace(
|
|
112
|
+
/\b(?:https?|file|blob|chrome|chrome-extension|edge|devtools):[^\s"'<>]+/giu,
|
|
113
|
+
(url) => sanitizeIncidentalUrl(url, maxLength)
|
|
114
|
+
);
|
|
115
|
+
text = text.replace(/\bdata:[^\s"'<>]+/giu, (url) => sanitizeIncidentalUrl(url, maxLength));
|
|
116
|
+
text = text.replace(/(?:^|[\s"'(])([a-z]:\\[^\s"')]+|\\\\[^\s"')]+)/giu, (match, path) =>
|
|
117
|
+
match.replace(path, sanitizeIncidentalPath(path))
|
|
118
|
+
);
|
|
119
|
+
text = text.replace(
|
|
120
|
+
/(?:^|[\s"'(])(\/(?:Users|home|private|var|tmp|opt|etc|root|usr|srv|mnt|Volumes|Library|Applications)\/[^\s"')]+)/gu,
|
|
121
|
+
(match, path) => match.replace(path, sanitizeIncidentalPath(path))
|
|
122
|
+
);
|
|
123
|
+
return text.slice(0, maxLength);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Recursively sanitize incidental structured data while bounding traversal.
|
|
128
|
+
* Explicit sensitive-read values must never pass through this helper.
|
|
129
|
+
*
|
|
130
|
+
* @param {unknown} value
|
|
131
|
+
* @param {{ maxDepth?: number, maxNodes?: number, maxStringLength?: number }} [options]
|
|
132
|
+
* @returns {unknown}
|
|
133
|
+
*/
|
|
134
|
+
export function sanitizeIncidentalValue(value, options = {}) {
|
|
135
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
136
|
+
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
137
|
+
const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH;
|
|
138
|
+
const seen = new WeakSet();
|
|
139
|
+
let nodes = 0;
|
|
140
|
+
|
|
141
|
+
/** @param {unknown} current @param {number} depth @param {string | null} key @returns {unknown} */
|
|
142
|
+
const visit = (current, depth, key) => {
|
|
143
|
+
nodes += 1;
|
|
144
|
+
if (nodes > maxNodes) return '[truncated]';
|
|
145
|
+
if (key !== null && isSensitiveIncidentalKey(key)) return REDACTED;
|
|
146
|
+
if (typeof current === 'string') {
|
|
147
|
+
if (key && /url|uri|href|endpoint/iu.test(key)) {
|
|
148
|
+
return sanitizeIncidentalUrl(current, maxStringLength);
|
|
149
|
+
}
|
|
150
|
+
if (key && /path|directory|filename/iu.test(key) && isAbsolutePath(current)) {
|
|
151
|
+
return sanitizeIncidentalPath(current);
|
|
152
|
+
}
|
|
153
|
+
return sanitizeIncidentalText(current, maxStringLength);
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
current === null ||
|
|
157
|
+
typeof current === 'number' ||
|
|
158
|
+
typeof current === 'boolean' ||
|
|
159
|
+
typeof current === 'undefined'
|
|
160
|
+
) {
|
|
161
|
+
return current;
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
typeof current === 'bigint' ||
|
|
165
|
+
typeof current === 'symbol' ||
|
|
166
|
+
typeof current === 'function'
|
|
167
|
+
) {
|
|
168
|
+
return sanitizeIncidentalText(current, maxStringLength);
|
|
169
|
+
}
|
|
170
|
+
if (current instanceof Error) {
|
|
171
|
+
return {
|
|
172
|
+
name: sanitizeIncidentalText(current.name, maxStringLength),
|
|
173
|
+
message: sanitizeIncidentalText(current.message, maxStringLength),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (depth >= maxDepth) return '[truncated]';
|
|
177
|
+
if (seen.has(current)) return '[circular]';
|
|
178
|
+
seen.add(current);
|
|
179
|
+
if (Array.isArray(current)) {
|
|
180
|
+
return current.map((item) => visit(item, depth + 1, null));
|
|
181
|
+
}
|
|
182
|
+
const result = /** @type {Record<string, unknown>} */ ({});
|
|
183
|
+
for (const [childKey, childValue] of Object.entries(current)) {
|
|
184
|
+
result[sanitizeIncidentalText(childKey, 160)] = visit(childValue, depth + 1, childKey);
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return visit(value, 0, null);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @param {string} value */
|
|
193
|
+
function isAbsolutePath(value) {
|
|
194
|
+
return /^(?:[a-z]:\\|\\\\|\/(?:Users|home|private|var|tmp|opt|etc|root|usr|srv|mnt|Volumes|Library|Applications)\/)/iu.test(
|
|
195
|
+
value
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
* Public protocol barrel for shared bridge helpers, errors, capabilities, and
|
|
5
5
|
* request/response normalization utilities.
|
|
6
6
|
*/
|
|
7
|
+
export * from './budget-presets.js';
|
|
7
8
|
export * from './budget.js';
|
|
8
9
|
export * from './capabilities.js';
|
|
9
10
|
export * from './defaults.js';
|
|
10
11
|
export * from './errors.js';
|
|
12
|
+
export * from './har.js';
|
|
13
|
+
export * from './incidental-sanitizer.js';
|
|
11
14
|
export * from './json-lines.js';
|
|
12
15
|
export * from './payload-cost.js';
|
|
13
16
|
export * from './protocol.js';
|
|
14
17
|
export * from './registry.js';
|
|
18
|
+
export * from './recovery-telemetry.js';
|
|
15
19
|
export * from './summary.js';
|
|
@@ -7,7 +7,7 @@ import { MAX_JSON_LINE_BYTES } from './defaults.js';
|
|
|
7
7
|
*
|
|
8
8
|
* @param {import('node:net').Socket} socket
|
|
9
9
|
* @param {(message: unknown) => void} onMessage
|
|
10
|
-
* @param {{ maxLineBytes?: number, onProtocolError?: (error: Error) => void }} [options]
|
|
10
|
+
* @param {{ maxLineBytes?: number, onProtocolError?: (error: Error) => void, onInvalidLine?: (error: Error) => void }} [options]
|
|
11
11
|
* @returns {void}
|
|
12
12
|
*/
|
|
13
13
|
export function parseJsonLines(socket, onMessage, options = {}) {
|
|
@@ -50,8 +50,9 @@ export function parseJsonLines(socket, onMessage, options = {}) {
|
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
52
|
onMessage(JSON.parse(line));
|
|
53
|
-
} catch {
|
|
53
|
+
} catch (error) {
|
|
54
54
|
// Malformed JSON line - skip it.
|
|
55
|
+
options.onInvalidLine?.(error instanceof Error ? error : new Error(String(error)));
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
});
|