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
@@ -208,8 +208,13 @@ export declare function callCDP(method: string, params?: Record<string, unknown>
208
208
  export declare function callBdgScript(method: string, params?: Record<string, unknown>): Promise<ClientResponse<'cdp_call'>>;
209
209
  /**
210
210
  * Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
211
+ *
212
+ * @param script - JavaScript expression
213
+ * @param frame - Iframe to evaluate in
214
+ * @param full - `--full`: copy an object or array result with every entry
215
+ * @returns The daemon's response
211
216
  */
212
- export declare function domEval(script: string, frame?: string): Promise<ClientResponse<'dom_eval'>>;
217
+ export declare function domEval(script: string, frame?: string, full?: boolean): Promise<ClientResponse<'dom_eval'>>;
213
218
  /**
214
219
  * List the page's iframes via the daemon.
215
220
  */
@@ -292,9 +292,18 @@ export function callBdgScript(method, params) {
292
292
  }
293
293
  /**
294
294
  * Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
295
+ *
296
+ * @param script - JavaScript expression
297
+ * @param frame - Iframe to evaluate in
298
+ * @param full - `--full`: copy an object or array result with every entry
299
+ * @returns The daemon's response
295
300
  */
296
- export async function domEval(script, frame) {
297
- return sendCommand('dom_eval', { script, ...(frame !== undefined && { frame }) });
301
+ export async function domEval(script, frame, full) {
302
+ return sendCommand('dom_eval', {
303
+ script,
304
+ ...(frame !== undefined && { frame }),
305
+ ...(full && { full }),
306
+ });
298
307
  }
299
308
  /**
300
309
  * List the page's iframes via the daemon.
@@ -187,6 +187,8 @@ export interface DomEvalCommand {
187
187
  script: string;
188
188
  /** Iframe to evaluate in: index, name/id attribute, or URL substring */
189
189
  frame?: string;
190
+ /** `--full`: copy an object or array result with every entry */
191
+ full?: boolean;
190
192
  }
191
193
  export interface DomEvalData {
192
194
  /** JSON value, or a readable description for values JSON cannot represent */
@@ -195,6 +197,8 @@ export interface DomEvalData {
195
197
  type: string;
196
198
  /** Object subtype (node, date, array, ...) */
197
199
  subtype?: string;
200
+ /** Elements of an array result in the page (`value` holds at most 1000 unless `full`) */
201
+ length?: number;
198
202
  /** URL of the iframe the script ran in (with `frame`; empty when it has none) */
199
203
  frame?: string;
200
204
  /** Set when the page replaced built-ins bdg's copy of the result uses */
@@ -18,6 +18,12 @@ type WithTypeAndSession = {
18
18
  * @param timeoutMs - How long to wait for the response (default: IPC timeout)
19
19
  * @param socketPath - Daemon socket (default: the selected session's)
20
20
  * @returns The daemon's response
21
+ * @throws CommandError (103) before connecting when the socket's session
22
+ * directory cannot be trusted (see {@link secureSessionDir}): a socket
23
+ * planted there by another user would receive the request. The check runs
24
+ * just before connecting, by path; replacing the socket in that window
25
+ * needs write access to a directory of the chain, which the check has
26
+ * just found only the user has
21
27
  */
22
28
  export declare function sendRequest<TRequest extends WithTypeAndSession, TResponse extends WithTypeAndSession>(request: TRequest, requestName: string, expectedType?: string, timeoutMs?: number, socketPath?: string): Promise<TResponse>;
23
29
  //# sourceMappingURL=index.d.ts.map
@@ -3,9 +3,13 @@
3
3
  *
4
4
  * Handles Unix domain socket communication with JSONL protocol.
5
5
  */
6
+ import * as path from 'path';
6
7
  import { getIPCRequestTimeout } from '../../constants.js';
7
- import { getDaemonSocketPath } from '../../session/paths.js';
8
+ import { CommandError } from '../../errors/index.js';
9
+ import { untrustedSessionDirError } from '../../errors/messages.js';
10
+ import { getDaemonSocketPath, secureSessionDir } from '../../session/paths.js';
8
11
  import { createLogger } from '../../ui/logging/index.js';
12
+ import { EXIT_CODES } from '../../utils/exitCodes.js';
9
13
  import { formatConnectionError, formatEarlyCloseError, formatParseError, formatTimeoutError, } from './errors.js';
10
14
  import { JSONLBuffer, parseJSONLFrame, toJSONLFrame } from './jsonl.js';
11
15
  import { createSocket } from './socket.js';
@@ -22,8 +26,19 @@ const log = createLogger('client');
22
26
  * @param timeoutMs - How long to wait for the response (default: IPC timeout)
23
27
  * @param socketPath - Daemon socket (default: the selected session's)
24
28
  * @returns The daemon's response
29
+ * @throws CommandError (103) before connecting when the socket's session
30
+ * directory cannot be trusted (see {@link secureSessionDir}): a socket
31
+ * planted there by another user would receive the request. The check runs
32
+ * just before connecting, by path; replacing the socket in that window
33
+ * needs write access to a directory of the chain, which the check has
34
+ * just found only the user has
25
35
  */
26
36
  export async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath()) {
37
+ const untrusted = secureSessionDir(path.dirname(socketPath));
38
+ if (untrusted) {
39
+ const err = untrustedSessionDirError(untrusted);
40
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SESSION_FILE_ERROR);
41
+ }
27
42
  return new Promise((resolve, reject) => {
28
43
  const buffer = new JSONLBuffer();
29
44
  let resolved = false;
@@ -70,6 +70,13 @@ export declare const FLAT_TEXT_JS = "(el, limit, fields) => {\n const composed
70
70
  export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const shown = rendered(el);\n const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n if (!shown && !boxless) return '';\n if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));\n if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
71
71
  /** Shown instead of a secret field value (the same for every length) */
72
72
  export declare const MASKED_VALUE = "\u2022\u2022\u2022\u2022";
73
+ /**
74
+ * Regular expression source (case-insensitive) matching names of fields that
75
+ * hold a password, one-time code or card code: `password`, `passwd`, `pwd`,
76
+ * `passcode`, `otp`, `cvv`, `cvc`. Shared by the page-side
77
+ * {@link SENSITIVE_FIELD_JS} and HAR sanitization.
78
+ */
79
+ export declare const SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc";
73
80
  /**
74
81
  * Page-side check whether a form control holds a secret whose value must
75
82
  * never leave the page: a password field (live type or type attribute), a
@@ -157,6 +157,13 @@ export const ELEMENT_TEXT_JS = `(el, full) => {
157
157
  }`;
158
158
  /** Shown instead of a secret field value (the same for every length) */
159
159
  export const MASKED_VALUE = '••••';
160
+ /**
161
+ * Regular expression source (case-insensitive) matching names of fields that
162
+ * hold a password, one-time code or card code: `password`, `passwd`, `pwd`,
163
+ * `passcode`, `otp`, `cvv`, `cvc`. Shared by the page-side
164
+ * {@link SENSITIVE_FIELD_JS} and HAR sanitization.
165
+ */
166
+ export const SENSITIVE_NAME_SOURCE = 'passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc';
160
167
  /**
161
168
  * Page-side check whether a form control holds a secret whose value must
162
169
  * never leave the page: a password field (live type or type attribute), a
@@ -171,7 +178,7 @@ export const SENSITIVE_FIELD_JS = `(el) => {
171
178
  if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
172
179
  if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
173
180
  const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
174
- if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
181
+ if (/${SENSITIVE_NAME_SOURCE}/i.test(names)) return true;
175
182
  try {
176
183
  const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
177
184
  return Boolean(security) && security !== 'none';
@@ -26,6 +26,8 @@ export interface EvalResult {
26
26
  type: string;
27
27
  /** Object subtype (`node`, `date`, `map`, `array`, ...) */
28
28
  subtype?: string;
29
+ /** Elements of an array result in the page (its copy holds at most 1000 unless `full`) */
30
+ length?: number;
29
31
  /** Set when the page replaced built-ins bdg's copy of the result uses, so the browser copied it */
30
32
  warning?: string;
31
33
  }
@@ -36,11 +38,27 @@ export interface EvalResult {
36
38
  * dates ISO strings, maps and sets entries, errors their message, BigInts
37
39
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
38
40
  * shared by two properties is copied twice). Works for objects of iframes
39
- * (other realms); lists and objects are cut after 1000 entries, and a
40
- * throwing getter becomes `[Error: …]`. It uses the page's built-ins
41
- * ({@link COPY_BUILTINS}), so it only runs when the page left them alone.
41
+ * (other realms); lists and objects are cut after 1000 entries and levels
42
+ * below the 20th become `[…]` (with `full`: no entries are cut, levels
43
+ * below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
44
+ * page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
45
+ * left them alone.
46
+ *
47
+ * @param full - `--full`: copy every entry
48
+ * @returns Function declaration for Runtime.callFunctionOn
49
+ */
50
+ export declare function jsonSafeCopyFunction(full?: boolean): string;
51
+ /**
52
+ * Elements of an array result (also a node list or typed array), read from
53
+ * its description (`Array(20000)`, `NodeList(5)`), since its copy holds
54
+ * at most 1000 unless `--full`.
55
+ *
56
+ * @param remote - Remote object returned by Runtime.evaluate
57
+ * @returns `length`, when the result is an array
42
58
  */
43
- export declare const JSON_SAFE_COPY_FUNCTION = "function () {\n const MAX_ITEMS = 1000;\n const ancestors = new Set();\n const kind = (value) => Object.prototype.toString.call(value).slice(8, -1);\n const isNode = (value) => typeof value.nodeType === 'number' && typeof value.nodeName === 'string';\n const describeNode = (node) => {\n if (node.nodeType !== 1) return node.nodeName.toLowerCase();\n return node.tagName.toLowerCase() + (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).join('.') : '');\n };\n const items = (list, next) => {\n const result = [];\n if (typeof list.length === 'number') {\n for (let i = 0; i < list.length; i++) {\n if (i === MAX_ITEMS) { result.push('\u2026'); break; }\n result.push(next(list[i]));\n }\n return result;\n }\n let count = 0;\n for (const item of list) {\n if (count++ === MAX_ITEMS) { result.push('\u2026'); break; }\n result.push(next(item));\n }\n return result;\n };\n const leaf = (value) => {\n if (value === undefined) return null;\n if (typeof value === 'number') {\n if (Number.isNaN(value) || !Number.isFinite(value)) return String(value);\n return Object.is(value, -0) ? '-0' : value;\n }\n if (typeof value === 'bigint') return value + 'n';\n if (typeof value === 'symbol') return value.toString();\n if (typeof value === 'function') return 'function ' + (value.name || '(anonymous)') + '()';\n return value;\n };\n const copy = (value, depth) => {\n if (value === null || typeof value !== 'object') return leaf(value);\n if (ancestors.has(value)) return '[Circular]';\n if (depth > 20) return '[\u2026]';\n const next = (item) => copy(item, depth + 1);\n const type = kind(value);\n if (isNode(value)) return describeNode(value);\n if (value.window === value) return 'Window';\n if (type === 'Date') return isNaN(value) ? 'Invalid Date' : value.toISOString();\n if (type === 'RegExp') return String(value);\n if (type === 'Error' || value instanceof Error) return value.name + ': ' + value.message;\n ancestors.add(value);\n try {\n if (type === 'Map') return items(value, ([k, v]) => [next(k), next(v)]);\n if (type === 'Set' || type === 'NodeList' || type === 'HTMLCollection') return items(value, next);\n if (ArrayBuffer.isView(value) && type !== 'DataView') return items(value, next);\n if (Array.isArray(value)) return items(value, next);\n const result = {};\n const keys = Object.keys(value);\n for (const key of keys.slice(0, MAX_ITEMS)) {\n try { result[key] = next(value[key]); } catch (e) { result[key] = '[Error: ' + (e && e.message) + ']'; }\n }\n if (keys.length > MAX_ITEMS) result['\u2026'] = (keys.length - MAX_ITEMS) + ' more keys';\n return result;\n } finally {\n ancestors.delete(value);\n }\n };\n return copy(this, 0);\n}";
59
+ export declare function arrayLength(remote: Protocol.Runtime.RemoteObject): {
60
+ length?: number;
61
+ };
44
62
  /** What a busy target is called in messages: the page, or an iframe (`--frame`) */
45
63
  export type BusyScope = 'page' | 'frame';
46
64
  /** When {@link withBusyPageRecovery} checks the target, and what it calls it */
@@ -100,6 +118,8 @@ export interface EvalTarget {
100
118
  recovery?: CDPSender;
101
119
  /** Execution context of a frame (`ExecutionContextDescription.uniqueId`) */
102
120
  uniqueContextId?: string;
121
+ /** `--full`: copy the result with every entry */
122
+ full?: boolean;
103
123
  }
104
124
  /**
105
125
  * Whether the page still answers. A page that does not answer in time is
@@ -156,6 +156,10 @@ const DESCRIBED_SUBTYPES = new Set([
156
156
  'iterator',
157
157
  'generator',
158
158
  ]);
159
+ /** Entries per list or object, and levels, of a copied eval result */
160
+ const COPY_LIMITS = { maxItems: 1000, maxDepth: 20 };
161
+ /** Limits of a copied eval result with `--full`: every entry, levels deep enough for real data */
162
+ const FULL_COPY_LIMITS = { maxItems: Infinity, maxDepth: 100 };
159
163
  /** Object subtypes shown by their short description (`button#submit`, `ArrayBuffer(8)`). */
160
164
  const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
161
165
  /**
@@ -165,12 +169,19 @@ const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
165
169
  * dates ISO strings, maps and sets entries, errors their message, BigInts
166
170
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
167
171
  * shared by two properties is copied twice). Works for objects of iframes
168
- * (other realms); lists and objects are cut after 1000 entries, and a
169
- * throwing getter becomes `[Error: …]`. It uses the page's built-ins
170
- * ({@link COPY_BUILTINS}), so it only runs when the page left them alone.
172
+ * (other realms); lists and objects are cut after 1000 entries and levels
173
+ * below the 20th become `[…]` (with `full`: no entries are cut, levels
174
+ * below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
175
+ * page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
176
+ * left them alone.
177
+ *
178
+ * @param full - `--full`: copy every entry
179
+ * @returns Function declaration for Runtime.callFunctionOn
171
180
  */
172
- export const JSON_SAFE_COPY_FUNCTION = `function () {
173
- const MAX_ITEMS = 1000;
181
+ export function jsonSafeCopyFunction(full = false) {
182
+ const limits = full ? FULL_COPY_LIMITS : COPY_LIMITS;
183
+ return `function () {
184
+ const MAX_ITEMS = ${limits.maxItems};
174
185
  const ancestors = new Set();
175
186
  const kind = (value) => Object.prototype.toString.call(value).slice(8, -1);
176
187
  const isNode = (value) => typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
@@ -209,7 +220,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
209
220
  const copy = (value, depth) => {
210
221
  if (value === null || typeof value !== 'object') return leaf(value);
211
222
  if (ancestors.has(value)) return '[Circular]';
212
- if (depth > 20) return '[…]';
223
+ if (depth > ${limits.maxDepth}) return '[…]';
213
224
  const next = (item) => copy(item, depth + 1);
214
225
  const type = kind(value);
215
226
  if (isNode(value)) return describeNode(value);
@@ -236,6 +247,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
236
247
  };
237
248
  return copy(this, 0);
238
249
  }`;
250
+ }
239
251
  /** Built-ins {@link JSON_SAFE_COPY_FUNCTION} uses (it runs in the page's world, next to the result) */
240
252
  const COPY_BUILTINS = [
241
253
  'Object.keys',
@@ -284,9 +296,10 @@ function isTerminated(details) {
284
296
  *
285
297
  * @param cdp - CDP connection
286
298
  * @param remote - Remote object returned by Runtime.evaluate
299
+ * @param full - `--full`: copy every entry ({@link jsonSafeCopyFunction})
287
300
  * @returns Value and type
288
301
  */
289
- async function toEvalResult(cdp, remote) {
302
+ async function toEvalResult(cdp, remote, full) {
290
303
  const kind = { type: remote.type, ...(remote.subtype && { subtype: remote.subtype }) };
291
304
  if (remote.unserializableValue !== undefined)
292
305
  return { value: remote.unserializableValue, ...kind };
@@ -301,12 +314,27 @@ async function toEvalResult(cdp, remote) {
301
314
  return { value: formatRemoteObject(remote), ...kind };
302
315
  }
303
316
  const replaced = await findReplacedBuiltins(cdp, COPY_BUILTINS, remote.objectId);
304
- const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ? JSON_SAFE_COPY_FUNCTION : SELF_FUNCTION);
317
+ const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ? jsonSafeCopyFunction(full) : SELF_FUNCTION);
305
318
  const value = copy ? copy.value : formatRemoteObject(remote);
319
+ const copied = { value, ...kind, ...arrayLength(remote) };
306
320
  if (replaced.length === 0)
307
- return { value, ...kind };
321
+ return copied;
308
322
  const warning = copy ? evalCopiedByBrowserWarning(replaced) : evalPreviewWarning(replaced);
309
- return { value, ...kind, warning };
323
+ return { ...copied, warning };
324
+ }
325
+ /**
326
+ * Elements of an array result (also a node list or typed array), read from
327
+ * its description (`Array(20000)`, `NodeList(5)`), since its copy holds
328
+ * at most 1000 unless `--full`.
329
+ *
330
+ * @param remote - Remote object returned by Runtime.evaluate
331
+ * @returns `length`, when the result is an array
332
+ */
333
+ export function arrayLength(remote) {
334
+ if (remote.subtype !== 'array' && remote.subtype !== 'typedarray')
335
+ return {};
336
+ const match = /\((\d+)\)$/.exec(remote.description ?? '');
337
+ return match ? { length: Number(match[1]) } : {};
310
338
  }
311
339
  /**
312
340
  * Copy a result by value: what a function called on it returns, which the
@@ -314,7 +342,7 @@ async function toEvalResult(cdp, remote) {
314
342
  *
315
343
  * @param cdp - CDP connection
316
344
  * @param objectId - The result
317
- * @param functionDeclaration - {@link JSON_SAFE_COPY_FUNCTION}, or
345
+ * @param functionDeclaration - {@link jsonSafeCopyFunction}'s, or
318
346
  * {@link SELF_FUNCTION} for the browser's plain copy (it fails on cycles
319
347
  * and BigInts)
320
348
  * @returns The copy, or undefined when it failed
@@ -560,7 +588,7 @@ export async function evaluateScript(cdp, script, target = {}) {
560
588
  try {
561
589
  const response = await withDeadline(executeScript(session, script, evaluateOptions(target)), EVAL_TIMEOUT_MS + TERMINATION_GRACE_MS, () => evaluationTimeoutError(target.recovery ?? session, scope));
562
590
  const settled = await withDeadline(settlePromise(session, response.result, script), EVAL_TIMEOUT_MS, promiseTimeout);
563
- return await toEvalResult(session, settled);
591
+ return await toEvalResult(session, settled, target.full ?? false);
564
592
  }
565
593
  catch (error) {
566
594
  throw scope === 'page' && isContextLostError(error)
@@ -84,11 +84,12 @@ export declare function frameContextLostError(conn: Pick<CDPConnection, 'send'>,
84
84
  * @param script - JavaScript expression
85
85
  * @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
86
86
  * @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
87
+ * @param full - `--full`: copy the result with every entry
87
88
  * @returns Value, type and the frame's URL
88
89
  * @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
89
90
  * when an index names another frame than when it was listed, (83) when it
90
91
  * navigated or was removed while the script ran, else as evaluateScript
91
92
  */
92
- export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string, listedIds?: string[]): Promise<DomEvalData>;
93
+ export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string, listedIds?: string[], full?: boolean): Promise<DomEvalData>;
93
94
  export {};
94
95
  //# sourceMappingURL=frames.d.ts.map
@@ -529,12 +529,13 @@ export async function frameContextLostError(conn, page, frame) {
529
529
  * @param script - JavaScript expression
530
530
  * @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
531
531
  * @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
532
+ * @param full - `--full`: copy the result with every entry
532
533
  * @returns Value, type and the frame's URL
533
534
  * @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
534
535
  * when an index names another frame than when it was listed, (83) when it
535
536
  * navigated or was removed while the script ran, else as evaluateScript
536
537
  */
537
- export async function evaluateInFrame(page, wsUrl, script, query, listedIds) {
538
+ export async function evaluateInFrame(page, wsUrl, script, query, listedIds, full = false) {
538
539
  return withFrameConnection(page, wsUrl, async (fc) => {
539
540
  const { frame, uniqueContextId } = await resolveFrame(fc, query, listedIds);
540
541
  try {
@@ -542,6 +543,7 @@ export async function evaluateInFrame(page, wsUrl, script, query, listedIds) {
542
543
  ...(frame.sessionId && { sessionId: frame.sessionId }),
543
544
  uniqueContextId,
544
545
  recovery: recoverySender(fc, frame.sessionId),
546
+ full,
545
547
  });
546
548
  return { ...result, frame: frame.info.url };
547
549
  }
@@ -60,12 +60,13 @@ export async function applySessionEmulation(cdp, emulation) {
60
60
  */
61
61
  async function emulatePhone(cdp, on) {
62
62
  await cdp.send('Emulation.setTouchEmulationEnabled', { enabled: on, maxTouchPoints: on ? 5 : 1 });
63
- const { userAgent } = (await cdp.send('Browser.getVersion', {}));
63
+ const version = (await cdp.send('Browser.getVersion', {}));
64
+ const { userAgent } = version;
64
65
  if (!on) {
65
- const headless = userAgent.includes('HeadlessChrome');
66
- await cdp.send('Emulation.setUserAgentOverride', { userAgent: headless ? userAgent : '' });
67
- if (headless)
68
- await hideHeadlessUserAgent(cdp, log);
66
+ if (userAgent.includes('HeadlessChrome'))
67
+ await hideHeadlessUserAgent(cdp, log, version);
68
+ else
69
+ await cdp.send('Emulation.setUserAgentOverride', { userAgent: '' });
69
70
  return;
70
71
  }
71
72
  const major = /Chrome\/(\d+)/.exec(userAgent)?.[1] ?? '';
@@ -3,15 +3,99 @@
3
3
  * client hints, so sites serve the page a user sees.
4
4
  */
5
5
  import type { CDPConnection } from '../../connection/cdp.js';
6
- import type { Logger } from '../../ui/logging/index.js';
6
+ import { type Logger } from '../../ui/logging/index.js';
7
+ /** A brand in the client hints (`Sec-CH-UA`, `navigator.userAgentData.brands`) */
8
+ export interface BrandVersion {
9
+ brand: string;
10
+ version: string;
11
+ }
12
+ /** CDP `Emulation.UserAgentMetadata`: the client hints Chrome sends and reports */
13
+ export interface UserAgentMetadata {
14
+ brands: BrandVersion[];
15
+ fullVersionList: BrandVersion[];
16
+ fullVersion: string;
17
+ platform: string;
18
+ platformVersion: string;
19
+ architecture: string;
20
+ bitness: string;
21
+ model: string;
22
+ mobile: boolean;
23
+ wow64: boolean;
24
+ formFactors: string[];
25
+ }
26
+ /** The machine the daemon runs on, as client hints name it */
27
+ export interface HostPlatform {
28
+ /** Client-hint platform (`macOS`, `Windows`, `Linux`) */
29
+ platform: string;
30
+ /** OS version as Chrome reports it */
31
+ platformVersion: string;
32
+ /** `arm` or `x86` */
33
+ architecture: string;
34
+ /** `64` or `32` */
35
+ bitness: string;
36
+ }
37
+ /** `Browser.getVersion` fields the metadata is built from */
38
+ export interface BrowserVersion {
39
+ product: string;
40
+ userAgent: string;
41
+ }
42
+ /**
43
+ * Chrome's brand list for a version, built the way Chrome builds it
44
+ * (`GenerateBrandVersionList` in Chromium's `user_agent_utils.cc`): a
45
+ * made-up brand, Chromium and the browser's brand, in an order and with a
46
+ * made-up name and version that depend on the major version.
47
+ *
48
+ * @param major - Major version (the seed)
49
+ * @param chromium - Chromium version to list
50
+ * @param browser - Browser brand and version
51
+ * @param greaseSuffix - Appended to the made-up version (`.0.0.0` in full versions)
52
+ * @returns Brand list
53
+ */
54
+ export declare function chromeBrandList(major: number, chromium: string, browser: BrandVersion, greaseSuffix?: string): BrandVersion[];
55
+ /**
56
+ * The client hints regular Chrome sends, for the browser `Browser.getVersion`
57
+ * describes. The browser brand is Microsoft Edge when the user agent says
58
+ * `Edg/`, Google Chrome otherwise (Chromium and Chrome for Testing look the
59
+ * same over CDP). Edge's Chromium version is only known to the major
60
+ * version. The OS version, architecture and bitness are the host's when it
61
+ * runs the platform the user agent names, empty otherwise (a remote Chrome).
62
+ *
63
+ * @param version - `Browser.getVersion` product and user agent
64
+ * @param host - The daemon's machine
65
+ * @returns Metadata for `Emulation.setUserAgentOverride`
66
+ */
67
+ export declare function regularChromeMetadata(version: BrowserVersion, host: HostPlatform): UserAgentMetadata;
68
+ /**
69
+ * The OS version Chrome reports on Linux or Windows, from `os.release()`:
70
+ * the kernel version's first three numbers on Linux, and on Windows `13.0.0`
71
+ * for Windows 11 and `10.0.0` before it (Chrome reports a Windows API
72
+ * version there, not the OS build).
73
+ *
74
+ * @param platform - `process.platform`
75
+ * @param release - `os.release()`
76
+ * @returns OS version, or empty when unknown
77
+ */
78
+ export declare function releasePlatformVersion(platform: string, release: string): string;
79
+ /**
80
+ * The daemon's machine as client hints describe it.
81
+ *
82
+ * @returns Platform, OS version, architecture and bitness
83
+ */
84
+ export declare function hostPlatform(): HostPlatform;
7
85
  /**
8
86
  * Send the user agent and client hints of regular Chrome from headless
9
87
  * Chrome: sites serve "HeadlessChrome" a different page (or a bot
10
88
  * challenge), so the page would not be the one a user sees. Not for a
11
89
  * session emulating a phone, whose emulation sets a mobile user agent.
12
90
  *
91
+ * The client hints are built from `Browser.getVersion` and the host
92
+ * ({@link regularChromeMetadata}) rather than read from the page: the page is
93
+ * still `about:blank`, which has no `navigator.userAgentData`, and an
94
+ * override without metadata empties the client hints.
95
+ *
13
96
  * @param cdp - CDP connection
14
97
  * @param logger - Logger for failures (the session works without it)
98
+ * @param known - `Browser.getVersion` result, when the caller has it
15
99
  */
16
- export declare function hideHeadlessUserAgent(cdp: CDPConnection, logger: Logger): Promise<void>;
100
+ export declare function hideHeadlessUserAgent(cdp: CDPConnection, logger: Logger, known?: BrowserVersion): Promise<void>;
17
101
  //# sourceMappingURL=userAgent.d.ts.map