@browserbridge/bbx 1.8.0 → 1.9.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 +35 -0
- package/packages/agent-client/src/cli-batch.js +121 -58
- package/packages/agent-client/src/cli-output.js +8 -1
- package/packages/agent-client/src/cli.js +227 -19
- package/packages/agent-client/src/client.js +156 -3
- package/packages/agent-client/src/command-registry.js +8 -2
- package/packages/agent-client/src/config.js +195 -0
- 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 +97 -5
- package/packages/agent-client/src/types.ts +38 -0
- 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 +180 -60
- package/packages/mcp-server/src/handlers-utils.js +35 -4
- package/packages/mcp-server/src/handlers.js +10 -3
- package/packages/mcp-server/src/server.js +75 -6
- 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 +543 -27
- package/packages/native-host/src/native-host.js +25 -2
- 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/incidental-sanitizer.js +197 -0
- package/packages/protocol/src/index.js +2 -0
- package/packages/protocol/src/protocol.js +316 -6
- package/packages/protocol/src/recovery-telemetry.js +302 -0
- package/packages/protocol/src/registry.js +143 -6
- package/packages/protocol/src/summary.js +116 -25
- package/packages/protocol/src/types.ts +489 -4
- package/skills/browser-bridge/SKILL.md +22 -8
- package/skills/browser-bridge/references/protocol.md +117 -13
- package/skills/browser-bridge/references/token-efficiency.md +17 -7
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createFailure,
|
|
7
|
+
ERROR_CODES,
|
|
8
|
+
MAX_JSON_LINE_BYTES,
|
|
9
|
+
sanitizeIncidentalText,
|
|
10
|
+
} from '../../protocol/src/index.js';
|
|
6
11
|
import { readBridgeAuthToken } from './auth-token.js';
|
|
7
12
|
import { createSocketBridgeTransport, getBridgeTransport } from './config.js';
|
|
8
13
|
import { spawnBridgeDaemonProcess } from './daemon-process.js';
|
|
@@ -110,6 +115,16 @@ function isHostActivity(message) {
|
|
|
110
115
|
);
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
/** @param {unknown} message */
|
|
119
|
+
function isHostArtifactMessage(message) {
|
|
120
|
+
return Boolean(
|
|
121
|
+
message &&
|
|
122
|
+
typeof message === 'object' &&
|
|
123
|
+
typeof (/** @type {Record<string, unknown>} */ (message).type) === 'string' &&
|
|
124
|
+
String(/** @type {Record<string, unknown>} */ (message).type).startsWith('host.artifact.')
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
/**
|
|
114
129
|
* @param {{ transport?: BridgeTransport, socketPath?: string }} [options={}]
|
|
115
130
|
* @returns {Promise<void>}
|
|
@@ -130,7 +145,7 @@ export async function runNativeHost({
|
|
|
130
145
|
response: createFailure(
|
|
131
146
|
'native_bootstrap',
|
|
132
147
|
ERROR_CODES.NATIVE_HOST_UNAVAILABLE,
|
|
133
|
-
error instanceof Error ? error.message : String(error)
|
|
148
|
+
sanitizeIncidentalText(error instanceof Error ? error.message : String(error))
|
|
134
149
|
),
|
|
135
150
|
});
|
|
136
151
|
return;
|
|
@@ -257,6 +272,14 @@ export async function runNativeHost({
|
|
|
257
272
|
});
|
|
258
273
|
return;
|
|
259
274
|
}
|
|
275
|
+
if (isHostArtifactMessage(message)) {
|
|
276
|
+
const record = /** @type {Record<string, unknown>} */ (message);
|
|
277
|
+
await writeJsonLine(socket, {
|
|
278
|
+
...record,
|
|
279
|
+
type: String(record.type).replace(/^host\./u, 'extension.'),
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
260
283
|
await writeJsonLine(socket, {
|
|
261
284
|
type: 'extension.response',
|
|
262
285
|
response: message,
|
|
@@ -20,6 +20,7 @@ export const CAPABILITIES = Object.freeze({
|
|
|
20
20
|
PERFORMANCE_READ: 'performance.read',
|
|
21
21
|
NETWORK_READ: 'network.read',
|
|
22
22
|
NETWORK_INTERCEPT: 'network.intercept',
|
|
23
|
+
SENSITIVE_READ: 'sensitive.read',
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
export const DEFAULT_CAPABILITIES = Object.freeze([
|
|
@@ -42,4 +43,5 @@ export const DEFAULT_CAPABILITIES = Object.freeze([
|
|
|
42
43
|
CAPABILITIES.PERFORMANCE_READ,
|
|
43
44
|
CAPABILITIES.NETWORK_READ,
|
|
44
45
|
CAPABILITIES.NETWORK_INTERCEPT,
|
|
46
|
+
CAPABILITIES.SENSITIVE_READ,
|
|
45
47
|
]);
|
|
@@ -21,6 +21,9 @@ export const DEFAULT_MAX_HTML_LENGTH = 2000;
|
|
|
21
21
|
export const DEFAULT_A11Y_MAX_NODES = 500;
|
|
22
22
|
export const DEFAULT_A11Y_MAX_DEPTH = 6;
|
|
23
23
|
export const DEFAULT_NETWORK_LIMIT = 50;
|
|
24
|
+
export const DEFAULT_HAR_LIMIT = 50;
|
|
25
|
+
export const MAX_HAR_ENTRIES = 200;
|
|
26
|
+
export const HAR_AUTO_INLINE_BYTES = 262_144;
|
|
24
27
|
export const DEFAULT_CONSOLE_LIMIT = 50;
|
|
25
28
|
export const DEFAULT_VIEWPORT_WIDTH = 1280;
|
|
26
29
|
export const DEFAULT_VIEWPORT_HEIGHT = 720;
|
|
@@ -34,6 +37,31 @@ export const MAX_NATIVE_MESSAGE_BYTES = 1_048_576;
|
|
|
34
37
|
/** Maximum size of one newline-delimited daemon socket message in bytes. */
|
|
35
38
|
export const MAX_JSON_LINE_BYTES = MAX_NATIVE_MESSAGE_BYTES;
|
|
36
39
|
|
|
40
|
+
/** Maximum UTF-8 size of an exact sensitive value returned atomically. */
|
|
41
|
+
export const MAX_SENSITIVE_VALUE_BYTES = 262_144;
|
|
42
|
+
|
|
43
|
+
/** Maximum calls accepted by any read-only batch surface. */
|
|
44
|
+
export const MAX_BATCH_CALLS = 20;
|
|
45
|
+
|
|
46
|
+
/** Maximum read-only batch calls executing concurrently. */
|
|
47
|
+
export const MAX_BATCH_CONCURRENCY = 5;
|
|
48
|
+
export const SCREENSHOT_AUTO_INLINE_BYTES = 262_144;
|
|
49
|
+
export const SCREENSHOT_MAX_INLINE_BYTES = 524_288;
|
|
50
|
+
export const ARTIFACT_CHUNK_BYTES = 196_608;
|
|
51
|
+
export const MAX_ARTIFACT_BYTES = 33_554_432;
|
|
52
|
+
export const MAX_ARTIFACT_CLIENT_BYTES = 67_108_864;
|
|
53
|
+
export const MAX_ARTIFACT_TOTAL_BYTES = 268_435_456;
|
|
54
|
+
export const MAX_ARTIFACTS_PER_CLIENT = 16;
|
|
55
|
+
export const ARTIFACT_TTL_MS = 300_000;
|
|
56
|
+
|
|
57
|
+
/** DOM baseline retention and storage limits. */
|
|
58
|
+
export const DOM_BASELINE_TTL_MS = 300_000;
|
|
59
|
+
export const MAX_DOM_BASELINES_PER_TAB = 8;
|
|
60
|
+
export const MAX_DOM_BASELINES_GLOBAL = 32;
|
|
61
|
+
export const MAX_DOM_BASELINE_BYTES_PER_TAB = 1_048_576;
|
|
62
|
+
export const MAX_DOM_BASELINE_BYTES_GLOBAL = 4_194_304;
|
|
63
|
+
export const MAX_DOM_BASELINE_BYTES = 262_144;
|
|
64
|
+
|
|
37
65
|
/** Default timeout for a bridge request awaiting an extension response (ms). */
|
|
38
66
|
export const DEFAULT_DAEMON_PENDING_TIMEOUT_MS = 30_000;
|
|
39
67
|
|
|
@@ -16,7 +16,15 @@ export const ERROR_CODES = Object.freeze({
|
|
|
16
16
|
DIALOG_NOT_OPEN: 'DIALOG_NOT_OPEN',
|
|
17
17
|
DIALOG_ACTION_CONFLICT: 'DIALOG_ACTION_CONFLICT',
|
|
18
18
|
RESULT_TRUNCATED: 'RESULT_TRUNCATED',
|
|
19
|
-
|
|
19
|
+
RESULT_TOO_LARGE: 'RESULT_TOO_LARGE',
|
|
20
|
+
SENSITIVE_TARGET_NOT_FOUND: 'SENSITIVE_TARGET_NOT_FOUND',
|
|
21
|
+
CONTENT_SCRIPT_UNAVAILABLE: 'CONTENT_SCRIPT_UNAVAILABLE',
|
|
22
|
+
ARTIFACT_NOT_FOUND: 'ARTIFACT_NOT_FOUND',
|
|
23
|
+
ARTIFACT_QUOTA_EXCEEDED: 'ARTIFACT_QUOTA_EXCEEDED',
|
|
24
|
+
ARTIFACT_TRANSFER_INVALID: 'ARTIFACT_TRANSFER_INVALID',
|
|
25
|
+
DOM_BASELINE_NOT_FOUND: 'DOM_BASELINE_NOT_FOUND',
|
|
26
|
+
DOM_BASELINE_INVALIDATED: 'DOM_BASELINE_INVALIDATED',
|
|
27
|
+
DOM_BASELINE_QUOTA_EXCEEDED: 'DOM_BASELINE_QUOTA_EXCEEDED',
|
|
20
28
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
21
29
|
INVALID_REQUEST: 'INVALID_REQUEST',
|
|
22
30
|
NATIVE_HOST_UNAVAILABLE: 'NATIVE_HOST_UNAVAILABLE',
|
|
@@ -38,6 +46,15 @@ export const ERROR_RECOVERY = Object.freeze({
|
|
|
38
46
|
retry: false,
|
|
39
47
|
hint: 'Result was truncated to fit the response budget. Narrow the query or raise the relevant budget if more detail is required.',
|
|
40
48
|
},
|
|
49
|
+
[ERROR_CODES.RESULT_TOO_LARGE]: {
|
|
50
|
+
retry: false,
|
|
51
|
+
hint: 'The result exceeds its atomic delivery limit. For screenshots, use artifact delivery, a smaller region, lower scale, or lossy compression. For sensitive reads, use a narrower exact target; partial values are never returned.',
|
|
52
|
+
},
|
|
53
|
+
[ERROR_CODES.SENSITIVE_TARGET_NOT_FOUND]: {
|
|
54
|
+
retry: false,
|
|
55
|
+
alternativeMethod: 'page.get_storage',
|
|
56
|
+
hint: 'The exact storage key does not exist. Use page.get_storage to inspect metadata before another deliberate sensitive read.',
|
|
57
|
+
},
|
|
41
58
|
[ERROR_CODES.ELEMENT_STALE]: {
|
|
42
59
|
retry: false,
|
|
43
60
|
alternativeMethod: 'dom.query',
|
|
@@ -97,10 +114,36 @@ export const ERROR_RECOVERY = Object.freeze({
|
|
|
97
114
|
retryAfterMs: 1000,
|
|
98
115
|
hint: 'Operation exceeded the time limit. Retry once, or simplify the request (smaller maxNodes, narrower selector).',
|
|
99
116
|
},
|
|
100
|
-
[ERROR_CODES.
|
|
101
|
-
retry:
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
[ERROR_CODES.CONTENT_SCRIPT_UNAVAILABLE]: {
|
|
118
|
+
retry: false,
|
|
119
|
+
hint: 'The page cannot host the Browser Bridge content script. Switch to a normal http(s) page in the enabled window.',
|
|
120
|
+
},
|
|
121
|
+
[ERROR_CODES.ARTIFACT_NOT_FOUND]: {
|
|
122
|
+
retry: false,
|
|
123
|
+
hint: 'The artifact is missing, expired, deleted, or owned by another client. Capture it again.',
|
|
124
|
+
},
|
|
125
|
+
[ERROR_CODES.ARTIFACT_QUOTA_EXCEEDED]: {
|
|
126
|
+
retry: false,
|
|
127
|
+
hint: 'Artifact storage quota was exceeded. Delete unneeded artifacts or retry after expiry.',
|
|
128
|
+
},
|
|
129
|
+
[ERROR_CODES.ARTIFACT_TRANSFER_INVALID]: {
|
|
130
|
+
retry: false,
|
|
131
|
+
hint: 'Artifact transfer validation failed. Capture the screenshot again.',
|
|
132
|
+
},
|
|
133
|
+
[ERROR_CODES.DOM_BASELINE_NOT_FOUND]: {
|
|
134
|
+
retry: false,
|
|
135
|
+
alternativeMethod: 'dom.baseline.create',
|
|
136
|
+
hint: 'The DOM baseline is missing, expired, or released. Create a new baseline before comparing or describing it.',
|
|
137
|
+
},
|
|
138
|
+
[ERROR_CODES.DOM_BASELINE_INVALIDATED]: {
|
|
139
|
+
retry: false,
|
|
140
|
+
alternativeMethod: 'dom.baseline.create',
|
|
141
|
+
hint: 'The page document or baseline scope changed. Create a new baseline in the current document.',
|
|
142
|
+
},
|
|
143
|
+
[ERROR_CODES.DOM_BASELINE_QUOTA_EXCEEDED]: {
|
|
144
|
+
retry: false,
|
|
145
|
+
alternativeMethod: 'dom.baseline.release',
|
|
146
|
+
hint: 'DOM baseline storage quota was exceeded. Release unneeded baselines, narrow the selector, or reduce maxNodes and textBudget before creating another baseline.',
|
|
104
147
|
},
|
|
105
148
|
[ERROR_CODES.EXTENSION_DISCONNECTED]: {
|
|
106
149
|
retry: true,
|
|
@@ -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
|
+
}
|
|
@@ -8,8 +8,10 @@ export * from './budget.js';
|
|
|
8
8
|
export * from './capabilities.js';
|
|
9
9
|
export * from './defaults.js';
|
|
10
10
|
export * from './errors.js';
|
|
11
|
+
export * from './incidental-sanitizer.js';
|
|
11
12
|
export * from './json-lines.js';
|
|
12
13
|
export * from './payload-cost.js';
|
|
13
14
|
export * from './protocol.js';
|
|
14
15
|
export * from './registry.js';
|
|
16
|
+
export * from './recovery-telemetry.js';
|
|
15
17
|
export * from './summary.js';
|