@fanboynz/network-scanner 2.0.50 → 2.0.52

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/lib/cloudflare.js CHANGED
@@ -58,11 +58,76 @@ const FAST_TIMEOUTS = {
58
58
  ELEMENT_INTERACTION_DELAY: 250, // Fast element interactions
59
59
  SELECTOR_WAIT: 3000, // Fast selector waits
60
60
  TURNSTILE_OPERATION: 6000, // Fast Turnstile operations
61
- JS_CHALLENGE: 19000, // Fast JS challenge completion
61
+ JS_CHALLENGE: 10000, // Fast JS challenge completion
62
62
  CHALLENGE_SOLVING: 30000, // Fast overall challenge solving
63
63
  CHALLENGE_COMPLETION: 8000 // Fast completion check
64
64
  };
65
65
 
66
+ /**
67
+ * Finds and clicks an element inside shadow DOM trees via page.evaluate
68
+ * Returns {found, clicked, x, y} - coordinates allow fallback mouse.click
69
+ */
70
+ async function clickInShadowDOM(context, selectors, forceDebug = false, waitMs = 1500) {
71
+ // Try Puppeteer's pierce/ selector first � handles CLOSED shadow roots via CDP
72
+ for (const selector of selectors) {
73
+ try {
74
+ // Wait for element to appear (handles delayed rendering)
75
+ const start = Date.now();
76
+ const element = await context.waitForSelector(`pierce/${selector}`, { timeout: waitMs });
77
+ if (element) {
78
+ const box = await element.boundingBox();
79
+ if (box && box.width > 0 && box.height > 0) {
80
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} matched in ${Date.now() - start}ms � box: ${box.width}x${box.height} at (${box.x},${box.y})`));
81
+ await element.click();
82
+ await element.dispose();
83
+ return { found: true, clicked: true, selector, x: box.x + box.width / 2, y: box.y + box.height / 2 };
84
+ }
85
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} found but not visible (0x0)`));
86
+ await element.dispose();
87
+ // Element found but not visible
88
+ return { found: true, clicked: false, selector, x: 0, y: 0 };
89
+ }
90
+ } catch (e) {
91
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} timeout after ${waitMs}ms`));
92
+ continue;
93
+ }
94
+ }
95
+
96
+ // Fallback: manual traversal for open shadow roots
97
+ const result = await context.evaluate((sels) => {
98
+ function deepQuery(root, selector) {
99
+ // Try direct query first
100
+ const el = root.querySelector(selector);
101
+ if (el) return el;
102
+
103
+ // Traverse shadow roots
104
+ const allElements = root.querySelectorAll('*');
105
+ for (const node of allElements) {
106
+ if (node.shadowRoot) {
107
+ const found = deepQuery(node.shadowRoot, selector);
108
+ if (found) return found;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+
114
+ for (const selector of sels) {
115
+ const el = deepQuery(document, selector);
116
+ if (el) {
117
+ const rect = el.getBoundingClientRect();
118
+ if (rect.width > 0 && rect.height > 0) {
119
+ el.click();
120
+ return { found: true, clicked: true, selector, x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
121
+ }
122
+ return { found: true, clicked: false, selector, x: 0, y: 0 };
123
+ }
124
+ }
125
+ return { found: false, clicked: false, selector: null, x: 0, y: 0 };
126
+ }, selectors);
127
+
128
+ return result;
129
+ }
130
+
66
131
  /**
67
132
  * Error categories for better handling
68
133
  */
@@ -306,12 +371,12 @@ function categorizeError(error) {
306
371
  /**
307
372
  * Implements exponential backoff delay
308
373
  */
309
- async function getRetryDelay(attempt) {
374
+ function getRetryDelay(attempt) {
310
375
  const delay = Math.min(
311
376
  RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffMultiplier, attempt - 1),
312
377
  RETRY_CONFIG.maxDelay
313
378
  );
314
- return new Promise(resolve => setTimeout(resolve, delay));
379
+ return delay;
315
380
  }
316
381
 
317
382
  /**
@@ -341,32 +406,6 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
341
406
  throw new Error('Page URL access failed - likely detached');
342
407
  }
343
408
 
344
- // Quick execution context validation with timeout
345
- const contextValid = await Promise.race([
346
- page.evaluate(() => {
347
- try {
348
- // Quick context validation
349
- if (typeof window === 'undefined' || !document) {
350
- return false;
351
- }
352
- // Check if document is ready for interaction
353
- if (document.readyState === 'uninitialized') {
354
- return false;
355
- }
356
- return true;
357
- } catch (e) {
358
- return false;
359
- }
360
- }),
361
- new Promise((_, reject) => {
362
- setTimeout(() => reject(new Error('Context validation timeout')), 3500);
363
- })
364
- ]).catch(() => false);
365
-
366
- if (!contextValid) {
367
- throw new Error('Page execution context is invalid');
368
- }
369
-
370
409
  const result = await Promise.race([
371
410
  page.evaluate(func),
372
411
  new Promise((_, reject) => {
@@ -418,7 +457,7 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
418
457
  }
419
458
 
420
459
  // Wait before retrying with exponential backoff
421
- await getRetryDelay(attempt);
460
+ await new Promise(resolve => setTimeout(resolve, getRetryDelay(attempt)));
422
461
  }
423
462
  }
424
463
 
@@ -440,15 +479,18 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
440
479
  * Safe element clicking with timeout protection
441
480
  */
442
481
  async function safeClick(page, selector, timeout = TIMEOUTS.CLICK_TIMEOUT) {
482
+ let timeoutId;
443
483
  try {
444
484
  return await Promise.race([
445
485
  page.click(selector, { timeout: timeout }),
446
486
  new Promise((_, reject) => {
447
- setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER);
487
+ timeoutId = setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER);
448
488
  })
449
489
  ]);
450
490
  } catch (error) {
451
491
  throw new Error(`Click failed: ${error.message}`);
492
+ } finally {
493
+ if (timeoutId) clearTimeout(timeoutId);
452
494
  }
453
495
  }
454
496
 
@@ -456,16 +498,18 @@ async function safeClick(page, selector, timeout = TIMEOUTS.CLICK_TIMEOUT) {
456
498
  * Safe navigation waiting with timeout protection
457
499
  */
458
500
  async function safeWaitForNavigation(page, timeout = TIMEOUTS.NAVIGATION_TIMEOUT) {
501
+ let timeoutId;
459
502
  try {
460
503
  return await Promise.race([
461
504
  page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: timeout }),
462
505
  new Promise((_, reject) => {
463
- setTimeout(() => reject(new Error('Navigation timeout')), timeout + TIMEOUTS.NAVIGATION_TIMEOUT_BUFFER);
506
+ timeoutId = setTimeout(() => reject(new Error('Navigation timeout')), timeout + TIMEOUTS.NAVIGATION_TIMEOUT_BUFFER);
464
507
  })
465
508
  ]);
466
509
  } catch (error) {
467
510
  console.warn(formatLogMessage('cloudflare', `Navigation wait failed: ${error.message}`));
468
- // Don't throw - just continue
511
+ } finally {
512
+ if (timeoutId) clearTimeout(timeoutId);
469
513
  }
470
514
  }
471
515
 
@@ -563,7 +607,14 @@ async function quickCloudflareDetection(page, forceDebug = false) {
563
607
  */
564
608
  async function analyzeCloudflareChallenge(page) {
565
609
  try {
566
- return await safePageEvaluate(page, () => {
610
+ // CDP-level frame check bypasses closed shadow roots
611
+ const frames = page.frames();
612
+ const hasChallengeFrame = frames.some(f => {
613
+ const url = f.url();
614
+ return url.includes('challenges.cloudflare.com') || url.includes('/cdn-cgi/challenge-platform/');
615
+ });
616
+
617
+ const result = await safePageEvaluate(page, () => {
567
618
  const title = document.title || '';
568
619
  const bodyText = document.body ? document.body.textContent : '';
569
620
 
@@ -635,6 +686,15 @@ async function analyzeCloudflareChallenge(page) {
635
686
  bodySnippet: bodyText.substring(0, 200)
636
687
  };
637
688
  }, TIMEOUTS.PAGE_EVALUATION);
689
+
690
+ // Merge CDP frame detection � catches iframes behind closed shadow roots
691
+ if (hasChallengeFrame && !result.hasTurnstileIframe) {
692
+ result.hasTurnstileIframe = true;
693
+ result.isTurnstile = true;
694
+ result.isChallengePresent = true;
695
+ }
696
+
697
+ return result;
638
698
  } catch (error) {
639
699
  return {
640
700
  isChallengePresent: false,
@@ -842,7 +902,7 @@ async function handleVerificationChallengeWithRetries(page, currentUrl, siteConf
842
902
 
843
903
  // If this wasn't the last attempt, wait before retrying
844
904
  if (attempt < retryConfig.maxAttempts) {
845
- const delay = await getRetryDelay(attempt);
905
+ const delay = getRetryDelay(attempt);
846
906
  if (forceDebug) {
847
907
  console.log(formatLogMessage('cloudflare', `Challenge attempt ${attempt} failed, retrying in ${delay}ms: ${result.error}`));
848
908
  }
@@ -884,7 +944,7 @@ async function handleVerificationChallengeWithRetries(page, currentUrl, siteConf
884
944
 
885
945
  // Wait before retrying with exponential backoff
886
946
  if (attempt < retryConfig.maxAttempts) {
887
- await getRetryDelay(attempt);
947
+ await new Promise(resolve => setTimeout(resolve, getRetryDelay(attempt)));
888
948
  }
889
949
  }
890
950
  }
@@ -925,7 +985,7 @@ async function handlePhishingWarningWithRetries(page, currentUrl, siteConfig, fo
925
985
 
926
986
  // If this wasn't the last attempt, wait before retrying
927
987
  if (attempt < retryConfig.maxAttempts) {
928
- const delay = await getRetryDelay(attempt);
988
+ const delay = getRetryDelay(attempt);
929
989
  if (forceDebug) {
930
990
  console.log(formatLogMessage('cloudflare', `Phishing warning attempt ${attempt} failed, retrying in ${delay}ms: ${result.error}`));
931
991
  }
@@ -955,7 +1015,7 @@ async function handlePhishingWarningWithRetries(page, currentUrl, siteConfig, fo
955
1015
 
956
1016
  // Wait before retrying with exponential backoff
957
1017
  if (attempt < retryConfig.maxAttempts) {
958
- await getRetryDelay(attempt);
1018
+ await new Promise(resolve => setTimeout(resolve, getRetryDelay(attempt)));
959
1019
  }
960
1020
  }
961
1021
  }
@@ -1026,6 +1086,23 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
1026
1086
 
1027
1087
  const jsResult = await waitForJSChallengeCompletion(page, forceDebug);
1028
1088
  if (jsResult.success) {
1089
+ // Wait for redirect after challenge completion
1090
+ try {
1091
+ const startUrl = await page.url();
1092
+ await page.waitForFunction(
1093
+ (origUrl) => {
1094
+ const bodyText = document.body?.textContent || '';
1095
+ return document.title !== 'Just a moment...' ||
1096
+ window.location.href !== origUrl ||
1097
+ bodyText.includes('Verification successful');
1098
+ },
1099
+ { timeout: 10000 },
1100
+ startUrl
1101
+ );
1102
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge page cleared for ${currentUrl}`));
1103
+ } catch (_) {
1104
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge page not cleared after 10s � continuing`));
1105
+ }
1029
1106
  result.success = true;
1030
1107
  result.method = 'js_challenge_wait';
1031
1108
  if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge completed successfully for ${currentUrl}`));
