@fanboynz/network-scanner 2.0.64 → 2.0.65

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/nwss.js CHANGED
@@ -360,6 +360,12 @@ let adblockEnabled = false;
360
360
  let adblockMatcher = null;
361
361
  let adblockStats = { blocked: 0, allowed: 0 };
362
362
 
363
+ // Cloudflare scan-wide stats. errorPages counts URLs where the returned page
364
+ // was a Cloudflare-served 5xx origin error (522/523/etc.) — no bypass
365
+ // possible, useful signal for diagnosing dead-origin scans. Named distinct
366
+ // from the local cloudflareStats = getCacheStats() in the debug stats block.
367
+ let cloudflareScanStats = { errorPages: 0 };
368
+
363
369
  // Validate --adblock-rules usage - ignore if used incorrectly instead of erroring
364
370
  if (adblockRulesMode) {
365
371
  if (!outputFile) {
@@ -478,78 +484,10 @@ if (testValidation) {
478
484
  }
479
485
  }
480
486
 
481
- if (validateConfig) {
482
- console.log(`\n${messageColors.processing('Validating configuration file...')}`);
483
- try {
484
- const validation = validateFullConfig(config, { forceDebug, silentMode });
485
-
486
- // Validate referrer_headers format
487
- for (const site of sites) {
488
- if (site.referrer_headers && typeof site.referrer_headers === 'object' && !Array.isArray(site.referrer_headers)) {
489
- const validation = validateReferrerConfig(site.referrer_headers);
490
- if (!validation.isValid) {
491
- console.warn(`⚠ Invalid referrer_headers configuration: ${validation.errors.join(', ')}`);
492
- }
493
- if (validation.warnings.length > 0) {
494
- console.warn(`⚠ Referrer warnings: ${validation.warnings.join(', ')}`);
495
- }
496
- }
497
- // Validate referrer_disable format
498
- if (site.referrer_disable) {
499
- const disableValidation = validateReferrerDisable(site.referrer_disable);
500
- if (!disableValidation.isValid) {
501
- console.warn(`⚠ Invalid referrer_disable configuration: ${disableValidation.errors.join(', ')}`);
502
- }
503
- if (disableValidation.warnings.length > 0) {
504
- console.warn(`⚠ Referrer disable warnings: ${disableValidation.warnings.join(', ')}`);
505
- }
506
- }
507
- }
508
-
509
- // Validate VPN configurations
510
- for (const site of sites) {
511
- if (site.vpn) {
512
- const vpnNorm = normalizeVpnConfig(site.vpn);
513
- const vpnValidation = validateVpnConfig(vpnNorm);
514
- if (!vpnValidation.isValid) {
515
- console.warn(`⚠ Invalid vpn configuration for ${site.url}: ${vpnValidation.errors.join(', ')}`);
516
- }
517
- if (vpnValidation.warnings.length > 0) {
518
- vpnValidation.warnings.forEach(w => console.warn(`⚠ VPN warning for ${site.url}: ${w}`));
519
- }
520
- }
521
- if (site.openvpn) {
522
- const ovpnNorm = normalizeOvpnConfig(site.openvpn);
523
- const ovpnValidation = validateOvpnConfig(ovpnNorm);
524
- if (!ovpnValidation.isValid) {
525
- console.warn(`⚠ Invalid openvpn configuration for ${site.url}: ${ovpnValidation.errors.join(', ')}`);
526
- }
527
- if (ovpnValidation.warnings.length > 0) {
528
- ovpnValidation.warnings.forEach(w => console.warn(`⚠ OpenVPN warning for ${site.url}: ${w}`));
529
- }
530
- }
531
- if (site.vpn && site.openvpn) {
532
- console.warn(`⚠ ${site.url} has both vpn and openvpn configured — only one will be used (vpn takes precedence)`);
533
- }
534
- }
535
-
536
- if (validation.isValid) {
537
- console.log(`${messageColors.success('✅ Configuration is valid!')}`);
538
- console.log(`${messageColors.info('Summary:')} ${validation.summary.validSites}/${validation.summary.totalSites} sites valid`);
539
- if (validation.summary.sitesWithWarnings > 0) {
540
- console.log(`${messageColors.warn('⚠ Warnings:')} ${validation.summary.sitesWithWarnings} sites have warnings`);
541
- }
542
- process.exit(0);
543
- } else {
544
- console.log(`${messageColors.error('❌ Configuration validation failed!')}`);
545
- console.log(`${messageColors.error('Errors:')} ${validation.globalErrors.length} global, ${validation.summary.sitesWithErrors} site-specific`);
546
- process.exit(1);
547
- }
548
- } catch (validationErr) {
549
- console.error(`❌ Validation failed: ${validationErr.message}`);
550
- process.exit(1);
551
- }
552
- }
487
+ // Note: --validate-config is handled further down, AFTER the config file is
488
+ // loaded and `config`/`sites` are populated. Running it here would fail with
489
+ // "Cannot access 'config' before initialization" since those are declared
490
+ // later in the module.
553
491
 
554
492
  if (validateRules || validateRulesFile) {
555
493
  const filesToValidate = validateRulesFile ? [validateRulesFile] : [outputFile, compareFile].filter(Boolean);
@@ -909,10 +847,86 @@ const {
909
847
  disable_ad_tagging = true,
910
848
  max_concurrent_sites = 6,
911
849
  resource_cleanup_interval = 80,
912
- comments: globalComments,
913
- ...otherGlobalConfig
850
+ comments: globalComments,
851
+ ...otherGlobalConfig
914
852
  } = config;
915
853
 
854
+ // --validate-config runs here, after `config` and `sites` are populated.
855
+ // Previously this block lived above the config load and triggered a TDZ
856
+ // "Cannot access 'config' before initialization" error.
857
+ if (validateConfig) {
858
+ console.log(`\n${messageColors.processing('Validating configuration file...')}`);
859
+ try {
860
+ const validation = validateFullConfig(config, { forceDebug, silentMode });
861
+
862
+ // Validate referrer_headers format
863
+ for (const site of sites) {
864
+ if (site.referrer_headers && typeof site.referrer_headers === 'object' && !Array.isArray(site.referrer_headers)) {
865
+ const refValidation = validateReferrerConfig(site.referrer_headers);
866
+ if (!refValidation.isValid) {
867
+ console.warn(`⚠ Invalid referrer_headers configuration: ${refValidation.errors.join(', ')}`);
868
+ }
869
+ if (refValidation.warnings.length > 0) {
870
+ console.warn(`⚠ Referrer warnings: ${refValidation.warnings.join(', ')}`);
871
+ }
872
+ }
873
+ // Validate referrer_disable format
874
+ if (site.referrer_disable) {
875
+ const disableValidation = validateReferrerDisable(site.referrer_disable);
876
+ if (!disableValidation.isValid) {
877
+ console.warn(`⚠ Invalid referrer_disable configuration: ${disableValidation.errors.join(', ')}`);
878
+ }
879
+ if (disableValidation.warnings.length > 0) {
880
+ console.warn(`⚠ Referrer disable warnings: ${disableValidation.warnings.join(', ')}`);
881
+ }
882
+ }
883
+ }
884
+
885
+ // Validate VPN configurations
886
+ for (const site of sites) {
887
+ if (site.vpn) {
888
+ const vpnNorm = normalizeVpnConfig(site.vpn);
889
+ const vpnValidation = validateVpnConfig(vpnNorm);
890
+ if (!vpnValidation.isValid) {
891
+ console.warn(`⚠ Invalid vpn configuration for ${site.url}: ${vpnValidation.errors.join(', ')}`);
892
+ }
893
+ if (vpnValidation.warnings.length > 0) {
894
+ vpnValidation.warnings.forEach(w => console.warn(`⚠ VPN warning for ${site.url}: ${w}`));
895
+ }
896
+ }
897
+ if (site.openvpn) {
898
+ const ovpnNorm = normalizeOvpnConfig(site.openvpn);
899
+ const ovpnValidation = validateOvpnConfig(ovpnNorm);
900
+ if (!ovpnValidation.isValid) {
901
+ console.warn(`⚠ Invalid openvpn configuration for ${site.url}: ${ovpnValidation.errors.join(', ')}`);
902
+ }
903
+ if (ovpnValidation.warnings.length > 0) {
904
+ ovpnValidation.warnings.forEach(w => console.warn(`⚠ OpenVPN warning for ${site.url}: ${w}`));
905
+ }
906
+ }
907
+ if (site.vpn && site.openvpn) {
908
+ console.warn(`⚠ ${site.url} has both vpn and openvpn configured — only one will be used (vpn takes precedence)`);
909
+ }
910
+ }
911
+
912
+ if (validation.isValid) {
913
+ console.log(`${messageColors.success('✅ Configuration is valid!')}`);
914
+ console.log(`${messageColors.info('Summary:')} ${validation.summary.validSites}/${validation.summary.totalSites} sites valid`);
915
+ if (validation.summary.sitesWithWarnings > 0) {
916
+ console.log(`${messageColors.warn('⚠ Warnings:')} ${validation.summary.sitesWithWarnings} sites have warnings`);
917
+ }
918
+ process.exit(0);
919
+ } else {
920
+ console.log(`${messageColors.error('❌ Configuration validation failed!')}`);
921
+ console.log(`${messageColors.error('Errors:')} ${validation.globalErrors.length} global, ${validation.summary.sitesWithErrors} site-specific`);
922
+ process.exit(1);
923
+ }
924
+ } catch (validationErr) {
925
+ console.error(`❌ Validation failed: ${validationErr.message}`);
926
+ process.exit(1);
927
+ }
928
+ }
929
+
916
930
  // Pre-compile global blocked regexes ONCE (used in every processUrl call)
917
931
  const globalBlockedRegexes = Array.isArray(globalBlocked)
918
932
  ? globalBlocked.map(pattern => new RegExp(pattern))
@@ -3425,7 +3439,7 @@ function setupFrameHandling(page, forceDebug) {
3425
3439
  }
3426
3440
  }
3427
3441
 
3428
- const { finalUrl, redirected, redirectChain, originalUrl, redirectDomains } = navigationResult;
3442
+ const { finalUrl, redirected, redirectChain, originalUrl, redirectDomains, httpStatus, cfRay } = navigationResult;
3429
3443
 
3430
3444
  // Check for same-page reload loops BEFORE redirect processing
3431
3445
  const loadCount = pageLoadHistory.get(currentUrl) || 0;
@@ -3534,8 +3548,14 @@ function setupFrameHandling(page, forceDebug) {
3534
3548
  }
3535
3549
  }
