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.
Files changed (102) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +304 -33
  3. package/dist/src/cli/commands/content.d.ts.map +1 -1
  4. package/dist/src/cli/commands/content.js +37 -0
  5. package/dist/src/cli/commands/content.js.map +1 -1
  6. package/dist/src/cli/commands/core.d.ts.map +1 -1
  7. package/dist/src/cli/commands/core.js +21 -4
  8. package/dist/src/cli/commands/core.js.map +1 -1
  9. package/dist/src/cli/commands/interaction.d.ts.map +1 -1
  10. package/dist/src/cli/commands/interaction.js +5 -14
  11. package/dist/src/cli/commands/interaction.js.map +1 -1
  12. package/dist/src/cli/commands/navigation.d.ts.map +1 -1
  13. package/dist/src/cli/commands/navigation.js +12 -6
  14. package/dist/src/cli/commands/navigation.js.map +1 -1
  15. package/dist/src/cli/commands/server.d.ts.map +1 -1
  16. package/dist/src/cli/commands/server.js +9 -3
  17. package/dist/src/cli/commands/server.js.map +1 -1
  18. package/dist/src/cli/commands/session.d.ts.map +1 -1
  19. package/dist/src/cli/commands/session.js +23 -5
  20. package/dist/src/cli/commands/session.js.map +1 -1
  21. package/dist/src/cli/server/manager.d.ts +1 -0
  22. package/dist/src/cli/server/manager.d.ts.map +1 -1
  23. package/dist/src/cli/server/manager.js +7 -12
  24. package/dist/src/cli/server/manager.js.map +1 -1
  25. package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
  26. package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
  27. package/dist/src/middleware/lifecycle-activity.js +21 -0
  28. package/dist/src/middleware/lifecycle-activity.js.map +1 -0
  29. package/dist/src/openapi/spec.d.ts +4 -0
  30. package/dist/src/openapi/spec.d.ts.map +1 -0
  31. package/dist/src/openapi/spec.js +730 -0
  32. package/dist/src/openapi/spec.js.map +1 -0
  33. package/dist/src/routes/core.d.ts.map +1 -1
  34. package/dist/src/routes/core.js +428 -53
  35. package/dist/src/routes/core.js.map +1 -1
  36. package/dist/src/routes/docs.d.ts +3 -0
  37. package/dist/src/routes/docs.d.ts.map +1 -0
  38. package/dist/src/routes/docs.js +23 -0
  39. package/dist/src/routes/docs.js.map +1 -0
  40. package/dist/src/routes/openclaw.d.ts.map +1 -1
  41. package/dist/src/routes/openclaw.js +244 -90
  42. package/dist/src/routes/openclaw.js.map +1 -1
  43. package/dist/src/server.js +55 -4
  44. package/dist/src/server.js.map +1 -1
  45. package/dist/src/services/context-pool.d.ts +19 -3
  46. package/dist/src/services/context-pool.d.ts.map +1 -1
  47. package/dist/src/services/context-pool.js +248 -65
  48. package/dist/src/services/context-pool.js.map +1 -1
  49. package/dist/src/services/download.d.ts +2 -0
  50. package/dist/src/services/download.d.ts.map +1 -1
  51. package/dist/src/services/download.js +110 -80
  52. package/dist/src/services/download.js.map +1 -1
  53. package/dist/src/services/lifecycle-controller.d.ts +40 -0
  54. package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
  55. package/dist/src/services/lifecycle-controller.js +106 -0
  56. package/dist/src/services/lifecycle-controller.js.map +1 -0
  57. package/dist/src/services/resource-extractor.d.ts +1 -0
  58. package/dist/src/services/resource-extractor.d.ts.map +1 -1
  59. package/dist/src/services/resource-extractor.js +7 -0
  60. package/dist/src/services/resource-extractor.js.map +1 -1
  61. package/dist/src/services/session.d.ts +84 -2
  62. package/dist/src/services/session.d.ts.map +1 -1
  63. package/dist/src/services/session.js +349 -47
  64. package/dist/src/services/session.js.map +1 -1
  65. package/dist/src/services/structured-extractor.d.ts +39 -0
  66. package/dist/src/services/structured-extractor.d.ts.map +1 -0
  67. package/dist/src/services/structured-extractor.js +487 -0
  68. package/dist/src/services/structured-extractor.js.map +1 -0
  69. package/dist/src/services/tab.d.ts +30 -3
  70. package/dist/src/services/tab.d.ts.map +1 -1
  71. package/dist/src/services/tab.js +877 -124
  72. package/dist/src/services/tab.js.map +1 -1
  73. package/dist/src/services/tracing.d.ts +7 -0
  74. package/dist/src/services/tracing.d.ts.map +1 -1
  75. package/dist/src/services/tracing.js +162 -19
  76. package/dist/src/services/tracing.js.map +1 -1
  77. package/dist/src/services/vnc.d.ts.map +1 -1
  78. package/dist/src/services/vnc.js +5 -3
  79. package/dist/src/services/vnc.js.map +1 -1
  80. package/dist/src/services/youtube.js +1 -1
  81. package/dist/src/services/youtube.js.map +1 -1
  82. package/dist/src/types.d.ts +71 -1
  83. package/dist/src/types.d.ts.map +1 -1
  84. package/dist/src/utils/config.d.ts +79 -3
  85. package/dist/src/utils/config.d.ts.map +1 -1
  86. package/dist/src/utils/config.js +145 -3
  87. package/dist/src/utils/config.js.map +1 -1
  88. package/dist/src/utils/presets.d.ts.map +1 -1
  89. package/dist/src/utils/presets.js +3 -1
  90. package/dist/src/utils/presets.js.map +1 -1
  91. package/dist/src/utils/proxy-profiles.d.ts +18 -0
  92. package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
  93. package/dist/src/utils/proxy-profiles.js +197 -0
  94. package/dist/src/utils/proxy-profiles.js.map +1 -0
  95. package/dist/src/utils/sidecar-version.d.ts +12 -0
  96. package/dist/src/utils/sidecar-version.d.ts.map +1 -0
  97. package/dist/src/utils/sidecar-version.js +63 -0
  98. package/dist/src/utils/sidecar-version.js.map +1 -0
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/openclaw.plugin.json +39 -0
  101. package/package.json +16 -4
  102. package/plugin.ts +949 -0