@@ -1034,6 +1111,8 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
1034
1111
  } catch (jsError) {
1035
1112
  if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge wait failed for ${currentUrl}: ${jsError.message}`));
1036
1113
  }
1114
+ } else if (forceDebug) {
1115
+ console.log(formatLogMessage('cloudflare', `Skipping JS challenge method (not detected)`));
1037
1116
  }
1038
1117
 
1039
1118
  // Method 2: Handle Turnstile challenges (interactive)
@@ -1051,6 +1130,8 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
1051
1130
  } catch (turnstileError) {
1052
1131
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Turnstile method failed for ${currentUrl}: ${turnstileError.message}`));
1053
1132
  }
1133
+ } else if (forceDebug) {
1134
+ console.log(formatLogMessage('cloudflare', `Skipping Turnstile method (not detected)`));
1054
1135
  }
1055
1136
 
1056
1137
  // Method 3: Legacy checkbox interaction (fallback)
@@ -1068,10 +1149,23 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
1068
1149
  } catch (legacyError) {
1069
1150
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Legacy checkbox method failed for ${currentUrl}: ${legacyError.message}`));
1070
1151
  }
1152
+ } else if (forceDebug) {
1153
+ console.log(formatLogMessage('cloudflare', `Skipping legacy checkbox method (not detected)`));
1071
1154
  }
1072
1155
 
1073
1156
  if (!result.success) {
1074
1157
  result.error = result.error || 'All challenge bypass methods failed';
1158
+ if (forceDebug) {
1159
+ try {
1160
+ const postState = await page.evaluate(() => ({
1161
+ title: document.title,
1162
+ url: window.location.href,
1163
+ body: (document.body?.textContent || '').substring(0, 300)
1164
+ }));
1165
+ console.log(formatLogMessage('cloudflare', `Post-attempt page state: title="${postState.title}" url=${postState.url}`));
1166
+ console.log(formatLogMessage('cloudflare', `Post-attempt body: ${postState.body}`));
1167
+ } catch (_) {}
1168
+ }
1075
1169
  }
1076
1170
 
1077
1171
  return result;
@@ -1089,88 +1183,57 @@ async function handleEmbeddedIframeChallenge(page, forceDebug = false) {
1089
1183
  try {
1090
1184
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Checking for embedded iframe challenges`));
1091
1185
 