3536
3550
 
3537
- // Handle all Cloudflare protections using the enhanced module
3538
- const cloudflareResult = await handleCloudflareProtection(page, currentUrl, siteConfig, forceDebug);
3551
+ // Handle all Cloudflare protections using the enhanced module. Pass
3552
+ // httpStatus and cfRay captured at goto time so the outcome log can
3553
+ // surface them — Puppeteer's response object is only available
3554
+ // immediately after page.goto, so handleCloudflareProtection can't
3555
+ // recover them from `page` alone.
3556
+ const cloudflareResult = await handleCloudflareProtection(page, currentUrl, siteConfig, forceDebug, { httpStatus, cfRay });
3557
+
3558
+ if (cloudflareResult.cloudflareErrorPage) cloudflareScanStats.errorPages++;
3539
3559
 
3540
3560
  // Check if Cloudflare handling exceeded max retries and should terminate processing
3541
3561
  if (!cloudflareResult.overallSuccess &&
@@ -3568,7 +3588,10 @@ function setupFrameHandling(page, forceDebug) {
3568
3588
  console.warn(` - ${error}`);
3569
3589
  });
3570
3590
  // Continue with scan despite Cloudflare issues
3571
- } else if (cloudflareResult.verificationChallenge?.success && forceDebug) {
3591
+ } else if (cloudflareResult.verificationChallenge?.attempted && cloudflareResult.verificationChallenge?.success && forceDebug) {
3592
+ // Require attempted === true so we don't log "Challenge solved using:
3593
+ // undefined" for pages that had no challenge to solve (success: true
3594
+ // is the natural state for that case).
3572
3595
  console.log(formatLogMessage('debug', `[cloudflare] Challenge solved using: ${cloudflareResult.verificationChallenge.method}`));
3573
3596
  }
