@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.
Files changed (48) hide show
  1. package/PRIVACY.md +29 -3
  2. package/README.md +10 -3
  3. package/SECURITY.md +12 -0
  4. package/package.json +7 -1
  5. package/packages/agent-client/src/cli-args.js +70 -6
  6. package/packages/agent-client/src/cli-batch.js +127 -59
  7. package/packages/agent-client/src/cli-helpers.js +8 -0
  8. package/packages/agent-client/src/cli-output.js +8 -1
  9. package/packages/agent-client/src/cli-setup-commands.js +3 -1
  10. package/packages/agent-client/src/cli.js +254 -26
  11. package/packages/agent-client/src/client.js +156 -3
  12. package/packages/agent-client/src/command-registry.js +14 -4
  13. package/packages/agent-client/src/config.js +195 -0
  14. package/packages/agent-client/src/mcp-config.js +8 -8
  15. package/packages/agent-client/src/npm-self-update.js +305 -0
  16. package/packages/agent-client/src/remotes.js +4 -2
  17. package/packages/agent-client/src/runtime.js +99 -7
  18. package/packages/agent-client/src/types.ts +38 -0
  19. package/packages/mcp-server/src/guidance.js +37 -6
  20. package/packages/mcp-server/src/handlers-capture.js +29 -2
  21. package/packages/mcp-server/src/handlers-dom.js +54 -3
  22. package/packages/mcp-server/src/handlers-page.js +333 -60
  23. package/packages/mcp-server/src/handlers-utils.js +141 -151
  24. package/packages/mcp-server/src/handlers.js +17 -4
  25. package/packages/mcp-server/src/server.js +269 -51
  26. package/packages/mcp-server/src/toolset.js +48 -0
  27. package/packages/native-host/src/artifact-store.js +353 -0
  28. package/packages/native-host/src/config.js +9 -1
  29. package/packages/native-host/src/daemon-logger.js +3 -1
  30. package/packages/native-host/src/daemon.js +457 -27
  31. package/packages/native-host/src/dom-baseline-owners.js +210 -0
  32. package/packages/native-host/src/native-host.js +38 -29
  33. package/packages/protocol/src/budget-presets.js +182 -0
  34. package/packages/protocol/src/capability-values.js +2 -0
  35. package/packages/protocol/src/defaults.js +28 -0
  36. package/packages/protocol/src/errors.js +48 -5
  37. package/packages/protocol/src/har.js +34 -0
  38. package/packages/protocol/src/incidental-sanitizer.js +197 -0
  39. package/packages/protocol/src/index.js +4 -0
  40. package/packages/protocol/src/json-lines.js +3 -2
  41. package/packages/protocol/src/protocol.js +316 -6
  42. package/packages/protocol/src/recovery-telemetry.js +302 -0
  43. package/packages/protocol/src/registry.js +226 -6
  44. package/packages/protocol/src/summary.js +116 -25
  45. package/packages/protocol/src/types.ts +490 -4
  46. package/skills/browser-bridge/SKILL.md +26 -8
  47. package/skills/browser-bridge/references/protocol.md +125 -15
  48. 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
  });