@mearl/browser-core 3.0.0-beta.5 → 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 +1 -1
- package/dist/agentChat.d.ts +2 -0
- package/dist/agentSkills.d.ts +1 -1
- package/dist/app-profile-runtime/types.d.ts +4 -8
- package/dist/appProfileDefinition.d.ts +4 -7
- package/dist/appProfileDefinition.js +11 -49
- package/dist/axSnapshot.js +1 -1
- package/dist/boundedRequestBuffer.d.ts +4 -4
- package/dist/browserActionProtocol.d.ts +46 -29
- package/dist/browserActionProtocol.js +6 -6
- package/dist/browserCapabilities.d.ts +2 -0
- package/dist/browserCapabilities.js +19 -5
- 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/deviceEnvironmentObservation.d.ts +48 -0
- package/dist/deviceEnvironmentObservation.js +77 -0
- package/dist/domClicking.d.ts +14 -0
- package/dist/domClicking.js +46 -10
- package/dist/extensionServiceProtocol.d.ts +6 -0
- package/dist/extensionServiceProtocol.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -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 +3 -3
- package/dist/requestMetadata.d.ts +9 -0
- package/dist/requestMetadata.js +8 -0
- package/dist/requestSerializer.d.ts +2 -2
- package/dist/requestStreamCapture.d.ts +5 -3
- package/dist/requestStreamCapture.js +2 -0
- package/dist/types.d.ts +4 -2
- package/package.json +2 -2
package/dist/pageAct.js
CHANGED
|
@@ -209,7 +209,21 @@ export async function runPageAct(input, dependencies) {
|
|
|
209
209
|
throw new Error('actionData must be an object');
|
|
210
210
|
}
|
|
211
211
|
const typedAction = action;
|
|
212
|
-
const
|
|
212
|
+
const dialogs = dependencies.dialogs;
|
|
213
|
+
const guarded = (work) => dialogs ? dialogs.command(work) : work();
|
|
214
|
+
const dialogResult = (failure, data) => ({
|
|
215
|
+
action: { name: typedAction, success: !failure, ...(data !== undefined ? { data } : {}), ...failure },
|
|
216
|
+
...(dialogs?.info() ? { dialog: dialogs.info() } : {}),
|
|
217
|
+
});
|
|
218
|
+
let checkpoint;
|
|
219
|
+
try {
|
|
220
|
+
checkpoint = await guarded(() => dependencies.startObservation());
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (!dialogs?.pending)
|
|
224
|
+
throw error;
|
|
225
|
+
return dialogResult({ ...describePageActionFailure(error), stage: 'precondition' });
|
|
226
|
+
}
|
|
213
227
|
const navigationOptions = resolveNavigationWaitOptions(observe);
|
|
214
228
|
const sourceTabId = typeof actionData.tabId === 'number' && Number.isInteger(actionData.tabId)
|
|
215
229
|
? actionData.tabId
|
|
@@ -232,6 +246,14 @@ export async function runPageAct(input, dependencies) {
|
|
|
232
246
|
catch (error) {
|
|
233
247
|
actionFailure = describePageActionFailure(error);
|
|
234
248
|
}
|
|
249
|
+
const dispose = () => Promise.allSettled([
|
|
250
|
+
Promise.resolve().then(() => navigationObserver?.dispose()),
|
|
251
|
+
Promise.resolve().then(() => dependencies.disposeObservation?.()),
|
|
252
|
+
]);
|
|
253
|
+
if (dialogs?.pending) {
|
|
254
|
+
await dispose();
|
|
255
|
+
return dialogResult(actionFailure, actionResult);
|
|
256
|
+
}
|
|
235
257
|
if (actionFailure?.stage === 'precondition') {
|
|
236
258
|
await Promise.allSettled([
|
|
237
259
|
Promise.resolve().then(() => navigationObserver?.dispose()),
|
|
@@ -248,11 +270,15 @@ export async function runPageAct(input, dependencies) {
|
|
|
248
270
|
}
|
|
249
271
|
let observation;
|
|
250
272
|
try {
|
|
251
|
-
observation = await dependencies.finishObservation(checkpoint, observe);
|
|
273
|
+
observation = await guarded(() => dependencies.finishObservation(checkpoint, observe));
|
|
252
274
|
}
|
|
253
275
|
catch (error) {
|
|
254
276
|
observation = unavailableObservation(checkpoint, error);
|
|
255
277
|
}
|
|
278
|
+
if (dialogs?.pending) {
|
|
279
|
+
await dispose();
|
|
280
|
+
return dialogResult(actionFailure, actionResult);
|
|
281
|
+
}
|
|
256
282
|
const navigationInterruptedAction = isNavigationInterruptedActionFailure(actionFailure);
|
|
257
283
|
if (navigationInterruptedAction && observation.mode === 'navigation') {
|
|
258
284
|
actionFailure = undefined;
|
|
@@ -277,22 +303,22 @@ export async function runPageAct(input, dependencies) {
|
|
|
277
303
|
(navigationObserver.hasStarted() ||
|
|
278
304
|
shouldWaitForPotentialNavigation(typedAction, observation))) {
|
|
279
305
|
const didNavigate = navigationObserver.hasStarted() ||
|
|
280
|
-
(await navigationObserver.waitForStart(navigationOptions.graceMs));
|
|
306
|
+
(await guarded(() => navigationObserver.waitForStart(navigationOptions.graceMs)));
|
|
281
307
|
if (didNavigate) {
|
|
282
|
-
observation = await navigationObserver.waitForResult(navigationOptions.timeoutMs);
|
|
308
|
+
observation = await guarded(() => navigationObserver.waitForResult(navigationOptions.timeoutMs));
|
|
283
309
|
}
|
|
284
310
|
}
|
|
285
311
|
if (navigationObserver &&
|
|
286
312
|
(actionFailure === undefined || navigationInterruptedAction) &&
|
|
287
313
|
observation.mode !== 'navigation' &&
|
|
288
314
|
navigationObserver.hasNetworkActivity()) {
|
|
289
|
-
const network = await navigationObserver.waitForNetworkIdle(navigationOptions.timeoutMs);
|
|
315
|
+
const network = await guarded(() => navigationObserver.waitForNetworkIdle(navigationOptions.timeoutMs));
|
|
290
316
|
if (navigationObserver.hasStarted()) {
|
|
291
|
-
observation = await navigationObserver.waitForResult(navigationOptions.timeoutMs);
|
|
317
|
+
observation = await guarded(() => navigationObserver.waitForResult(navigationOptions.timeoutMs));
|
|
292
318
|
}
|
|
293
319
|
else {
|
|
294
320
|
try {
|
|
295
|
-
observation = await dependencies.finishObservation(checkpoint, observe);
|
|
321
|
+
observation = await guarded(() => dependencies.finishObservation(checkpoint, observe));
|
|
296
322
|
}
|
|
297
323
|
catch (error) {
|
|
298
324
|
observation = unavailableObservation(checkpoint, error);
|
|
@@ -309,7 +335,7 @@ export async function runPageAct(input, dependencies) {
|
|
|
309
335
|
}
|
|
310
336
|
if (dependencies.finalizeObservation) {
|
|
311
337
|
try {
|
|
312
|
-
observation = await dependencies.finalizeObservation(observation);
|
|
338
|
+
observation = await guarded(() => dependencies.finalizeObservation(observation));
|
|
313
339
|
}
|
|
314
340
|
catch {
|
|
315
341
|
// Ref enrichment and other response finalizers are best-effort and must
|
|
@@ -317,19 +343,25 @@ export async function runPageAct(input, dependencies) {
|
|
|
317
343
|
}
|
|
318
344
|
}
|
|
319
345
|
if (navigationObserver) {
|
|
320
|
-
const openedTabs = await navigationObserver
|
|
321
|
-
.getOpenedTabs(navigationOptions.timeoutMs)
|
|
346
|
+
const openedTabs = await guarded(() => navigationObserver.getOpenedTabs(navigationOptions.timeoutMs))
|
|
322
347
|
.catch(() => []);
|
|
323
348
|
if (openedTabs.length > 0)
|
|
324
349
|
observation = { ...observation, openedTabs };
|
|
325
350
|
}
|
|
326
351
|
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (!dialogs?.pending)
|
|
354
|
+
throw error;
|
|
355
|
+
return dialogResult(actionFailure, actionResult);
|
|
356
|
+
}
|
|
327
357
|
finally {
|
|
328
358
|
await Promise.allSettled([
|
|
329
359
|
Promise.resolve().then(() => navigationObserver?.dispose()),
|
|
330
360
|
Promise.resolve().then(() => dependencies.disposeObservation?.()),
|
|
331
361
|
]);
|
|
332
362
|
}
|
|
363
|
+
if (dialogs?.pending)
|
|
364
|
+
return dialogResult(actionFailure, actionResult);
|
|
333
365
|
return {
|
|
334
366
|
action: {
|
|
335
367
|
name: typedAction,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface PageDialogInfo {
|
|
2
|
+
id: string;
|
|
3
|
+
type: 'alert' | 'confirm' | 'prompt' | 'beforeunload';
|
|
4
|
+
message: string;
|
|
5
|
+
defaultPrompt?: string;
|
|
6
|
+
truncated?: true;
|
|
7
|
+
}
|
|
8
|
+
export declare class PageDialogInterruptedError extends Error {
|
|
9
|
+
readonly pageActionFailureStage: 'precondition' | 'dispatch-uncertain';
|
|
10
|
+
readonly code = "DIALOG_PENDING";
|
|
11
|
+
readonly dialog?: PageDialogInfo;
|
|
12
|
+
readonly details: {
|
|
13
|
+
dialog?: PageDialogInfo;
|
|
14
|
+
};
|
|
15
|
+
constructor(dialog?: PageDialogInfo, pageActionFailureStage?: 'precondition' | 'dispatch-uncertain');
|
|
16
|
+
}
|
|
17
|
+
/** Host-side state only: no page injection, implicit acceptance, or action replay. */
|
|
18
|
+
export declare class PageDialogState {
|
|
19
|
+
private epoch;
|
|
20
|
+
private readonly prefix;
|
|
21
|
+
private current?;
|
|
22
|
+
private defaultPromptTruncated;
|
|
23
|
+
private readonly work;
|
|
24
|
+
private readonly listeners;
|
|
25
|
+
private handling?;
|
|
26
|
+
onIdle?: () => void;
|
|
27
|
+
get pending(): boolean;
|
|
28
|
+
get busy(): boolean;
|
|
29
|
+
info(maxChars?: number): PageDialogInfo | undefined;
|
|
30
|
+
opened(event: {
|
|
31
|
+
type?: unknown;
|
|
32
|
+
message?: unknown;
|
|
33
|
+
defaultPrompt?: unknown;
|
|
34
|
+
}): void;
|
|
35
|
+
closed(): void;
|
|
36
|
+
invalidate(): void;
|
|
37
|
+
assertCanDispatch(): void;
|
|
38
|
+
/** Register the actual handler, including its finally; never retain a raw browser Promise here. */
|
|
39
|
+
execute<T>(work: () => Promise<T>, interrupt?: boolean): Promise<T>;
|
|
40
|
+
/** Every real eval/input dispatch must pass here, so a late ack cannot resume its handler. */
|
|
41
|
+
command<T>(work: () => Promise<T>): Promise<T>;
|
|
42
|
+
private race;
|
|
43
|
+
press(input: {
|
|
44
|
+
dialogId?: string;
|
|
45
|
+
key: string;
|
|
46
|
+
promptText?: string;
|
|
47
|
+
selector?: string;
|
|
48
|
+
modifiers?: unknown[];
|
|
49
|
+
pressMode?: string;
|
|
50
|
+
}, send: (params: {
|
|
51
|
+
accept: boolean;
|
|
52
|
+
promptText?: string;
|
|
53
|
+
}) => Promise<unknown>): Promise<{
|
|
54
|
+
pressed: true;
|
|
55
|
+
key: string;
|
|
56
|
+
dispatchMode: 'native-dialog';
|
|
57
|
+
}>;
|
|
58
|
+
private notifyIdle;
|
|
59
|
+
}
|
|
60
|
+
export declare function pageDialogSnapshot(dialogs: PageDialogState, mode?: string, maxChars?: number): {
|
|
61
|
+
snapshot: string;
|
|
62
|
+
mode: string;
|
|
63
|
+
elementCount: number;
|
|
64
|
+
outputNodeCount: number;
|
|
65
|
+
dialog: PageDialogInfo;
|
|
66
|
+
} | undefined;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { PageActionDispatchUncertainError } from './pageAct.js';
|
|
2
|
+
export class PageDialogInterruptedError extends Error {
|
|
3
|
+
pageActionFailureStage;
|
|
4
|
+
code = 'DIALOG_PENDING';
|
|
5
|
+
dialog;
|
|
6
|
+
details;
|
|
7
|
+
constructor(dialog, pageActionFailureStage = 'dispatch-uncertain') {
|
|
8
|
+
super(dialog
|
|
9
|
+
? `DIALOG_PENDING: native ${dialog.type} dialog ${dialog.id}. Read it with page_snapshot, then use page_press with dialogId and Enter/Escape.`
|
|
10
|
+
: 'DIALOG_PENDING: a previous page operation was interrupted; observe the page before continuing.');
|
|
11
|
+
this.pageActionFailureStage = pageActionFailureStage;
|
|
12
|
+
this.name = 'PageDialogInterruptedError';
|
|
13
|
+
this.dialog = dialog;
|
|
14
|
+
this.details = { ...(dialog ? { dialog } : {}) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Host-side state only: no page injection, implicit acceptance, or action replay. */
|
|
18
|
+
export class PageDialogState {
|
|
19
|
+
epoch = 0;
|
|
20
|
+
prefix = crypto.randomUUID();
|
|
21
|
+
current;
|
|
22
|
+
defaultPromptTruncated = false;
|
|
23
|
+
work = new Set();
|
|
24
|
+
listeners = new Set();
|
|
25
|
+
handling;
|
|
26
|
+
onIdle;
|
|
27
|
+
get pending() { return this.current !== undefined; }
|
|
28
|
+
get busy() { return this.work.size > 0 || this.handling !== undefined; }
|
|
29
|
+
info(maxChars = 240) {
|
|
30
|
+
if (!this.current)
|
|
31
|
+
return undefined;
|
|
32
|
+
const limit = Number.isFinite(maxChars) ? Math.max(0, Math.min(maxChars, 100000)) : 240;
|
|
33
|
+
const { message, defaultPrompt, ...rest } = this.current;
|
|
34
|
+
return {
|
|
35
|
+
...rest,
|
|
36
|
+
message: message.slice(0, limit),
|
|
37
|
+
...(defaultPrompt ? { defaultPrompt: defaultPrompt.slice(0, limit) } : {}),
|
|
38
|
+
...(message.length > limit || (defaultPrompt?.length ?? 0) > limit ? { truncated: true } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
opened(event) {
|
|
42
|
+
if (!['alert', 'confirm', 'prompt', 'beforeunload'].includes(String(event.type)))
|
|
43
|
+
return;
|
|
44
|
+
this.defaultPromptTruncated = typeof event.defaultPrompt === 'string' && event.defaultPrompt.length > 65536;
|
|
45
|
+
this.current = {
|
|
46
|
+
id: `${this.prefix}:${++this.epoch}`,
|
|
47
|
+
type: event.type,
|
|
48
|
+
message: typeof event.message === 'string' ? event.message.slice(0, 65536) : '',
|
|
49
|
+
...(typeof event.defaultPrompt === 'string' && event.defaultPrompt
|
|
50
|
+
? { defaultPrompt: event.defaultPrompt.slice(0, 65536) } : {}),
|
|
51
|
+
...((typeof event.message === 'string' && event.message.length > 65536) ||
|
|
52
|
+
(typeof event.defaultPrompt === 'string' && event.defaultPrompt.length > 65536) ? { truncated: true } : {}),
|
|
53
|
+
};
|
|
54
|
+
for (const listener of this.listeners)
|
|
55
|
+
listener();
|
|
56
|
+
}
|
|
57
|
+
closed() {
|
|
58
|
+
this.current = undefined;
|
|
59
|
+
this.notifyIdle();
|
|
60
|
+
}
|
|
61
|
+
invalidate() {
|
|
62
|
+
this.epoch += 1;
|
|
63
|
+
this.current = undefined;
|
|
64
|
+
for (const listener of this.listeners)
|
|
65
|
+
listener();
|
|
66
|
+
this.notifyIdle();
|
|
67
|
+
}
|
|
68
|
+
assertCanDispatch() {
|
|
69
|
+
if (this.pending || [...this.work].some(entry => entry.epoch !== this.epoch)) {
|
|
70
|
+
throw new PageDialogInterruptedError(this.info(), 'precondition');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Register the actual handler, including its finally; never retain a raw browser Promise here. */
|
|
74
|
+
execute(work, interrupt = true) {
|
|
75
|
+
this.assertCanDispatch();
|
|
76
|
+
const entry = { epoch: this.epoch };
|
|
77
|
+
this.work.add(entry);
|
|
78
|
+
let started = false;
|
|
79
|
+
const release = () => {
|
|
80
|
+
this.work.delete(entry);
|
|
81
|
+
this.notifyIdle();
|
|
82
|
+
};
|
|
83
|
+
const start = () => {
|
|
84
|
+
started = true;
|
|
85
|
+
return Promise.resolve().then(() => {
|
|
86
|
+
this.assertCanDispatch();
|
|
87
|
+
return work();
|
|
88
|
+
}).finally(release);
|
|
89
|
+
};
|
|
90
|
+
return (interrupt ? this.race(start) : start()).finally(() => {
|
|
91
|
+
if (!started)
|
|
92
|
+
release();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Every real eval/input dispatch must pass here, so a late ack cannot resume its handler. */
|
|
96
|
+
command(work) {
|
|
97
|
+
this.assertCanDispatch();
|
|
98
|
+
return this.race(work);
|
|
99
|
+
}
|
|
100
|
+
race(work) {
|
|
101
|
+
if (this.pending)
|
|
102
|
+
return Promise.reject(new PageDialogInterruptedError(this.info()));
|
|
103
|
+
const epoch = this.epoch;
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const changed = () => {
|
|
106
|
+
if (epoch === this.epoch)
|
|
107
|
+
return;
|
|
108
|
+
this.listeners.delete(changed);
|
|
109
|
+
reject(new PageDialogInterruptedError(this.info()));
|
|
110
|
+
};
|
|
111
|
+
this.listeners.add(changed);
|
|
112
|
+
let result;
|
|
113
|
+
try {
|
|
114
|
+
result = work();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.listeners.delete(changed);
|
|
118
|
+
reject(error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
Promise.resolve(result).then(value => {
|
|
122
|
+
this.listeners.delete(changed);
|
|
123
|
+
resolve(value);
|
|
124
|
+
}, error => {
|
|
125
|
+
this.listeners.delete(changed);
|
|
126
|
+
reject(error);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async press(input, send) {
|
|
131
|
+
const dialog = this.current;
|
|
132
|
+
if (!input.dialogId || dialog?.id !== input.dialogId)
|
|
133
|
+
throw new Error('page_press: dialogId is stale or no native dialog is open. Run page_snapshot.');
|
|
134
|
+
if (this.handling)
|
|
135
|
+
throw new Error('page_press: this native dialog is already being handled.');
|
|
136
|
+
if (!['Enter', 'Escape'].includes(input.key) || input.selector !== undefined || input.modifiers?.length || (input.pressMode && input.pressMode !== 'auto')) {
|
|
137
|
+
throw new Error('page_press: dialogId requires Enter/Escape without selector, modifiers, or an explicit pressMode.');
|
|
138
|
+
}
|
|
139
|
+
if (input.promptText !== undefined && (typeof input.promptText !== 'string' || dialog.type !== 'prompt' || input.key !== 'Enter')) {
|
|
140
|
+
throw new Error('page_press: promptText is only valid when accepting a native prompt.');
|
|
141
|
+
}
|
|
142
|
+
const acceptsPrompt = dialog.type === 'prompt' && input.key === 'Enter';
|
|
143
|
+
if (acceptsPrompt && input.promptText === undefined && this.defaultPromptTruncated) {
|
|
144
|
+
throw new Error('page_press: the native prompt default was truncated. Specify promptText explicitly.');
|
|
145
|
+
}
|
|
146
|
+
this.handling = dialog.id;
|
|
147
|
+
try {
|
|
148
|
+
// No await between the last ID check and send. CDP itself has no atomic dialog-ID CAS.
|
|
149
|
+
await send({ accept: input.key === 'Enter', ...(acceptsPrompt ? { promptText: input.promptText ?? dialog.defaultPrompt ?? '' } : {}) });
|
|
150
|
+
return { pressed: true, key: input.key, dispatchMode: 'native-dialog' };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
throw new PageActionDispatchUncertainError('page_press: native dialog handling was not acknowledged. Read page_snapshot before trying again.', { cause: error });
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
this.handling = undefined;
|
|
157
|
+
// Closed/Opening events own current state; an ack for A must not clear a new dialog B.
|
|
158
|
+
this.notifyIdle();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
notifyIdle() {
|
|
162
|
+
if (!this.pending && !this.busy)
|
|
163
|
+
this.onIdle?.();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function pageDialogSnapshot(dialogs, mode = 'full', maxChars = 25000) {
|
|
167
|
+
const dialog = dialogs.info(maxChars);
|
|
168
|
+
return dialog ? {
|
|
169
|
+
snapshot: `[native ${dialog.type} dialog]`,
|
|
170
|
+
mode,
|
|
171
|
+
elementCount: 0,
|
|
172
|
+
outputNodeCount: 0,
|
|
173
|
+
dialog,
|
|
174
|
+
} : undefined;
|
|
175
|
+
}
|
package/dist/pageObserver.js
CHANGED
|
@@ -28,9 +28,7 @@ export function isPotentialNavigationRequest(resourceType, url = '') {
|
|
|
28
28
|
const normalizedType = resourceType.toLowerCase();
|
|
29
29
|
if (normalizedType === 'xhr' || normalizedType === 'fetch')
|
|
30
30
|
return true;
|
|
31
|
-
|
|
32
|
-
return false;
|
|
33
|
-
return /(?:\/h5\/(?:mtop|mopen)|\/mtop\.|api-proxy)/i.test(url);
|
|
31
|
+
return false;
|
|
34
32
|
}
|
|
35
33
|
export function resolveNavigationWaitOptions(options = {}) {
|
|
36
34
|
const timeoutMs = Math.min(Math.max(options.navigationTimeoutMs ?? 15000, 1000), 30000);
|
|
@@ -171,7 +169,7 @@ export function installPageObserverInPage(maxBatches = 50) {
|
|
|
171
169
|
const escapeAttribute = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
172
170
|
const isInternal = (input) => {
|
|
173
171
|
const element = input?.nodeType === Node.ELEMENT_NODE ? input : (input?.parentElement ?? null);
|
|
174
|
-
return Boolean(element?.closest('[data-mearl-overlay], #
|
|
172
|
+
return Boolean(element?.closest('[data-mearl-overlay], #__mearl_click_style'));
|
|
175
173
|
};
|
|
176
174
|
const isVisible = (element) => {
|
|
177
175
|
if (element.closest('[hidden], [aria-hidden="true"]'))
|
|
@@ -810,7 +808,20 @@ export function installPageObserverInPage(maxBatches = 50) {
|
|
|
810
808
|
const texts = [];
|
|
811
809
|
let incompleteText = latestTexts.size > textLimit;
|
|
812
810
|
for (const [element, effect] of latestTexts) {
|
|
813
|
-
|
|
811
|
+
let current = readPlainText(element);
|
|
812
|
+
// Frameworks can replace a just-observed text leaf with an equivalent node.
|
|
813
|
+
// Only reuse its stable selector when the replacement is unique and passes
|
|
814
|
+
// the same visibility, clipping and text-completeness checks.
|
|
815
|
+
if (!element.isConnected) {
|
|
816
|
+
try {
|
|
817
|
+
const matches = document.querySelectorAll(effect.selector);
|
|
818
|
+
if (matches.length === 1)
|
|
819
|
+
current = readPlainText(matches[0]);
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
// Preserve the fallback when the recorded selector cannot be resolved.
|
|
823
|
+
}
|
|
824
|
+
}
|
|
814
825
|
if (!current || current.selector !== effect.selector || current.text !== effect.text) {
|
|
815
826
|
incompleteText = true;
|
|
816
827
|
continue;
|
package/dist/pagePressing.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface PagePressDomTarget {
|
|
|
31
31
|
export interface PagePressResultData {
|
|
32
32
|
pressed: true;
|
|
33
33
|
key: string;
|
|
34
|
-
dispatchMode: PagePressDispatchMode;
|
|
34
|
+
dispatchMode: PagePressDispatchMode | 'native-dialog';
|
|
35
35
|
fallbackReason?: PagePressFallbackReason;
|
|
36
36
|
target?: PagePressDomTarget;
|
|
37
37
|
keydownCanceled?: boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AppProfileOptions } from './appProfileDefinition.js';
|
|
2
|
+
import type { InjectedAppProfileConfig } from './app-profile-runtime/types.js';
|
|
3
|
+
import type { BrowserCapabilityCatalog } from './browserCapabilities.js';
|
|
4
|
+
import type { GetCapabilitiesParams, BrowserDevicePresetName } from './browserActionProtocol.js';
|
|
5
|
+
import type { ProviderEnhancementTarget } from './browserEnhancement.js';
|
|
6
|
+
export declare const PROVIDER_APP_RESOURCE_PROTOCOL_VERSION = 1;
|
|
7
|
+
export declare const MAX_PROVIDER_APP_RESOURCE_MESSAGE_BYTES: number;
|
|
8
|
+
export type ProviderAppResourceRequest = {
|
|
9
|
+
operation: 'catalog';
|
|
10
|
+
params?: GetCapabilitiesParams;
|
|
11
|
+
} | {
|
|
12
|
+
operation: 'resolve';
|
|
13
|
+
providerTabId: number;
|
|
14
|
+
profile: string;
|
|
15
|
+
options?: AppProfileOptions;
|
|
16
|
+
};
|
|
17
|
+
export interface ProviderAppResourceRevision {
|
|
18
|
+
capabilityRevision: string | null;
|
|
19
|
+
installationRevision: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface ResolvedProviderAppResources extends ProviderAppResourceRevision {
|
|
22
|
+
config: InjectedAppProfileConfig;
|
|
23
|
+
recommendedPreset: BrowserDevicePresetName;
|
|
24
|
+
runtime?: {
|
|
25
|
+
binding: {
|
|
26
|
+
id: string;
|
|
27
|
+
sha256: string;
|
|
28
|
+
};
|
|
29
|
+
source: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Code is returned only by explicit resolution, never with ordinary capability discovery. */
|
|
33
|
+
export type ProviderAppResourceResponse = (ProviderAppResourceRevision & {
|
|
34
|
+
catalog: BrowserCapabilityCatalog['apps'];
|
|
35
|
+
}) | (ResolvedProviderAppResources & {
|
|
36
|
+
target: Extract<ProviderEnhancementTarget, {
|
|
37
|
+
ready: true;
|
|
38
|
+
}>;
|
|
39
|
+
});
|
package/dist/proxyPermissions.js
CHANGED
|
@@ -6,7 +6,7 @@ export class DomainPermissionError extends Error {
|
|
|
6
6
|
domain;
|
|
7
7
|
code = 'DOMAIN_PERMISSION_REQUIRED';
|
|
8
8
|
requestDispatched = false;
|
|
9
|
-
constructor(action, domain, message = `${action}: Access to "${domain}" is not authorized. Call
|
|
9
|
+
constructor(action, domain, message = `${action}: Access to "${domain}" is not authorized. Call request_permission with domain="${domain}" to request user authorization, then retry.`) {
|
|
10
10
|
super(message);
|
|
11
11
|
this.action = action;
|
|
12
12
|
this.domain = domain;
|
|
@@ -83,11 +83,11 @@ export function isDomainAuthorized(domain, allowedOrigins) {
|
|
|
83
83
|
* wrappers whose individual steps are checked independently.
|
|
84
84
|
*/
|
|
85
85
|
export const ACTIONS_SKIP_DOMAIN_CHECK = new Set([
|
|
86
|
-
'
|
|
86
|
+
'request_permission',
|
|
87
87
|
'get_browser_info',
|
|
88
88
|
'get_capabilities',
|
|
89
89
|
'__export_cookies',
|
|
90
|
-
'
|
|
90
|
+
'send_protocol_request',
|
|
91
91
|
'run_actions',
|
|
92
92
|
'browser_release',
|
|
93
93
|
'__control_cleanup',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Optional protocol annotations; the collector retains the original HTTP record. */
|
|
2
|
+
export interface RequestMetadata {
|
|
3
|
+
apiName: string;
|
|
4
|
+
namespace?: string;
|
|
5
|
+
isProxy: boolean;
|
|
6
|
+
proxyRequestUrl: string | null;
|
|
7
|
+
environment?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseRequestMetadata(value: unknown): RequestMetadata | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function parseRequestMetadata(value) {
|
|
2
|
+
if (value === null || value === undefined)
|
|
3
|
+
return null;
|
|
4
|
+
const metadata = value;
|
|
5
|
+
if (typeof value !== 'object' || Array.isArray(value) || metadata.namespace !== undefined && (typeof metadata.namespace !== 'string' || metadata.namespace.length > 256) || typeof metadata.apiName !== 'string' || metadata.apiName.length > 4096 || typeof metadata.isProxy !== 'boolean' || metadata.proxyRequestUrl !== null && typeof metadata.proxyRequestUrl !== 'string' || metadata.environment !== undefined && (typeof metadata.environment !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,63}$/.test(metadata.environment)))
|
|
6
|
+
throw new Error('Invalid request metadata');
|
|
7
|
+
return { ...(metadata.namespace ? { namespace: metadata.namespace } : {}), apiName: metadata.apiName, isProxy: metadata.isProxy, proxyRequestUrl: metadata.proxyRequestUrl, ...(metadata.environment ? { environment: metadata.environment } : {}) };
|
|
8
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Request serialization utilities — pure computation, no environment dependencies.
|
|
3
3
|
*/
|
|
4
|
-
import type {
|
|
4
|
+
import type { CapturedRequest } from './types.js';
|
|
5
5
|
export interface SerializeOptions {
|
|
6
6
|
includeBody?: boolean;
|
|
7
7
|
includeHeaders?: boolean;
|
|
@@ -10,4 +10,4 @@ export interface SerializeOptions {
|
|
|
10
10
|
keysOnly?: boolean;
|
|
11
11
|
}
|
|
12
12
|
export declare function setValueByPath(obj: any, path: string, value: any): void;
|
|
13
|
-
export declare function serializeRequest(request:
|
|
13
|
+
export declare function serializeRequest(request: CapturedRequest, options?: SerializeOptions): any;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CapturedRequest } from './types.js';
|
|
2
2
|
export interface RequestStreamCapture {
|
|
3
|
-
request:
|
|
3
|
+
request: CapturedRequest;
|
|
4
4
|
startTimeMs: number;
|
|
5
5
|
lastMessageTimeMs?: number;
|
|
6
6
|
}
|
|
@@ -8,7 +8,9 @@ export interface RequestStreamCaptureStart {
|
|
|
8
8
|
id: number;
|
|
9
9
|
api: string;
|
|
10
10
|
url: string;
|
|
11
|
-
source?:
|
|
11
|
+
source?: CapturedRequest['source'];
|
|
12
|
+
namespace?: string;
|
|
13
|
+
environment?: string;
|
|
12
14
|
method: string;
|
|
13
15
|
requestHeaders?: Array<{
|
|
14
16
|
name: string;
|
|
@@ -34,6 +34,8 @@ export function createRequestStreamCapture(data) {
|
|
|
34
34
|
method: data.method,
|
|
35
35
|
url,
|
|
36
36
|
apiName: data.api,
|
|
37
|
+
...(data.namespace !== undefined ? { namespace: data.namespace } : {}),
|
|
38
|
+
...(data.environment !== undefined ? { environment: data.environment } : {}),
|
|
37
39
|
status: 0,
|
|
38
40
|
statusText: 'SSE Streaming',
|
|
39
41
|
headers: redactSensitiveHeaders(data.requestHeaders ?? []),
|
package/dist/types.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ export interface ResponseOverride {
|
|
|
19
19
|
disabled?: boolean;
|
|
20
20
|
isSSE?: boolean;
|
|
21
21
|
}
|
|
22
|
-
export interface
|
|
22
|
+
export interface CapturedRequest {
|
|
23
|
+
namespace?: string;
|
|
24
|
+
environment?: string;
|
|
23
25
|
id: number;
|
|
24
26
|
timestamp: string;
|
|
25
27
|
timestampMs: number;
|
|
@@ -59,7 +61,7 @@ export interface MtopRequest {
|
|
|
59
61
|
_createdByInjected?: boolean;
|
|
60
62
|
tabId?: number;
|
|
61
63
|
isAutoTranslated?: boolean;
|
|
62
|
-
source?: '
|
|
64
|
+
source?: 'protocol' | 'requests';
|
|
63
65
|
resourceType?: string;
|
|
64
66
|
/** Monotonic sequence assigned when the request started. */
|
|
65
67
|
captureSequence?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/browser-core",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.7",
|
|
4
4
|
"description": "Shared pure-logic utilities for Mearl — usable in both browser extension and Node.js environments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"repository": {
|
|
62
62
|
"type": "git",
|
|
63
|
-
"url": "git+https://github.com/
|
|
63
|
+
"url": "git+https://github.com/mearlhq/mearl.git",
|
|
64
64
|
"directory": "packages/browser-core"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|