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.
Files changed (69) hide show
  1. package/.claude/skills/bdg/SKILL.md +1 -1
  2. package/dist/commands/cdp.js +1 -0
  3. package/dist/commands/cleanup.js +3 -0
  4. package/dist/commands/dom/eval.d.ts +2 -1
  5. package/dist/commands/dom/eval.js +6 -21
  6. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  7. package/dist/commands/dom/helpers/evalResult.js +59 -0
  8. package/dist/commands/helpJson.d.ts +1 -1
  9. package/dist/commands/helpJson.js +3 -3
  10. package/dist/commands/helpTopic.js +10 -4
  11. package/dist/commands/network/har.js +18 -14
  12. package/dist/commands/optionBehaviors.js +8 -3
  13. package/dist/commands/shared/optionTypes.d.ts +1 -0
  14. package/dist/commands/shared/outputFile.d.ts +2 -1
  15. package/dist/commands/shared/outputFile.js +7 -4
  16. package/dist/commands/status.js +3 -1
  17. package/dist/commands/stop.js +2 -1
  18. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  19. package/dist/connection/launcher/flagsBuilder.js +107 -23
  20. package/dist/connection/launcher.d.ts +1 -1
  21. package/dist/connection/launcher.js +1 -2
  22. package/dist/constants.d.ts +2 -4
  23. package/dist/constants.js +2 -4
  24. package/dist/daemon/launcher.d.ts +17 -3
  25. package/dist/daemon/launcher.js +37 -7
  26. package/dist/daemon/session/commandRegistry.js +2 -2
  27. package/dist/daemon.js +8107 -7962
  28. package/dist/errors/messages.d.ts +23 -0
  29. package/dist/errors/messages.js +86 -6
  30. package/dist/index.js +555 -166
  31. package/dist/ipc/client.d.ts +6 -1
  32. package/dist/ipc/client.js +11 -2
  33. package/dist/ipc/protocol/commands.d.ts +4 -0
  34. package/dist/ipc/transport/index.d.ts +6 -0
  35. package/dist/ipc/transport/index.js +16 -1
  36. package/dist/runtime/dom/elementInfo.d.ts +7 -0
  37. package/dist/runtime/dom/elementInfo.js +8 -1
  38. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  39. package/dist/runtime/dom/evalHelpers.js +40 -12
  40. package/dist/runtime/dom/frames.d.ts +2 -1
  41. package/dist/runtime/dom/frames.js +3 -1
  42. package/dist/runtime/page/emulation.js +6 -5
  43. package/dist/runtime/page/userAgent.d.ts +86 -2
  44. package/dist/runtime/page/userAgent.js +154 -33
  45. package/dist/session/paths.d.ts +38 -3
  46. package/dist/session/paths.js +154 -7
  47. package/dist/session/portClaims.d.ts +0 -8
  48. package/dist/session/portClaims.js +1 -22
  49. package/dist/session/sessionList.d.ts +5 -1
  50. package/dist/session/sessionList.js +5 -1
  51. package/dist/telemetry/har/builder.d.ts +12 -1
  52. package/dist/telemetry/har/builder.js +10 -2
  53. package/dist/telemetry/har/sanitize.d.ts +24 -0
  54. package/dist/telemetry/har/sanitize.js +138 -0
  55. package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
  56. package/dist/telemetry/har/sanitizeBody.js +168 -0
  57. package/dist/ui/formatters/sessions.d.ts +3 -2
  58. package/dist/ui/formatters/sessions.js +10 -3
  59. package/dist/ui/messages/chrome.d.ts +14 -6
  60. package/dist/ui/messages/chrome.js +52 -12
  61. package/dist/ui/messages/networkMessages.d.ts +26 -0
  62. package/dist/ui/messages/networkMessages.js +21 -0
  63. package/dist/ui/messages/session.d.ts +8 -0
  64. package/dist/ui/messages/session.js +10 -0
  65. package/dist/utils/atomicFile.d.ts +2 -1
  66. package/dist/utils/atomicFile.js +5 -2
  67. package/dist/utils/directories.d.ts +41 -0
  68. package/dist/utils/directories.js +48 -0
  69. 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 and
4
- * the cleanup commands of crashed and stale sessions.
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 and
7
- * the cleanup commands of crashed and stale sessions.
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
- * Retrieve Chrome diagnostics and format for error messages.
37
- *
38
- * @returns Array of formatted diagnostic strings
40
+ * Options of {@link formatDiagnosticsForError}.
39
41
  */
40
- export declare function getFormattedDiagnostics(): string[];
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 diagnostics = getFormattedDiagnostics();
56
- return joinLines(header, '', 'Possible causes:', ` - Port ${port} conflict (check: lsof -ti:${port})`, ` - Chrome binary not found`, ` - Insufficient permissions`, ` - Chrome crashed on startup`, '', ...diagnostics, '', '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>')}`);
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
- const diagnostics = getFormattedDiagnostics();
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
- * Retrieve Chrome diagnostics and format for error messages.
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
- * @returns Array of formatted diagnostic strings
131
+ * @param platform - OS the example is for
132
+ * @returns Message lines, with an example command where one is known
102
133
  */
103
- export function getFormattedDiagnostics() {
104
- return formatDiagnosticsForError(getChromeDiagnostics());
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.installationCount === 0) {
159
+ if (noChromeFound(diagnostics)) {
122
160
  lines.push('Error: No Chrome installations detected\n');
123
- lines.push('Install Chrome from:');
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).
@@ -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, { encoding: options.encoding ?? 'utf-8' });
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) {