1092
- // Enhanced iframe selectors including challenges.cloudflare.com
1093
- const iframeSelectors = [
1094
- 'iframe[src*="challenges.cloudflare.com"]',
1095
- 'iframe[title*="Verify you are human"]',
1096
- 'iframe[title*="Cloudflare security challenge"]',
1097
- 'iframe[title*="Widget containing a Cloudflare"]'
1098
- ];
1099
-
1100
- // Wait for iframe to appear
1101
- let iframeFound = false;
1102
- for (const selector of iframeSelectors) {
1103
- try {
1104
- await Promise.race([
1105
- page.waitForSelector(selector, { timeout: FAST_TIMEOUTS.SELECTOR_WAIT }),
1106
- new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), FAST_TIMEOUTS.SELECTOR_WAIT + 1000))
1107
- ]);
1108
- iframeFound = true;
1109
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Found iframe: ${selector}`));
1110
- break;
1111
- } catch (e) {
1112
- continue;
1186
+ // Use CDP-level frame detection � bypasses closed shadow roots
1187
+ const frames = page.frames();
1188
+ if (forceDebug) {
1189
+ console.log(formatLogMessage('cloudflare', `Available frames (${frames.length}):`));
1190
+ for (const f of frames) {
1191
+ console.log(formatLogMessage('cloudflare', ` ${f.url()}`));
1113
1192
  }
1114
1193
  }
1115
-
1116
- if (!iframeFound) {
1117
- result.error = 'No embedded iframe found';
1118
- return result;
1119
- }
1120
-
1121
- // Find challenge frame using existing frame detection logic
1122
- const frames = await page.frames();
1123
1194
  const challengeFrame = frames.find(frame => {
1124
1195
  const frameUrl = frame.url();
1125
1196
  return frameUrl.includes('challenges.cloudflare.com') ||
1197
+ frameUrl.includes('/cdn-cgi/challenge-platform/') ||
1126
1198
  frameUrl.includes('/turnstile/if/') ||
1127
- frameUrl.includes('captcha-delivery.com') ||
1128
- frameUrl.includes('/challenge-platform/') ||
1129
1199
  frameUrl.includes('turnstile');
1130
1200
  });
1131
1201
 
1132
1202
  if (!challengeFrame) {
1133
- result.error = 'Challenge iframe not accessible';
1203
+ result.error = 'No challenge frame found via CDP';
1134
1204
  return result;
1135
1205
  }
1136
1206
 
1137
1207
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Interacting with iframe: ${challengeFrame.url()}`));
1138
1208
 
