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
package/dist/ipc/client.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/ipc/client.js
CHANGED
|
@@ -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', {
|
|
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 {
|
|
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 (/
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
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
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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
|
|
173
|
-
|
|
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 >
|
|
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 ?
|
|
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
|
|
321
|
+
return copied;
|
|
308
322
|
const warning = copy ? evalCopiedByBrowserWarning(replaced) : evalPreviewWarning(replaced);
|
|
309
|
-
return {
|
|
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
|
|
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
|
|
63
|
+
const version = (await cdp.send('Browser.getVersion', {}));
|
|
64
|
+
const { userAgent } = version;
|
|
64
65
|
if (!on) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await
|
|
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
|
|
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
|