camofox-browser 2.1.0 → 2.4.1
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 +126 -0
- package/README.md +304 -33
- 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 +428 -53
- 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 +244 -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 +19 -3
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +248 -65
- 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 +84 -2
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +349 -47
- 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 +877 -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 +162 -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,61 +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
|
-
throw new Error(`Unknown ref: ${ref} (valid refs: e1-${maxRef}, ${tabState.refs.size} total). Refs reset after navigation - call snapshot first.`);
|
|
509
|
-
}
|
|
510
|
-
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 };
|
|
511
1231
|
}
|
|
512
|
-
|
|
513
|
-
|
|
1232
|
+
catch (err) {
|
|
1233
|
+
rethrowWithBlockedNavigationError(tabState.page, err);
|
|
514
1234
|
}
|
|
515
|
-
await tabState.page.waitForTimeout(500);
|
|
516
|
-
tabState.refs = await buildRefs(tabState.page);
|
|
517
|
-
const newUrl = tabState.page.url();
|
|
518
|
-
tabState.visitedUrls.add(newUrl);
|
|
519
|
-
return { ok: true, url: newUrl };
|
|
520
1235
|
});
|
|
521
1236
|
}
|
|
522
1237
|
/**
|
|
@@ -607,28 +1322,43 @@ async function typeTab(tabId, tabState, params) {
|
|
|
607
1322
|
let locator;
|
|
608
1323
|
if (ref) {
|
|
609
1324
|
const resolved = await refToLocator(tabState.page, ref, tabState.refs);
|
|
610
|
-
if (!resolved)
|
|
611
|
-
|
|
1325
|
+
if (!resolved) {
|
|
1326
|
+
const err = new Error(`Unknown ref: ${ref}. Call snapshot first.`);
|
|
1327
|
+
err.statusCode = 400;
|
|
1328
|
+
throw err;
|
|
1329
|
+
}
|
|
612
1330
|
locator = resolved;
|
|
613
1331
|
}
|
|
614
1332
|
else {
|
|
615
1333
|
locator = tabState.page.locator(selector);
|
|
616
1334
|
}
|
|
617
|
-
await
|
|
1335
|
+
await withBlockedNavigationTracking(tabState.page, async () => {
|
|
1336
|
+
await smartFill(locator, tabState.page, text);
|
|
1337
|
+
});
|
|
618
1338
|
});
|
|
619
1339
|
return { ok: true };
|
|
620
1340
|
}
|
|
621
1341
|
async function pressTab(tabId, tabState, key) {
|
|
622
1342
|
await withTabLock(tabId, async () => {
|
|
623
|
-
|
|
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
|
+
}
|
|
624
1351
|
});
|
|
625
1352
|
return { ok: true };
|
|
626
1353
|
}
|
|
627
1354
|
async function scrollTab(tabState, params) {
|
|
628
1355
|
const { direction = 'down', amount = 500 } = params;
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
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
|
+
});
|
|
632
1362
|
return { ok: true };
|
|
633
1363
|
}
|
|
634
1364
|
async function scrollElementTab(tabId, tabState, params) {
|
|
@@ -661,28 +1391,30 @@ async function scrollElementTab(tabId, tabState, params) {
|
|
|
661
1391
|
}
|
|
662
1392
|
}
|
|
663
1393
|
const element = locator.first();
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
+
});
|
|
686
1418
|
const scrollPosition = (await element.evaluate((el) => {
|
|
687
1419
|
const e = el;
|
|
688
1420
|
return {
|
|
@@ -712,7 +1444,9 @@ async function _evaluateInternal(tabId, tabState, params, config) {
|
|
|
712
1444
|
timeoutId = setTimeout(() => reject(new Error('EVAL_TIMEOUT')), timeout);
|
|
713
1445
|
});
|
|
714
1446
|
try {
|
|
715
|
-
const result = await
|
|
1447
|
+
const result = await withBlockedNavigationTracking(page, async () => {
|
|
1448
|
+
return Promise.race([page.evaluate(params.expression), timeoutPromise]);
|
|
1449
|
+
});
|
|
716
1450
|
const serialized = JSON.stringify(result);
|
|
717
1451
|
if (serialized === undefined) {
|
|
718
1452
|
return {
|
|
@@ -739,7 +1473,11 @@ async function _evaluateInternal(tabId, tabState, params, config) {
|
|
|
739
1473
|
};
|
|
740
1474
|
}
|
|
741
1475
|
catch (err) {
|
|
1476
|
+
if (typeof err === 'object' && err !== null && 'statusCode' in err && typeof err.statusCode === 'number') {
|
|
1477
|
+
throw err;
|
|
1478
|
+
}
|
|
742
1479
|
const message = err instanceof Error ? err.message : String(err);
|
|
1480
|
+
throwBlockedNavigationErrorIfPresent(page);
|
|
743
1481
|
if (message === 'EVAL_TIMEOUT') {
|
|
744
1482
|
return {
|
|
745
1483
|
ok: false,
|
|
@@ -767,23 +1505,38 @@ async function evaluateTabExtended(tabId, tabState, params) {
|
|
|
767
1505
|
}
|
|
768
1506
|
async function backTab(tabId, tabState) {
|
|
769
1507
|
return withTabLock(tabId, async () => {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
+
}
|
|
773
1516
|
});
|
|
774
1517
|
}
|
|
775
1518
|
async function forwardTab(tabId, tabState) {
|
|
776
1519
|
return withTabLock(tabId, async () => {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
+
}
|
|
780
1528
|
});
|
|
781
1529
|
}
|
|
782
1530
|
async function refreshTab(tabId, tabState) {
|
|
783
1531
|
return withTabLock(tabId, async () => {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
+
}
|
|
787
1540
|
});
|
|
788
1541
|
}
|
|
789
1542
|
async function getLinks(tabState, params) {
|