1139
- // Reuse existing checkbox interaction logic
1140
- const checkboxSelectors = [
1209
+ await waitForTimeout(page, 500);
1210
+
1211
+ let checkboxInteractionSuccess = false;
1212
+ try {
1213
+ const shadowResult = await clickInShadowDOM(challengeFrame, [
1141
1214
  'input[type="checkbox"]',
1142
1215
  '.ctp-checkbox',
1143
- 'input.ctp-checkbox',
1144
- '.cf-turnstile input',
1145
- '.ctp-checkbox-label'
1146
- ];
1147
-
1148
- let checkboxInteractionSuccess = false;
1149
- for (const selector of checkboxSelectors) {
1150
- try {
1151
- await Promise.race([
1152
- challengeFrame.waitForSelector(selector, { timeout: FAST_TIMEOUTS.SELECTOR_WAIT }),
1153
- new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), FAST_TIMEOUTS.SELECTOR_WAIT + 1000))
1154
- ]);
1155
-
1156
- await waitForTimeout(page, FAST_TIMEOUTS.ELEMENT_INTERACTION_DELAY);
1157
- await challengeFrame.click(selector);
1158
-
1159
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Clicked iframe element: ${selector}`));
1216
+ '.ctp-checkbox-label',
1217
+ '[role="checkbox"]',
1218
+ 'label.cb-lb',
1219
+ 'label'
1220
+ ], forceDebug);
1221
+
1222
+ if (shadowResult.clicked) {
1160
1223
  checkboxInteractionSuccess = true;
1161
- break;
1162
- } catch (e) {
1163
- continue;
1224
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM click succeeded: ${shadowResult.selector}`));
1225
+ } else if (shadowResult.found && shadowResult.x > 0) {
1226
+ await page.mouse.click(shadowResult.x, shadowResult.y);
1227
+ checkboxInteractionSuccess = true;
1228
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM mouse fallback at (${shadowResult.x}, ${shadowResult.y})`));
1164
1229
  }
1230
+ } catch (shadowErr) {
1231
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM click failed: ${shadowErr.message}`));
1165
1232
  }
