browser-debugger-cli 0.14.0 → 0.15.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/.claude/skills/bdg/SKILL.md +1 -1
- package/dist/commands/cdp.js +1 -0
- package/dist/commands/cleanup.js +3 -0
- package/dist/commands/dom/eval.d.ts +2 -1
- package/dist/commands/dom/eval.js +6 -21
- package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
- package/dist/commands/dom/helpers/evalResult.js +59 -0
- package/dist/commands/helpJson.d.ts +1 -1
- package/dist/commands/helpJson.js +3 -3
- package/dist/commands/helpTopic.js +10 -4
- package/dist/commands/network/har.js +18 -14
- package/dist/commands/optionBehaviors.js +8 -3
- package/dist/commands/shared/optionTypes.d.ts +1 -0
- package/dist/commands/shared/outputFile.d.ts +2 -1
- package/dist/commands/shared/outputFile.js +7 -4
- package/dist/commands/status.js +3 -1
- package/dist/commands/stop.js +2 -1
- package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
- package/dist/connection/launcher/flagsBuilder.js +107 -23
- package/dist/connection/launcher.d.ts +1 -1
- package/dist/connection/launcher.js +1 -2
- package/dist/constants.d.ts +2 -4
- package/dist/constants.js +2 -4
- package/dist/daemon/launcher.d.ts +17 -3
- package/dist/daemon/launcher.js +37 -7
- package/dist/daemon/session/commandRegistry.js +2 -2
- package/dist/daemon.js +8107 -7962
- package/dist/errors/messages.d.ts +23 -0
- package/dist/errors/messages.js +86 -6
- package/dist/index.js +555 -166
- package/dist/ipc/client.d.ts +6 -1
- package/dist/ipc/client.js +11 -2
- package/dist/ipc/protocol/commands.d.ts +4 -0
- package/dist/ipc/transport/index.d.ts +6 -0
- package/dist/ipc/transport/index.js +16 -1
- package/dist/runtime/dom/elementInfo.d.ts +7 -0
- package/dist/runtime/dom/elementInfo.js +8 -1
- package/dist/runtime/dom/evalHelpers.d.ts +24 -4
- package/dist/runtime/dom/evalHelpers.js +40 -12
- package/dist/runtime/dom/frames.d.ts +2 -1
- package/dist/runtime/dom/frames.js +3 -1
- package/dist/runtime/page/emulation.js +6 -5
- package/dist/runtime/page/userAgent.d.ts +86 -2
- package/dist/runtime/page/userAgent.js +154 -33
- package/dist/session/paths.d.ts +38 -3
- package/dist/session/paths.js +154 -7
- package/dist/session/portClaims.d.ts +0 -8
- package/dist/session/portClaims.js +1 -22
- package/dist/session/sessionList.d.ts +5 -1
- package/dist/session/sessionList.js +5 -1
- package/dist/telemetry/har/builder.d.ts +12 -1
- package/dist/telemetry/har/builder.js +10 -2
- package/dist/telemetry/har/sanitize.d.ts +24 -0
- package/dist/telemetry/har/sanitize.js +138 -0
- package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
- package/dist/telemetry/har/sanitizeBody.js +168 -0
- package/dist/ui/formatters/sessions.d.ts +3 -2
- package/dist/ui/formatters/sessions.js +10 -3
- package/dist/ui/messages/chrome.d.ts +14 -6
- package/dist/ui/messages/chrome.js +52 -12
- package/dist/ui/messages/networkMessages.d.ts +26 -0
- package/dist/ui/messages/networkMessages.js +21 -0
- package/dist/ui/messages/session.d.ts +8 -0
- package/dist/ui/messages/session.js +10 -0
- package/dist/utils/atomicFile.d.ts +2 -1
- package/dist/utils/atomicFile.js +5 -2
- package/dist/utils/directories.d.ts +41 -0
- package/dist/utils/directories.js +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction for HAR exports.
|
|
3
|
+
*
|
|
4
|
+
* HAR files are made to be shared (bug reports, tickets), so `bdg network har`
|
|
5
|
+
* redacts credentials by default, as Chrome DevTools does since Chrome 130.
|
|
6
|
+
* Chrome's sanitized export drops the `Cookie`, `Set-Cookie` and
|
|
7
|
+
* `Authorization` headers and empties the `cookies` arrays; bdg instead keeps
|
|
8
|
+
* every header, cookie and parameter name (and cookie attributes such as
|
|
9
|
+
* `httpOnly`) with the value `[redacted]`, so the export still shows that a
|
|
10
|
+
* request was authenticated and which cookies were set. It also covers API
|
|
11
|
+
* key, token and session headers, credential query parameters in URLs
|
|
12
|
+
* (`?code=`, `?access_token=`) and credential fields of request bodies
|
|
13
|
+
* (sanitizeBody.ts). `headersSize` and `bodySize` stay those of the captured
|
|
14
|
+
* request. Response bodies and WebSocket messages are not redacted.
|
|
15
|
+
*/
|
|
16
|
+
import { REDACTED, isSensitiveField, redactPairs, redactRequestBody, } from './sanitizeBody.js';
|
|
17
|
+
/** {@link REDACTED} as written in a URL */
|
|
18
|
+
const URL_REDACTED = encodeURIComponent(REDACTED);
|
|
19
|
+
/** Headers whose values are credentials, lowercased (others match by pattern) */
|
|
20
|
+
const SENSITIVE_HEADERS = new Set([
|
|
21
|
+
'authorization',
|
|
22
|
+
'proxy-authorization',
|
|
23
|
+
'authentication',
|
|
24
|
+
'cookie',
|
|
25
|
+
'set-cookie',
|
|
26
|
+
]);
|
|
27
|
+
/** Custom headers carrying keys or tokens (`X-Api-Key`, `X-Auth-Token`, `X-CSRF-Token`) */
|
|
28
|
+
const SENSITIVE_CUSTOM_HEADER = /^x-.*(key|token|secret|auth)/i;
|
|
29
|
+
/**
|
|
30
|
+
* Headers with a credential segment between hyphens (`api-key`,
|
|
31
|
+
* `private-token`, `cf-access-jwt-assertion`, `ocp-apim-subscription-key`,
|
|
32
|
+
* `session-id`); `www-authenticate` and `proxy-authenticate` do not match
|
|
33
|
+
*/
|
|
34
|
+
const SENSITIVE_HEADER_SEGMENT = /(^|-)(api-?key|apikey|token|secret|jwt|subscription-key|session(-?id)?)(-|$)/i;
|
|
35
|
+
/** Headers whose values are URLs that may carry credential parameters */
|
|
36
|
+
const URL_HEADERS = new Set(['location', 'referer']);
|
|
37
|
+
/** Query parameter names that hold credentials in URLs only (OAuth codes, signed URLs, API keys) */
|
|
38
|
+
const SENSITIVE_URL_PARAM = /^(code|sig|key)$/i;
|
|
39
|
+
/**
|
|
40
|
+
* Redact the credentials of a HAR entry.
|
|
41
|
+
*
|
|
42
|
+
* @param entry - Entry built from the captured request
|
|
43
|
+
* @returns Copy of the entry with credential values replaced by {@link REDACTED}
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeEntry(entry) {
|
|
46
|
+
const { request, response } = entry;
|
|
47
|
+
const postData = request.postData;
|
|
48
|
+
return {
|
|
49
|
+
...entry,
|
|
50
|
+
request: {
|
|
51
|
+
...request,
|
|
52
|
+
url: redactUrl(request.url),
|
|
53
|
+
cookies: request.cookies.map(redactCookie),
|
|
54
|
+
headers: request.headers.map(redactHeader),
|
|
55
|
+
queryString: request.queryString.map(redactQueryParam),
|
|
56
|
+
...(postData?.text !== undefined && {
|
|
57
|
+
postData: { ...postData, text: redactRequestBody(postData.text, postData.mimeType) },
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
60
|
+
response: {
|
|
61
|
+
...response,
|
|
62
|
+
cookies: response.cookies.map(redactCookie),
|
|
63
|
+
headers: response.headers.map(redactHeader),
|
|
64
|
+
redirectURL: redactUrl(response.redirectURL),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a header carries a credential.
|
|
70
|
+
*
|
|
71
|
+
* @param name - Header name (any case)
|
|
72
|
+
* @returns True for auth, cookie, API key, token, secret and session headers
|
|
73
|
+
*/
|
|
74
|
+
function isSensitiveHeader(name) {
|
|
75
|
+
return (SENSITIVE_HEADERS.has(name.toLowerCase()) ||
|
|
76
|
+
SENSITIVE_CUSTOM_HEADER.test(name) ||
|
|
77
|
+
SENSITIVE_HEADER_SEGMENT.test(name));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Redact a header's value if it carries a credential, or the credential
|
|
81
|
+
* parameters of a `Location` or `Referer` URL.
|
|
82
|
+
*
|
|
83
|
+
* @param header - HAR header
|
|
84
|
+
* @returns The header, or a copy with its value redacted
|
|
85
|
+
*/
|
|
86
|
+
function redactHeader(header) {
|
|
87
|
+
if (isSensitiveHeader(header.name))
|
|
88
|
+
return { ...header, value: REDACTED };
|
|
89
|
+
if (!URL_HEADERS.has(header.name.toLowerCase()))
|
|
90
|
+
return header;
|
|
91
|
+
const value = redactUrl(header.value);
|
|
92
|
+
return value === header.value ? header : { ...header, value };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Redact a cookie's value, keeping its name and attributes.
|
|
96
|
+
*
|
|
97
|
+
* @param cookie - HAR cookie
|
|
98
|
+
* @returns Copy with the value redacted
|
|
99
|
+
*/
|
|
100
|
+
function redactCookie(cookie) {
|
|
101
|
+
return { ...cookie, value: REDACTED };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a URL query or fragment parameter holds a credential.
|
|
105
|
+
*
|
|
106
|
+
* @param name - Decoded parameter name
|
|
107
|
+
* @returns True for credential field names, `code`, `sig` and `key`
|
|
108
|
+
*/
|
|
109
|
+
function isSensitiveParam(name) {
|
|
110
|
+
return isSensitiveField(name) || SENSITIVE_URL_PARAM.test(name);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Redact a parsed query parameter if it holds a credential.
|
|
114
|
+
*
|
|
115
|
+
* @param param - HAR query parameter
|
|
116
|
+
* @returns The parameter, or a copy with its value redacted
|
|
117
|
+
*/
|
|
118
|
+
function redactQueryParam(param) {
|
|
119
|
+
return isSensitiveParam(param.name) ? { ...param, value: REDACTED } : param;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Redact credential parameter values in a URL's query and fragment
|
|
123
|
+
* (`#access_token=` of the OAuth implicit flow), leaving the rest byte for byte.
|
|
124
|
+
*
|
|
125
|
+
* @param url - URL (empty for none)
|
|
126
|
+
* @returns URL with credential values replaced by the URL-encoded {@link REDACTED}
|
|
127
|
+
*/
|
|
128
|
+
function redactUrl(url) {
|
|
129
|
+
const hash = url.indexOf('#');
|
|
130
|
+
const beforeHash = hash === -1 ? url : url.slice(0, hash);
|
|
131
|
+
const fragment = hash === -1 ? '' : `#${redactPairs(url.slice(hash + 1), isSensitiveParam, URL_REDACTED)}`;
|
|
132
|
+
const question = beforeHash.indexOf('?');
|
|
133
|
+
if (question === -1)
|
|
134
|
+
return beforeHash + fragment;
|
|
135
|
+
const query = redactPairs(beforeHash.slice(question + 1), isSensitiveParam, URL_REDACTED);
|
|
136
|
+
return `${beforeHash.slice(0, question)}?${query}${fragment}`;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction in request bodies and `name=value` lists, for
|
|
3
|
+
* sanitized HAR exports (see sanitize.ts).
|
|
4
|
+
*
|
|
5
|
+
* Matching is by field name only, so it over-redacts: any primitive whose
|
|
6
|
+
* name looks like a credential (`tokenCount: 5`) is replaced too.
|
|
7
|
+
*/
|
|
8
|
+
/** Replaces a credential value */
|
|
9
|
+
export declare const REDACTED = "[redacted]";
|
|
10
|
+
/**
|
|
11
|
+
* Whether a body field, form field or query parameter name looks like it
|
|
12
|
+
* holds a credential.
|
|
13
|
+
*
|
|
14
|
+
* @param name - Field name
|
|
15
|
+
* @returns True for password, token, secret, key, session and signature names
|
|
16
|
+
*/
|
|
17
|
+
export declare function isSensitiveField(name: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Redact credential values in an `&`-separated `name=value` list (a form body,
|
|
20
|
+
* query string or fragment), leaving the other pairs byte for byte.
|
|
21
|
+
*
|
|
22
|
+
* @param text - List like `user=ann&password=hunter2`
|
|
23
|
+
* @param isSensitive - Whether a decoded name holds a credential
|
|
24
|
+
* @param replacement - Value written instead
|
|
25
|
+
* @returns List with credential values replaced
|
|
26
|
+
*/
|
|
27
|
+
export declare function redactPairs(text: string, isSensitive: (name: string) => boolean, replacement: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Redact credential fields of a request body: multipart parts, form fields
|
|
30
|
+
* (by Content-Type or shape) and JSON fields at any depth.
|
|
31
|
+
*
|
|
32
|
+
* @param text - Body text
|
|
33
|
+
* @param mimeType - Content-Type of the body
|
|
34
|
+
* @returns Body with credential values replaced; the text unchanged when
|
|
35
|
+
* there were none
|
|
36
|
+
*/
|
|
37
|
+
export declare function redactRequestBody(text: string, mimeType: string): string;
|
|
38
|
+
//# sourceMappingURL=sanitizeBody.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction in request bodies and `name=value` lists, for
|
|
3
|
+
* sanitized HAR exports (see sanitize.ts).
|
|
4
|
+
*
|
|
5
|
+
* Matching is by field name only, so it over-redacts: any primitive whose
|
|
6
|
+
* name looks like a credential (`tokenCount: 5`) is replaced too.
|
|
7
|
+
*/
|
|
8
|
+
import { SENSITIVE_NAME_SOURCE } from '../../runtime/dom/elementInfo.js';
|
|
9
|
+
import { createLogger } from '../../ui/logging/index.js';
|
|
10
|
+
import { getErrorMessage } from '../../utils/errors.js';
|
|
11
|
+
const log = createLogger('network');
|
|
12
|
+
/** Replaces a credential value */
|
|
13
|
+
export const REDACTED = '[redacted]';
|
|
14
|
+
/**
|
|
15
|
+
* Field names holding credentials: password-like names (shared with form
|
|
16
|
+
* masking), tokens, secrets, keys, sessions, signatures and credentials
|
|
17
|
+
*/
|
|
18
|
+
const SENSITIVE_FIELD = new RegExp(`${SENSITIVE_NAME_SOURCE}|token|secret|api[-_]?key|authoriz|credential|jwt|private[-_]?key|access[-_]?key|session|signature`, 'i');
|
|
19
|
+
/** A body that is a form whatever its Content-Type: `k=v&k=v`, no whitespace, not JSON */
|
|
20
|
+
const FORM_BODY = /^[^\s=&{["]+=[^\s&]*(?:&[^\s=&]+=[^\s&]*)*$/;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a body field, form field or query parameter name looks like it
|
|
23
|
+
* holds a credential.
|
|
24
|
+
*
|
|
25
|
+
* @param name - Field name
|
|
26
|
+
* @returns True for password, token, secret, key, session and signature names
|
|
27
|
+
*/
|
|
28
|
+
export function isSensitiveField(name) {
|
|
29
|
+
return SENSITIVE_FIELD.test(name);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Redact credential values in an `&`-separated `name=value` list (a form body,
|
|
33
|
+
* query string or fragment), leaving the other pairs byte for byte.
|
|
34
|
+
*
|
|
35
|
+
* @param text - List like `user=ann&password=hunter2`
|
|
36
|
+
* @param isSensitive - Whether a decoded name holds a credential
|
|
37
|
+
* @param replacement - Value written instead
|
|
38
|
+
* @returns List with credential values replaced
|
|
39
|
+
*/
|
|
40
|
+
export function redactPairs(text, isSensitive, replacement) {
|
|
41
|
+
return text
|
|
42
|
+
.split('&')
|
|
43
|
+
.map((pair) => {
|
|
44
|
+
const eq = pair.indexOf('=');
|
|
45
|
+
if (eq === -1 || !isSensitive(decodeName(pair.slice(0, eq))))
|
|
46
|
+
return pair;
|
|
47
|
+
return `${pair.slice(0, eq)}=${replacement}`;
|
|
48
|
+
})
|
|
49
|
+
.join('&');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Redact credential fields of a request body: multipart parts, form fields
|
|
53
|
+
* (by Content-Type or shape) and JSON fields at any depth.
|
|
54
|
+
*
|
|
55
|
+
* @param text - Body text
|
|
56
|
+
* @param mimeType - Content-Type of the body
|
|
57
|
+
* @returns Body with credential values replaced; the text unchanged when
|
|
58
|
+
* there were none
|
|
59
|
+
*/
|
|
60
|
+
export function redactRequestBody(text, mimeType) {
|
|
61
|
+
if (/multipart\/form-data/i.test(mimeType))
|
|
62
|
+
return redactMultipart(text, mimeType);
|
|
63
|
+
if (/x-www-form-urlencoded/i.test(mimeType) || FORM_BODY.test(text)) {
|
|
64
|
+
return redactPairs(text, isSensitiveField, REDACTED);
|
|
65
|
+
}
|
|
66
|
+
return redactJsonBody(text);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decode a form or query name.
|
|
70
|
+
*
|
|
71
|
+
* @param name - Encoded name
|
|
72
|
+
* @returns Decoded name, or the raw name when it is not valid encoding
|
|
73
|
+
*/
|
|
74
|
+
function decodeName(name) {
|
|
75
|
+
try {
|
|
76
|
+
return decodeURIComponent(name.replace(/\+/g, ' '));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
log.debug(`Field name not decodable: ${getErrorMessage(error)}`);
|
|
80
|
+
return name;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Redact the values of credential parts of a multipart body, leaving the
|
|
85
|
+
* other parts byte for byte.
|
|
86
|
+
*
|
|
87
|
+
* @param text - Body text
|
|
88
|
+
* @param mimeType - Content-Type with the boundary
|
|
89
|
+
* @returns Body with credential part values replaced
|
|
90
|
+
*/
|
|
91
|
+
function redactMultipart(text, mimeType) {
|
|
92
|
+
const match = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(mimeType);
|
|
93
|
+
const boundary = match?.[1] ?? match?.[2];
|
|
94
|
+
if (!boundary)
|
|
95
|
+
return text;
|
|
96
|
+
const delimiter = `--${boundary}`;
|
|
97
|
+
return text.split(delimiter).map(redactPart).join(delimiter);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Redact the value of one multipart part if its name holds a credential.
|
|
101
|
+
*
|
|
102
|
+
* @param part - Text between two boundary delimiters
|
|
103
|
+
* @returns The part, or the part with its value replaced
|
|
104
|
+
*/
|
|
105
|
+
function redactPart(part) {
|
|
106
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
107
|
+
if (headerEnd === -1)
|
|
108
|
+
return part;
|
|
109
|
+
const name = /;\s*name="([^"]*)"/i.exec(part.slice(0, headerEnd))?.[1];
|
|
110
|
+
if (name === undefined || !isSensitiveField(name))
|
|
111
|
+
return part;
|
|
112
|
+
const valueEnd = part.endsWith('\r\n') ? part.length - 2 : part.length;
|
|
113
|
+
return `${part.slice(0, headerEnd + 4)}${REDACTED}${part.slice(valueEnd)}`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Redact credential fields of a JSON body.
|
|
117
|
+
*
|
|
118
|
+
* @param text - Body text
|
|
119
|
+
* @returns Re-serialized JSON when a field was redacted, the text unchanged
|
|
120
|
+
* when none was or it is not JSON, and {@link REDACTED} for JSON too deep
|
|
121
|
+
* to walk
|
|
122
|
+
*/
|
|
123
|
+
function redactJsonBody(text) {
|
|
124
|
+
if (!/^\s*[[{]/.test(text))
|
|
125
|
+
return text;
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = JSON.parse(text);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
log.debug(`Request body not JSON: ${getErrorMessage(error)}`);
|
|
132
|
+
return error instanceof SyntaxError ? text : REDACTED;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const hits = { count: 0 };
|
|
136
|
+
const redacted = redactJson(parsed, false, hits);
|
|
137
|
+
return hits.count > 0 ? JSON.stringify(redacted) : text;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
log.debug(`Request body redacted whole: ${getErrorMessage(error)}`);
|
|
141
|
+
return REDACTED;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Copy of parsed JSON with the primitives under credential names replaced.
|
|
146
|
+
* Objects and arrays keep their structure; every string, number and boolean
|
|
147
|
+
* inside a credential-named field is replaced, at any depth.
|
|
148
|
+
*
|
|
149
|
+
* @param value - Parsed JSON value
|
|
150
|
+
* @param sensitive - Whether an enclosing field name holds a credential
|
|
151
|
+
* @param hits - Counter of replaced values
|
|
152
|
+
* @returns Redacted copy
|
|
153
|
+
*/
|
|
154
|
+
function redactJson(value, sensitive, hits) {
|
|
155
|
+
if (Array.isArray(value))
|
|
156
|
+
return value.map((item) => redactJson(item, sensitive, hits));
|
|
157
|
+
if (value !== null && typeof value === 'object') {
|
|
158
|
+
return Object.fromEntries(Object.entries(value).map(([key, field]) => [
|
|
159
|
+
key,
|
|
160
|
+
redactJson(field, sensitive || isSensitiveField(key), hits),
|
|
161
|
+
]));
|
|
162
|
+
}
|
|
163
|
+
if (!sensitive || !['string', 'number', 'boolean'].includes(typeof value))
|
|
164
|
+
return value;
|
|
165
|
+
hits.count++;
|
|
166
|
+
return REDACTED;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=sanitizeBody.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RunningSessionInfo } from '../../session/sessionList.js';
|
|
2
2
|
/**
|
|
3
|
-
* Format the sessions as a table, followed by why ended sessions ended
|
|
4
|
-
*
|
|
3
|
+
* Format the sessions as a table, followed by why ended sessions ended, why
|
|
4
|
+
* untrusted sessions' directories are not safe to use, and the cleanup
|
|
5
|
+
* commands of crashed and stale sessions.
|
|
5
6
|
*
|
|
6
7
|
* @param data - Sessions
|
|
7
8
|
* @returns Human-readable list
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { OutputFormatter } from '../formatting.js';
|
|
2
|
-
import { endedSessionText } from '../messages/session.js';
|
|
2
|
+
import { endedSessionText, untrustedSessionText } from '../messages/session.js';
|
|
3
3
|
/** Label of the default session in the list */
|
|
4
4
|
const DEFAULT_SESSION_LABEL = '(default)';
|
|
5
5
|
/**
|
|
6
|
-
* Format the sessions as a table, followed by why ended sessions ended
|
|
7
|
-
*
|
|
6
|
+
* Format the sessions as a table, followed by why ended sessions ended, why
|
|
7
|
+
* untrusted sessions' directories are not safe to use, and the cleanup
|
|
8
|
+
* commands of crashed and stale sessions.
|
|
8
9
|
*
|
|
9
10
|
* @param data - Sessions
|
|
10
11
|
* @returns Human-readable list
|
|
@@ -38,6 +39,12 @@ export function formatSessionList(data) {
|
|
|
38
39
|
if (ended.length > 0) {
|
|
39
40
|
fmt.blank().section('Ended without bdg stop:', ended);
|
|
40
41
|
}
|
|
42
|
+
const untrusted = data.sessions.flatMap(({ name, untrusted: why }) => why ? [untrustedSessionText(name ?? DEFAULT_SESSION_LABEL, why)] : []);
|
|
43
|
+
if (untrusted.length > 0) {
|
|
44
|
+
fmt
|
|
45
|
+
.blank()
|
|
46
|
+
.section('Directory not safe to use (not asked; bdg status --session <name> says how to fix it):', untrusted);
|
|
47
|
+
}
|
|
41
48
|
const cleanups = data.sessions.flatMap((session) => (session.cleanup ? [session.cleanup] : []));
|
|
42
49
|
if (cleanups.length > 0) {
|
|
43
50
|
fmt.hints('Crashed or stale sessions; clean up with:', cleanups);
|
|
@@ -20,8 +20,12 @@ export declare function formatChromeNotice(notice: NoticeDetails<ChromeNoticeCod
|
|
|
20
20
|
* Called at the UI boundary when a ChromeLaunchError with `issue` details
|
|
21
21
|
* reaches a CLI or daemon log sink. Core modules produce the IssueDetails;
|
|
22
22
|
* this function is the only place wording is assembled.
|
|
23
|
+
*
|
|
24
|
+
* @param issue - Structured issue
|
|
25
|
+
* @param diagnostics - Source of Chrome installation diagnostics (tests stub it)
|
|
26
|
+
* @returns User-facing message
|
|
23
27
|
*/
|
|
24
|
-
export declare function formatChromeIssue(issue: IssueDetails): string;
|
|
28
|
+
export declare function formatChromeIssue(issue: IssueDetails, diagnostics?: () => ChromeDiagnostics): string;
|
|
25
29
|
/**
|
|
26
30
|
* Chrome exited before its debugging port opened.
|
|
27
31
|
*
|
|
@@ -33,15 +37,19 @@ export declare function formatChromeIssue(issue: IssueDetails): string;
|
|
|
33
37
|
*/
|
|
34
38
|
export declare function chromeExitedDuringStartupError(exitCode: number | null, output: string[], userDataDir: string, profileInUse: boolean): string;
|
|
35
39
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* @returns Array of formatted diagnostic strings
|
|
40
|
+
* Options of {@link formatDiagnosticsForError}.
|
|
39
41
|
*/
|
|
40
|
-
export
|
|
42
|
+
export interface DiagnosticsFormatOptions {
|
|
43
|
+
/** OS the CHROME_PATH example is for (default: this one) */
|
|
44
|
+
platform?: NodeJS.Platform;
|
|
45
|
+
/** Suggest CHROME_PATH when no Chrome is found (off when CHROME_PATH is the problem) */
|
|
46
|
+
suggestChromePath?: boolean;
|
|
47
|
+
}
|
|
41
48
|
/**
|
|
42
49
|
* Format Chrome diagnostics for error reporting when Chrome launch fails.
|
|
43
50
|
*
|
|
44
51
|
* @param diagnostics - Chrome diagnostics information
|
|
52
|
+
* @param options - Platform of the example and whether to suggest CHROME_PATH
|
|
45
53
|
* @returns Formatted error message lines with troubleshooting steps
|
|
46
54
|
*
|
|
47
55
|
* @example
|
|
@@ -51,7 +59,7 @@ export declare function getFormattedDiagnostics(): string[];
|
|
|
51
59
|
* console.error(errorLines.join('\n'));
|
|
52
60
|
* ```
|
|
53
61
|
*/
|
|
54
|
-
export declare function formatDiagnosticsForError(diagnostics: ChromeDiagnostics): string[];
|
|
62
|
+
export declare function formatDiagnosticsForError(diagnostics: ChromeDiagnostics, { platform, suggestChromePath }?: DiagnosticsFormatOptions): string[];
|
|
55
63
|
/**
|
|
56
64
|
* Generate invalid port error message.
|
|
57
65
|
*
|
|
@@ -32,8 +32,12 @@ export function formatChromeNotice(notice) {
|
|
|
32
32
|
* Called at the UI boundary when a ChromeLaunchError with `issue` details
|
|
33
33
|
* reaches a CLI or daemon log sink. Core modules produce the IssueDetails;
|
|
34
34
|
* this function is the only place wording is assembled.
|
|
35
|
+
*
|
|
36
|
+
* @param issue - Structured issue
|
|
37
|
+
* @param diagnostics - Source of Chrome installation diagnostics (tests stub it)
|
|
38
|
+
* @returns User-facing message
|
|
35
39
|
*/
|
|
36
|
-
export function formatChromeIssue(issue) {
|
|
40
|
+
export function formatChromeIssue(issue, diagnostics = getChromeDiagnostics) {
|
|
37
41
|
const ctx = issue.context ?? {};
|
|
38
42
|
switch (issue.code) {
|
|
39
43
|
case 'PORT_IN_USE':
|
|
@@ -52,16 +56,18 @@ export function formatChromeIssue(issue) {
|
|
|
52
56
|
: reason
|
|
53
57
|
? chromeLaunchFailedError(reason)
|
|
54
58
|
: `Chrome failed to launch`;
|
|
55
|
-
const
|
|
56
|
-
|
|
59
|
+
const found = diagnostics();
|
|
60
|
+
const diagnosticLines = formatDiagnosticsForError(found);
|
|
61
|
+
if (noChromeFound(found))
|
|
62
|
+
return joinLines(header, '', ...diagnosticLines);
|
|
63
|
+
return joinLines(header, '', 'Possible causes:', ` - Port ${port} conflict (check: lsof -ti:${port})`, ` - Chrome binary not found`, ` - Insufficient permissions`, ` - Chrome crashed on startup`, '', ...diagnosticLines, '', 'Try:', ` - ${sessionCommand('bdg cleanup')}`, ` - See what uses the port: lsof -i :${port}`, ` - Use different port: ${sessionCommand(`bdg <url> --port ${port + 1}`)}`, ` - In a container where Chrome's sandbox fails: BDG_NO_SANDBOX=1 ${sessionCommand('bdg <url>')}`);
|
|
57
64
|
}
|
|
58
65
|
case 'CHROME_EXITED_DURING_STARTUP':
|
|
59
66
|
return chromeExitedDuringStartupError(ctx['exitCode'], ctx['output'] ?? [], ctx['userDataDir'], ctx['profileInUse'] === true);
|
|
60
67
|
case 'NO_PAGE_TARGET_FOUND':
|
|
61
68
|
return noPageTargetFoundError(ctx['port'], ctx['availableTargets']);
|
|
62
69
|
case 'CHROME_BINARY_NOT_FOUND': {
|
|
63
|
-
|
|
64
|
-
return joinLines(chromeBinaryOverrideNotFound(ctx['chromePath'], ctx['source']), '', ...diagnostics);
|
|
70
|
+
return joinLines(chromeBinaryOverrideNotFound(ctx['chromePath'], ctx['source']), '', ...formatDiagnosticsForError(diagnostics(), { suggestChromePath: false }));
|
|
65
71
|
}
|
|
66
72
|
case 'CHROME_BINARY_IS_DIRECTORY':
|
|
67
73
|
return chromeBinaryOverrideIsDirectory(ctx['chromePath'], ctx['source']);
|
|
@@ -96,17 +102,49 @@ export function chromeExitedDuringStartupError(exitCode, output, userDataDir, pr
|
|
|
96
102
|
return joinLines(`Chrome exited during startup (exit code ${exitCode ?? 'none'})`, ...(output.length > 0 ? ['Chrome said:', ...output.map((line) => ` ${line}`)] : []), 'Check --chrome-flags and BDG_CHROME_FLAGS (an unknown flag or value can stop Chrome)');
|
|
97
103
|
}
|
|
98
104
|
/**
|
|
99
|
-
*
|
|
105
|
+
* Whether no Chrome was found at all: no installation and no default binary
|
|
106
|
+
* (chrome-launcher's default honors a valid CHROME_PATH).
|
|
107
|
+
*
|
|
108
|
+
* @param diagnostics - Chrome diagnostics
|
|
109
|
+
* @returns True when there is nothing to launch
|
|
110
|
+
*/
|
|
111
|
+
function noChromeFound(diagnostics) {
|
|
112
|
+
return diagnostics.installationCount === 0 && !diagnostics.defaultPath;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A Chromium-based browser binary that chrome-launcher does not find on its
|
|
116
|
+
* own, as an example value for CHROME_PATH.
|
|
117
|
+
*
|
|
118
|
+
* @param platform - OS the path is for
|
|
119
|
+
* @returns Microsoft Edge's binary on macOS or Linux; none on other systems
|
|
120
|
+
*/
|
|
121
|
+
function exampleChromePath(platform) {
|
|
122
|
+
if (platform === 'darwin')
|
|
123
|
+
return '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge';
|
|
124
|
+
if (platform === 'linux')
|
|
125
|
+
return '/usr/bin/microsoft-edge';
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* How to point bdg at another Chromium-based browser through CHROME_PATH.
|
|
100
130
|
*
|
|
101
|
-
* @
|
|
131
|
+
* @param platform - OS the example is for
|
|
132
|
+
* @returns Message lines, with an example command where one is known
|
|
102
133
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
function chromePathSuggestion(platform) {
|
|
135
|
+
const example = exampleChromePath(platform);
|
|
136
|
+
if (!example)
|
|
137
|
+
return ['Set CHROME_PATH to a Chromium-based browser (Edge, Brave, Chromium)\n'];
|
|
138
|
+
return [
|
|
139
|
+
'Set CHROME_PATH to a Chromium-based browser (Edge, Brave, Chromium), e.g.:',
|
|
140
|
+
` CHROME_PATH="${example}" ${sessionCommand('bdg <url>')}\n`,
|
|
141
|
+
];
|
|
105
142
|
}
|
|
106
143
|
/**
|
|
107
144
|
* Format Chrome diagnostics for error reporting when Chrome launch fails.
|
|
108
145
|
*
|
|
109
146
|
* @param diagnostics - Chrome diagnostics information
|
|
147
|
+
* @param options - Platform of the example and whether to suggest CHROME_PATH
|
|
110
148
|
* @returns Formatted error message lines with troubleshooting steps
|
|
111
149
|
*
|
|
112
150
|
* @example
|
|
@@ -116,11 +154,13 @@ export function getFormattedDiagnostics() {
|
|
|
116
154
|
* console.error(errorLines.join('\n'));
|
|
117
155
|
* ```
|
|
118
156
|
*/
|
|
119
|
-
export function formatDiagnosticsForError(diagnostics) {
|
|
157
|
+
export function formatDiagnosticsForError(diagnostics, { platform = process.platform, suggestChromePath = true } = {}) {
|
|
120
158
|
const lines = [];
|
|
121
|
-
if (diagnostics
|
|
159
|
+
if (noChromeFound(diagnostics)) {
|
|
122
160
|
lines.push('Error: No Chrome installations detected\n');
|
|
123
|
-
|
|
161
|
+
if (suggestChromePath)
|
|
162
|
+
lines.push(...chromePathSuggestion(platform));
|
|
163
|
+
lines.push(suggestChromePath ? 'Or install Chrome from:' : 'Install Chrome from:');
|
|
124
164
|
lines.push(' https://www.google.com/chrome/\n');
|
|
125
165
|
}
|
|
126
166
|
else {
|
|
@@ -53,4 +53,30 @@ export declare function headerRepeatedNote(count: number): string;
|
|
|
53
53
|
* @returns Note text
|
|
54
54
|
*/
|
|
55
55
|
export declare function localProxyNote(): string;
|
|
56
|
+
/**
|
|
57
|
+
* HAR log comment of a sanitized export.
|
|
58
|
+
*
|
|
59
|
+
* @returns Comment naming what was redacted and the flag that keeps it
|
|
60
|
+
*/
|
|
61
|
+
export declare function harSanitizedComment(): string;
|
|
62
|
+
/**
|
|
63
|
+
* Result of a HAR export to a file.
|
|
64
|
+
*/
|
|
65
|
+
export interface HarExportSummary {
|
|
66
|
+
/** Absolute path written */
|
|
67
|
+
file: string;
|
|
68
|
+
/** Requests exported */
|
|
69
|
+
entries: number;
|
|
70
|
+
/** Whether --filter left requests out */
|
|
71
|
+
filtered: boolean;
|
|
72
|
+
/** Whether credentials were redacted */
|
|
73
|
+
sanitized: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Success message of `bdg network har`.
|
|
77
|
+
*
|
|
78
|
+
* @param result - Export result
|
|
79
|
+
* @returns e.g. `✓ Exported 4 requests to /tmp/out.har` and a line on sanitization
|
|
80
|
+
*/
|
|
81
|
+
export declare function harExportedMessage(result: HarExportSummary): string;
|
|
56
82
|
//# sourceMappingURL=networkMessages.d.ts.map
|
|
@@ -82,4 +82,25 @@ export function headerRepeatedNote(count) {
|
|
|
82
82
|
export function localProxyNote() {
|
|
83
83
|
return '(loopback; likely a local proxy)';
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* HAR log comment of a sanitized export.
|
|
87
|
+
*
|
|
88
|
+
* @returns Comment naming what was redacted and the flag that keeps it
|
|
89
|
+
*/
|
|
90
|
+
export function harSanitizedComment() {
|
|
91
|
+
return 'Sanitized by bdg: values of auth, cookie, API key, token and session headers, cookies, credential query parameters in URLs, and password/token fields of request bodies are [redacted] (by name, so some harmless values are too); response bodies and WebSocket messages are not sanitized. Export with --include-sensitive to keep everything';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Success message of `bdg network har`.
|
|
95
|
+
*
|
|
96
|
+
* @param result - Export result
|
|
97
|
+
* @returns e.g. `✓ Exported 4 requests to /tmp/out.har` and a line on sanitization
|
|
98
|
+
*/
|
|
99
|
+
export function harExportedMessage(result) {
|
|
100
|
+
const filterNote = result.filtered ? ' (filtered)' : '';
|
|
101
|
+
const note = result.sanitized
|
|
102
|
+
? 'Credentials sanitized (auth/cookie/API key/token headers, cookies, URL tokens, password and token body fields are [redacted]); --include-sensitive keeps them'
|
|
103
|
+
: '⚠ Includes credentials (--include-sensitive): share this file with care';
|
|
104
|
+
return `✓ Exported ${result.entries} requests${filterNote} to ${result.file}\n ${note}`;
|
|
105
|
+
}
|
|
85
106
|
//# sourceMappingURL=networkMessages.js.map
|
|
@@ -88,6 +88,14 @@ export declare function endedSessionText(label: string, end: {
|
|
|
88
88
|
reason: string;
|
|
89
89
|
endedAt: number;
|
|
90
90
|
}): string;
|
|
91
|
+
/**
|
|
92
|
+
* A session whose directory is not safe to use, for `bdg sessions`.
|
|
93
|
+
*
|
|
94
|
+
* @param label - Session name as listed
|
|
95
|
+
* @param why - Untrusted directory and why
|
|
96
|
+
* @returns One line
|
|
97
|
+
*/
|
|
98
|
+
export declare function untrustedSessionText(label: string, why: string): string;
|
|
91
99
|
/**
|
|
92
100
|
* Note after a failed start or a stop whose daemon had not exited when bdg stopped waiting.
|
|
93
101
|
*
|
|
@@ -101,6 +101,16 @@ export function lastSessionEndText(end) {
|
|
|
101
101
|
export function endedSessionText(label, end) {
|
|
102
102
|
return `${label} ended ${sessionEndText(end)}`;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* A session whose directory is not safe to use, for `bdg sessions`.
|
|
106
|
+
*
|
|
107
|
+
* @param label - Session name as listed
|
|
108
|
+
* @param why - Untrusted directory and why
|
|
109
|
+
* @returns One line
|
|
110
|
+
*/
|
|
111
|
+
export function untrustedSessionText(label, why) {
|
|
112
|
+
return `${label}: ${why}`;
|
|
113
|
+
}
|
|
104
114
|
/**
|
|
105
115
|
* When and why a session ended without `bdg stop`.
|
|
106
116
|
*
|
|
@@ -38,12 +38,13 @@ export declare class AtomicFileWriter {
|
|
|
38
38
|
*
|
|
39
39
|
* @param filePath - Target file path
|
|
40
40
|
* @param data - Data to write
|
|
41
|
-
* @param options - Write options
|
|
41
|
+
* @param options - Write options (`mode`: permissions of the new file, less the umask)
|
|
42
42
|
* @returns Promise that resolves when write completes
|
|
43
43
|
* @throws Error if write operation fails
|
|
44
44
|
*/
|
|
45
45
|
static writeAsync(filePath: string, data: string, options?: {
|
|
46
46
|
encoding?: BufferEncoding;
|
|
47
|
+
mode?: number;
|
|
47
48
|
}): Promise<void>;
|
|
48
49
|
/**
|
|
49
50
|
* Write binary data (Buffer) to a file atomically (asynchronous).
|
package/dist/utils/atomicFile.js
CHANGED
|
@@ -59,14 +59,17 @@ export class AtomicFileWriter {
|
|
|
59
59
|
*
|
|
60
60
|
* @param filePath - Target file path
|
|
61
61
|
* @param data - Data to write
|
|
62
|
-
* @param options - Write options
|
|
62
|
+
* @param options - Write options (`mode`: permissions of the new file, less the umask)
|
|
63
63
|
* @returns Promise that resolves when write completes
|
|
64
64
|
* @throws Error if write operation fails
|
|
65
65
|
*/
|
|
66
66
|
static async writeAsync(filePath, data, options = {}) {
|
|
67
67
|
const tmpPath = this.getTempPath(filePath);
|
|
68
68
|
try {
|
|
69
|
-
await fs.promises.writeFile(tmpPath, data, {
|
|
69
|
+
await fs.promises.writeFile(tmpPath, data, {
|
|
70
|
+
encoding: options.encoding ?? 'utf-8',
|
|
71
|
+
...(options.mode !== undefined && { mode: options.mode }),
|
|
72
|
+
});
|
|
70
73
|
await fs.promises.rename(tmpPath, filePath);
|
|
71
74
|
}
|
|
72
75
|
catch (error) {
|