@mearl/browser-core 3.0.0-beta.4 → 3.0.0-beta.7
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/README.md +2 -2
- package/dist/agentChat.d.ts +2 -0
- package/dist/agentSkills.d.ts +1 -1
- package/dist/app-profile-runtime/types.d.ts +10 -12
- package/dist/appProfileDefinition.d.ts +32 -14
- package/dist/appProfileDefinition.js +63 -69
- package/dist/axSnapshot.js +1 -1
- package/dist/boundedRequestBuffer.d.ts +4 -4
- package/dist/browserActionProtocol.d.ts +65 -42
- package/dist/browserActionProtocol.js +15 -15
- package/dist/browserCapabilities.d.ts +47 -0
- package/dist/browserCapabilities.js +63 -0
- package/dist/browserDiscovery.d.ts +3 -0
- package/dist/browserDiscovery.js +20 -0
- package/dist/browserEnhancement.d.ts +161 -0
- package/dist/browserEnhancement.js +80 -0
- package/dist/cookie.d.ts +6 -0
- package/dist/cookie.js +20 -5
- package/dist/dataStructure.d.ts +8 -0
- package/dist/dataStructure.js +58 -0
- package/dist/deviceEnvironmentObservation.d.ts +48 -0
- package/dist/deviceEnvironmentObservation.js +77 -0
- package/dist/domClicking.d.ts +14 -0
- package/dist/domClicking.js +98 -10
- package/dist/domInspection.js +13 -1
- package/dist/extensionServiceProtocol.d.ts +7 -0
- package/dist/extensionServiceProtocol.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/pageAct.d.ts +2 -0
- package/dist/pageAct.js +42 -10
- package/dist/pageDialog.d.ts +66 -0
- package/dist/pageDialog.js +175 -0
- package/dist/pageObserver.js +16 -5
- package/dist/pagePressing.d.ts +1 -1
- package/dist/providerAppResources.d.ts +39 -0
- package/dist/providerAppResources.js +2 -0
- package/dist/proxyPermissions.js +4 -3
- package/dist/requestMetadata.d.ts +9 -0
- package/dist/requestMetadata.js +8 -0
- package/dist/requestSerializer.d.ts +2 -2
- package/dist/requestSerializer.js +1 -29
- package/dist/requestStreamCapture.d.ts +5 -3
- package/dist/requestStreamCapture.js +2 -0
- package/dist/runActions.js +1 -1
- package/dist/types.d.ts +4 -2
- package/package.json +2 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isDeviceEnvironmentObservation } from './deviceEnvironmentObservation.js';
|
|
2
|
+
export const BROWSER_ENHANCEMENT_PROTOCOL_VERSION = 1;
|
|
3
|
+
export const BROWSER_ENHANCEMENT_NAVIGATION_ORIGIN_VERSION = 1;
|
|
4
|
+
export const BROWSER_ENHANCEMENT_PREPARATION_VERSION = 1;
|
|
5
|
+
export const BROWSER_ENHANCEMENT_HOST_ENVIRONMENT_VERSION = 1;
|
|
6
|
+
export const BROWSER_ENHANCEMENT_PREPARATION_PATH = 'src/pages/browser-transport/index.html';
|
|
7
|
+
/** Only the installed Extension's inert top-level page can prepare a new document. */
|
|
8
|
+
export function isBrowserEnhancementPreparationUrl(value) {
|
|
9
|
+
if (typeof value !== 'string')
|
|
10
|
+
return false;
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
return /^[a-p]{32}$/.test(url.hostname) && value === `chrome-extension://${url.hostname}/${BROWSER_ENHANCEMENT_PREPARATION_PATH}`;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isBrowserEnhancementAction(value) {
|
|
20
|
+
return value === 'set_device_emulation' || value === 'set_app_profile' || value === 'set_timezone';
|
|
21
|
+
}
|
|
22
|
+
export function requiredBrowserEnhancementActions(invocation) {
|
|
23
|
+
return invocation.action === 'set_app_profile' && invocation.data?.emulation !== undefined
|
|
24
|
+
? ['set_app_profile', 'set_device_emulation'] : [invocation.action];
|
|
25
|
+
}
|
|
26
|
+
export function isProviderInitialEnvironment(value) {
|
|
27
|
+
if (!value || typeof value !== 'object')
|
|
28
|
+
return false;
|
|
29
|
+
const device = value.device;
|
|
30
|
+
const only = (record, keys) => Object.keys(record).every(key => keys.includes(key));
|
|
31
|
+
try {
|
|
32
|
+
return isDeviceEnvironmentObservation(device) && device.userAgent.length <= 4096 &&
|
|
33
|
+
device.platform.length <= 256 && JSON.stringify(value).length <= 32_768 && only(value, ['device', 'preserveHostPlatform', 'preserveHostInput']) &&
|
|
34
|
+
(value.preserveHostPlatform === undefined || value.preserveHostPlatform === true) &&
|
|
35
|
+
(value.preserveHostInput === undefined || value.preserveHostInput === true) &&
|
|
36
|
+
only(device, ['userAgent', 'platform', 'userAgentMetadata', 'screen', 'touchPoints', 'safeAreaInsets']) &&
|
|
37
|
+
only(device.screen, ['width', 'height', 'deviceScaleFactor', 'orientation', 'angle']) && only(device.safeAreaInsets, ['top', 'right', 'bottom', 'left']) &&
|
|
38
|
+
(device.userAgentMetadata === null || (only(device.userAgentMetadata, ['brands', 'fullVersionList', 'fullVersion', 'platform', 'platformVersion', 'architecture', 'model', 'mobile', 'bitness', 'wow64', 'formFactors']) &&
|
|
39
|
+
[...device.userAgentMetadata.brands, ...device.userAgentMetadata.fullVersionList].every(brand => only(brand, ['brand', 'version']))));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function isEnhancementNavigationOrigin(value) {
|
|
46
|
+
if (typeof value !== 'string' || value.length > 4096)
|
|
47
|
+
return false;
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(value);
|
|
50
|
+
return ['http:', 'https:'].includes(url.protocol) && url.origin === value;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function isProviderEnhancementTarget(value) {
|
|
57
|
+
if (!value || typeof value !== 'object')
|
|
58
|
+
return false;
|
|
59
|
+
const target = value;
|
|
60
|
+
return Number.isSafeInteger(target.tabId) && target.tabId >= 0 &&
|
|
61
|
+
(value.ready === false || (target.ready === true &&
|
|
62
|
+
(target.mainFrameId === undefined || validIdentity(target.mainFrameId)) &&
|
|
63
|
+
(target.targetId === undefined || validIdentity(target.targetId)) &&
|
|
64
|
+
(target.initialEnvironment === undefined || isProviderInitialEnvironment(target.initialEnvironment)) &&
|
|
65
|
+
(target.mainFrameId !== undefined || target.targetId !== undefined) && typeof target.url === 'string' && Boolean(target.url)));
|
|
66
|
+
}
|
|
67
|
+
function validIdentity(value) { return typeof value === 'string' && value.length > 0 && value.length <= 256; }
|
|
68
|
+
/** Match every identity asserted by the external owner; URL alone is never proof. */
|
|
69
|
+
export function matchesProviderEnhancementTarget(target, physical) {
|
|
70
|
+
return Boolean(target.mainFrameId || target.targetId) &&
|
|
71
|
+
(target.mainFrameId === undefined || target.mainFrameId === physical.mainFrameId) &&
|
|
72
|
+
(target.targetId === undefined || target.targetId === physical.targetId);
|
|
73
|
+
}
|
|
74
|
+
export function requireReadyProviderEnhancementTarget(value, tabId) {
|
|
75
|
+
if (!isProviderEnhancementTarget(value) || value.tabId !== tabId)
|
|
76
|
+
throw new Error('BROWSER_ENHANCEMENT_PAGE_IDENTITY_UNAVAILABLE');
|
|
77
|
+
if (!value.ready)
|
|
78
|
+
throw new Error('EXTERNAL_CONTROL_PAUSED: the external page owner has not granted Agent control');
|
|
79
|
+
return value;
|
|
80
|
+
}
|
package/dist/cookie.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface CookieCandidate {
|
|
|
25
25
|
partitionKey?: unknown;
|
|
26
26
|
partitionKeyOpaque?: boolean;
|
|
27
27
|
}
|
|
28
|
+
export type RequestCookie = Pick<CookieCandidate, 'name' | 'value'>;
|
|
29
|
+
export declare function serializeRequestCookies(cookies: readonly RequestCookie[]): string;
|
|
30
|
+
/** Token lookup follows the first (most specific path) occurrence in the Cookie header. */
|
|
31
|
+
export declare function requestCookiesToMap(cookies: readonly RequestCookie[]): Map<string, string>;
|
|
32
|
+
/** Chromium also treats localhost and loopback URLs as trustworthy for Secure cookies. */
|
|
33
|
+
export declare function isSecureCookieUrl(url: URL): boolean;
|
|
28
34
|
/** Filter browser-context storage results to the cookies applicable to the requested URL. */
|
|
29
35
|
export declare function filterCookiesForUrl<T extends CookieCandidate>(cookies: readonly T[], cookieUrl: string): T[];
|
|
30
36
|
/** Select one readable, non-partitioned named cookie from URL-filtered backend results. */
|
package/dist/cookie.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export function serializeRequestCookies(cookies) {
|
|
2
|
+
return cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
|
|
3
|
+
}
|
|
4
|
+
/** Token lookup follows the first (most specific path) occurrence in the Cookie header. */
|
|
5
|
+
export function requestCookiesToMap(cookies) {
|
|
6
|
+
const values = new Map();
|
|
7
|
+
for (const cookie of cookies) {
|
|
8
|
+
if (!values.has(cookie.name))
|
|
9
|
+
values.set(cookie.name, cookie.value);
|
|
10
|
+
}
|
|
11
|
+
return values;
|
|
12
|
+
}
|
|
1
13
|
function normalizeCookieDomain(input, action) {
|
|
2
14
|
const value = input.trim();
|
|
3
15
|
if (!value)
|
|
@@ -14,21 +26,24 @@ function normalizeCookieDomain(input, action) {
|
|
|
14
26
|
throw new Error(`${action}: invalid domain "${input}"`);
|
|
15
27
|
return hostname;
|
|
16
28
|
}
|
|
17
|
-
/**
|
|
18
|
-
export function
|
|
19
|
-
const url = new URL(cookieUrl);
|
|
29
|
+
/** Chromium also treats localhost and loopback URLs as trustworthy for Secure cookies. */
|
|
30
|
+
export function isSecureCookieUrl(url) {
|
|
20
31
|
const hostname = url.hostname.replace(/\.$/, '');
|
|
21
32
|
const isLocalhost = hostname === 'localhost' ||
|
|
22
33
|
hostname.endsWith('.localhost') ||
|
|
23
34
|
hostname === '[::1]' ||
|
|
24
35
|
/^127\.\d+\.\d+\.\d+$/.test(hostname);
|
|
36
|
+
return url.protocol === 'https:' || isLocalhost;
|
|
37
|
+
}
|
|
38
|
+
/** Filter browser-context storage results to the cookies applicable to the requested URL. */
|
|
39
|
+
export function filterCookiesForUrl(cookies, cookieUrl) {
|
|
40
|
+
const url = new URL(cookieUrl);
|
|
25
41
|
return cookies.filter(item => {
|
|
26
42
|
const domain = item.domain.replace(/^\./, '').toLowerCase();
|
|
27
43
|
const hostOnly = item.hostOnly ?? !item.domain.startsWith('.');
|
|
28
44
|
if (url.hostname !== domain && (hostOnly || !url.hostname.endsWith(`.${domain}`)))
|
|
29
45
|
return false;
|
|
30
|
-
|
|
31
|
-
if (item.secure && url.protocol !== 'https:' && !isLocalhost)
|
|
46
|
+
if (item.secure && !isSecureCookieUrl(url))
|
|
32
47
|
return false;
|
|
33
48
|
return (url.pathname === item.path ||
|
|
34
49
|
(url.pathname.startsWith(item.path) &&
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface StructureOptions {
|
|
2
|
+
maxDepth?: number;
|
|
3
|
+
maxKeys?: number;
|
|
4
|
+
arraySamples?: number;
|
|
5
|
+
maxNodes?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
|
|
8
|
+
export declare function getStructure(data: unknown, options?: StructureOptions): unknown;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Describes a bounded sample of a value without invoking accessors or exposing leaf values. */
|
|
2
|
+
export function getStructure(data, options = {}) {
|
|
3
|
+
const { maxDepth = 5, maxKeys = 50, arraySamples = 1, maxNodes = 2000 } = options;
|
|
4
|
+
for (const [name, value, limit] of [
|
|
5
|
+
['maxDepth', maxDepth, 20],
|
|
6
|
+
['maxKeys', maxKeys, 1000],
|
|
7
|
+
['arraySamples', arraySamples, 20],
|
|
8
|
+
['maxNodes', maxNodes, 10000],
|
|
9
|
+
]) {
|
|
10
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > limit)
|
|
11
|
+
throw new Error(`${name} must be an integer between 1 and ${limit}`);
|
|
12
|
+
}
|
|
13
|
+
const ancestors = new WeakSet();
|
|
14
|
+
let nodes = 0;
|
|
15
|
+
const visit = (value, depth) => {
|
|
16
|
+
if (++nodes > maxNodes)
|
|
17
|
+
return '<max nodes reached>';
|
|
18
|
+
if (depth >= maxDepth)
|
|
19
|
+
return '<max depth reached>';
|
|
20
|
+
if (value === null)
|
|
21
|
+
return 'null';
|
|
22
|
+
if (typeof value !== 'object')
|
|
23
|
+
return typeof value;
|
|
24
|
+
if (ancestors.has(value))
|
|
25
|
+
return '<circular>';
|
|
26
|
+
ancestors.add(value);
|
|
27
|
+
try {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
if (!value.length)
|
|
30
|
+
return 'array[0]';
|
|
31
|
+
const samples = Array.from({ length: Math.min(value.length, arraySamples) }, (_, index) => {
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, index);
|
|
33
|
+
return descriptor && 'value' in descriptor
|
|
34
|
+
? visit(descriptor.value, depth + 1)
|
|
35
|
+
: descriptor
|
|
36
|
+
? '<accessor>'
|
|
37
|
+
: 'undefined';
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
_type: `array[${value.length}]`,
|
|
41
|
+
...(arraySamples === 1 ? { _sample: samples[0] } : { _samples: samples }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const keys = Object.keys(value);
|
|
45
|
+
const entries = keys.slice(0, maxKeys).map(key => {
|
|
46
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
47
|
+
return [key, 'value' in descriptor ? visit(descriptor.value, depth + 1) : '<accessor>'];
|
|
48
|
+
});
|
|
49
|
+
if (keys.length > maxKeys)
|
|
50
|
+
entries.push(['<truncated keys>', keys.length - maxKeys]);
|
|
51
|
+
return Object.fromEntries(entries);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
ancestors.delete(value);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return visit(data, 0);
|
|
58
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ResolvedEmulationConfig } from './deviceEmulation.js';
|
|
2
|
+
export interface DeviceUserAgentMetadata {
|
|
3
|
+
brands: Array<{
|
|
4
|
+
brand: string;
|
|
5
|
+
version: string;
|
|
6
|
+
}>;
|
|
7
|
+
fullVersionList: Array<{
|
|
8
|
+
brand: string;
|
|
9
|
+
version: string;
|
|
10
|
+
}>;
|
|
11
|
+
fullVersion?: string;
|
|
12
|
+
platform: string;
|
|
13
|
+
platformVersion: string;
|
|
14
|
+
architecture: string;
|
|
15
|
+
model: string;
|
|
16
|
+
mobile: boolean;
|
|
17
|
+
bitness?: string;
|
|
18
|
+
wow64?: boolean;
|
|
19
|
+
formFactors?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface DeviceEnvironmentObservation {
|
|
22
|
+
userAgent: string;
|
|
23
|
+
platform: string;
|
|
24
|
+
userAgentMetadata: DeviceUserAgentMetadata | null;
|
|
25
|
+
screen: {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
deviceScaleFactor: number;
|
|
29
|
+
orientation: string;
|
|
30
|
+
angle: number;
|
|
31
|
+
};
|
|
32
|
+
touchPoints: number;
|
|
33
|
+
safeAreaInsets: {
|
|
34
|
+
top: number;
|
|
35
|
+
right: number;
|
|
36
|
+
bottom: number;
|
|
37
|
+
left: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Explicitly disables UA Client Hints for an emulated UA, matching Chrome's omitted-metadata behavior. */
|
|
41
|
+
export declare function emptyDeviceUserAgentMetadata(): DeviceUserAgentMetadata;
|
|
42
|
+
/** Self-contained observation for an isolated page world; no retained DOM or page globals. */
|
|
43
|
+
export declare function observeDeviceEnvironment(): Promise<DeviceEnvironmentObservation>;
|
|
44
|
+
export declare function isDeviceUserAgentMetadata(value: unknown): value is DeviceUserAgentMetadata;
|
|
45
|
+
export declare function isDeviceEnvironmentObservation(value: unknown): value is DeviceEnvironmentObservation;
|
|
46
|
+
/** CDP transports can reorder object keys. Values and array order remain significant. */
|
|
47
|
+
export declare function deviceEnvironmentObservationsEqual(left: DeviceEnvironmentObservation, right: DeviceEnvironmentObservation): boolean;
|
|
48
|
+
export declare function matchesEmulatedDeviceEnvironment(config: ResolvedEmulationConfig, observed: DeviceEnvironmentObservation): boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Explicitly disables UA Client Hints for an emulated UA, matching Chrome's omitted-metadata behavior. */
|
|
2
|
+
export function emptyDeviceUserAgentMetadata() {
|
|
3
|
+
return { brands: [], fullVersionList: [], fullVersion: '', platform: '', platformVersion: '', architecture: '', model: '',
|
|
4
|
+
mobile: false, bitness: '', wow64: false, formFactors: [] };
|
|
5
|
+
}
|
|
6
|
+
/** Self-contained observation for an isolated page world; no retained DOM or page globals. */
|
|
7
|
+
export async function observeDeviceEnvironment() {
|
|
8
|
+
const data = navigator.userAgentData;
|
|
9
|
+
let userAgentMetadata = null;
|
|
10
|
+
if (data) {
|
|
11
|
+
const values = await data.getHighEntropyValues(['architecture', 'bitness', 'fullVersionList', 'model', 'platformVersion', 'uaFullVersion', 'wow64', 'formFactors']);
|
|
12
|
+
userAgentMetadata = {
|
|
13
|
+
brands: values.brands, fullVersionList: values.fullVersionList,
|
|
14
|
+
...(typeof values.uaFullVersion === 'string' ? { fullVersion: values.uaFullVersion } : {}),
|
|
15
|
+
platform: values.platform, platformVersion: values.platformVersion, architecture: values.architecture, model: values.model, mobile: values.mobile,
|
|
16
|
+
...(typeof values.bitness === 'string' ? { bitness: values.bitness } : {}),
|
|
17
|
+
...(typeof values.wow64 === 'boolean' ? { wow64: values.wow64 } : {}),
|
|
18
|
+
...(Array.isArray(values.formFactors) ? { formFactors: values.formFactors } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const probe = document.createElement('div');
|
|
22
|
+
probe.style.cssText = 'all:initial!important;position:fixed!important;visibility:hidden!important;pointer-events:none!important;width:0!important;height:0!important;padding-top:env(safe-area-inset-top)!important;padding-right:env(safe-area-inset-right)!important;padding-bottom:env(safe-area-inset-bottom)!important;padding-left:env(safe-area-inset-left)!important;';
|
|
23
|
+
document.documentElement.appendChild(probe);
|
|
24
|
+
try {
|
|
25
|
+
const style = getComputedStyle(probe);
|
|
26
|
+
return {
|
|
27
|
+
userAgent: navigator.userAgent, platform: navigator.platform, userAgentMetadata,
|
|
28
|
+
screen: { width: screen.width, height: screen.height, deviceScaleFactor: devicePixelRatio, orientation: screen.orientation.type, angle: screen.orientation.angle },
|
|
29
|
+
touchPoints: navigator.maxTouchPoints,
|
|
30
|
+
safeAreaInsets: { top: parseFloat(style.paddingTop) || 0, right: parseFloat(style.paddingRight) || 0, bottom: parseFloat(style.paddingBottom) || 0, left: parseFloat(style.paddingLeft) || 0 },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
probe.remove();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function isDeviceUserAgentMetadata(value) {
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
const metadata = value;
|
|
41
|
+
const brands = (value) => Array.isArray(value) && value.every(item => item && typeof item.brand === 'string' && typeof item.version === 'string');
|
|
42
|
+
return brands(metadata.brands) && brands(metadata.fullVersionList) &&
|
|
43
|
+
[metadata.platform, metadata.platformVersion, metadata.architecture, metadata.model].every(value => typeof value === 'string') &&
|
|
44
|
+
typeof metadata.mobile === 'boolean' && (metadata.fullVersion === undefined || typeof metadata.fullVersion === 'string') &&
|
|
45
|
+
(metadata.bitness === undefined || typeof metadata.bitness === 'string') && (metadata.wow64 === undefined || typeof metadata.wow64 === 'boolean') &&
|
|
46
|
+
(metadata.formFactors === undefined || (Array.isArray(metadata.formFactors) && metadata.formFactors.every(value => typeof value === 'string')));
|
|
47
|
+
}
|
|
48
|
+
export function isDeviceEnvironmentObservation(value) {
|
|
49
|
+
if (!value || typeof value !== 'object')
|
|
50
|
+
return false;
|
|
51
|
+
const current = value;
|
|
52
|
+
return typeof current.userAgent === 'string' && typeof current.platform === 'string' &&
|
|
53
|
+
(current.userAgentMetadata === null || isDeviceUserAgentMetadata(current.userAgentMetadata)) &&
|
|
54
|
+
Boolean(current.screen) && [current.screen.width, current.screen.height, current.screen.deviceScaleFactor].every(value => Number.isFinite(value) && value > 0) &&
|
|
55
|
+
typeof current.screen.orientation === 'string' && Number.isFinite(current.screen.angle) &&
|
|
56
|
+
Number.isSafeInteger(current.touchPoints) && current.touchPoints >= 0 && Boolean(current.safeAreaInsets) &&
|
|
57
|
+
[current.safeAreaInsets.top, current.safeAreaInsets.right, current.safeAreaInsets.bottom, current.safeAreaInsets.left].every(value => Number.isFinite(value) && value >= 0);
|
|
58
|
+
}
|
|
59
|
+
/** CDP transports can reorder object keys. Values and array order remain significant. */
|
|
60
|
+
export function deviceEnvironmentObservationsEqual(left, right) {
|
|
61
|
+
const ordered = (value) => {
|
|
62
|
+
if (Array.isArray(value))
|
|
63
|
+
return value.map(ordered);
|
|
64
|
+
if (value && typeof value === 'object')
|
|
65
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, ordered(value[key])]));
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
return JSON.stringify(ordered(left)) === JSON.stringify(ordered(right));
|
|
69
|
+
}
|
|
70
|
+
export function matchesEmulatedDeviceEnvironment(config, observed) {
|
|
71
|
+
return observed.userAgent === config.userAgent && observed.platform === config.platform &&
|
|
72
|
+
observed.screen.width === config.width && observed.screen.height === config.height &&
|
|
73
|
+
Math.abs(observed.screen.deviceScaleFactor - config.deviceScaleFactor) < 0.0001 &&
|
|
74
|
+
observed.screen.orientation.startsWith(config.orientation) && observed.screen.angle === (config.orientation === 'landscape' ? 90 : 0) &&
|
|
75
|
+
observed.touchPoints === (config.touch ? 5 : 0) &&
|
|
76
|
+
['top', 'right', 'bottom', 'left'].every(edge => observed.safeAreaInsets[edge] === config.safeAreaInsets[edge]);
|
|
77
|
+
}
|
package/dist/domClicking.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface PageClickDispatchTarget {
|
|
|
40
40
|
frame?: PageClickFrameTarget;
|
|
41
41
|
coordinates?: PageClickCoordinates;
|
|
42
42
|
controlState?: PageClickControlState;
|
|
43
|
+
selectionState?: PageClickSelectionState;
|
|
43
44
|
dispatchMode?: PageClickDispatchMode;
|
|
44
45
|
pointerType?: PageClickPointerType;
|
|
45
46
|
}
|
|
@@ -84,6 +85,15 @@ export interface PageClickControlState {
|
|
|
84
85
|
checkedBefore: boolean;
|
|
85
86
|
checkedAfter: boolean;
|
|
86
87
|
}
|
|
88
|
+
export interface PageClickSelectionState {
|
|
89
|
+
valueBefore: string;
|
|
90
|
+
valueAfter: string;
|
|
91
|
+
selectedAfter: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** Native options need value selection; clicking their select only opens the menu. */
|
|
94
|
+
export declare function isStateAwareClickTarget(target: PageClickDispatchTarget & {
|
|
95
|
+
inputType?: string;
|
|
96
|
+
}): boolean;
|
|
87
97
|
export interface PageClickResolvedTarget {
|
|
88
98
|
tag: string;
|
|
89
99
|
text: string;
|
|
@@ -102,6 +112,7 @@ export interface PageClickResultData {
|
|
|
102
112
|
coordinates?: PageClickCoordinates;
|
|
103
113
|
actionableAncestorCandidates?: PageClickAncestorCandidate[];
|
|
104
114
|
controlState?: PageClickControlState;
|
|
115
|
+
selectionState?: PageClickSelectionState;
|
|
105
116
|
dispatchMode?: PageClickDispatchMode;
|
|
106
117
|
pointerType?: PageClickPointerType;
|
|
107
118
|
fallbackReason?: PageClickFallbackReason;
|
|
@@ -127,6 +138,7 @@ export interface PageClickDispatchAttemptEvidence {
|
|
|
127
138
|
coordinates?: PageClickCoordinates;
|
|
128
139
|
actionableAncestorCandidates?: Array<Pick<PageClickAncestorCandidate, 'selector' | 'distance' | 'reason'>>;
|
|
129
140
|
controlState?: PageClickControlState;
|
|
141
|
+
selectionState?: PageClickSelectionState;
|
|
130
142
|
dispatchMode?: PageClickDispatchMode;
|
|
131
143
|
pointerType?: PageClickPointerType;
|
|
132
144
|
}
|
|
@@ -161,6 +173,7 @@ export declare function clickElementViaJS(this: any, expectedText?: string): {
|
|
|
161
173
|
text: string;
|
|
162
174
|
};
|
|
163
175
|
controlState?: PageClickControlState;
|
|
176
|
+
selectionState?: PageClickSelectionState;
|
|
164
177
|
error?: string;
|
|
165
178
|
} | null;
|
|
166
179
|
/**
|
|
@@ -179,6 +192,7 @@ export declare function clickElementBySelector(selector: string, invalidSelector
|
|
|
179
192
|
strategy?: PageClickTargetStrategy;
|
|
180
193
|
distance?: number;
|
|
181
194
|
controlState?: PageClickControlState;
|
|
195
|
+
selectionState?: PageClickSelectionState;
|
|
182
196
|
error?: string;
|
|
183
197
|
};
|
|
184
198
|
/**
|
package/dist/domClicking.js
CHANGED
|
@@ -12,6 +12,12 @@ export const PAGE_CLICK_INPUT_TIMEOUT_MS = 10_000;
|
|
|
12
12
|
export const PAGE_CLICK_HIDDEN_TOUCH_ERROR = 'page_click: clickMode "touch" requires a visible page, but the target is hidden or in a background tab. ' +
|
|
13
13
|
'Use clickMode "auto" for DOM fallback, or call tab_open for the same URL with reuse "require" and active true. ' +
|
|
14
14
|
'Extension activation uses background focus emulation while Do not disturb is enabled; check activation.visibilityState before retrying trusted input; use "mouse" only when touch semantics are not required.';
|
|
15
|
+
/** Native options need value selection; clicking their select only opens the menu. */
|
|
16
|
+
export function isStateAwareClickTarget(target) {
|
|
17
|
+
return (target.tag === 'option' ||
|
|
18
|
+
target.sourceTarget?.tag === 'option' ||
|
|
19
|
+
/^(checkbox|radio)$/i.test(target.inputType ?? ''));
|
|
20
|
+
}
|
|
15
21
|
/** Resolve the click path from the requested mode, page visibility, and device emulation state. */
|
|
16
22
|
export function resolvePageClickDispatchPolicy(clickMode, visibilityState, touchEmulationEnabled) {
|
|
17
23
|
if (clickMode !== undefined && clickMode !== 'auto') {
|
|
@@ -62,6 +68,7 @@ export function createPageClickResult(requestedTarget, dispatchTarget) {
|
|
|
62
68
|
? { actionableAncestorCandidates: evidence.actionableAncestorCandidates }
|
|
63
69
|
: {}),
|
|
64
70
|
...(evidence.controlState ? { controlState: evidence.controlState } : {}),
|
|
71
|
+
...(evidence.selectionState ? { selectionState: evidence.selectionState } : {}),
|
|
65
72
|
...(evidence.dispatchMode ? { dispatchMode: evidence.dispatchMode } : {}),
|
|
66
73
|
...(evidence.pointerType ? { pointerType: evidence.pointerType } : {}),
|
|
67
74
|
};
|
|
@@ -90,6 +97,7 @@ export function describePageClickDispatchTarget(dispatchTarget) {
|
|
|
90
97
|
? { actionableAncestorCandidates: dispatchTarget.actionableAncestorCandidates }
|
|
91
98
|
: {}),
|
|
92
99
|
...(dispatchTarget.controlState ? { controlState: dispatchTarget.controlState } : {}),
|
|
100
|
+
...(dispatchTarget.selectionState ? { selectionState: dispatchTarget.selectionState } : {}),
|
|
93
101
|
...(dispatchTarget.dispatchMode ? { dispatchMode: dispatchTarget.dispatchMode } : {}),
|
|
94
102
|
...(dispatchTarget.pointerType ? { pointerType: dispatchTarget.pointerType } : {}),
|
|
95
103
|
};
|
|
@@ -126,6 +134,7 @@ export function describePageClickDispatchAttempt(dispatchTarget) {
|
|
|
126
134
|
}
|
|
127
135
|
: {}),
|
|
128
136
|
...(evidence.controlState ? { controlState: evidence.controlState } : {}),
|
|
137
|
+
...(evidence.selectionState ? { selectionState: evidence.selectionState } : {}),
|
|
129
138
|
...(evidence.dispatchMode ? { dispatchMode: evidence.dispatchMode } : {}),
|
|
130
139
|
...(evidence.pointerType ? { pointerType: evidence.pointerType } : {}),
|
|
131
140
|
};
|
|
@@ -149,11 +158,24 @@ export function clickElementViaJS(expectedText) {
|
|
|
149
158
|
if (el.tagName === 'OPTION') {
|
|
150
159
|
const select = el.closest('select');
|
|
151
160
|
if (select) {
|
|
152
|
-
|
|
161
|
+
if (select.matches(':disabled') || el.disabled || el.closest('optgroup')?.disabled) {
|
|
162
|
+
return {
|
|
163
|
+
tag: 'option',
|
|
164
|
+
text: sourceTarget.text,
|
|
165
|
+
cx: 0,
|
|
166
|
+
cy: 0,
|
|
167
|
+
strategy: 'self',
|
|
168
|
+
distance: 0,
|
|
169
|
+
sourceTarget,
|
|
170
|
+
error: 'Cannot select a disabled option',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const valueBefore = select.value;
|
|
174
|
+
const desc = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'selectedIndex');
|
|
153
175
|
if (desc && desc.set)
|
|
154
|
-
desc.set.call(select, el.
|
|
176
|
+
desc.set.call(select, el.index);
|
|
155
177
|
else
|
|
156
|
-
select.
|
|
178
|
+
select.selectedIndex = el.index;
|
|
157
179
|
select.dispatchEvent(new Event('input', { bubbles: true }));
|
|
158
180
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
159
181
|
const rect = select.getBoundingClientRect();
|
|
@@ -165,6 +187,7 @@ export function clickElementViaJS(expectedText) {
|
|
|
165
187
|
strategy: 'self',
|
|
166
188
|
distance: 0,
|
|
167
189
|
sourceTarget,
|
|
190
|
+
selectionState: { valueBefore, valueAfter: select.value, selectedAfter: el.selected },
|
|
168
191
|
};
|
|
169
192
|
}
|
|
170
193
|
}
|
|
@@ -365,9 +388,22 @@ export function clickElementViaJS(expectedText) {
|
|
|
365
388
|
let hitTarget = null;
|
|
366
389
|
try {
|
|
367
390
|
hitTarget = document.elementFromPoint(cx, cy);
|
|
391
|
+
const visited = new Set();
|
|
392
|
+
while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
|
|
393
|
+
visited.add(hitTarget);
|
|
394
|
+
const inner = hitTarget.shadowRoot.elementFromPoint(cx, cy);
|
|
395
|
+
if (!inner || inner === hitTarget)
|
|
396
|
+
break;
|
|
397
|
+
hitTarget = inner;
|
|
398
|
+
}
|
|
368
399
|
}
|
|
369
400
|
catch { }
|
|
401
|
+
let composedHit = hitTarget;
|
|
402
|
+
while (composedHit && composedHit !== target) {
|
|
403
|
+
composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
|
|
404
|
+
}
|
|
370
405
|
if (hitTarget &&
|
|
406
|
+
!composedHit &&
|
|
371
407
|
hitTarget !== target &&
|
|
372
408
|
!target.contains?.(hitTarget) &&
|
|
373
409
|
!hitTarget.contains?.(target)) {
|
|
@@ -511,11 +547,23 @@ export function clickElementBySelector(selector, invalidSelectorHint) {
|
|
|
511
547
|
if (element.tagName === 'OPTION') {
|
|
512
548
|
const select = element.closest('select');
|
|
513
549
|
if (select) {
|
|
514
|
-
const
|
|
550
|
+
const option = element;
|
|
551
|
+
if (select.matches(':disabled') || option.disabled || option.closest('optgroup')?.disabled) {
|
|
552
|
+
return {
|
|
553
|
+
clicked: false,
|
|
554
|
+
tag: 'option',
|
|
555
|
+
text: (option.textContent || '').trim().slice(0, 100),
|
|
556
|
+
cx: 0,
|
|
557
|
+
cy: 0,
|
|
558
|
+
error: 'Cannot select a disabled option',
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const valueBefore = select.value;
|
|
562
|
+
const desc = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'selectedIndex');
|
|
515
563
|
if (desc && desc.set)
|
|
516
|
-
desc.set.call(select,
|
|
564
|
+
desc.set.call(select, option.index);
|
|
517
565
|
else
|
|
518
|
-
select.
|
|
566
|
+
select.selectedIndex = option.index;
|
|
519
567
|
select.dispatchEvent(new Event('input', { bubbles: true }));
|
|
520
568
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
521
569
|
const rect = select.getBoundingClientRect();
|
|
@@ -527,6 +575,7 @@ export function clickElementBySelector(selector, invalidSelectorHint) {
|
|
|
527
575
|
cy: rect.y + rect.height / 2,
|
|
528
576
|
strategy: 'self',
|
|
529
577
|
distance: 0,
|
|
578
|
+
selectionState: { valueBefore, valueAfter: select.value, selectedAfter: option.selected },
|
|
530
579
|
};
|
|
531
580
|
}
|
|
532
581
|
}
|
|
@@ -680,9 +729,22 @@ export function clickElementBySelector(selector, invalidSelectorHint) {
|
|
|
680
729
|
let hitTarget = null;
|
|
681
730
|
try {
|
|
682
731
|
hitTarget = document.elementFromPoint(cx, cy);
|
|
732
|
+
const visited = new Set();
|
|
733
|
+
while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
|
|
734
|
+
visited.add(hitTarget);
|
|
735
|
+
const inner = hitTarget.shadowRoot.elementFromPoint(cx, cy);
|
|
736
|
+
if (!inner || inner === hitTarget)
|
|
737
|
+
break;
|
|
738
|
+
hitTarget = inner;
|
|
739
|
+
}
|
|
683
740
|
}
|
|
684
741
|
catch { }
|
|
742
|
+
let composedHit = hitTarget;
|
|
743
|
+
while (composedHit && composedHit !== target) {
|
|
744
|
+
composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
|
|
745
|
+
}
|
|
685
746
|
if (hitTarget &&
|
|
747
|
+
!composedHit &&
|
|
686
748
|
hitTarget !== target &&
|
|
687
749
|
!target.contains(hitTarget) &&
|
|
688
750
|
!hitTarget.contains(target)) {
|
|
@@ -1001,9 +1063,22 @@ export function getClickCoordsFromSelector(selector, invalidSelectorHint) {
|
|
|
1001
1063
|
let hitTarget = null;
|
|
1002
1064
|
try {
|
|
1003
1065
|
hitTarget = document.elementFromPoint(x, y);
|
|
1066
|
+
const visited = new Set();
|
|
1067
|
+
while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
|
|
1068
|
+
visited.add(hitTarget);
|
|
1069
|
+
const inner = hitTarget.shadowRoot.elementFromPoint(x, y);
|
|
1070
|
+
if (!inner || inner === hitTarget)
|
|
1071
|
+
break;
|
|
1072
|
+
hitTarget = inner;
|
|
1073
|
+
}
|
|
1004
1074
|
}
|
|
1005
1075
|
catch { }
|
|
1076
|
+
let composedHit = hitTarget;
|
|
1077
|
+
while (composedHit && composedHit !== target) {
|
|
1078
|
+
composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
|
|
1079
|
+
}
|
|
1006
1080
|
if (hitTarget &&
|
|
1081
|
+
!composedHit &&
|
|
1007
1082
|
hitTarget !== target &&
|
|
1008
1083
|
!target.contains(hitTarget) &&
|
|
1009
1084
|
!hitTarget.contains(target)) {
|
|
@@ -1295,9 +1370,22 @@ export function getClickCoordsFromNode(expectedText) {
|
|
|
1295
1370
|
let hitTarget = null;
|
|
1296
1371
|
try {
|
|
1297
1372
|
hitTarget = document.elementFromPoint(x, y);
|
|
1373
|
+
const visited = new Set();
|
|
1374
|
+
while (hitTarget?.shadowRoot?.elementFromPoint && !visited.has(hitTarget)) {
|
|
1375
|
+
visited.add(hitTarget);
|
|
1376
|
+
const inner = hitTarget.shadowRoot.elementFromPoint(x, y);
|
|
1377
|
+
if (!inner || inner === hitTarget)
|
|
1378
|
+
break;
|
|
1379
|
+
hitTarget = inner;
|
|
1380
|
+
}
|
|
1298
1381
|
}
|
|
1299
1382
|
catch { }
|
|
1383
|
+
let composedHit = hitTarget;
|
|
1384
|
+
while (composedHit && composedHit !== target) {
|
|
1385
|
+
composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
|
|
1386
|
+
}
|
|
1300
1387
|
if (hitTarget &&
|
|
1388
|
+
!composedHit &&
|
|
1301
1389
|
hitTarget !== target &&
|
|
1302
1390
|
!target.contains?.(hitTarget) &&
|
|
1303
1391
|
!hitTarget.contains?.(target)) {
|
|
@@ -1353,12 +1441,12 @@ export function showClickIndicator(px, py, mode) {
|
|
|
1353
1441
|
ring +
|
|
1354
1442
|
';' +
|
|
1355
1443
|
'pointer-events:none; z-index:2147483647;' +
|
|
1356
|
-
'animation:
|
|
1357
|
-
if (!document.getElementById('
|
|
1444
|
+
'animation:__mearl_click_fade 600ms ease-out forwards;';
|
|
1445
|
+
if (!document.getElementById('__mearl_click_style')) {
|
|
1358
1446
|
const style = document.createElement('style');
|
|
1359
|
-
style.id = '
|
|
1447
|
+
style.id = '__mearl_click_style';
|
|
1360
1448
|
style.textContent =
|
|
1361
|
-
'@keyframes
|
|
1449
|
+
'@keyframes __mearl_click_fade {' +
|
|
1362
1450
|
'0% { opacity:1; transform:scale(1); }' +
|
|
1363
1451
|
'50% { opacity:0.8; transform:scale(1.8); }' +
|
|
1364
1452
|
'100% { opacity:0; transform:scale(2.2); }' +
|
package/dist/domInspection.js
CHANGED
|
@@ -211,9 +211,21 @@ export function inspectElementsInPage(selector, filter, styles, maxResults, inva
|
|
|
211
211
|
let hit = null;
|
|
212
212
|
try {
|
|
213
213
|
hit = visible ? (documentRef.elementFromPoint?.(x, y) ?? null) : null;
|
|
214
|
+
const visited = new Set();
|
|
215
|
+
while (hit?.shadowRoot?.elementFromPoint && !visited.has(hit)) {
|
|
216
|
+
visited.add(hit);
|
|
217
|
+
const inner = hit.shadowRoot.elementFromPoint(x, y);
|
|
218
|
+
if (!inner || inner === hit)
|
|
219
|
+
break;
|
|
220
|
+
hit = inner;
|
|
221
|
+
}
|
|
214
222
|
}
|
|
215
223
|
catch { }
|
|
216
|
-
|
|
224
|
+
let composedHit = hit;
|
|
225
|
+
while (composedHit && composedHit !== element) {
|
|
226
|
+
composedHit = composedHit.parentElement || composedHit.getRootNode?.().host;
|
|
227
|
+
}
|
|
228
|
+
const topmost = !!hit && (!!composedHit || element.contains?.(hit));
|
|
217
229
|
const target = hit
|
|
218
230
|
? {
|
|
219
231
|
selector: selectorFor(hit),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BrowserAccessResult, BrowserActionMap, BrowserJsonValue, BrowserProviderListResult, BrowserProviderLaunchForm, ControlSessionInfo, CookieCopyScope, ManagedBrowserInfo, BrowserReleaseParams, BrowserReleaseResult, PageDownloadParams } from './browserActionProtocol.js';
|
|
2
2
|
import { type AgentServiceActionMap } from './agentServiceProtocol.js';
|
|
3
|
+
import type { BrowserEnhancementRequest, BrowserEnhancementResponse } from './browserEnhancement.js';
|
|
3
4
|
import { type ToolboxServiceActionMap, type ToolboxTaskRequest, type ToolboxTaskResponse } from './toolbox.js';
|
|
4
5
|
export declare const EXTENSION_RPC_VERSION = 1;
|
|
5
6
|
export type OperationRequest<M> = {
|
|
@@ -171,6 +172,10 @@ export type ExtensionUiServiceRequest<A extends ExtensionUiServiceAction> = Exte
|
|
|
171
172
|
export type ExtensionUiServiceResponse<A extends ExtensionUiServiceAction, R> = A extends BrowserManagementAction ? BrowserManagementResponse<A, R> : A extends 'toolbox_task' ? R extends ToolboxTaskRequest ? ToolboxTaskResponse<R> : never : ExtensionUiServiceActionMap[A]['response'];
|
|
172
173
|
export declare function isExtensionUiServiceAction(value: unknown): value is ExtensionUiServiceAction;
|
|
173
174
|
export interface HostToExtensionActionMap {
|
|
175
|
+
__browser_enhancement: {
|
|
176
|
+
request: BrowserEnhancementRequest;
|
|
177
|
+
response: BrowserEnhancementResponse;
|
|
178
|
+
};
|
|
174
179
|
__preset_installation: {
|
|
175
180
|
request: Record<string, unknown>;
|
|
176
181
|
response: unknown;
|
|
@@ -206,6 +211,7 @@ export interface HostToExtensionActionMap {
|
|
|
206
211
|
__control_cleanup: {
|
|
207
212
|
request: BrowserReleaseParams & {
|
|
208
213
|
tabIds: number[];
|
|
214
|
+
returningToAgent?: boolean;
|
|
209
215
|
};
|
|
210
216
|
response: BrowserReleaseResult;
|
|
211
217
|
};
|
|
@@ -249,6 +255,7 @@ export interface HostToExtensionActionMap {
|
|
|
249
255
|
};
|
|
250
256
|
}
|
|
251
257
|
export declare const HOST_TO_EXTENSION_ACTIONS: {
|
|
258
|
+
readonly __browser_enhancement: true;
|
|
252
259
|
readonly __preset_installation: true;
|
|
253
260
|
readonly __preset_browser: true;
|
|
254
261
|
readonly __export_cookies: true;
|