1166
1233
 
1167
- // Try alternative interaction only if standard selectors failed
1168
1234
  if (!checkboxInteractionSuccess) {
1169
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Checkbox interactions failed, trying container fallback`));
1170
- await waitForTimeout(page, 1000);
1171
1235
 
1172
1236
  try {
1173
- // Try clicking on the iframe container itself as fallback
1174
1237
  const iframeElement = await page.$('iframe[src*="challenges.cloudflare.com"]');
1175
1238
  if (iframeElement) {
1176
1239
  await iframeElement.click();
@@ -1179,8 +1242,6 @@ async function handleEmbeddedIframeChallenge(page, forceDebug = false) {
1179
1242
  } catch (containerClickError) {
1180
1243
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Container click failed: ${containerClickError.message}`));
1181
1244
  }
1182
- } else {
1183
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Checkbox interaction successful, skipping container fallback`));
1184
1245
  }
1185
1246
 
1186
1247
  // Reuse existing completion check pattern with error handling
@@ -1237,8 +1298,10 @@ async function waitForJSChallengeCompletion(page, forceDebug = false) {
1237
1298
  await Promise.race([
1238
1299
  page.waitForFunction(
1239
1300
  () => {
1240
- return !document.body.textContent.includes('Checking your browser') &&
1241
- !document.body.textContent.includes('Please wait while we verify') &&
1301
+ const bodyText = document.body.textContent;
1302
+ if (bodyText.includes('Verification successful')) return true;
1303
+ return !bodyText.includes('Checking your browser') &&
1304
+ !bodyText.includes('Please wait while we verify') &&
1242
1305
  !document.querySelector('.cf-challenge-running') &&
1243
1306
  !document.querySelector('[data-cf-challenge]');
1244
1307
  },
@@ -1322,28 +1385,26 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
1322
1385
  console.log(formatLogMessage('cloudflare', `Found Turnstile iframe with URL: ${turnstileFrame.url()}`));
1323
1386
  }
1324
1387
 
1325
- const checkboxSelectors = [
1326
- 'input[type="checkbox"].ctp-checkbox',
1327
- '.ctp-checkbox-label',
1328
- '.ctp-checkbox'
1329
- ];
1330
-
1331
- for (const selector of checkboxSelectors) {
1332
- try {
1333
- await Promise.race([
1334
- turnstileFrame.waitForSelector(selector, { timeout: FAST_TIMEOUTS.SELECTOR_WAIT }),
1335
- new Promise((_, reject) => setTimeout(() => reject(new Error('Checkbox timeout')), FAST_TIMEOUTS.SELECTOR_WAIT + 500))
1336
- ]);
1337
-
1338
- await waitForTimeout(page, FAST_TIMEOUTS.ELEMENT_INTERACTION_DELAY);
1339
- await turnstileFrame.click(selector);
1340
-
1341
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Clicked Turnstile checkbox: ${selector}`));
1342
- break;
1343
- } catch (e) {
1344
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Checkbox selector ${selector} not found or failed to click`));
1345
- continue;
1388
+ await waitForTimeout(page, FAST_TIMEOUTS.ELEMENT_INTERACTION_DELAY);
1389
+
1390
+ try {
1391
+ const shadowResult = await clickInShadowDOM(turnstileFrame, [
1392
+ 'input[type="checkbox"]',
1393
+ '.ctp-checkbox',
1394
+ '.ctp-checkbox-label',
1395
+ '[role="checkbox"]',
1396
+ 'label.cb-lb',
1397
+ 'label'
1398
+ ], forceDebug);
1399
+
1400
+ if (shadowResult.clicked) {
1401
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Turnstile shadow DOM click succeeded: ${shadowResult.selector}`));
1402
+ } else if (shadowResult.found && shadowResult.x > 0) {
1403
+ await page.mouse.click(shadowResult.x, shadowResult.y);
1404
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Turnstile shadow DOM mouse fallback at (${shadowResult.x}, ${shadowResult.y})`));
1346
1405
  }
1406
+ } catch (shadowErr) {
1407
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM fallback failed: ${shadowErr.message}`));
1347
1408
  }
