browser-debugger-cli 0.13.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 +101 -187
- package/README.md +4 -4
- package/dist/commands/cdp.js +1 -0
- package/dist/commands/cleanup.js +3 -0
- package/dist/commands/console.js +5 -1
- package/dist/commands/dom/a11y.d.ts +1 -1
- package/dist/commands/dom/a11y.js +20 -20
- package/dist/commands/dom/eval.d.ts +3 -1
- package/dist/commands/dom/eval.js +8 -5
- package/dist/commands/dom/formInteraction.js +1 -1
- package/dist/commands/dom/get.js +25 -7
- package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
- package/dist/commands/dom/helpers/evalResult.js +59 -0
- package/dist/commands/dom/index.js +7 -2
- package/dist/commands/dom/query.d.ts +2 -1
- package/dist/commands/dom/query.js +5 -3
- package/dist/commands/dom/screenshot.js +1 -0
- package/dist/commands/helpJson.d.ts +1 -1
- package/dist/commands/helpJson.js +4 -4
- package/dist/commands/helpTopic.js +10 -4
- package/dist/commands/network/har.js +18 -14
- package/dist/commands/network/list.js +46 -3
- package/dist/commands/optionBehaviors.d.ts +25 -2
- package/dist/commands/optionBehaviors.js +60 -42
- package/dist/commands/peek.js +3 -0
- package/dist/commands/shared/CommandRunner.js +13 -13
- package/dist/commands/shared/daemonErrorHandler.js +2 -2
- package/dist/commands/shared/dataFetcher.d.ts +4 -2
- package/dist/commands/shared/dataFetcher.js +11 -3
- package/dist/commands/shared/handleValidationError.js +3 -3
- package/dist/commands/shared/optionTypes.d.ts +15 -3
- package/dist/commands/shared/outputFile.d.ts +2 -1
- package/dist/commands/shared/outputFile.js +7 -4
- package/dist/commands/shared/startHelpers.js +3 -3
- package/dist/commands/status.js +3 -1
- package/dist/commands/stop.js +2 -1
- package/dist/connection/chromeIdentity.d.ts +8 -2
- package/dist/connection/chromeIdentity.js +85 -13
- 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 +31 -5
- package/dist/constants.js +37 -5
- package/dist/daemon/SessionController.js +2 -0
- package/dist/daemon/launcher.d.ts +17 -3
- package/dist/daemon/launcher.js +37 -7
- package/dist/daemon/session/Session.d.ts +2 -1
- package/dist/daemon/session/Session.js +10 -2
- package/dist/daemon/session/TelemetryStore.d.ts +7 -0
- package/dist/daemon/session/TelemetryStore.js +6 -0
- package/dist/daemon/session/commandRegistry.js +25 -7
- package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
- package/dist/daemon/session/matchedStylesReset.js +46 -0
- package/dist/daemon/session/plugins.js +1 -0
- package/dist/daemon/session/triggeredRequests.d.ts +0 -5
- package/dist/daemon/session/triggeredRequests.js +13 -7
- package/dist/daemon.js +8742 -8315
- package/dist/errors/messages.d.ts +31 -0
- package/dist/errors/messages.js +96 -6
- package/dist/index.js +1129 -548
- package/dist/ipc/client.d.ts +6 -1
- package/dist/ipc/client.js +11 -2
- package/dist/ipc/protocol/commands.d.ts +8 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
- package/dist/ipc/session/types.d.ts +5 -1
- package/dist/ipc/transport/index.d.ts +6 -0
- package/dist/ipc/transport/index.js +16 -1
- package/dist/program.d.ts +14 -0
- package/dist/program.js +53 -0
- package/dist/runtime/dom/elementGeometry.d.ts +23 -0
- package/dist/runtime/dom/elementGeometry.js +17 -15
- package/dist/runtime/dom/elementInfo.d.ts +13 -4
- package/dist/runtime/dom/elementInfo.js +15 -5
- package/dist/runtime/dom/evalHelpers.d.ts +24 -4
- package/dist/runtime/dom/evalHelpers.js +40 -12
- package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
- package/dist/runtime/dom/frameScopedConnection.js +2 -2
- package/dist/runtime/dom/frames.d.ts +2 -1
- package/dist/runtime/dom/frames.js +3 -1
- package/dist/runtime/dom/inspect.d.ts +17 -3
- package/dist/runtime/dom/inspect.js +40 -26
- package/dist/runtime/dom/inspectModel.d.ts +3 -3
- package/dist/runtime/dom/inspectRules.d.ts +29 -3
- package/dist/runtime/dom/inspectRules.js +205 -11
- package/dist/runtime/dom/layout.d.ts +0 -2
- package/dist/runtime/dom/layout.js +1 -2
- package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
- package/dist/runtime/dom/reactEventHelpers.js +9 -2
- package/dist/runtime/dom/targetNode.d.ts +10 -6
- package/dist/runtime/dom/targetNode.js +15 -8
- 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/a11y.d.ts +15 -1
- package/dist/telemetry/a11y.js +83 -0
- package/dist/telemetry/har/builder.d.ts +12 -1
- package/dist/telemetry/har/builder.js +11 -3
- 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/telemetry/network.d.ts +13 -16
- package/dist/telemetry/network.js +30 -52
- package/dist/telemetry/networkRetention.d.ts +83 -0
- package/dist/telemetry/networkRetention.js +117 -0
- package/dist/types.d.ts +26 -0
- package/dist/ui/OutputBuilder.d.ts +10 -0
- package/dist/ui/OutputBuilder.js +12 -0
- package/dist/ui/formatters/a11y.d.ts +5 -7
- package/dist/ui/formatters/a11y.js +7 -61
- package/dist/ui/formatters/console/chronological.js +4 -4
- package/dist/ui/formatters/console/follow.d.ts +4 -2
- package/dist/ui/formatters/console/follow.js +6 -3
- package/dist/ui/formatters/console/json.d.ts +3 -6
- package/dist/ui/formatters/console/json.js +9 -13
- package/dist/ui/formatters/console/shared.d.ts +17 -2
- package/dist/ui/formatters/console/shared.js +17 -0
- package/dist/ui/formatters/console/summarize.d.ts +2 -2
- package/dist/ui/formatters/console/summarize.js +22 -7
- package/dist/ui/formatters/console.d.ts +1 -1
- package/dist/ui/formatters/console.js +1 -5
- package/dist/ui/formatters/details.js +1 -1
- package/dist/ui/formatters/dom.d.ts +13 -4
- package/dist/ui/formatters/dom.js +25 -7
- package/dist/ui/formatters/layout.js +2 -1
- package/dist/ui/formatters/longValues.d.ts +14 -0
- package/dist/ui/formatters/longValues.js +23 -0
- package/dist/ui/formatters/networkList.d.ts +8 -2
- package/dist/ui/formatters/networkList.js +11 -2
- package/dist/ui/formatters/preview.d.ts +4 -1
- package/dist/ui/formatters/preview.js +55 -13
- package/dist/ui/formatters/sessions.d.ts +3 -2
- package/dist/ui/formatters/sessions.js +10 -3
- package/dist/ui/formatters/status.js +7 -0
- package/dist/ui/formatters/triggeredRequests.js +2 -1
- package/dist/ui/messages/chrome.d.ts +34 -7
- package/dist/ui/messages/chrome.js +81 -15
- package/dist/ui/messages/commands.d.ts +29 -8
- package/dist/ui/messages/commands.js +36 -8
- package/dist/ui/messages/networkMessages.d.ts +50 -0
- package/dist/ui/messages/networkMessages.js +66 -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/dist/utils/http.d.ts +9 -2
- package/dist/utils/http.js +4 -3
- package/dist/utils/strings.d.ts +19 -0
- package/dist/utils/strings.js +16 -0
- package/package.json +2 -2
|
@@ -26,16 +26,20 @@ export declare const UNBIND_TARGET_SCRIPT = "delete window.__bdgTarget; delete w
|
|
|
26
26
|
* the text nodes of hidden ones (display or visibility; not those of
|
|
27
27
|
* `<script>`, `<style>` or `<noscript>`), so text filters match hidden
|
|
28
28
|
* elements like Playwright's and `:visible` decides visibility; button inputs
|
|
29
|
-
* use their value.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* use their value. Text is read in the flat tree, as `bdg dom query` shows
|
|
30
|
+
* it: the text a web component's open shadow root renders, slotted content
|
|
31
|
+
* in place of its slots. A visible component, slot or element holding either
|
|
32
|
+
* ({@link COMPOSED_JS}) is read with {@link FLAT_TEXT_JS}, including the
|
|
33
|
+
* selects and editable regions `innerText` reads. Whitespace is collapsed;
|
|
34
|
+
* filter texts arrive normalized (`has-text` lowercased). `:visible` is
|
|
35
|
+
* checked before the text filters, which read the text.
|
|
32
36
|
*
|
|
33
37
|
* Steps and `:has()` match CSS relative to an element (`:scope > css`). A
|
|
34
38
|
* descendant step also searches the open shadow roots under the element (the
|
|
35
39
|
* whole step CSS inside one shadow tree); a child step stays in the
|
|
36
40
|
* element's own tree.
|
|
37
41
|
*/
|
|
38
|
-
export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const
|
|
42
|
+
export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\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 skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n}";
|
|
39
43
|
/**
|
|
40
44
|
* Page-side selector search that also reaches into open shadow roots and
|
|
41
45
|
* same-origin iframes (recursively), like a user sees the page.
|
|
@@ -49,7 +53,7 @@ export declare const FILTER_MATCHING_JS = "(shadowRoots) => {\n const skipped =
|
|
|
49
53
|
* reached. Cross-origin iframes are separate processes and cannot be
|
|
50
54
|
* searched. Throws a `SyntaxError` for an invalid selector.
|
|
51
55
|
*/
|
|
52
|
-
export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const
|
|
56
|
+
export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\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 skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n}";
|
|
53
57
|
/**
|
|
54
58
|
* Page-side element lookup shared by the interaction scripts.
|
|
55
59
|
*
|
|
@@ -58,7 +62,7 @@ export declare const DEEP_QUERY_JS = "function (selector, parts) {\n const css
|
|
|
58
62
|
* built-ins ({@link bindMatches}), otherwise all matches of the selector
|
|
59
63
|
* ({@link DEEP_QUERY_JS}).
|
|
60
64
|
*/
|
|
61
|
-
export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n const bound = window.__bdgMatches;\n if (bound && bound.selector === selector) {\n const connected = [];\n for (let i = 0; i < bound.nodes.length; i++) {\n if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];\n }\n return connected;\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\n const
|
|
65
|
+
export declare const FIND_ELEMENTS_JS = "function (selector, parts) {\n if (selector === '__bdg_bound_target__') {\n const el = window.__bdgTarget;\n return el && el.isConnected ? [el] : [];\n }\n const bound = window.__bdgMatches;\n if (bound && bound.selector === selector) {\n const connected = [];\n for (let i = 0; i < bound.nodes.length; i++) {\n if (bound.nodes[i] && bound.nodes[i].isConnected) connected[connected.length] = bound.nodes[i];\n }\n return connected;\n }\n return (function (selector, parts) {\n const css = parts ? parts.map((part) => part.css).join(', ') : selector;\n const found = [];\n const roots = [];\n const visit = (root) => {\n roots.push(root);\n for (const match of root.querySelectorAll(css)) found.push(match);\n for (const el of root.querySelectorAll('*')) {\n if (el.shadowRoot) visit(el.shadowRoot);\n if (el.tagName === 'IFRAME' || el.tagName === 'FRAME') {\n let frameDocument = null;\n try { frameDocument = el.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument) visit(frameDocument);\n }\n }\n };\n visit(document);\n if (!parts) return found;\n const shadowRoots = roots.filter((root) => root.host);\n const { passesAll, descend } = ((shadowRoots) => {\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 skipped = /^(script|style|noscript|template)$/;\n const hiddenText = (el) => {\n const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;\n let text = '';\n for (const node of nodes) {\n if (node.nodeType === 3) text += node.data;\n else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);\n }\n return text;\n };\n const textOf = (el) => {\n if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;\n const rendered = typeof el.innerText === 'string' &&\n (typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));\n const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;\n return text.replace(/\\s+/g, ' ').trim();\n };\n const passes = (el, filter) => {\n if (filter.kind === 'visible') {\n if (typeof el.checkVisibility === 'function' && !el.checkVisibility()) return false;\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0 && el.ownerDocument.defaultView.getComputedStyle(el).visibility === 'visible';\n }\n if (filter.kind === 'has') return filter.selectors.some((chain) => descend(el, chain).length > 0);\n const text = textOf(el);\n return filter.kind === 'text-is' ? text === filter.text : text.toLowerCase().includes(filter.text);\n };\n const passesAll = (el, filters) =>\n filters.every((filter) => filter.kind !== 'visible' || passes(el, filter)) &&\n filters.every((filter) => filter.kind === 'visible' || passes(el, filter));\n const isUnder = (scope, node) => {\n for (let current = node; current; current = current.getRootNode().host) {\n if (scope === current || scope.contains(current)) return true;\n }\n return false;\n };\n const stepMatches = (scope, step) => {\n const matches = [...scope.querySelectorAll(':scope ' + step.combinator + ' ' + step.css)];\n if (step.combinator !== ' ') return matches;\n for (const root of shadowRoots) {\n if (isUnder(scope, root.host)) matches.push(...root.querySelectorAll(step.css));\n }\n return matches;\n };\n const descend = (scope, steps) => {\n let current = [scope];\n for (const step of steps) {\n const next = new Set();\n for (const el of current) {\n for (const match of stepMatches(el, step)) {\n if (passesAll(match, step.filters)) next.add(match);\n }\n }\n current = [...next];\n }\n return current;\n };\n return { passesAll: passesAll, descend: descend };\n})(shadowRoots);\n const result = new Set();\n for (const el of found) {\n for (const part of parts) {\n if (!el.matches(part.css) || !passesAll(el, part.filters)) continue;\n for (const match of descend(el, part.steps || [])) result.add(match);\n }\n }\n const rootOrder = new Map(roots.map((root, i) => [root, i]));\n const rootIndex = (el) => rootOrder.get(el.getRootNode()) ?? roots.length;\n return [...result].sort((a, b) =>\n rootIndex(a) - rootIndex(b) || (a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1)\n );\n})(selector, parts);\n}";
|
|
62
66
|
/**
|
|
63
67
|
* Page-side check of what selectors cannot search in the top document:
|
|
64
68
|
* iframes whose document cannot be read (cross-origin) and `<object>`/`<embed>`
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { CommandError } from '../../errors/index.js';
|
|
12
12
|
import { actionBrokenByPageError, actionScriptFailedError, emptySelectorError, indexOutOfRangeError, noNodesFoundError, staleNodeError, } from '../../errors/messages.js';
|
|
13
|
+
import { COMPOSED_JS, FLAT_TEXT_JS } from './elementInfo.js';
|
|
13
14
|
import { ActionScriptError, throwIfInvalidSelector } from './formFillHelpers/shared.js';
|
|
14
15
|
import { frameScopedConnection } from './frameScopedConnection.js';
|
|
15
16
|
import { evaluateInBdgWorld } from '../page/bdgWorld.js';
|
|
@@ -102,9 +103,13 @@ const log = createLogger('dom');
|
|
|
102
103
|
* the text nodes of hidden ones (display or visibility; not those of
|
|
103
104
|
* `<script>`, `<style>` or `<noscript>`), so text filters match hidden
|
|
104
105
|
* elements like Playwright's and `:visible` decides visibility; button inputs
|
|
105
|
-
* use their value.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
106
|
+
* use their value. Text is read in the flat tree, as `bdg dom query` shows
|
|
107
|
+
* it: the text a web component's open shadow root renders, slotted content
|
|
108
|
+
* in place of its slots. A visible component, slot or element holding either
|
|
109
|
+
* ({@link COMPOSED_JS}) is read with {@link FLAT_TEXT_JS}, including the
|
|
110
|
+
* selects and editable regions `innerText` reads. Whitespace is collapsed;
|
|
111
|
+
* filter texts arrive normalized (`has-text` lowercased). `:visible` is
|
|
112
|
+
* checked before the text filters, which read the text.
|
|
108
113
|
*
|
|
109
114
|
* Steps and `:has()` match CSS relative to an element (`:scope > css`). A
|
|
110
115
|
* descendant step also searches the open shadow roots under the element (the
|
|
@@ -112,14 +117,15 @@ const log = createLogger('dom');
|
|
|
112
117
|
* element's own tree.
|
|
113
118
|
*/
|
|
114
119
|
export const FILTER_MATCHING_JS = `(shadowRoots) => {
|
|
120
|
+
const composed = ${COMPOSED_JS};
|
|
121
|
+
const flatText = ${FLAT_TEXT_JS};
|
|
115
122
|
const skipped = /^(script|style|noscript|template)$/;
|
|
116
123
|
const hiddenText = (el) => {
|
|
117
|
-
const
|
|
118
|
-
acceptNode: (node) => (node.nodeType === 1 && skipped.test(node.localName) ? 2 : 1)
|
|
119
|
-
});
|
|
124
|
+
const nodes = el.localName === 'slot' ? el.assignedNodes({ flatten: true }) : (el.shadowRoot || el).childNodes;
|
|
120
125
|
let text = '';
|
|
121
|
-
for (
|
|
126
|
+
for (const node of nodes) {
|
|
122
127
|
if (node.nodeType === 3) text += node.data;
|
|
128
|
+
else if (node.nodeType === 1 && !skipped.test(node.localName)) text += hiddenText(node);
|
|
123
129
|
}
|
|
124
130
|
return text;
|
|
125
131
|
};
|
|
@@ -127,7 +133,8 @@ export const FILTER_MATCHING_JS = `(shadowRoots) => {
|
|
|
127
133
|
if (el.localName === 'input' && /^(submit|button|reset)$/i.test(el.type)) return el.value;
|
|
128
134
|
const rendered = typeof el.innerText === 'string' &&
|
|
129
135
|
(typeof el.checkVisibility !== 'function' || el.checkVisibility({ visibilityProperty: true }));
|
|
130
|
-
|
|
136
|
+
const text = !rendered ? hiddenText(el) : composed(el) ? flatText(el, Infinity, true) : el.innerText;
|
|
137
|
+
return text.replace(/\\s+/g, ' ').trim();
|
|
131
138
|
};
|
|
132
139
|
const passes = (el, filter) => {
|
|
133
140
|
if (filter.kind === 'visible') {
|
|
@@ -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
|
|
@@ -2,52 +2,173 @@
|
|
|
2
2
|
* The session's user agent in headless Chrome: regular Chrome's string and
|
|
3
3
|
* client hints, so sites serve the page a user sees.
|
|
4
4
|
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { createLogger } from '../../ui/logging/index.js';
|
|
5
8
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
const log = createLogger('session');
|
|
10
|
+
/** Characters Chrome picks from for its made-up ("GREASE") brand name */
|
|
11
|
+
const GREASE_CHARS = [' ', '(', ':', '-', '.', '/', ')', ';', '=', '?', '_'];
|
|
12
|
+
/** Versions Chrome picks from for its made-up brand */
|
|
13
|
+
const GREASE_VERSIONS = ['8', '99', '24'];
|
|
14
|
+
/** Positions of the made-up brand, Chromium and the browser brand, by major version */
|
|
15
|
+
const BRAND_ORDERS = [
|
|
16
|
+
[0, 1, 2],
|
|
17
|
+
[0, 2, 1],
|
|
18
|
+
[1, 0, 2],
|
|
19
|
+
[1, 2, 0],
|
|
20
|
+
[2, 0, 1],
|
|
21
|
+
[2, 1, 0],
|
|
22
|
+
];
|
|
23
|
+
/** Client-hint platform for each `process.platform` Chrome runs on */
|
|
24
|
+
const HOST_PLATFORMS = {
|
|
25
|
+
darwin: 'macOS',
|
|
26
|
+
win32: 'Windows',
|
|
27
|
+
linux: 'Linux',
|
|
28
|
+
};
|
|
29
|
+
/** Client-hint platform for each platform token of the user agent string */
|
|
30
|
+
const USER_AGENT_PLATFORMS = [
|
|
31
|
+
[/Android/, 'Android'],
|
|
32
|
+
[/CrOS/, 'Chrome OS'],
|
|
33
|
+
[/Macintosh/, 'macOS'],
|
|
34
|
+
[/Windows/, 'Windows'],
|
|
35
|
+
[/Linux|X11/, 'Linux'],
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Chrome's brand list for a version, built the way Chrome builds it
|
|
39
|
+
* (`GenerateBrandVersionList` in Chromium's `user_agent_utils.cc`): a
|
|
40
|
+
* made-up brand, Chromium and the browser's brand, in an order and with a
|
|
41
|
+
* made-up name and version that depend on the major version.
|
|
42
|
+
*
|
|
43
|
+
* @param major - Major version (the seed)
|
|
44
|
+
* @param chromium - Chromium version to list
|
|
45
|
+
* @param browser - Browser brand and version
|
|
46
|
+
* @param greaseSuffix - Appended to the made-up version (`.0.0.0` in full versions)
|
|
47
|
+
* @returns Brand list
|
|
48
|
+
*/
|
|
49
|
+
export function chromeBrandList(major, chromium, browser, greaseSuffix = '') {
|
|
50
|
+
const grease = {
|
|
51
|
+
brand: `Not${GREASE_CHARS[major % GREASE_CHARS.length]}A${GREASE_CHARS[(major + 1) % GREASE_CHARS.length]}Brand`,
|
|
52
|
+
version: `${GREASE_VERSIONS[major % GREASE_VERSIONS.length]}${greaseSuffix}`,
|
|
53
|
+
};
|
|
54
|
+
const [greaseAt, chromiumAt, browserAt] = BRAND_ORDERS[major % BRAND_ORDERS.length] ?? [0, 1, 2];
|
|
55
|
+
const list = [];
|
|
56
|
+
list[greaseAt] = grease;
|
|
57
|
+
list[chromiumAt] = { brand: 'Chromium', version: chromium };
|
|
58
|
+
list[browserAt] = browser;
|
|
59
|
+
return list;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The client hints regular Chrome sends, for the browser `Browser.getVersion`
|
|
63
|
+
* describes. The browser brand is Microsoft Edge when the user agent says
|
|
64
|
+
* `Edg/`, Google Chrome otherwise (Chromium and Chrome for Testing look the
|
|
65
|
+
* same over CDP). Edge's Chromium version is only known to the major
|
|
66
|
+
* version. The OS version, architecture and bitness are the host's when it
|
|
67
|
+
* runs the platform the user agent names, empty otherwise (a remote Chrome).
|
|
68
|
+
*
|
|
69
|
+
* @param version - `Browser.getVersion` product and user agent
|
|
70
|
+
* @param host - The daemon's machine
|
|
71
|
+
* @returns Metadata for `Emulation.setUserAgentOverride`
|
|
72
|
+
*/
|
|
73
|
+
export function regularChromeMetadata(version, host) {
|
|
74
|
+
const fullVersion = /\/([\d.]+)/.exec(version.product)?.[1] ?? '';
|
|
75
|
+
const major = Number.parseInt(fullVersion, 10) || 0;
|
|
76
|
+
const isEdge = / Edg\//.test(version.userAgent);
|
|
77
|
+
const chromiumVersion = isEdge ? `${major}.0.0.0` : fullVersion;
|
|
78
|
+
const brand = isEdge ? 'Microsoft Edge' : 'Google Chrome';
|
|
79
|
+
const platform = USER_AGENT_PLATFORMS.find(([token]) => token.test(version.userAgent))?.[1] ?? host.platform;
|
|
80
|
+
const isHost = platform === host.platform;
|
|
81
|
+
return {
|
|
82
|
+
brands: chromeBrandList(major, String(major), { brand, version: String(major) }),
|
|
83
|
+
fullVersionList: chromeBrandList(major, chromiumVersion, { brand, version: fullVersion }, '.0.0.0'),
|
|
84
|
+
fullVersion,
|
|
85
|
+
platform,
|
|
86
|
+
platformVersion: isHost ? host.platformVersion : '',
|
|
87
|
+
architecture: isHost ? host.architecture : '',
|
|
88
|
+
bitness: isHost ? host.bitness : '',
|
|
89
|
+
model: '',
|
|
90
|
+
mobile: false,
|
|
91
|
+
wow64: false,
|
|
92
|
+
formFactors: ['Desktop'],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** First Windows build of Windows 11, which client hints report as version 13 */
|
|
96
|
+
const WINDOWS_11_BUILD = 22000;
|
|
97
|
+
/**
|
|
98
|
+
* The OS version Chrome reports on Linux or Windows, from `os.release()`:
|
|
99
|
+
* the kernel version's first three numbers on Linux, and on Windows `13.0.0`
|
|
100
|
+
* for Windows 11 and `10.0.0` before it (Chrome reports a Windows API
|
|
101
|
+
* version there, not the OS build).
|
|
102
|
+
*
|
|
103
|
+
* @param platform - `process.platform`
|
|
104
|
+
* @param release - `os.release()`
|
|
105
|
+
* @returns OS version, or empty when unknown
|
|
106
|
+
*/
|
|
107
|
+
export function releasePlatformVersion(platform, release) {
|
|
108
|
+
if (platform === 'win32') {
|
|
109
|
+
const build = Number(release.split('.')[2]);
|
|
110
|
+
if (!build)
|
|
111
|
+
return '';
|
|
112
|
+
return build >= WINDOWS_11_BUILD ? '13.0.0' : '10.0.0';
|
|
113
|
+
}
|
|
114
|
+
return /^\d+(\.\d+){0,2}/.exec(release)?.[0] ?? '';
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The OS version as Chrome reports it: the macOS product version (from
|
|
118
|
+
* `SystemVersion.plist`, as `sw_vers` prints it), or
|
|
119
|
+
* {@link releasePlatformVersion} elsewhere.
|
|
120
|
+
*
|
|
121
|
+
* @returns OS version, or empty when unknown
|
|
122
|
+
*/
|
|
123
|
+
function hostPlatformVersion() {
|
|
124
|
+
if (process.platform !== 'darwin')
|
|
125
|
+
return releasePlatformVersion(process.platform, os.release());
|
|
126
|
+
try {
|
|
127
|
+
const plist = readFileSync('/System/Library/CoreServices/SystemVersion.plist', 'utf8');
|
|
128
|
+
return /<key>ProductVersion<\/key>\s*<string>([\d.]+)<\/string>/.exec(plist)?.[1] ?? '';
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
log.debug(`macOS version unknown: ${getErrorMessage(error)}`);
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The daemon's machine as client hints describe it.
|
|
137
|
+
*
|
|
138
|
+
* @returns Platform, OS version, architecture and bitness
|
|
139
|
+
*/
|
|
140
|
+
export function hostPlatform() {
|
|
141
|
+
const arch = process.arch;
|
|
142
|
+
return {
|
|
143
|
+
platform: HOST_PLATFORMS[process.platform] ?? '',
|
|
144
|
+
platformVersion: hostPlatformVersion(),
|
|
145
|
+
architecture: arch.startsWith('arm') ? 'arm' : 'x86',
|
|
146
|
+
bitness: arch === 'arm64' || arch === 'x64' ? '64' : '32',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
29
149
|
/**
|
|
30
150
|
* Send the user agent and client hints of regular Chrome from headless
|
|
31
151
|
* Chrome: sites serve "HeadlessChrome" a different page (or a bot
|
|
32
152
|
* challenge), so the page would not be the one a user sees. Not for a
|
|
33
153
|
* session emulating a phone, whose emulation sets a mobile user agent.
|
|
34
154
|
*
|
|
155
|
+
* The client hints are built from `Browser.getVersion` and the host
|
|
156
|
+
* ({@link regularChromeMetadata}) rather than read from the page: the page is
|
|
157
|
+
* still `about:blank`, which has no `navigator.userAgentData`, and an
|
|
158
|
+
* override without metadata empties the client hints.
|
|
159
|
+
*
|
|
35
160
|
* @param cdp - CDP connection
|
|
36
161
|
* @param logger - Logger for failures (the session works without it)
|
|
162
|
+
* @param known - `Browser.getVersion` result, when the caller has it
|
|
37
163
|
*/
|
|
38
|
-
export async function hideHeadlessUserAgent(cdp, logger) {
|
|
164
|
+
export async function hideHeadlessUserAgent(cdp, logger, known) {
|
|
39
165
|
try {
|
|
40
|
-
const
|
|
41
|
-
if (!userAgent.includes('HeadlessChrome'))
|
|
166
|
+
const version = known ?? (await cdp.send('Browser.getVersion'));
|
|
167
|
+
if (!version.userAgent.includes('HeadlessChrome'))
|
|
42
168
|
return;
|
|
43
|
-
const metadata = (await cdp.send('Runtime.evaluate', {
|
|
44
|
-
expression: USER_AGENT_METADATA_SCRIPT,
|
|
45
|
-
awaitPromise: true,
|
|
46
|
-
returnByValue: true,
|
|
47
|
-
}));
|
|
48
169
|
await cdp.send('Emulation.setUserAgentOverride', {
|
|
49
|
-
userAgent: userAgent.replace('HeadlessChrome', 'Chrome'),
|
|
50
|
-
|
|
170
|
+
userAgent: version.userAgent.replace('HeadlessChrome', 'Chrome'),
|
|
171
|
+
userAgentMetadata: regularChromeMetadata(version, hostPlatform()),
|
|
51
172
|
});
|
|
52
173
|
}
|
|
53
174
|
catch (error) {
|
package/dist/session/paths.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* (or `~/.bdg/sessions/<name>/` for a named session).
|
|
6
6
|
* WHY: Single source of truth for file locations prevents path inconsistencies.
|
|
7
7
|
*/
|
|
8
|
+
import { type DirTrustKind } from '../utils/directories.js';
|
|
8
9
|
/**
|
|
9
10
|
* Session file paths relative to ~/.bdg/
|
|
10
11
|
* Centralized definition for all session-related files.
|
|
@@ -123,12 +124,46 @@ export declare function getDaemonSocketPath(): string;
|
|
|
123
124
|
/**
|
|
124
125
|
* Ensure the session directory exists.
|
|
125
126
|
*
|
|
126
|
-
* Creates
|
|
127
|
-
*
|
|
128
|
-
* would spin on a
|
|
127
|
+
* Creates it, and missing parents (the base directory, `sessions/`), with
|
|
128
|
+
* mode 0700. Safe to call multiple times (idempotent). A path that cannot
|
|
129
|
+
* hold a directory is refused before `mkdir`, which would spin on a
|
|
130
|
+
* pseudo-filesystem ({@link makeDirectory}).
|
|
129
131
|
*
|
|
130
132
|
* @throws Error if the directory cannot be created
|
|
131
133
|
*/
|
|
132
134
|
export declare function ensureSessionDir(): void;
|
|
135
|
+
/** A session directory (or one above it) that cannot be trusted */
|
|
136
|
+
export interface UntrustedSessionDir {
|
|
137
|
+
/** The untrusted directory */
|
|
138
|
+
dir: string;
|
|
139
|
+
/** Why, e.g. `writable by others (mode 777)` */
|
|
140
|
+
reason: string;
|
|
141
|
+
kind: DirTrustKind;
|
|
142
|
+
/** bdg owns it by convention (`~/.bdg`, `sessions/`, `sessions/<name>`) */
|
|
143
|
+
bdgOwned: boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check that a session directory and the directories above it up to the base
|
|
147
|
+
* directory can be trusted before a daemon is started there or its socket is
|
|
148
|
+
* connected to: another user who can write to one of them could replace the
|
|
149
|
+
* socket (and receive every command) or plant files. Each existing directory
|
|
150
|
+
* must be a real directory (not a symlink), owned by the user, and not
|
|
151
|
+
* writable by others ({@link dirTrustProblem}); group write is accepted, since
|
|
152
|
+
* under umask 002 (per-user groups) older versions created `~/.bdg` 0775.
|
|
153
|
+
* The base directory may be a symlink whose target passes the same rule
|
|
154
|
+
* ({@link chainDirProblem}). A directory owned by another uid (a bind mount,
|
|
155
|
+
* `sudo -E`) is refused.
|
|
156
|
+
*
|
|
157
|
+
* Trusted directories bdg owns (the default `~/.bdg`, `sessions/` and named
|
|
158
|
+
* session directories) that group or others can still use are tightened to
|
|
159
|
+
* 0700; a base directory chosen with `$BDG_SESSION_DIR`, or a symlinked base
|
|
160
|
+
* (opened without following links), is never changed.
|
|
161
|
+
* Missing paths are skipped (as is a path through a file, which fails on its
|
|
162
|
+
* own): {@link ensureSessionDir} creates them 0700.
|
|
163
|
+
*
|
|
164
|
+
* @param dir - Session directory (default: the selected session's)
|
|
165
|
+
* @returns The first untrusted directory and why, or null
|
|
166
|
+
*/
|
|
167
|
+
export declare function secureSessionDir(dir?: string): UntrustedSessionDir | null;
|
|
133
168
|
export {};
|
|
134
169
|
//# sourceMappingURL=paths.d.ts.map
|