camofox-browser 2.1.1 → 2.4.3
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/CHANGELOG.md +150 -0
- package/README.md +310 -34
- package/dist/src/cli/commands/content.d.ts.map +1 -1
- package/dist/src/cli/commands/content.js +37 -0
- package/dist/src/cli/commands/content.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +21 -4
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/interaction.d.ts.map +1 -1
- package/dist/src/cli/commands/interaction.js +5 -14
- package/dist/src/cli/commands/interaction.js.map +1 -1
- package/dist/src/cli/commands/navigation.d.ts.map +1 -1
- package/dist/src/cli/commands/navigation.js +12 -6
- package/dist/src/cli/commands/navigation.js.map +1 -1
- package/dist/src/cli/commands/server.d.ts.map +1 -1
- package/dist/src/cli/commands/server.js +9 -3
- package/dist/src/cli/commands/server.js.map +1 -1
- package/dist/src/cli/commands/session.d.ts.map +1 -1
- package/dist/src/cli/commands/session.js +23 -5
- package/dist/src/cli/commands/session.js.map +1 -1
- package/dist/src/cli/server/manager.d.ts +1 -0
- package/dist/src/cli/server/manager.d.ts.map +1 -1
- package/dist/src/cli/server/manager.js +7 -12
- package/dist/src/cli/server/manager.js.map +1 -1
- package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
- package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
- package/dist/src/middleware/lifecycle-activity.js +21 -0
- package/dist/src/middleware/lifecycle-activity.js.map +1 -0
- package/dist/src/openapi/spec.d.ts +4 -0
- package/dist/src/openapi/spec.d.ts.map +1 -0
- package/dist/src/openapi/spec.js +730 -0
- package/dist/src/openapi/spec.js.map +1 -0
- package/dist/src/routes/core.d.ts.map +1 -1
- package/dist/src/routes/core.js +545 -58
- package/dist/src/routes/core.js.map +1 -1
- package/dist/src/routes/docs.d.ts +3 -0
- package/dist/src/routes/docs.d.ts.map +1 -0
- package/dist/src/routes/docs.js +23 -0
- package/dist/src/routes/docs.js.map +1 -0
- package/dist/src/routes/openclaw.d.ts.map +1 -1
- package/dist/src/routes/openclaw.js +317 -90
- package/dist/src/routes/openclaw.js.map +1 -1
- package/dist/src/server.js +55 -4
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/context-pool.d.ts +21 -4
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +290 -71
- package/dist/src/services/context-pool.js.map +1 -1
- package/dist/src/services/download.d.ts +2 -0
- package/dist/src/services/download.d.ts.map +1 -1
- package/dist/src/services/download.js +110 -80
- package/dist/src/services/download.js.map +1 -1
- package/dist/src/services/lifecycle-controller.d.ts +40 -0
- package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
- package/dist/src/services/lifecycle-controller.js +106 -0
- package/dist/src/services/lifecycle-controller.js.map +1 -0
- package/dist/src/services/resource-extractor.d.ts +1 -0
- package/dist/src/services/resource-extractor.d.ts.map +1 -1
- package/dist/src/services/resource-extractor.js +7 -0
- package/dist/src/services/resource-extractor.js.map +1 -1
- package/dist/src/services/session.d.ts +109 -4
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +622 -64
- package/dist/src/services/session.js.map +1 -1
- package/dist/src/services/structured-extractor.d.ts +39 -0
- package/dist/src/services/structured-extractor.d.ts.map +1 -0
- package/dist/src/services/structured-extractor.js +487 -0
- package/dist/src/services/structured-extractor.js.map +1 -0
- package/dist/src/services/tab.d.ts +30 -3
- package/dist/src/services/tab.d.ts.map +1 -1
- package/dist/src/services/tab.js +872 -124
- package/dist/src/services/tab.js.map +1 -1
- package/dist/src/services/tracing.d.ts +7 -0
- package/dist/src/services/tracing.d.ts.map +1 -1
- package/dist/src/services/tracing.js +200 -19
- package/dist/src/services/tracing.js.map +1 -1
- package/dist/src/services/vnc.d.ts.map +1 -1
- package/dist/src/services/vnc.js +5 -3
- package/dist/src/services/vnc.js.map +1 -1
- package/dist/src/services/youtube.js +1 -1
- package/dist/src/services/youtube.js.map +1 -1
- package/dist/src/types.d.ts +71 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/config.d.ts +79 -3
- package/dist/src/utils/config.d.ts.map +1 -1
- package/dist/src/utils/config.js +145 -3
- package/dist/src/utils/config.js.map +1 -1
- package/dist/src/utils/presets.d.ts.map +1 -1
- package/dist/src/utils/presets.js +3 -1
- package/dist/src/utils/presets.js.map +1 -1
- package/dist/src/utils/proxy-profiles.d.ts +18 -0
- package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
- package/dist/src/utils/proxy-profiles.js +197 -0
- package/dist/src/utils/proxy-profiles.js.map +1 -0
- package/dist/src/utils/sidecar-version.d.ts +12 -0
- package/dist/src/utils/sidecar-version.d.ts.map +1 -0
- package/dist/src/utils/sidecar-version.js +63 -0
- package/dist/src/utils/sidecar-version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/openclaw.plugin.json +39 -0
- package/package.json +16 -4
- package/plugin.ts +949 -0
package/dist/src/services/tab.js
CHANGED
|
@@ -7,7 +7,12 @@ exports.clearTabLock = clearTabLock;
|
|
|
7
7
|
exports.clearAllTabLocks = clearAllTabLocks;
|
|
8
8
|
exports.calculateTypeTimeoutMs = calculateTypeTimeoutMs;
|
|
9
9
|
exports.safePageClose = safePageClose;
|
|
10
|
+
exports.throwBlockedNavigationErrorIfPresent = throwBlockedNavigationErrorIfPresent;
|
|
11
|
+
exports.flushBlockedNavigationError = flushBlockedNavigationError;
|
|
12
|
+
exports.withBlockedNavigationTracking = withBlockedNavigationTracking;
|
|
10
13
|
exports.validateUrl = validateUrl;
|
|
14
|
+
exports.validateNavigationUrl = validateNavigationUrl;
|
|
15
|
+
exports.navigateWithSafetyGuard = navigateWithSafetyGuard;
|
|
11
16
|
exports.annotateAriaYamlWithRefs = annotateAriaYamlWithRefs;
|
|
12
17
|
exports.waitForPageReady = waitForPageReady;
|
|
13
18
|
exports.dismissConsentDialogs = dismissConsentDialogs;
|
|
@@ -19,6 +24,7 @@ exports.refToLocator = refToLocator;
|
|
|
19
24
|
exports.createTabState = createTabState;
|
|
20
25
|
exports.navigateTab = navigateTab;
|
|
21
26
|
exports.snapshotTab = snapshotTab;
|
|
27
|
+
exports.buildSnapshotPayload = buildSnapshotPayload;
|
|
22
28
|
exports.clickTab = clickTab;
|
|
23
29
|
exports.smartFill = smartFill;
|
|
24
30
|
exports.typeTab = typeTab;
|
|
@@ -33,10 +39,16 @@ exports.refreshTab = refreshTab;
|
|
|
33
39
|
exports.getLinks = getLinks;
|
|
34
40
|
exports.screenshotTab = screenshotTab;
|
|
35
41
|
exports.waitTab = waitTab;
|
|
42
|
+
const promises_1 = require("node:dns/promises");
|
|
43
|
+
const node_net_1 = require("node:net");
|
|
36
44
|
const selector_1 = require("../cli/utils/selector");
|
|
37
45
|
const macros_1 = require("../utils/macros");
|
|
46
|
+
const config_1 = require("../utils/config");
|
|
47
|
+
const snapshot_1 = require("../utils/snapshot");
|
|
38
48
|
const logging_1 = require("../middleware/logging");
|
|
39
49
|
const ALLOWED_URL_SCHEMES = ['http:', 'https:'];
|
|
50
|
+
const CONFIG = (0, config_1.loadConfig)();
|
|
51
|
+
const METADATA_HOSTNAMES = new Set(['metadata.google.internal']);
|
|
40
52
|
// Selective set of actionable roles worth indexing as refs.
|
|
41
53
|
const INTERACTIVE_ROLES = [
|
|
42
54
|
'button',
|
|
@@ -61,17 +73,26 @@ const INTERACTIVE_ROLES = [
|
|
|
61
73
|
];
|
|
62
74
|
// Patterns to skip (date pickers, calendar widgets)
|
|
63
75
|
const SKIP_PATTERNS = [/date/i, /calendar/i, /picker/i, /datepicker/i];
|
|
64
|
-
const
|
|
65
|
-
const parsedMaxSnapshotNodes = Number.parseInt(process.env.CAMOFOX_MAX_SNAPSHOT_NODES || String(DEFAULT_MAX_SNAPSHOT_NODES), 10);
|
|
66
|
-
const MAX_SNAPSHOT_NODES = Number.isFinite(parsedMaxSnapshotNodes) && parsedMaxSnapshotNodes > 0
|
|
67
|
-
? parsedMaxSnapshotNodes
|
|
68
|
-
: DEFAULT_MAX_SNAPSHOT_NODES;
|
|
76
|
+
const MAX_SNAPSHOT_NODES = CONFIG.maxSnapshotNodes;
|
|
69
77
|
const MAX_EVAL_TIMEOUT = 300000;
|
|
70
78
|
const DEFAULT_EVAL_TIMEOUT = 5000;
|
|
71
79
|
const MAX_EVAL_EXTENDED_TIMEOUT = 300000;
|
|
72
80
|
const DEFAULT_EVAL_EXTENDED_TIMEOUT = 30000;
|
|
73
81
|
const MAX_RESULT_SIZE = 1048576; // 1MB
|
|
74
|
-
const CONSOLE_BUFFER_SIZE =
|
|
82
|
+
const CONSOLE_BUFFER_SIZE = CONFIG.consoleBufferSize;
|
|
83
|
+
const POST_ACTION_NAVIGATION_SETTLE_MS = 500;
|
|
84
|
+
const ACTION_TRACKER_POLL_MS = 10;
|
|
85
|
+
const navigationGuardHandlers = new WeakMap();
|
|
86
|
+
const blockedNavigationErrors = new WeakMap();
|
|
87
|
+
const trackedBlockedNavigationErrors = new WeakMap();
|
|
88
|
+
const popupOpenerPages = new WeakMap();
|
|
89
|
+
const inFlightGuardChecks = new WeakMap();
|
|
90
|
+
const trackedInFlightGuardChecks = new WeakMap();
|
|
91
|
+
const trackedPendingCounts = new WeakMap();
|
|
92
|
+
const actionTrackerInstalledPages = new WeakSet();
|
|
93
|
+
const actionTrackerBindingContexts = new WeakSet();
|
|
94
|
+
const actionTrackerTokens = new WeakMap();
|
|
95
|
+
const activeTrackedActionTokens = new WeakMap();
|
|
75
96
|
exports.LONG_TEXT_THRESHOLD = 400;
|
|
76
97
|
exports.TYPE_TIMEOUT_BASE_MS = 10000;
|
|
77
98
|
exports.TYPE_TIMEOUT_PER_CHAR_MS = 80;
|
|
@@ -149,18 +170,606 @@ async function safePageClose(page) {
|
|
|
149
170
|
console.warn(`[camofox] page close failed: ${message}`);
|
|
150
171
|
}
|
|
151
172
|
}
|
|
152
|
-
function
|
|
173
|
+
function normalizeHostname(hostname) {
|
|
174
|
+
return hostname.trim().replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
|
|
175
|
+
}
|
|
176
|
+
function parseIpv4Octets(hostname) {
|
|
177
|
+
const parts = hostname.split('.');
|
|
178
|
+
if (parts.length !== 4)
|
|
179
|
+
return null;
|
|
180
|
+
const octets = parts.map((part) => Number.parseInt(part, 10));
|
|
181
|
+
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255))
|
|
182
|
+
return null;
|
|
183
|
+
return octets;
|
|
184
|
+
}
|
|
185
|
+
function isBlockedIpv4(hostname) {
|
|
186
|
+
const octets = parseIpv4Octets(hostname);
|
|
187
|
+
if (!octets)
|
|
188
|
+
return false;
|
|
189
|
+
const [a, b] = octets;
|
|
190
|
+
if (a === 0 || a === 10 || a === 127)
|
|
191
|
+
return true;
|
|
192
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
193
|
+
return true;
|
|
194
|
+
if (a === 169 && b === 254)
|
|
195
|
+
return true;
|
|
196
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
197
|
+
return true;
|
|
198
|
+
if (a === 192 && b === 168)
|
|
199
|
+
return true;
|
|
200
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
201
|
+
return true;
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
function isBlockedIpv6(hostname) {
|
|
205
|
+
const normalized = normalizeHostname(hostname);
|
|
206
|
+
if (normalized === '::1')
|
|
207
|
+
return true;
|
|
208
|
+
const nat64DottedMatch = normalized.match(/^64:ff9b::(\d+\.\d+\.\d+\.\d+)$/i);
|
|
209
|
+
if (nat64DottedMatch) {
|
|
210
|
+
return isBlockedIpv4(nat64DottedMatch[1]);
|
|
211
|
+
}
|
|
212
|
+
const nat64HexMatch = normalized.match(/^64:ff9b::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
213
|
+
if (nat64HexMatch) {
|
|
214
|
+
const high = Number.parseInt(nat64HexMatch[1], 16);
|
|
215
|
+
const low = Number.parseInt(nat64HexMatch[2], 16);
|
|
216
|
+
if (Number.isFinite(high) && Number.isFinite(low)) {
|
|
217
|
+
const mappedIpv4 = [
|
|
218
|
+
(high >> 8) & 0xff,
|
|
219
|
+
high & 0xff,
|
|
220
|
+
(low >> 8) & 0xff,
|
|
221
|
+
low & 0xff,
|
|
222
|
+
].join('.');
|
|
223
|
+
return isBlockedIpv4(mappedIpv4);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const mappedIpv4Match = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
227
|
+
if (mappedIpv4Match) {
|
|
228
|
+
return isBlockedIpv4(mappedIpv4Match[1]);
|
|
229
|
+
}
|
|
230
|
+
const mappedIpv4HexMatch = normalized.match(/^::ffff:([0-9a-f]{1,4})(?::([0-9a-f]{1,4}))?$/i);
|
|
231
|
+
if (mappedIpv4HexMatch) {
|
|
232
|
+
const high = Number.parseInt(mappedIpv4HexMatch[1], 16);
|
|
233
|
+
const low = Number.parseInt(mappedIpv4HexMatch[2] || '0', 16);
|
|
234
|
+
if (Number.isFinite(high) && Number.isFinite(low)) {
|
|
235
|
+
const mappedIpv4 = [
|
|
236
|
+
(high >> 8) & 0xff,
|
|
237
|
+
high & 0xff,
|
|
238
|
+
(low >> 8) & 0xff,
|
|
239
|
+
low & 0xff,
|
|
240
|
+
].join('.');
|
|
241
|
+
return isBlockedIpv4(mappedIpv4);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const firstHextetRaw = normalized.split(':', 1)[0] || '0';
|
|
245
|
+
const firstHextet = Number.parseInt(firstHextetRaw, 16);
|
|
246
|
+
if (!Number.isFinite(firstHextet))
|
|
247
|
+
return false;
|
|
248
|
+
if ((firstHextet & 0xfe00) === 0xfc00)
|
|
249
|
+
return true;
|
|
250
|
+
if ((firstHextet & 0xffc0) === 0xfe80)
|
|
251
|
+
return true;
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
function isBlockedPrivateNetworkHostname(hostname) {
|
|
255
|
+
const normalized = normalizeHostname(hostname);
|
|
256
|
+
if (normalized === 'localhost' || normalized === 'localhost.localdomain')
|
|
257
|
+
return true;
|
|
258
|
+
if (METADATA_HOSTNAMES.has(normalized))
|
|
259
|
+
return true;
|
|
260
|
+
const ipVersion = (0, node_net_1.isIP)(normalized);
|
|
261
|
+
if (ipVersion === 4)
|
|
262
|
+
return isBlockedIpv4(normalized);
|
|
263
|
+
if (ipVersion === 6)
|
|
264
|
+
return isBlockedIpv6(normalized);
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
function createNavigationBlockError(message) {
|
|
268
|
+
const error = new Error(message);
|
|
269
|
+
error.statusCode = 400;
|
|
270
|
+
return error;
|
|
271
|
+
}
|
|
272
|
+
function takeBlockedNavigationError(page) {
|
|
273
|
+
const message = blockedNavigationErrors.get(page);
|
|
274
|
+
if (!message)
|
|
275
|
+
return null;
|
|
276
|
+
blockedNavigationErrors.delete(page);
|
|
277
|
+
return createNavigationBlockError(message);
|
|
278
|
+
}
|
|
279
|
+
function setTrackedBlockedNavigationError(page, token, message) {
|
|
280
|
+
const existing = trackedBlockedNavigationErrors.get(page) || new Map();
|
|
281
|
+
existing.set(token, message);
|
|
282
|
+
trackedBlockedNavigationErrors.set(page, existing);
|
|
283
|
+
}
|
|
284
|
+
function takeTrackedBlockedNavigationError(page, token) {
|
|
285
|
+
const existing = trackedBlockedNavigationErrors.get(page);
|
|
286
|
+
const message = existing?.get(token);
|
|
287
|
+
if (!message)
|
|
288
|
+
return null;
|
|
289
|
+
existing?.delete(token);
|
|
290
|
+
if (existing && existing.size === 0) {
|
|
291
|
+
trackedBlockedNavigationErrors.delete(page);
|
|
292
|
+
}
|
|
293
|
+
return createNavigationBlockError(message);
|
|
294
|
+
}
|
|
295
|
+
function rethrowWithBlockedNavigationError(page, err) {
|
|
296
|
+
const blockedError = takeBlockedNavigationError(page);
|
|
297
|
+
if (blockedError)
|
|
298
|
+
throw blockedError;
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
function rethrowWithTrackedBlockedNavigationError(page, token, err) {
|
|
302
|
+
const blockedError = takeTrackedBlockedNavigationError(page, token);
|
|
303
|
+
if (blockedError)
|
|
304
|
+
throw blockedError;
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
function throwBlockedNavigationErrorIfPresent(page) {
|
|
308
|
+
const blockedError = takeBlockedNavigationError(page);
|
|
309
|
+
if (blockedError)
|
|
310
|
+
throw blockedError;
|
|
311
|
+
}
|
|
312
|
+
function throwTrackedBlockedNavigationErrorIfPresent(page, token) {
|
|
313
|
+
const blockedError = takeTrackedBlockedNavigationError(page, token);
|
|
314
|
+
if (blockedError)
|
|
315
|
+
throw blockedError;
|
|
316
|
+
}
|
|
317
|
+
function clearBlockedNavigationError(page) {
|
|
318
|
+
blockedNavigationErrors.delete(page);
|
|
319
|
+
}
|
|
320
|
+
function installActionTrackerScript() {
|
|
321
|
+
const browserGlobal = globalThis;
|
|
322
|
+
if (browserGlobal.__camofoxActionTracker)
|
|
323
|
+
return;
|
|
324
|
+
const state = {
|
|
325
|
+
activeToken: 0,
|
|
326
|
+
pendingCounts: new Map(),
|
|
327
|
+
timeoutTokens: new Map(),
|
|
328
|
+
intervalTokens: new Map(),
|
|
329
|
+
rafTokens: new Map(),
|
|
330
|
+
};
|
|
331
|
+
const increment = (token) => {
|
|
332
|
+
state.pendingCounts.set(token, (state.pendingCounts.get(token) || 0) + 1);
|
|
333
|
+
void browserGlobal.__camofoxUpdatePendingCount?.(token, state.pendingCounts.get(token) || 0);
|
|
334
|
+
};
|
|
335
|
+
const decrement = (token) => {
|
|
336
|
+
const next = (state.pendingCounts.get(token) || 0) - 1;
|
|
337
|
+
if (next > 0) {
|
|
338
|
+
state.pendingCounts.set(token, next);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
state.pendingCounts.delete(token);
|
|
342
|
+
}
|
|
343
|
+
void browserGlobal.__camofoxUpdatePendingCount?.(token, state.pendingCounts.get(token) || 0);
|
|
344
|
+
};
|
|
345
|
+
const syncActiveToken = (token) => {
|
|
346
|
+
state.activeToken = token;
|
|
347
|
+
void browserGlobal.__camofoxUpdateActiveToken?.(token);
|
|
348
|
+
};
|
|
349
|
+
const withToken = (token, fn) => {
|
|
350
|
+
const previousToken = state.activeToken;
|
|
351
|
+
syncActiveToken(token);
|
|
352
|
+
try {
|
|
353
|
+
return fn();
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
syncActiveToken(previousToken);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const runHandler = (handler, args) => {
|
|
360
|
+
if (typeof handler === 'function') {
|
|
361
|
+
return handler(...args);
|
|
362
|
+
}
|
|
363
|
+
return browserGlobal.eval(String(handler));
|
|
364
|
+
};
|
|
365
|
+
const originalSetTimeout = browserGlobal.setTimeout.bind(browserGlobal);
|
|
366
|
+
const originalClearTimeout = browserGlobal.clearTimeout.bind(browserGlobal);
|
|
367
|
+
const originalSetInterval = browserGlobal.setInterval.bind(browserGlobal);
|
|
368
|
+
const originalClearInterval = browserGlobal.clearInterval.bind(browserGlobal);
|
|
369
|
+
const originalRequestAnimationFrame = typeof browserGlobal.requestAnimationFrame === 'function'
|
|
370
|
+
? browserGlobal.requestAnimationFrame.bind(browserGlobal)
|
|
371
|
+
: null;
|
|
372
|
+
const originalCancelAnimationFrame = typeof browserGlobal.cancelAnimationFrame === 'function'
|
|
373
|
+
? browserGlobal.cancelAnimationFrame.bind(browserGlobal)
|
|
374
|
+
: null;
|
|
375
|
+
const originalQueueMicrotask = typeof browserGlobal.queueMicrotask === 'function'
|
|
376
|
+
? browserGlobal.queueMicrotask.bind(browserGlobal)
|
|
377
|
+
: null;
|
|
378
|
+
browserGlobal.setTimeout = (handler, delay, ...args) => {
|
|
379
|
+
const token = state.activeToken;
|
|
380
|
+
if (!token) {
|
|
381
|
+
return originalSetTimeout(handler, delay, ...args);
|
|
382
|
+
}
|
|
383
|
+
increment(token);
|
|
384
|
+
let timeoutId;
|
|
385
|
+
const wrapped = (...callbackArgs) => {
|
|
386
|
+
const trackedToken = state.timeoutTokens.get(timeoutId) || token;
|
|
387
|
+
state.timeoutTokens.delete(timeoutId);
|
|
388
|
+
try {
|
|
389
|
+
return withToken(trackedToken, () => runHandler(handler, callbackArgs));
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
decrement(trackedToken);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
timeoutId = originalSetTimeout(wrapped, delay, ...args);
|
|
396
|
+
state.timeoutTokens.set(timeoutId, token);
|
|
397
|
+
return timeoutId;
|
|
398
|
+
};
|
|
399
|
+
browserGlobal.clearTimeout = (timeoutId) => {
|
|
400
|
+
const token = state.timeoutTokens.get(timeoutId);
|
|
401
|
+
if (token) {
|
|
402
|
+
state.timeoutTokens.delete(timeoutId);
|
|
403
|
+
decrement(token);
|
|
404
|
+
}
|
|
405
|
+
return originalClearTimeout(timeoutId);
|
|
406
|
+
};
|
|
407
|
+
browserGlobal.setInterval = (handler, delay, ...args) => {
|
|
408
|
+
const token = state.activeToken;
|
|
409
|
+
if (!token) {
|
|
410
|
+
return originalSetInterval(handler, delay, ...args);
|
|
411
|
+
}
|
|
412
|
+
let intervalId;
|
|
413
|
+
const wrapped = (...callbackArgs) => {
|
|
414
|
+
const trackedToken = state.intervalTokens.get(intervalId) || token;
|
|
415
|
+
increment(trackedToken);
|
|
416
|
+
try {
|
|
417
|
+
return withToken(trackedToken, () => runHandler(handler, callbackArgs));
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
decrement(trackedToken);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
intervalId = originalSetInterval(wrapped, delay, ...args);
|
|
424
|
+
state.intervalTokens.set(intervalId, token);
|
|
425
|
+
return intervalId;
|
|
426
|
+
};
|
|
427
|
+
browserGlobal.clearInterval = (intervalId) => {
|
|
428
|
+
state.intervalTokens.delete(intervalId);
|
|
429
|
+
return originalClearInterval(intervalId);
|
|
430
|
+
};
|
|
431
|
+
if (originalRequestAnimationFrame && originalCancelAnimationFrame) {
|
|
432
|
+
browserGlobal.requestAnimationFrame = (callback) => {
|
|
433
|
+
const token = state.activeToken;
|
|
434
|
+
if (!token) {
|
|
435
|
+
return originalRequestAnimationFrame(callback);
|
|
436
|
+
}
|
|
437
|
+
increment(token);
|
|
438
|
+
let rafId = 0;
|
|
439
|
+
const wrapped = (timestamp) => {
|
|
440
|
+
const trackedToken = state.rafTokens.get(rafId) || token;
|
|
441
|
+
state.rafTokens.delete(rafId);
|
|
442
|
+
try {
|
|
443
|
+
return withToken(trackedToken, () => callback(timestamp));
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
decrement(trackedToken);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
rafId = originalRequestAnimationFrame(wrapped);
|
|
450
|
+
state.rafTokens.set(rafId, token);
|
|
451
|
+
return rafId;
|
|
452
|
+
};
|
|
453
|
+
browserGlobal.cancelAnimationFrame = (rafId) => {
|
|
454
|
+
const token = state.rafTokens.get(rafId);
|
|
455
|
+
if (token) {
|
|
456
|
+
state.rafTokens.delete(rafId);
|
|
457
|
+
decrement(token);
|
|
458
|
+
}
|
|
459
|
+
return originalCancelAnimationFrame(rafId);
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (originalQueueMicrotask) {
|
|
463
|
+
browserGlobal.queueMicrotask = (callback) => {
|
|
464
|
+
const token = state.activeToken;
|
|
465
|
+
if (!token) {
|
|
466
|
+
return originalQueueMicrotask(callback);
|
|
467
|
+
}
|
|
468
|
+
increment(token);
|
|
469
|
+
return originalQueueMicrotask(() => {
|
|
470
|
+
try {
|
|
471
|
+
return withToken(token, callback);
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
decrement(token);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
browserGlobal.__camofoxActionTracker = {
|
|
480
|
+
startAction(token) {
|
|
481
|
+
syncActiveToken(token);
|
|
482
|
+
},
|
|
483
|
+
finishAction(token) {
|
|
484
|
+
if (state.activeToken === token) {
|
|
485
|
+
syncActiveToken(0);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
getPendingCount(token) {
|
|
489
|
+
return state.pendingCounts.get(token) || 0;
|
|
490
|
+
},
|
|
491
|
+
getActiveToken() {
|
|
492
|
+
return state.activeToken || 0;
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async function ensureActionNavigationTracker(page) {
|
|
497
|
+
if (actionTrackerInstalledPages.has(page))
|
|
498
|
+
return true;
|
|
499
|
+
if (typeof page.addInitScript !== 'function' || typeof page.evaluate !== 'function')
|
|
500
|
+
return false;
|
|
501
|
+
try {
|
|
502
|
+
const context = page.context();
|
|
503
|
+
if (!actionTrackerBindingContexts.has(context) && typeof context.exposeBinding === 'function') {
|
|
504
|
+
await context.exposeBinding('__camofoxUpdateActiveToken', async ({ page: bindingPage }, token) => {
|
|
505
|
+
if (!bindingPage)
|
|
506
|
+
return;
|
|
507
|
+
if (typeof token === 'number' && token > 0) {
|
|
508
|
+
activeTrackedActionTokens.set(bindingPage, token);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
activeTrackedActionTokens.delete(bindingPage);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
await context.exposeBinding('__camofoxUpdatePendingCount', async ({ page: bindingPage }, token, count) => {
|
|
515
|
+
if (!bindingPage || typeof token !== 'number' || token <= 0)
|
|
516
|
+
return;
|
|
517
|
+
const existing = trackedPendingCounts.get(bindingPage) || new Map();
|
|
518
|
+
if (typeof count === 'number' && count > 0) {
|
|
519
|
+
existing.set(token, count);
|
|
520
|
+
trackedPendingCounts.set(bindingPage, existing);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
existing.delete(token);
|
|
524
|
+
if (existing.size === 0) {
|
|
525
|
+
trackedPendingCounts.delete(bindingPage);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
trackedPendingCounts.set(bindingPage, existing);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
actionTrackerBindingContexts.add(context);
|
|
532
|
+
}
|
|
533
|
+
await page.addInitScript(installActionTrackerScript);
|
|
534
|
+
await page.evaluate(installActionTrackerScript);
|
|
535
|
+
actionTrackerInstalledPages.add(page);
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function nextActionTrackerToken(page) {
|
|
543
|
+
const nextToken = (actionTrackerTokens.get(page) || 0) + 1;
|
|
544
|
+
actionTrackerTokens.set(page, nextToken);
|
|
545
|
+
return nextToken;
|
|
546
|
+
}
|
|
547
|
+
async function startTrackedAction(page) {
|
|
548
|
+
const trackerReady = await ensureActionNavigationTracker(page);
|
|
549
|
+
if (!trackerReady)
|
|
550
|
+
return null;
|
|
551
|
+
const token = nextActionTrackerToken(page);
|
|
552
|
+
await page.evaluate((trackedToken) => {
|
|
553
|
+
globalThis.__camofoxActionTracker?.startAction(trackedToken);
|
|
554
|
+
}, token);
|
|
555
|
+
activeTrackedActionTokens.set(page, token);
|
|
556
|
+
return token;
|
|
557
|
+
}
|
|
558
|
+
async function finishTrackedAction(page, token) {
|
|
559
|
+
if (token === null || typeof page.evaluate !== 'function')
|
|
560
|
+
return;
|
|
561
|
+
await page.evaluate((trackedToken) => {
|
|
562
|
+
globalThis.__camofoxActionTracker?.finishAction(trackedToken);
|
|
563
|
+
}, token).catch(() => { });
|
|
564
|
+
if (activeTrackedActionTokens.get(page) === token) {
|
|
565
|
+
activeTrackedActionTokens.delete(page);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async function getTrackedPendingCount(page, token) {
|
|
569
|
+
const syncedCount = trackedPendingCounts.get(page)?.get(token);
|
|
570
|
+
if (typeof syncedCount === 'number') {
|
|
571
|
+
return syncedCount;
|
|
572
|
+
}
|
|
573
|
+
if (typeof page.evaluate !== 'function')
|
|
574
|
+
return 0;
|
|
575
|
+
try {
|
|
576
|
+
return await page.evaluate((trackedToken) => {
|
|
577
|
+
return globalThis.__camofoxActionTracker?.getPendingCount(trackedToken) || 0;
|
|
578
|
+
}, token);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function getCurrentTrackedActionToken(page) {
|
|
585
|
+
if (typeof page.evaluate !== 'function' || !actionTrackerInstalledPages.has(page)) {
|
|
586
|
+
return activeTrackedActionTokens.get(page) || null;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const token = await page.evaluate(() => {
|
|
590
|
+
return globalThis.__camofoxActionTracker?.getActiveToken?.() || 0;
|
|
591
|
+
});
|
|
592
|
+
if (typeof token === 'number' && token > 0) {
|
|
593
|
+
return token;
|
|
594
|
+
}
|
|
595
|
+
return activeTrackedActionTokens.get(page) || null;
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return activeTrackedActionTokens.get(page) || null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function incrementInFlightGuardCheck(page) {
|
|
602
|
+
inFlightGuardChecks.set(page, (inFlightGuardChecks.get(page) || 0) + 1);
|
|
603
|
+
}
|
|
604
|
+
function decrementInFlightGuardCheck(page) {
|
|
605
|
+
const next = (inFlightGuardChecks.get(page) || 0) - 1;
|
|
606
|
+
if (next > 0) {
|
|
607
|
+
inFlightGuardChecks.set(page, next);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
inFlightGuardChecks.delete(page);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function incrementTrackedInFlightGuardCheck(page, token) {
|
|
614
|
+
const existing = trackedInFlightGuardChecks.get(page) || new Map();
|
|
615
|
+
existing.set(token, (existing.get(token) || 0) + 1);
|
|
616
|
+
trackedInFlightGuardChecks.set(page, existing);
|
|
617
|
+
}
|
|
618
|
+
function decrementTrackedInFlightGuardCheck(page, token) {
|
|
619
|
+
const existing = trackedInFlightGuardChecks.get(page);
|
|
620
|
+
if (!existing)
|
|
621
|
+
return;
|
|
622
|
+
const next = (existing.get(token) || 0) - 1;
|
|
623
|
+
if (next > 0) {
|
|
624
|
+
existing.set(token, next);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
existing.delete(token);
|
|
628
|
+
}
|
|
629
|
+
if (existing.size === 0) {
|
|
630
|
+
trackedInFlightGuardChecks.delete(page);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function getTrackedInFlightGuardCheckCount(page, token) {
|
|
634
|
+
return trackedInFlightGuardChecks.get(page)?.get(token) || 0;
|
|
635
|
+
}
|
|
636
|
+
async function flushBlockedNavigationError(page) {
|
|
637
|
+
if (typeof page.waitForTimeout === 'function') {
|
|
638
|
+
await page.waitForTimeout(POST_ACTION_NAVIGATION_SETTLE_MS);
|
|
639
|
+
}
|
|
640
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
641
|
+
}
|
|
642
|
+
async function withBlockedNavigationTracking(page, action) {
|
|
643
|
+
if (CONFIG.allowPrivateNetworkTargets) {
|
|
644
|
+
return action();
|
|
645
|
+
}
|
|
646
|
+
clearBlockedNavigationError(page);
|
|
647
|
+
const actionToken = await startTrackedAction(page);
|
|
648
|
+
clearBlockedNavigationError(page);
|
|
649
|
+
let finishedTracking = false;
|
|
650
|
+
const finish = async () => {
|
|
651
|
+
if (finishedTracking)
|
|
652
|
+
return;
|
|
653
|
+
finishedTracking = true;
|
|
654
|
+
await finishTrackedAction(page, actionToken);
|
|
655
|
+
};
|
|
656
|
+
try {
|
|
657
|
+
const result = await action();
|
|
658
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
659
|
+
if (actionToken === null) {
|
|
660
|
+
await finish();
|
|
661
|
+
await flushBlockedNavigationError(page);
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
let sawPendingWork = false;
|
|
665
|
+
while (true) {
|
|
666
|
+
throwTrackedBlockedNavigationErrorIfPresent(page, actionToken);
|
|
667
|
+
if (sawPendingWork) {
|
|
668
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
669
|
+
}
|
|
670
|
+
const pendingCount = await getTrackedPendingCount(page, actionToken);
|
|
671
|
+
const inFlightGuardCount = getTrackedInFlightGuardCheckCount(page, actionToken);
|
|
672
|
+
if (pendingCount === 0 && inFlightGuardCount === 0) {
|
|
673
|
+
if (!sawPendingWork) {
|
|
674
|
+
await new Promise((resolve) => setTimeout(resolve, ACTION_TRACKER_POLL_MS));
|
|
675
|
+
throwTrackedBlockedNavigationErrorIfPresent(page, actionToken);
|
|
676
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
677
|
+
if ((await getTrackedPendingCount(page, actionToken)) === 0 && getTrackedInFlightGuardCheckCount(page, actionToken) === 0) {
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
sawPendingWork = true;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
await new Promise((resolve) => setTimeout(resolve, ACTION_TRACKER_POLL_MS));
|
|
684
|
+
throwTrackedBlockedNavigationErrorIfPresent(page, actionToken);
|
|
685
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
686
|
+
if ((await getTrackedPendingCount(page, actionToken)) === 0 && getTrackedInFlightGuardCheckCount(page, actionToken) === 0)
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
sawPendingWork = true;
|
|
691
|
+
await new Promise((resolve) => setTimeout(resolve, ACTION_TRACKER_POLL_MS));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
throwTrackedBlockedNavigationErrorIfPresent(page, actionToken);
|
|
695
|
+
if (sawPendingWork) {
|
|
696
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
697
|
+
}
|
|
698
|
+
await finish();
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
await finish();
|
|
703
|
+
if (typeof err === 'object' && err !== null && 'statusCode' in err && typeof err.statusCode === 'number') {
|
|
704
|
+
throw err;
|
|
705
|
+
}
|
|
706
|
+
if (actionToken !== null) {
|
|
707
|
+
rethrowWithTrackedBlockedNavigationError(page, actionToken, err);
|
|
708
|
+
}
|
|
709
|
+
rethrowWithBlockedNavigationError(page, err);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function validateUrl(url, options = {}) {
|
|
713
|
+
const { allowPrivateNetworkTargets = CONFIG.allowPrivateNetworkTargets } = options;
|
|
153
714
|
try {
|
|
154
715
|
const parsed = new URL(url);
|
|
155
716
|
if (!ALLOWED_URL_SCHEMES.includes(parsed.protocol)) {
|
|
156
717
|
return `Blocked URL scheme: ${parsed.protocol} (only http/https allowed)`;
|
|
157
718
|
}
|
|
719
|
+
if (!allowPrivateNetworkTargets && isBlockedPrivateNetworkHostname(parsed.hostname)) {
|
|
720
|
+
return `Blocked private network target: ${parsed.hostname}`;
|
|
721
|
+
}
|
|
158
722
|
return null;
|
|
159
723
|
}
|
|
160
724
|
catch {
|
|
161
725
|
return `Invalid URL: ${url}`;
|
|
162
726
|
}
|
|
163
727
|
}
|
|
728
|
+
async function hostnameResolvesToBlockedAddress(hostname) {
|
|
729
|
+
const normalized = normalizeHostname(hostname);
|
|
730
|
+
if (isBlockedPrivateNetworkHostname(normalized))
|
|
731
|
+
return true;
|
|
732
|
+
try {
|
|
733
|
+
const results = await (0, promises_1.lookup)(normalized, { all: true, verbatim: true });
|
|
734
|
+
return results.some((result) => isBlockedPrivateNetworkHostname(result.address));
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async function validateNavigationUrl(url, options = {}) {
|
|
741
|
+
const urlError = validateUrl(url, options);
|
|
742
|
+
if (urlError)
|
|
743
|
+
return urlError;
|
|
744
|
+
const { allowPrivateNetworkTargets = CONFIG.allowPrivateNetworkTargets } = options;
|
|
745
|
+
if (allowPrivateNetworkTargets)
|
|
746
|
+
return null;
|
|
747
|
+
const parsed = new URL(url);
|
|
748
|
+
if (await hostnameResolvesToBlockedAddress(parsed.hostname)) {
|
|
749
|
+
return `Blocked private network target: ${parsed.hostname}`;
|
|
750
|
+
}
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
async function navigateWithSafetyGuard(page, targetUrl, options = {}) {
|
|
754
|
+
const { allowPrivateNetworkTargets = CONFIG.allowPrivateNetworkTargets, waitUntil = 'domcontentloaded', timeout = 30000 } = options;
|
|
755
|
+
const initialError = await validateNavigationUrl(targetUrl, { allowPrivateNetworkTargets });
|
|
756
|
+
if (initialError) {
|
|
757
|
+
throw createNavigationBlockError(initialError);
|
|
758
|
+
}
|
|
759
|
+
if (allowPrivateNetworkTargets) {
|
|
760
|
+
await page.goto(targetUrl, { waitUntil, timeout });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const typedPage = page;
|
|
764
|
+
await ensureNavigationSafetyGuard(typedPage, { allowPrivateNetworkTargets });
|
|
765
|
+
blockedNavigationErrors.delete(typedPage);
|
|
766
|
+
try {
|
|
767
|
+
await page.goto(targetUrl, { waitUntil, timeout });
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
rethrowWithBlockedNavigationError(typedPage, err);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
164
773
|
function annotateAriaYamlWithRefs(ariaYaml, refs) {
|
|
165
774
|
let annotatedYaml = ariaYaml || '';
|
|
166
775
|
if (!annotatedYaml || !refs || refs.size === 0)
|
|
@@ -200,40 +809,48 @@ function annotateAriaYamlWithRefs(ariaYaml, refs) {
|
|
|
200
809
|
async function waitForPageReady(page, options = {}) {
|
|
201
810
|
const { timeout = 10000, waitForNetwork = true } = options;
|
|
202
811
|
try {
|
|
203
|
-
await page
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Framework hydration wait (React/Next.js/Vue) - mirrors Swift WebView.swift logic
|
|
210
|
-
// Wait for readyState === 'complete' + network quiet (40 iterations × 250ms max)
|
|
211
|
-
await page
|
|
212
|
-
.evaluate(async () => {
|
|
213
|
-
const perf = globalThis.performance;
|
|
214
|
-
const doc = globalThis.document;
|
|
215
|
-
const raf = globalThis.requestAnimationFrame;
|
|
216
|
-
for (let i = 0; i < 40; i++) {
|
|
217
|
-
// Check if network is quiet (no recent resource loads)
|
|
218
|
-
const entries = perf.getEntriesByType('resource');
|
|
219
|
-
const recentEntries = entries.slice(-5);
|
|
220
|
-
const netQuiet = recentEntries.every((e) => (perf.now() - e.responseEnd) > 400);
|
|
221
|
-
if (doc.readyState === 'complete' && netQuiet) {
|
|
222
|
-
// Double RAF to ensure paint is complete
|
|
223
|
-
await new Promise((r) => raf(() => raf(() => r())));
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
812
|
+
await withBlockedNavigationTracking(page, async () => {
|
|
813
|
+
await page.waitForLoadState('domcontentloaded', { timeout });
|
|
814
|
+
if (waitForNetwork) {
|
|
815
|
+
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
|
816
|
+
(0, logging_1.log)('warn', 'networkidle timeout, continuing');
|
|
817
|
+
});
|
|
227
818
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
819
|
+
// Framework hydration wait (React/Next.js/Vue) - mirrors Swift WebView.swift logic
|
|
820
|
+
// Wait for readyState === 'complete' + network quiet (40 iterations × 250ms max)
|
|
821
|
+
await page
|
|
822
|
+
.evaluate(async () => {
|
|
823
|
+
const perf = globalThis.performance;
|
|
824
|
+
const doc = globalThis.document;
|
|
825
|
+
const raf = globalThis.requestAnimationFrame;
|
|
826
|
+
for (let i = 0; i < 40; i++) {
|
|
827
|
+
// Check if network is quiet (no recent resource loads)
|
|
828
|
+
const entries = perf.getEntriesByType('resource');
|
|
829
|
+
const recentEntries = entries.slice(-5);
|
|
830
|
+
const netQuiet = recentEntries.every((e) => (perf.now() - e.responseEnd) > 400);
|
|
831
|
+
if (doc.readyState === 'complete' && netQuiet) {
|
|
832
|
+
// Double RAF to ensure paint is complete
|
|
833
|
+
await new Promise((r) => raf(() => raf(() => r())));
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
837
|
+
}
|
|
838
|
+
})
|
|
839
|
+
.catch(() => {
|
|
840
|
+
(0, logging_1.log)('warn', 'hydration wait failed, continuing');
|
|
841
|
+
});
|
|
842
|
+
await page.waitForTimeout(200);
|
|
843
|
+
await dismissConsentDialogs(page);
|
|
231
844
|
});
|
|
232
|
-
await page.waitForTimeout(200);
|
|
233
|
-
await dismissConsentDialogs(page);
|
|
234
845
|
return true;
|
|
235
846
|
}
|
|
236
847
|
catch (err) {
|
|
848
|
+
if (typeof err === 'object' && err !== null && 'statusCode' in err && typeof err.statusCode === 'number') {
|
|
849
|
+
throw err;
|
|
850
|
+
}
|
|
851
|
+
const blockedError = takeBlockedNavigationError(page);
|
|
852
|
+
if (blockedError)
|
|
853
|
+
throw blockedError;
|
|
237
854
|
const message = err instanceof Error ? err.message : String(err);
|
|
238
855
|
(0, logging_1.log)('warn', 'page ready failed', { error: message });
|
|
239
856
|
return false;
|
|
@@ -409,7 +1026,76 @@ function attachConsoleListeners(state) {
|
|
|
409
1026
|
}
|
|
410
1027
|
});
|
|
411
1028
|
}
|
|
412
|
-
function
|
|
1029
|
+
async function ensureNavigationSafetyGuard(page, options = {}) {
|
|
1030
|
+
const { allowPrivateNetworkTargets = CONFIG.allowPrivateNetworkTargets } = options;
|
|
1031
|
+
if (allowPrivateNetworkTargets)
|
|
1032
|
+
return;
|
|
1033
|
+
const context = page.context();
|
|
1034
|
+
if (navigationGuardHandlers.has(context))
|
|
1035
|
+
return;
|
|
1036
|
+
const routeHandler = async (route) => {
|
|
1037
|
+
const request = route.request();
|
|
1038
|
+
if (typeof request.isNavigationRequest === 'function' && !request.isNavigationRequest()) {
|
|
1039
|
+
return route.continue();
|
|
1040
|
+
}
|
|
1041
|
+
const requestFrame = typeof request.frame === 'function' ? request.frame() : null;
|
|
1042
|
+
const requestPage = requestFrame && typeof requestFrame.page === 'function' ? requestFrame.page() : null;
|
|
1043
|
+
const relatedPages = new Set();
|
|
1044
|
+
let trackedToken = null;
|
|
1045
|
+
if (requestPage) {
|
|
1046
|
+
relatedPages.add(requestPage);
|
|
1047
|
+
trackedToken = await getCurrentTrackedActionToken(requestPage);
|
|
1048
|
+
const mappedOpener = popupOpenerPages.get(requestPage);
|
|
1049
|
+
if (mappedOpener) {
|
|
1050
|
+
relatedPages.add(mappedOpener);
|
|
1051
|
+
if (trackedToken === null) {
|
|
1052
|
+
trackedToken = await getCurrentTrackedActionToken(mappedOpener);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
else if (typeof requestPage.opener === 'function') {
|
|
1056
|
+
const openerPage = await requestPage.opener().catch(() => null);
|
|
1057
|
+
if (openerPage) {
|
|
1058
|
+
relatedPages.add(openerPage);
|
|
1059
|
+
if (trackedToken === null) {
|
|
1060
|
+
trackedToken = await getCurrentTrackedActionToken(openerPage);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
for (const relatedPage of relatedPages) {
|
|
1066
|
+
incrementInFlightGuardCheck(relatedPage);
|
|
1067
|
+
if (trackedToken !== null) {
|
|
1068
|
+
incrementTrackedInFlightGuardCheck(relatedPage, trackedToken);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
const requestError = await validateNavigationUrl(request.url(), { allowPrivateNetworkTargets: false });
|
|
1073
|
+
if (requestError) {
|
|
1074
|
+
for (const relatedPage of relatedPages) {
|
|
1075
|
+
if (trackedToken !== null) {
|
|
1076
|
+
setTrackedBlockedNavigationError(relatedPage, trackedToken, requestError);
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
blockedNavigationErrors.set(relatedPage, requestError);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return route.abort('blockedbyclient');
|
|
1083
|
+
}
|
|
1084
|
+
return route.continue();
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
for (const relatedPage of relatedPages) {
|
|
1088
|
+
decrementInFlightGuardCheck(relatedPage);
|
|
1089
|
+
if (trackedToken !== null) {
|
|
1090
|
+
decrementTrackedInFlightGuardCheck(relatedPage, trackedToken);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
await context.route('**/*', routeHandler);
|
|
1096
|
+
navigationGuardHandlers.set(context, routeHandler);
|
|
1097
|
+
}
|
|
1098
|
+
async function createTabState(page) {
|
|
413
1099
|
const state = {
|
|
414
1100
|
page,
|
|
415
1101
|
refs: new Map(),
|
|
@@ -419,6 +1105,10 @@ function createTabState(page) {
|
|
|
419
1105
|
pageErrors: [],
|
|
420
1106
|
};
|
|
421
1107
|
attachConsoleListeners(state);
|
|
1108
|
+
page.on('popup', (popupPage) => {
|
|
1109
|
+
popupOpenerPages.set(popupPage, page);
|
|
1110
|
+
});
|
|
1111
|
+
await ensureNavigationSafetyGuard(page, { allowPrivateNetworkTargets: CONFIG.allowPrivateNetworkTargets });
|
|
422
1112
|
return state;
|
|
423
1113
|
}
|
|
424
1114
|
async function navigateTab(tabId, tabState, params) {
|
|
@@ -430,7 +1120,7 @@ async function navigateTab(tabId, tabState, params) {
|
|
|
430
1120
|
if (!targetUrl) {
|
|
431
1121
|
throw new Error('url or macro required');
|
|
432
1122
|
}
|
|
433
|
-
const urlErr =
|
|
1123
|
+
const urlErr = await validateNavigationUrl(targetUrl, { allowPrivateNetworkTargets: params.allowPrivateNetworkTargets });
|
|
434
1124
|
if (urlErr) {
|
|
435
1125
|
const err = new Error(urlErr);
|
|
436
1126
|
// Used by routes to map to 400.
|
|
@@ -438,7 +1128,11 @@ async function navigateTab(tabId, tabState, params) {
|
|
|
438
1128
|
throw err;
|
|
439
1129
|
}
|
|
440
1130
|
return withTabLock(tabId, async () => {
|
|
441
|
-
await tabState.page
|
|
1131
|
+
await navigateWithSafetyGuard(tabState.page, targetUrl, {
|
|
1132
|
+
allowPrivateNetworkTargets: params.allowPrivateNetworkTargets,
|
|
1133
|
+
waitUntil: 'domcontentloaded',
|
|
1134
|
+
timeout: 30000,
|
|
1135
|
+
});
|
|
442
1136
|
tabState.visitedUrls.add(targetUrl);
|
|
443
1137
|
tabState.refs = await buildRefs(tabState.page);
|
|
444
1138
|
return { ok: true, url: tabState.page.url() };
|
|
@@ -454,6 +1148,19 @@ async function snapshotTab(tabState) {
|
|
|
454
1148
|
refsCount: tabState.refs.size,
|
|
455
1149
|
};
|
|
456
1150
|
}
|
|
1151
|
+
function buildSnapshotPayload(raw, offset = 0) {
|
|
1152
|
+
const windowed = (0, snapshot_1.windowSnapshot)(raw.snapshot, offset, CONFIG.maxSnapshotChars, CONFIG.snapshotTailChars);
|
|
1153
|
+
return {
|
|
1154
|
+
url: raw.url,
|
|
1155
|
+
snapshot: windowed.text,
|
|
1156
|
+
refsCount: raw.refsCount,
|
|
1157
|
+
offset: windowed.offset,
|
|
1158
|
+
truncated: windowed.truncated,
|
|
1159
|
+
totalChars: windowed.totalChars,
|
|
1160
|
+
hasMore: windowed.hasMore ?? false,
|
|
1161
|
+
nextOffset: windowed.nextOffset ?? null,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
457
1164
|
async function clickTab(tabId, tabState, params) {
|
|
458
1165
|
const { ref, selector } = params;
|
|
459
1166
|
if (!ref && !selector) {
|
|
@@ -462,63 +1169,69 @@ async function clickTab(tabId, tabState, params) {
|
|
|
462
1169
|
throw err;
|
|
463
1170
|
}
|
|
464
1171
|
return withTabLock(tabId, async () => {
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
(
|
|
487
|
-
|
|
488
|
-
|
|
1172
|
+
try {
|
|
1173
|
+
const dispatchMouseSequence = async (locator) => {
|
|
1174
|
+
const box = await locator.boundingBox();
|
|
1175
|
+
if (!box)
|
|
1176
|
+
throw new Error('Element not visible (no bounding box)');
|
|
1177
|
+
const x = box.x + box.width / 2;
|
|
1178
|
+
const y = box.y + box.height / 2;
|
|
1179
|
+
await tabState.page.mouse.move(x, y);
|
|
1180
|
+
await tabState.page.waitForTimeout(50);
|
|
1181
|
+
await tabState.page.mouse.down();
|
|
1182
|
+
await tabState.page.waitForTimeout(50);
|
|
1183
|
+
await tabState.page.mouse.up();
|
|
1184
|
+
(0, logging_1.log)('info', 'mouse sequence dispatched', { x: x.toFixed(0), y: y.toFixed(0) });
|
|
1185
|
+
};
|
|
1186
|
+
const doClick = async (locatorOrSelector, isLocator) => {
|
|
1187
|
+
const locator = isLocator ? locatorOrSelector : tabState.page.locator(locatorOrSelector);
|
|
1188
|
+
try {
|
|
1189
|
+
await locator.click({ timeout: 5000 });
|
|
1190
|
+
}
|
|
1191
|
+
catch (err) {
|
|
1192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1193
|
+
if (message.includes('intercepts pointer events')) {
|
|
1194
|
+
(0, logging_1.log)('warn', 'click intercepted, retrying with force');
|
|
1195
|
+
try {
|
|
1196
|
+
await locator.click({ timeout: 5000, force: true });
|
|
1197
|
+
}
|
|
1198
|
+
catch {
|
|
1199
|
+
(0, logging_1.log)('warn', 'force click failed, trying mouse sequence');
|
|
1200
|
+
await dispatchMouseSequence(locator);
|
|
1201
|
+
}
|
|
489
1202
|
}
|
|
490
|
-
|
|
491
|
-
(0, logging_1.log)('warn', '
|
|
1203
|
+
else if (message.includes('not visible') || message.includes('timeout')) {
|
|
1204
|
+
(0, logging_1.log)('warn', 'click timeout, trying mouse sequence');
|
|
492
1205
|
await dispatchMouseSequence(locator);
|
|
493
1206
|
}
|
|
1207
|
+
else {
|
|
1208
|
+
throw err;
|
|
1209
|
+
}
|
|
494
1210
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1211
|
+
};
|
|
1212
|
+
await withBlockedNavigationTracking(tabState.page, async () => {
|
|
1213
|
+
if (ref) {
|
|
1214
|
+
const locator = await refToLocator(tabState.page, ref, tabState.refs);
|
|
1215
|
+
if (!locator) {
|
|
1216
|
+
const maxRef = tabState.refs.size > 0 ? `e${tabState.refs.size}` : 'none';
|
|
1217
|
+
const err = new Error(`Unknown ref: ${ref} (valid refs: e1-${maxRef}, ${tabState.refs.size} total). Refs reset after navigation - call snapshot first.`);
|
|
1218
|
+
err.statusCode = 400;
|
|
1219
|
+
throw err;
|
|
1220
|
+
}
|
|
1221
|
+
await doClick(locator, true);
|
|
498
1222
|
}
|
|
499
1223
|
else {
|
|
500
|
-
|
|
1224
|
+
await doClick(selector, false);
|
|
501
1225
|
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const maxRef = tabState.refs.size > 0 ? `e${tabState.refs.size}` : 'none';
|
|
508
|
-
const err = new Error(`Unknown ref: ${ref} (valid refs: e1-${maxRef}, ${tabState.refs.size} total). Refs reset after navigation - call snapshot first.`);
|
|
509
|
-
err.statusCode = 400;
|
|
510
|
-
throw err;
|
|
511
|
-
}
|
|
512
|
-
await doClick(locator, true);
|
|
1226
|
+
});
|
|
1227
|
+
tabState.refs = await buildRefs(tabState.page);
|
|
1228
|
+
const newUrl = tabState.page.url();
|
|
1229
|
+
tabState.visitedUrls.add(newUrl);
|
|
1230
|
+
return { ok: true, url: newUrl };
|
|
513
1231
|
}
|
|
514
|
-
|
|
515
|
-
|
|
1232
|
+
catch (err) {
|
|
1233
|
+
rethrowWithBlockedNavigationError(tabState.page, err);
|
|
516
1234
|
}
|
|
517
|
-
await tabState.page.waitForTimeout(500);
|
|
518
|
-
tabState.refs = await buildRefs(tabState.page);
|
|
519
|
-
const newUrl = tabState.page.url();
|
|
520
|
-
tabState.visitedUrls.add(newUrl);
|
|
521
|
-
return { ok: true, url: newUrl };
|
|
522
1235
|
});
|
|
523
1236
|
}
|
|
524
1237
|
/**
|
|
@@ -619,21 +1332,33 @@ async function typeTab(tabId, tabState, params) {
|
|
|
619
1332
|
else {
|
|
620
1333
|
locator = tabState.page.locator(selector);
|
|
621
1334
|
}
|
|
622
|
-
await
|
|
1335
|
+
await withBlockedNavigationTracking(tabState.page, async () => {
|
|
1336
|
+
await smartFill(locator, tabState.page, text);
|
|
1337
|
+
});
|
|
623
1338
|
});
|
|
624
1339
|
return { ok: true };
|
|
625
1340
|
}
|
|
626
1341
|
async function pressTab(tabId, tabState, key) {
|
|
627
1342
|
await withTabLock(tabId, async () => {
|
|
628
|
-
|
|
1343
|
+
try {
|
|
1344
|
+
await withBlockedNavigationTracking(tabState.page, async () => {
|
|
1345
|
+
await tabState.page.keyboard.press(key);
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
catch (err) {
|
|
1349
|
+
rethrowWithBlockedNavigationError(tabState.page, err);
|
|
1350
|
+
}
|
|
629
1351
|
});
|
|
630
1352
|
return { ok: true };
|
|
631
1353
|
}
|
|
632
1354
|
async function scrollTab(tabState, params) {
|
|
633
1355
|
const { direction = 'down', amount = 500 } = params;
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
await tabState.page
|
|
1356
|
+
const isHorizontal = direction === 'left' || direction === 'right';
|
|
1357
|
+
const delta = direction === 'up' || direction === 'left' ? -amount : amount;
|
|
1358
|
+
await withBlockedNavigationTracking(tabState.page, async () => {
|
|
1359
|
+
await tabState.page.mouse.wheel(isHorizontal ? delta : 0, isHorizontal ? 0 : delta);
|
|
1360
|
+
await tabState.page.waitForTimeout(300);
|
|
1361
|
+
});
|
|
637
1362
|
return { ok: true };
|
|
638
1363
|
}
|
|
639
1364
|
async function scrollElementTab(tabId, tabState, params) {
|
|
@@ -666,28 +1391,30 @@ async function scrollElementTab(tabId, tabState, params) {
|
|
|
666
1391
|
}
|
|
667
1392
|
}
|
|
668
1393
|
const element = locator.first();
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1394
|
+
await withBlockedNavigationTracking(page, async () => {
|
|
1395
|
+
if (scrollTo) {
|
|
1396
|
+
await element.evaluate((el, pos) => {
|
|
1397
|
+
const e = el;
|
|
1398
|
+
const p = pos;
|
|
1399
|
+
if (p.top !== undefined)
|
|
1400
|
+
e.scrollTop = p.top;
|
|
1401
|
+
if (p.left !== undefined)
|
|
1402
|
+
e.scrollLeft = p.left;
|
|
1403
|
+
}, scrollTo);
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
const deltaX = params.deltaX ?? 0;
|
|
1407
|
+
const deltaY = params.deltaY ?? 300;
|
|
1408
|
+
await element.evaluate((el, delta) => {
|
|
1409
|
+
el.scrollBy({
|
|
1410
|
+
top: delta.y,
|
|
1411
|
+
left: delta.x,
|
|
1412
|
+
behavior: 'auto',
|
|
1413
|
+
});
|
|
1414
|
+
}, { x: deltaX, y: deltaY });
|
|
1415
|
+
}
|
|
1416
|
+
await page.waitForTimeout(200);
|
|
1417
|
+
});
|
|
691
1418
|
const scrollPosition = (await element.evaluate((el) => {
|
|
692
1419
|
const e = el;
|
|
693
1420
|
return {
|
|
@@ -717,7 +1444,9 @@ async function _evaluateInternal(tabId, tabState, params, config) {
|
|
|
717
1444
|
timeoutId = setTimeout(() => reject(new Error('EVAL_TIMEOUT')), timeout);
|
|
718
1445
|
});
|
|
719
1446
|
try {
|
|
720
|
-
const result = await
|
|
1447
|
+
const result = await withBlockedNavigationTracking(page, async () => {
|
|
1448
|
+
return Promise.race([page.evaluate(params.expression), timeoutPromise]);
|
|
1449
|
+
});
|
|
721
1450
|
const serialized = JSON.stringify(result);
|
|
722
1451
|
if (serialized === undefined) {
|
|
723
1452
|
return {
|
|
@@ -744,7 +1473,11 @@ async function _evaluateInternal(tabId, tabState, params, config) {
|
|
|
744
1473
|
};
|
|
745
1474
|
}
|
|
746
1475
|
catch (err) {
|
|
1476
|
+
if (typeof err === 'object' && err !== null && 'statusCode' in err && typeof err.statusCode === 'number') {
|
|
1477
|
+
throw err;
|
|
1478
|
+
}
|
|
747
1479
|
const message = err instanceof Error ? err.message : String(err);
|
|
1480
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
748
1481
|
if (message === 'EVAL_TIMEOUT') {
|
|
749
1482
|
return {
|
|
750
1483
|
ok: false,
|
|
@@ -772,23 +1505,38 @@ async function evaluateTabExtended(tabId, tabState, params) {
|
|
|
772
1505
|
}
|
|
773
1506
|
async function backTab(tabId, tabState) {
|
|
774
1507
|
return withTabLock(tabId, async () => {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1508
|
+
try {
|
|
1509
|
+
await tabState.page.goBack({ timeout: 10000 });
|
|
1510
|
+
tabState.refs = await buildRefs(tabState.page);
|
|
1511
|
+
return { ok: true, url: tabState.page.url() };
|
|
1512
|
+
}
|
|
1513
|
+
catch (err) {
|
|
1514
|
+
rethrowWithBlockedNavigationError(tabState.page, err);
|
|
1515
|
+
}
|
|
778
1516
|
});
|
|
779
1517
|
}
|
|
780
1518
|
async function forwardTab(tabId, tabState) {
|
|
781
1519
|
return withTabLock(tabId, async () => {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1520
|
+
try {
|
|
1521
|
+
await tabState.page.goForward({ timeout: 10000 });
|
|
1522
|
+
tabState.refs = await buildRefs(tabState.page);
|
|
1523
|
+
return { ok: true, url: tabState.page.url() };
|
|
1524
|
+
}
|
|
1525
|
+
catch (err) {
|
|
1526
|
+
rethrowWithBlockedNavigationError(tabState.page, err);
|
|
1527
|
+
}
|
|
785
1528
|
});
|
|
786
1529
|
}
|
|
787
1530
|
async function refreshTab(tabId, tabState) {
|
|
788
1531
|
return withTabLock(tabId, async () => {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1532
|
+
try {
|
|
1533
|
+
await tabState.page.reload({ timeout: 30000 });
|
|
1534
|
+
tabState.refs = await buildRefs(tabState.page);
|
|
1535
|
+
return { ok: true, url: tabState.page.url() };
|
|
1536
|
+
}
|
|
1537
|
+
catch (err) {
|
|
1538
|
+
rethrowWithBlockedNavigationError(tabState.page, err);
|
|
1539
|
+
}
|
|
792
1540
|
});
|
|
793
1541
|
}
|
|
794
1542
|
async function getLinks(tabState, params) {
|