1348
1409
 
1349
1410
  // Wait for Turnstile completion with reduced timeout
@@ -1531,7 +1592,11 @@ async function checkChallengeCompletion(page) {
1531
1592
  * }
1532
1593
  */
1533
1594
  async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDebug = false) {
1534
- if (forceDebug) {
1595
+ const cfDebug = forceDebug || siteConfig.cloudflare_bypass === 'debug' || siteConfig.cloudflare_phish === 'debug';
1596
+ const cfBypassEnabled = siteConfig.cloudflare_bypass === true || siteConfig.cloudflare_bypass === 'debug';
1597
+ const cfPhishEnabled = siteConfig.cloudflare_phish === true || siteConfig.cloudflare_phish === 'debug';
1598
+
1599
+ if (cfDebug) {
1535
1600
  console.log(formatLogMessage('cloudflare', `Using Cloudflare module v${CLOUDFLARE_MODULE_VERSION} for ${currentUrl}`));
1536
1601
  }
1537
1602
 
@@ -1561,7 +1626,7 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
1561
1626
  // Sets attempted: false, success: true for both protection types
1562
1627
 
1563
1628
  // Only proceed if we have indicators OR explicit config enables Cloudflare handling
1564
- if (!quickDetection.hasIndicators && !siteConfig.cloudflare_phish && !siteConfig.cloudflare_bypass) {
1629
+ if (!quickDetection.hasIndicators && !cfPhishEnabled && !cfBypassEnabled) {
1565
1630
  if (forceDebug) console.log(formatLogMessage('cloudflare', `No Cloudflare indicators found and no explicit config, skipping protection handling for ${currentUrl}`));
1566
1631
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Quick detection details: title="${quickDetection.title}", bodySnippet="${quickDetection.bodySnippet}"`));
1567
1632
  return {
@@ -1586,7 +1651,7 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
1586
1651
  try {
1587
1652
  // Adaptive timeout based on detection results and explicit config
1588
1653
  let adaptiveTimeout;
1589
- if (siteConfig.cloudflare_phish || siteConfig.cloudflare_bypass) {
1654
+ if (cfPhishEnabled || cfBypassEnabled) {
1590
1655
  // Explicit config - give more time
1591
1656
  adaptiveTimeout = quickDetection.hasIndicators ? TIMEOUTS.ADAPTIVE_TIMEOUT_WITH_INDICATORS : TIMEOUTS.ADAPTIVE_TIMEOUT_WITHOUT_INDICATORS;
1592
1657
  } else {
@@ -1599,7 +1664,7 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
1599
1664
  }
1600
1665
 
1601
1666
  return await Promise.race([
1602
- performCloudflareHandling(page, currentUrl, siteConfig, forceDebug),
1667
+ performCloudflareHandling(page, currentUrl, siteConfig, cfDebug),
1603
1668
  new Promise((resolve) => {
1604
1669
  setTimeout(() => {
1605
1670
  console.warn(formatLogMessage('cloudflare', `Adaptive timeout (${adaptiveTimeout}ms) for ${currentUrl} - continuing with scan`));
@@ -1631,6 +1696,9 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
1631
1696
  * @returns {Promise<Object>} Same structure as handleCloudflareProtection()
1632
1697
  */
1633
1698
  async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebug = false) {
1699
+ const cfBypassEnabled = siteConfig.cloudflare_bypass === true || siteConfig.cloudflare_bypass === 'debug';
1700
+ const cfPhishEnabled = siteConfig.cloudflare_phish === true || siteConfig.cloudflare_phish === 'debug';
1701
+
1634
1702
  const result = {
1635
1703
  phishingWarning: { attempted: false, success: false },
1636
1704
  verificationChallenge: { attempted: false, success: false },
@@ -1643,7 +1711,7 @@ async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebu
1643
1711
  // Handle phishing warnings first - updates result.phishingWarning
1644
1712
  // Only runs if siteConfig.cloudflare_phish === true
1645
1713
  // Handle phishing warnings if enabled
1646
- if (siteConfig.cloudflare_phish === true) {
1714
+ if (cfPhishEnabled) {
1647
1715
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Phishing warning bypass enabled for ${currentUrl}`));
1648
1716
 
1649
1717
  const phishingResult = await handlePhishingWarningWithRetries(page, currentUrl, siteConfig, forceDebug);
@@ -1678,7 +1746,7 @@ async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebu
1678
1746
  // Only runs if siteConfig.cloudflare_bypass === true
1679
1747
  // Sets requiresHuman: true if CAPTCHA detected (no bypass attempted)
1680
1748
  // Handle verification challenges if enabled
1681
- if (siteConfig.cloudflare_bypass === true) {
1749
+ if (cfBypassEnabled) {
1682
1750
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge bypass enabled for ${currentUrl}`));
1683
1751
 
1684
1752
  const challengeResult = await handleVerificationChallengeWithRetries(page, currentUrl, siteConfig, forceDebug);
@@ -1731,55 +1799,28 @@ async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebu
1731
1799
  * Performs parallel detection of multiple challenge types for better performance
1732
1800
  */
1733
1801
  async function parallelChallengeDetection(page, forceDebug = false) {
1734
- const detectionPromises = [];
1735
-
1736
- // Check for JS challenge
1737
- detectionPromises.push(
1738
- page.evaluate(() => {
1739
- return {
1740
- type: 'js',
1741
- detected: document.querySelector('script[src*="/cdn-cgi/challenge-platform/"]') !== null ||
1742
- document.body?.textContent?.includes('Checking your browser') ||
1743
- document.body?.textContent?.includes('Please wait while we verify')
1744
- };
1745
- }).catch(err => ({ type: 'js', detected: false, error: err.message }))
1746
- );
1747
-
1748
- // Check for Turnstile
1749
- detectionPromises.push(
1750
- page.evaluate(() => {
1751
- return {
1752
- type: 'turnstile',
1753
- detected: document.querySelector('.cf-turnstile') !== null ||
1754
- document.querySelector('iframe[src*="challenges.cloudflare.com"]') !== null ||
1755
- document.querySelector('.ctp-checkbox-container') !== null
1756
- };
1757
- }).catch(err => ({ type: 'turnstile', detected: false, error: err.message }))
1758
- );
1759
-
1760
- // Check for phishing warning
1761
- detectionPromises.push(
1762
- page.evaluate(() => {
1763
- return {
1764
- type: 'phishing',
1765
- detected: document.body?.textContent?.includes('This website has been reported for potential phishing') ||
1766
- document.querySelector('a[href*="continue"]') !== null
1767
- };
1768
- }).catch(err => ({ type: 'phishing', detected: false, error: err.message }))
1769
- );
1770
-
1771
- // Check for managed challenge
1772
- detectionPromises.push(
1773
- page.evaluate(() => {
1774
- return {
1775
- type: 'managed',
1776
- detected: document.querySelector('.cf-managed-challenge') !== null ||
1777
- document.querySelector('[data-cf-managed]') !== null
1778
- };
1779
- }).catch(err => ({ type: 'managed', detected: false, error: err.message }))
1780
- );
1781
-
1782
- const results = await Promise.all(detectionPromises);
1802
+ let results;
1803
+ try {
1804
+ results = await page.evaluate(() => {
1805
+ const bodyText = document.body?.textContent || '';
1806
+ return [
1807
+ { type: 'js', detected: document.querySelector('script[src*="/cdn-cgi/challenge-platform/"]') !== null ||
1808
+ bodyText.includes('Checking your browser') || bodyText.includes('Please wait while we verify') },
1809
+ { type: 'turnstile', detected: document.querySelector('.cf-turnstile') !== null ||
1810
+ document.querySelector('iframe[src*="challenges.cloudflare.com"]') !== null ||
1811
+ document.querySelector('.ctp-checkbox-container') !== null },
1812
+ { type: 'phishing', detected: bodyText.includes('This website has been reported for potential phishing') ||
1813
+ document.querySelector('a[href*="continue"]') !== null },
1814
+ { type: 'managed', detected: document.querySelector('.cf-managed-challenge') !== null ||
1815
+ document.querySelector('[data-cf-managed]') !== null }
1816
+ ];
1817
+ });
1818
+ } catch (err) {
1819
+ results = [
1820
+ { type: 'js', detected: false }, { type: 'turnstile', detected: false },
1821
+ { type: 'phishing', detected: false }, { type: 'managed', detected: false }
1822
+ ];
1823
+ }
1783
1824
 
1784
1825
  const detectedChallenges = results.filter(r => r.detected).map(r => r.type);
1785
1826
 
@@ -843,7 +843,28 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
843
843
  }
844
844
  }, 'WebGL spoofing');
845
845
 
846
- // Permissions API spoofing
846
+ // WebGL null-context safety net � prevents ad script crashes in headless
847
+ safeExecute(() => {
848
+ const originalGetContext = HTMLCanvasElement.prototype.getContext;
849
+ HTMLCanvasElement.prototype.getContext = function(type, attrs) {
850
+ const ctx = originalGetContext.call(this, type, attrs);
851
+ if (ctx !== null || (type !== 'webgl' && type !== 'experimental-webgl' && type !== 'webgl2')) return ctx;
852
+ // Return minimal mock so scripts calling getShaderPrecisionFormat etc. don't crash
853
+ const noop = () => {};
854
+ return new Proxy({}, {
855
+ get(_, prop) {
856
+ if (prop === 'getShaderPrecisionFormat') return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
857
+ if (prop === 'getParameter') return (p) => ({ 37445: 'Intel Inc.', 37446: 'Intel(R) UHD Graphics 630' }[p] || 0);
858
+ if (prop === 'getSupportedExtensions') return () => [];
859
+ if (prop === 'getExtension') return () => null;
860
+ if (prop === 'canvas') return this;
861
+ if (prop === 'drawingBufferWidth') return 1920;
862
+ if (prop === 'drawingBufferHeight') return 1080;
863
+ return noop;
864
+ }
865
+ });
866
+ };
867
+ }, 'WebGL null-context safety net'); // Permissions API spoofing
847
868
  //
848
869
  safeExecute(() => {
849
870
  if (navigator.permissions?.query) {