@@ -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 DEFAULT_MAX_SNAPSHOT_NODES = 2000;
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 = Math.max(100, parseInt(process.env.CAMOFOX_CONSOLE_BUFFER_SIZE || '1000', 10));
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 validateUrl(url) {
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.waitForLoadState('domcontentloaded', { timeout });
204
- if (waitForNetwork) {
205
- await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
206
- (0, logging_1.log)('warn', 'networkidle timeout, continuing');
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
- .catch(() => {
230
- (0, logging_1.log)('warn', 'hydration wait failed, continuing');
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 createTabState(page) {
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 = validateUrl(targetUrl);
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.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
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
- const dispatchMouseSequence = async (locator) => {
466
- const box = await locator.boundingBox();
467
- if (!box)
468
- throw new Error('Element not visible (no bounding box)');
469
- const x = box.x + box.width / 2;
470
- const y = box.y + box.height / 2;
471
- await tabState.page.mouse.move(x, y);
472
- await tabState.page.waitForTimeout(50);
473
- await tabState.page.mouse.down();
474
- await tabState.page.waitForTimeout(50);
475
- await tabState.page.mouse.up();
476
- (0, logging_1.log)('info', 'mouse sequence dispatched', { x: x.toFixed(0), y: y.toFixed(0) });
477
- };
478
- const doClick = async (locatorOrSelector, isLocator) => {
479
- const locator = isLocator ? locatorOrSelector : tabState.page.locator(locatorOrSelector);
480
- try {
481
- await locator.click({ timeout: 5000 });
482
- }
483
- catch (err) {
484
- const message = err instanceof Error ? err.message : String(err);
485
- if (message.includes('intercepts pointer events')) {
486
- (0, logging_1.log)('warn', 'click intercepted, retrying with force');
487
- try {
488
- await locator.click({ timeout: 5000, force: true });
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
- catch {
491
- (0, logging_1.log)('warn', 'force click failed, trying mouse sequence');
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
- else if (message.includes('not visible') || message.includes('timeout')) {
496
- (0, logging_1.log)('warn', 'click timeout, trying mouse sequence');
497
- await dispatchMouseSequence(locator);
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
- throw err;
1224
+ await doClick(selector, false);
501
1225
  }
502
- }
503
- };
504
- if (ref) {
505
- const locator = await refToLocator(tabState.page, ref, tabState.refs);
506
- if (!locator) {
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
- else {
513
- await doClick(selector, false);
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
- throw new Error(`Unknown ref: ${ref}`);
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 smartFill(locator, tabState.page, text);
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
- await tabState.page.keyboard.press(key);
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 delta = direction === 'up' ? -amount : amount;
630
- await tabState.page.mouse.wheel(0, delta);
631
- await tabState.page.waitForTimeout(300);
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
- if (scrollTo) {
665
- await element.evaluate((el, pos) => {
666
- const e = el;
667
- const p = pos;
668
- if (p.top !== undefined)
669
- e.scrollTop = p.top;
670
- if (p.left !== undefined)
671
- e.scrollLeft = p.left;
672
- }, scrollTo);
673
- }
674
- else {
675
- const deltaX = params.deltaX ?? 0;
676
- const deltaY = params.deltaY ?? 300;
677
- await element.evaluate((el, delta) => {
678
- el.scrollBy({
679
- top: delta.y,
680
- left: delta.x,
681
- behavior: 'auto',
682
- });
683
- }, { x: deltaX, y: deltaY });
684
- }
685
- await page.waitForTimeout(200);
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 Promise.race([page.evaluate(params.expression), timeoutPromise]);
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
- await tabState.page.goBack({ timeout: 10000 });
771
- tabState.refs = await buildRefs(tabState.page);
772
- return { ok: true, url: tabState.page.url() };
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
- await tabState.page.goForward({ timeout: 10000 });
778
- tabState.refs = await buildRefs(tabState.page);
779
- return { ok: true, url: tabState.page.url() };
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
- await tabState.page.reload({ timeout: 30000 });
785
- tabState.refs = await buildRefs(tabState.page);
786
- return { ok: true, url: tabState.page.url() };
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) {