3574
3597
 
@@ -4299,27 +4322,39 @@ function setupFrameHandling(page, forceDebug) {
4299
4322
  let forceRestartFlag = false; // Flag to trigger restart on next iteration
4300
4323
 
4301
4324
  const hangDetectionInterval = setInterval(() => {
4325
+ // Progress check, counter, and forceRestartFlag MUST run regardless of
4326
+ // debug mode — previously the entire body was gated on forceDebug, which
4327
+ // made hang recovery a debug-only feature even though the restart
4328
+ // machinery exists for production scans. Only the verbose diagnostic
4329
+ // logs stay debug-gated; the "no progress" warning and the
4330
+ // "triggering restart" error are user-visible recovery events.
4331
+ if (processedUrlCount === lastProcessedCount) {
4332
+ hangCheckCount++;
4333
+ if (forceDebug) {
4334
+ console.log(formatLogMessage('warn', `[HANG CHECK] No progress for ${hangCheckCount * 30}s`));
4335
+ }
4336
+ if (hangCheckCount >= 5) {
4337
+ console.log(formatLogMessage('error', `[HANG CHECK] Hung for 2.5 minutes. Triggering emergency browser restart.`));
4338
+ forceRestartFlag = true; // Set flag instead of exiting
4339
+ hangCheckCount = 0; // Reset counter for next cycle
4340
+ }
4341
+ } else {
4342
+ hangCheckCount = 0;
4343
+ }
4344
+ lastProcessedCount = processedUrlCount;
4345
+
4346
+ // Debug-only diagnostic snapshot
4302
4347
  if (forceDebug) {
4303
4348
  const currentBatch = Math.floor(currentBatchInfo.batchStart / RESOURCE_CLEANUP_INTERVAL) + 1;
4304
4349
  const totalBatches = Math.ceil(totalUrls / RESOURCE_CLEANUP_INTERVAL);
4305
4350
  console.log(formatLogMessage('debug', `[HANG CHECK] Processed: ${processedUrlCount}/${totalUrls} URLs, Batch: ${currentBatch}/${totalBatches}, Current batch size: ${currentBatchInfo.batchSize}`));
4306
4351
  console.log(formatLogMessage('debug', `[HANG CHECK] URLs since cleanup: ${urlsSinceLastCleanup}, Recent failures: ${results.slice(-3).filter(r => !r.success).length}/3`));
4307
-
4308
- // Check progress and trigger browser restart if hung
4309
- if (processedUrlCount === lastProcessedCount) {
4310
- hangCheckCount++;
4311
- console.log(formatLogMessage('warn', `[HANG CHECK] No progress for ${hangCheckCount * 30}s`));
4312
- if (hangCheckCount >= 5) {
4313
- console.log(formatLogMessage('error', `[HANG CHECK] Hung for 2.5 minutes. Triggering emergency browser restart.`));
4314
- forceRestartFlag = true; // Set flag instead of exiting
4315
- hangCheckCount = 0; // Reset counter for next cycle
4316
- }
4317
- } else {
4318
- hangCheckCount = 0;
4319
- }
4320
- lastProcessedCount = processedUrlCount;
4321
4352
  }
4322
4353
  }, 30000);
4354
+ // Don't keep the event loop alive solely for the hang-check interval — the
4355
+ // clearInterval calls at the normal-exit and error paths already cover the
4356
+ // cleanup, this is belt-and-suspenders in case a future refactor moves them.
4357
+ hangDetectionInterval.unref();
4323
4358
 
4324
4359
  // Process URLs in batches with exception handling
4325
4360
  let siteGroupIndex = 0;
@@ -4387,8 +4422,14 @@ function setupFrameHandling(page, forceDebug) {
4387
4422
  !healthCheck.reason?.includes('Scheduled cleanup') &&
4388
4423
  (healthCheck.reason?.includes('Critical') || healthCheck.reason?.includes('disconnected'));
4389
4424
 
4390
- // Restart browser if we've processed enough URLs, health check suggests it, hang detected, and this isn't the last site
4391
- if ((wouldExceedLimit || shouldRestartFromHealth || forceRestartFlag || (hasHighFailureRate && recentResults.length >= 6)) && urlsSinceLastCleanup > 8 && isNotLastBatch) {
4425
+ // Restart conditions split into hang recovery vs proactive triggers.
4426
+ // Hang recovery (forceRestartFlag set by 2.5-min HANG CHECK or a per-URL
4427
+ // timeout) bypasses the urlsSinceLastCleanup > 8 gate — a confirmed hang
4428
+ // needs immediate restart even if we just cleaned up. Proactive triggers
4429
+ // keep the gate to prevent thrashing.
4430
+ const hangRecoveryRestart = forceRestartFlag;
4431
+ const proactiveRestart = (wouldExceedLimit || shouldRestartFromHealth || (hasHighFailureRate && recentResults.length >= 6)) && urlsSinceLastCleanup > 8;
4432
+ if ((hangRecoveryRestart || proactiveRestart) && isNotLastBatch) {
4392
4433
  let restartReason = 'Unknown';
4393
4434
  if (forceRestartFlag) {
4394
4435
  restartReason = 'Emergency restart due to 2.5-minute hang detection';
@@ -4517,16 +4558,58 @@ function setupFrameHandling(page, forceDebug) {
4517
4558
  console.log(formatLogMessage('debug', `[CONCURRENCY] Starting ${batchSize} concurrent tasks with limit ${MAX_CONCURRENT_SITES}`));
4518
4559
  }
4519
4560
 
4520
- // Create tasks with timeout protection — skip domains that repeatedly timed out
4521
- const batchTasks = currentBatch.map(task => originalLimit(() => {
4561
+ // Create tasks with timeout protection — skip domains that repeatedly timed out.
4562
+ // Wrapped in an outer try/finally so processedUrlCount is incremented exactly
4563
+ // once per URL no matter which return/throw path is taken — that turns HANG
4564
+ // CHECK's signal from "did the batch finish?" into "did any URL finish?",
4565
+ // which is what 30-second tick granularity actually needs.
4566
+ const batchTasks = currentBatch.map(task => originalLimit(async () => {
4522
4567
  try {
4523
- const taskDomain = new URL(task.url).hostname;
4524
- if ((domainTimeoutCounts.get(taskDomain) || 0) >= DOMAIN_TIMEOUT_THRESHOLD) {
4525
- if (!silentMode) console.log(formatLogMessage('info', `Skipping ${task.url} ${taskDomain} timed out ${DOMAIN_TIMEOUT_THRESHOLD} times`));
4526
- return { url: task.url, rules: [], success: false, error: 'Domain repeatedly timed out', skipped: true };
4568
+ // Short-circuit queued URLs once any URL in this batch has triggered a
4569
+ // restart. Without this, the 80-URL batch in the user's hang trace
4570
+ // would have to fail one-by-one at 120s each (~28 min total) before
4571
+ // the boundary restart could fire. Now: first hang fires the flag,
4572
+ // remaining queued URLs return immediately, batch completes, restart.
4573
+ if (forceRestartFlag) {
4574
+ return { url: task.url, rules: [], success: false, error: 'Browser restart pending', skipped: true };
4575
+ }
4576
+
4577
+ try {
4578
+ const taskDomain = new URL(task.url).hostname;
4579
+ if ((domainTimeoutCounts.get(taskDomain) || 0) >= DOMAIN_TIMEOUT_THRESHOLD) {
4580
+ if (!silentMode) console.log(formatLogMessage('info', `Skipping ${task.url} — ${taskDomain} timed out ${DOMAIN_TIMEOUT_THRESHOLD} times`));
4581
+ return { url: task.url, rules: [], success: false, error: 'Domain repeatedly timed out', skipped: true };
4582
+ }
4583
+ } catch {}
4584
+
4585
+ // Per-URL timeout so a single hung processUrl can't block the batch
4586
+ // forever. 120s is well past any legitimate slow page: Cloudflare
4587
+ // adaptive max ~25s, nettools overall ~65s, navigation 15s.
4588
+ const processUrlPromise = processUrl(task.url, task.config, browser);
4589
+ let perUrlTimer;
4590
+ try {
4591
+ return await Promise.race([
4592
+ processUrlPromise,
4593
+ new Promise((_, reject) => {
4594
+ perUrlTimer = setTimeout(() => reject(new Error('Per-URL timeout (120s)')), 120000);
4595
+ })
4596
+ ]);
4597
+ } catch (err) {
4598
+ if (err && err.message === 'Per-URL timeout (120s)') {
4599
+ processUrlPromise.catch(() => {});
4600
+ forceRestartFlag = true;
4601
+ return { url: task.url, rules: [], success: false, error: 'Per-URL timeout (120s)', needsImmediateRestart: true };
4602
+ }
4603
+ throw err;
4604
+ } finally {
4605
+ if (perUrlTimer) clearTimeout(perUrlTimer);
4527
4606
  }
4528
- } catch {}
4529
- return processUrl(task.url, task.config, browser);
4607
+ } finally {
4608
+ // Always count completion — even on unexpected throw — so HANG CHECK's
4609
+ // per-tick progress signal stays accurate. Replaces the old
4610
+ // `processedUrlCount += batchSize` that ran after the whole batch.
4611
+ processedUrlCount++;
4612
+ }
4530
4613
  }));
4531
4614
 
4532
4615
  let batchResults;
@@ -4628,7 +4711,8 @@ function setupFrameHandling(page, forceDebug) {
4628
4711
  }
4629
4712
  }
4630
4713
 
4631
- processedUrlCount += batchSize;
4714
+ // processedUrlCount is now incremented per-URL inside the batchTasks
4715
+ // wrapper above; no batch-level += batchSize here.
4632
4716
  urlsSinceLastCleanup += batchSize;
4633
4717
 
4634
4718
  // Force browser restart if any URL had critical errors
@@ -4809,12 +4893,40 @@ function setupFrameHandling(page, forceDebug) {
4809
4893
  console.log(formatLogMessage('debug', `Cache hit rate: ${cloudflareStats.hitRate}, Total hits: ${cloudflareStats.hits}, Misses: ${cloudflareStats.misses}`));
4810
4894
  console.log(formatLogMessage('debug', `Cached detections: ${cloudflareStats.size}`));
4811
4895
  }
4896
+ if (cloudflareScanStats.errorPages > 0) {
4897
+ console.log(formatLogMessage('debug', `Cloudflare 5xx origin-error pages: ${cloudflareScanStats.errorPages} (no bypass possible — origin unreachable)`));
4898
+ }
4812
4899
  // Log smart cache statistics (if cache is enabled)
4813
4900
  // Adblock statistics
4814
4901
  if (adblockEnabled) {
4815
4902
  console.log(formatLogMessage('debug', '=== Adblock Statistics ==='));
4816
4903
  const blockRate = ((adblockStats.blocked / (adblockStats.blocked + adblockStats.allowed)) * 100).toFixed(1);
4817
4904
  console.log(formatLogMessage('debug', `Blocked: ${adblockStats.blocked} requests (${blockRate}% block rate), Allowed: ${adblockStats.allowed}`));
4905
+
4906
+ // Engine-specific stats from the matcher itself. Both engines expose
4907
+ // getStats() but with slightly different cache shapes — JS engine
4908
+ // tracks urlCacheSize + resultCacheSize separately, rust wrapper
4909
+ // tracks a single size. Handle both.
4910
+ if (adblockMatcher && typeof adblockMatcher.getStats === 'function') {
4911
+ try {
4912
+ const es = adblockMatcher.getStats();
4913
+ const engine = es.engine || 'js';
4914
+ console.log(formatLogMessage('debug', `Engine: ${engine}${es.fromDiskCache ? ' (loaded from disk cache)' : ''}`));
4915
+ if (es.cache && (es.cache.hits != null || es.cache.misses != null)) {
4916
+ // rust wrapper: single `size`; JS engine: split into urlCacheSize + resultCacheSize
4917
+ const sizeDesc = es.cache.size != null
4918
+ ? `${es.cache.size}/${es.cache.maxSize}`
4919
+ : `url ${es.cache.urlCacheSize}, result ${es.cache.resultCacheSize}, cap ${es.cache.maxSize}`;
4920
+ console.log(formatLogMessage('debug', `Matcher cache: ${es.cache.hits} hits / ${es.cache.misses} misses (${es.cache.hitRate}), ${sizeDesc}`));
4921
+ }
4922
+ if (es.exceptions != null && es.exceptions > 0) {
4923
+ console.log(formatLogMessage('debug', `Whitelist exceptions: ${es.exceptions}`));
4924
+ }
4925
+ if (es.errors != null && es.errors > 0) {
4926
+ console.log(formatLogMessage('debug', `Engine errors: ${es.errors}`));
4927
+ }
4928
+ } catch (_) { /* getStats shape mismatch — don't crash the exit path */ }
4929
+ }
4818
4930
  }
4819
4931
  if (smartCache) {
4820
4932
  const cacheStats = smartCache.getStats();
@@ -4993,8 +5105,17 @@ function setupFrameHandling(page, forceDebug) {
4993
5105
  }
4994
5106
  }
4995
5107
 
5108
+ // Run the same cleanup the SIGINT/SIGTERM emergency handler does, so normal
5109
+ // scan completion isn't left depending on process.exit(0) to override
5110
+ // lingering setInterval handles (the cloudflare detection cache schedules
5111
+ // one that's otherwise only stopped on signal-driven shutdown).
5112
+ try { cleanupCloudflareCache(); } catch (_) {}
5113
+ try { wgDisconnectAll(forceDebug); } catch (_) {}
5114
+ try { ovpnDisconnectAll(forceDebug); } catch (_) {}
5115
+ try { purgeStaleTrackers(); } catch (_) {}
5116
+
4996
5117
  // Clean process termination
4997
5118
  if (forceDebug) console.log(formatLogMessage('debug', `About to exit process...`));
4998
5119
  process.exit(0);
4999
-
5120
+
5000
5121
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.64",
3
+ "version": "2.0.65",
4
4
  "description": "A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.",
5
5
  "main": "nwss.js",
6
6
  "scripts": {