@fanboynz/network-scanner 2.0.66 → 3.0.0

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/redirect.js CHANGED
@@ -165,8 +165,14 @@ async function navigateWithRedirectHandling(page, currentUrl, siteConfig, gotoOp
165
165
  // Inject JavaScript redirect detection
166
166
  await jsRedirectDetector();
167
167
 
168
- if (forceDebug && Object.keys(gotoOptions).length > 0) {
169
- console.log(formatLogMessage('debug', `Using goto options: ${JSON.stringify(gotoOptions)}`));
168
+ if (forceDebug) {
169
+ // Avoid Object.keys allocation just to check emptiness — a for...in
170
+ // early-exit on the first own key is enough.
171
+ let hasOpts = false;
172
+ for (const _k in gotoOptions) { hasOpts = true; break; }
173
+ if (hasOpts) {
174
+ console.log(formatLogMessage('debug', `Using goto options: ${JSON.stringify(gotoOptions)}`));
175
+ }
170
176
  }
171
177
 
172
178
  // Initial navigation. Puppeteer's page.goto returns the response for the
@@ -184,7 +190,7 @@ async function navigateWithRedirectHandling(page, currentUrl, siteConfig, gotoOp
184
190
  } catch (_) { /* response disposed or detached — fine, stays null */ }
185
191
  }
186
192
 
187
- if (response && response.url() !== currentUrl) {
193
+ if (response && response.url() !== currentUrl && !response.url().startsWith('chrome-error://')) {
188
194
  // Check redirect limit before adding
189
195
  if (redirectChain.length >= maxRedirects) {
190
196
  if (forceDebug) {
@@ -192,12 +198,12 @@ async function navigateWithRedirectHandling(page, currentUrl, siteConfig, gotoOp
192
198
  }
193
199
  finalUrl = currentUrl; // Keep original URL
194
200
  } else {
195
- finalUrl = response.url();
196
- redirected = true;
197
- if (!redirectChain.includes(finalUrl)) redirectChain.push(finalUrl);
201
+ finalUrl = response.url();
202
+ redirected = true;
203
+ if (!redirectChain.includes(finalUrl)) redirectChain.push(finalUrl);
198
204
  }
199
205
  if (forceDebug) {
200
- console.log(formatLogMessage('debug', `HTTP redirect detected: ${currentUrl} ? ${finalUrl}`));
206
+ console.log(formatLogMessage('debug', `HTTP redirect detected: ${currentUrl} -> ${finalUrl}`));
201
207
  }
202
208
  }
203
209
 
@@ -223,9 +229,11 @@ async function navigateWithRedirectHandling(page, currentUrl, siteConfig, gotoOp
223
229
  };
224
230
  });
225
231
 
226
- // Check if URL changed (either through JS redirect or automatic redirect)
232
+ // Check if URL changed (either through JS redirect or automatic redirect).
233
+ // Skip chrome-error://* — it's Puppeteer's landing page on DNS/connection
234
+ // failure and adding it to the chain produces bogus intermediate hops.
227
235
  const currentPageUrl = page.url();
228
- if (currentPageUrl && currentPageUrl !== finalUrl && !redirectChain.includes(currentPageUrl)) {
236
+ if (currentPageUrl && currentPageUrl !== finalUrl && !currentPageUrl.startsWith('chrome-error://') && !redirectChain.includes(currentPageUrl)) {
229
237
  // Check redirect limit before adding
230
238
  if (redirectChain.length >= maxRedirects) {
231
239
  if (forceDebug) {
@@ -275,21 +283,23 @@ async function navigateWithRedirectHandling(page, currentUrl, siteConfig, gotoOp
275
283
  await detectCommonJSRedirects(page, forceDebug, formatLogMessage);
276
284
  }
277
285
 
278
- // Final URL check
286
+ // Final URL check. Same chrome-error://* skip as the earlier branches —
287
+ // a navigation that ended in a chrome-error landing shouldn't be treated
288
+ // as the "final" URL of a successful redirect chain.
279
289
  const finalPageUrl = page.url();
280
- if (finalPageUrl && finalPageUrl !== finalUrl) {
290
+ if (finalPageUrl && finalPageUrl !== finalUrl && !finalPageUrl.startsWith('chrome-error://')) {
281
291
  // Check redirect limit before final update
282
292
  if (redirectChain.length >= maxRedirects) {
283
293
  if (forceDebug) {
284
294
  console.log(formatLogMessage('debug', `Maximum redirects (${maxRedirects}) reached, keeping current finalUrl`));
285
295
  }
286
296
  } else {
287
- finalUrl = finalPageUrl;
288
- redirected = true;
289
- if (!redirectChain.includes(finalUrl)) {
290
- redirectChain.push(finalUrl);
297
+ finalUrl = finalPageUrl;
298
+ redirected = true;
299
+ if (!redirectChain.includes(finalUrl)) {
300
+ redirectChain.push(finalUrl);
301
+ }
291
302
  }
292
- }
293
303
  }
294
304
 
295
305
  } finally {
@@ -298,21 +308,19 @@ async function navigateWithRedirectHandling(page, currentUrl, siteConfig, gotoOp
298
308
 
299
309
  // Log redirect summary
300
310
  if (redirected && forceDebug) {
301
- console.log(formatLogMessage('debug', `Redirect chain: ${redirectChain.join(' ? ')}`));
311
+ console.log(formatLogMessage('debug', `Redirect chain: ${redirectChain.join(' -> ')}`));
302
312
  }
303
313
 
304
- // Extract redirect domains to exclude from matching
305
- let redirectDomains = [];
314
+ // Extract intermediate redirect domains (exclude the final entry). Single
315
+ // loop instead of slice().map().filter() — three array allocations down to
316
+ // one push-loop. redirectChain is bounded at maxRedirects (default 10).
317
+ const redirectDomains = [];
306
318
  if (redirected && redirectChain.length > 1) {
307
- // Get all intermediate domains (exclude the final domain)
308
- const intermediateDomains = redirectChain.slice(0, -1).map(url => {
319
+ for (let i = 0; i < redirectChain.length - 1; i++) {
309
320
  try {
310
- return new URL(url).hostname;
311
- } catch {
312
- return null;
313
- }
314
- }).filter(Boolean);
315
- redirectDomains = intermediateDomains;
321
+ redirectDomains.push(new URL(redirectChain[i]).hostname);
322
+ } catch (_) { /* skip malformed entries */ }
323
+ }
316
324
  }
317
325
 
318
326
  return { finalUrl, redirected, redirectChain, originalUrl: currentUrl, redirectDomains, httpStatus, cfRay };
@@ -410,13 +418,21 @@ async function handleRedirectTimeout(page, originalUrl, error, safeGetDomain, fo
410
418
 
411
419
  try {
412
420
  const currentPageUrl = page.url();
413
- if (currentPageUrl && currentPageUrl !== 'about:blank' && currentPageUrl !== originalUrl) {
421
+ // Skip chrome-error://* the same way navigateWithRedirectHandling does:
422
+ // a DNS/connection-failure landing isn't a "partial redirect recovery",
423
+ // and safeGetDomain('chrome-error://chromewebdata/') returns
424
+ // 'chromewebdata', which would otherwise differ from the original
425
+ // domain and falsely report success here.
426
+ if (currentPageUrl
427
+ && currentPageUrl !== 'about:blank'
428
+ && !currentPageUrl.startsWith('chrome-error://')
429
+ && currentPageUrl !== originalUrl) {
414
430
  const originalDomain = safeGetDomain(originalUrl);
415
431
  const currentDomain = safeGetDomain(currentPageUrl);
416
-
432
+
417
433
  if (originalDomain !== currentDomain) {
418
434
  if (forceDebug) {
419
- console.log(formatLogMessage('debug', `Partial redirect timeout recovered: ${originalDomain} ? ${currentDomain}`));
435
+ console.log(formatLogMessage('debug', `Partial redirect timeout recovered: ${originalDomain} -> ${currentDomain}`));
420
436
  }
421
437
  return { success: true, finalUrl: currentPageUrl, redirected: true };
422
438
  }
package/lib/referrer.js CHANGED
@@ -1,6 +1,12 @@
1
1
  // === Referrer Header Generation Module ===
2
2
  // This module handles generation of referrer headers for different traffic simulation modes
3
3
 
4
+ const { formatLogMessage, messageColors } = require('./colorize');
5
+
6
+ // Precomputed colored '[referrer]' subsystem prefix — matches the project
7
+ // convention used by other modules (flowproxy/cloudflare/smart-cache/etc.).
8
+ const REFERRER_TAG = messageColors.processing('[referrer]');
9
+
4
10
  /**
5
11
  * Performance utility: Get random element from array
6
12
  * Reduces code duplication and improves readability
@@ -78,7 +84,10 @@ const REFERRER_COLLECTIONS = Object.freeze({
78
84
  * @returns {string} Selected search term
79
85
  */
80
86
  function generateSearchTerm(customTerms, context = null) {
81
- if (customTerms && customTerms.length > 0) {
87
+ // Array.isArray guard belt-and-braces: validateReferrerConfig now blocks
88
+ // non-array search_terms at config load, but a direct internal caller
89
+ // could still pass a non-array and trigger a TypeError on .length.
90
+ if (Array.isArray(customTerms) && customTerms.length > 0) {
82
91
  return getRandomElement(customTerms);
83
92
  }
84
93
 
@@ -107,7 +116,7 @@ function generateSearchReferrer(searchTerms, context, forceDebug) {
107
116
  const referrerUrl = randomEngine + encodeURIComponent(searchTerm);
108
117
 
109
118
  if (forceDebug) {
110
- console.log(`[debug] Generated search referrer: ${referrerUrl} (engine: ${randomEngine.split('//')[1].split('/')[0]}, term: "${searchTerm}")`);
119
+ console.log(formatLogMessage('debug', `${REFERRER_TAG} Generated search referrer: ${referrerUrl} (engine: ${randomEngine.split('//')[1].split('/')[0]}, term: "${searchTerm}")`));
111
120
  }
112
121
 
113
122
  return referrerUrl;
@@ -122,7 +131,7 @@ function generateSocialMediaReferrer(forceDebug) {
122
131
  const randomSocial = getRandomElement(REFERRER_COLLECTIONS.SOCIAL_MEDIA);
123
132
 
124
133
  if (forceDebug) {
125
- console.log(`[debug] Generated social media referrer: ${randomSocial}`);
134
+ console.log(formatLogMessage('debug', `${REFERRER_TAG} Generated social media referrer: ${randomSocial}`));
126
135
  }
127
136
 
128
137
  return randomSocial;
@@ -137,7 +146,7 @@ function generateNewsReferrer(forceDebug) {
137
146
  const randomNews = getRandomElement(REFERRER_COLLECTIONS.NEWS_SITES);
138
147
 
139
148
  if (forceDebug) {
140
- console.log(`[debug] Generated news referrer: ${randomNews}`);
149
+ console.log(formatLogMessage('debug', `${REFERRER_TAG} Generated news referrer: ${randomNews}`));
141
150
  }
142
151
 
143
152
  return randomNews;
@@ -179,35 +188,44 @@ function shouldDisableReferrer(targetUrl, disableList, forceDebug = false) {
179
188
 
180
189
  for (const disablePattern of disableList) {
181
190
  if (typeof disablePattern !== 'string') continue;
182
-
191
+
183
192
  // Fast check: Exact URL match (no parsing needed)
184
193
  if (targetUrl === disablePattern) {
185
- if (forceDebug) console.log(`[debug] Referrer disabled for exact match: ${targetUrl}`);
194
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Referrer disabled for exact match: ${targetUrl}`));
186
195
  return true;
187
196
  }
188
-
189
- // Domain/hostname match (use cached parsed URL)
190
- if (targetUrlParsed) {
191
- try {
192
- const disableHostname = new URL(disablePattern).hostname;
193
- if (targetHostname === disableHostname) {
194
- if (forceDebug) console.log(`[debug] Referrer disabled for domain match: ${targetHostname}`);
195
- return true;
196
- }
197
- } catch (e) {
198
- // disablePattern is not a valid URL, try substring match below
199
- }
197
+
198
+ // Resolve pattern to a hostname full URL patterns ('https://example.com')
199
+ // and bare-hostname patterns ('example.com') both end up running through
200
+ // the same suffix-match logic so they behave identically. Previously
201
+ // full-URL patterns only did exact hostname equality (no subdomain
202
+ // match), while bare-hostname patterns did suffix match — same user
203
+ // intent, different result depending on string form.
204
+ let patternHostname = null;
205
+ if (disablePattern.includes('/')) {
206
+ try { patternHostname = new URL(disablePattern).hostname; } catch (_) { /* fall through */ }
207
+ } else {
208
+ patternHostname = disablePattern;
200
209
  }
201
-
202
- // Fallback: Simple substring match (for patterns like 'example.com')
203
- if (!targetUrlParsed || disablePattern.includes('/') === false) {
210
+
211
+ if (targetUrlParsed && patternHostname) {
212
+ const p = patternHostname.toLowerCase();
213
+ const h = targetHostname.toLowerCase();
214
+ if (h === p || h.endsWith('.' + p)) {
215
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Referrer disabled for hostname match: ${p} matches ${h}`));
216
+ return true;
217
+ }
218
+ } else if (!targetUrlParsed) {
219
+ // Pathological fallback: target URL didn't parse. Substring match
220
+ // as last resort. Shouldn't fire in practice — we only call this
221
+ // on URLs we're about to navigate to.
204
222
  if (targetUrl.includes(disablePattern)) {
205
- if (forceDebug) console.log(`[debug] Referrer disabled for pattern match: ${disablePattern} in ${targetUrl}`);
223
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Referrer disabled for pattern match (unparseable target): ${disablePattern}`));
206
224
  return true;
207
225
  }
208
226
  }
209
227
  }
210
-
228
+
211
229
  return false;
212
230
  }
213
231
 
@@ -218,94 +236,94 @@ function shouldDisableReferrer(targetUrl, disableList, forceDebug = false) {
218
236
  * @returns {string} Generated referrer URL or empty string
219
237
  */
220
238
  function generateReferrerUrl(referrerConfig, forceDebug = false) {
221
- try {
222
- // Handle simple string URLs
223
- if (typeof referrerConfig === 'string') {
224
- const url = isValidUrl(referrerConfig) ? referrerConfig : '';
225
- if (forceDebug && url) {
226
- console.log(`[debug] Using direct referrer URL: ${url}`);
227
- } else if (forceDebug && !url) {
228
- console.log(`[debug] Invalid referrer URL provided: ${referrerConfig}`);
229
- }
230
- return url;
239
+ // No top-level try/catch — nothing in here throws synchronously.
240
+ // typeof / Array.isArray / object property access / string concat /
241
+ // console.log / the helper functions are all non-throwing. The old
242
+ // try/catch was unreachable defensive scaffolding.
243
+
244
+ // Handle simple string URLs
245
+ if (typeof referrerConfig === 'string') {
246
+ const url = isValidUrl(referrerConfig) ? referrerConfig : '';
247
+ if (forceDebug && url) {
248
+ console.log(formatLogMessage('debug', `${REFERRER_TAG} Using direct referrer URL: ${url}`));
249
+ } else if (forceDebug && !url) {
250
+ console.log(formatLogMessage('debug', `${REFERRER_TAG} Invalid referrer URL provided: ${referrerConfig}`));
231
251
  }
232
-
233
- // Handle arrays - pick random URL
234
- if (Array.isArray(referrerConfig)) {
235
- if (referrerConfig.length === 0) {
236
- if (forceDebug) console.log(`[debug] Empty referrer array provided`);
237
- return '';
252
+ return url;
253
+ }
254
+
255
+ // Handle arrays - pick random URL
256
+ if (Array.isArray(referrerConfig)) {
257
+ if (referrerConfig.length === 0) {
258
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Empty referrer array provided`));
259
+ return '';
260
+ }
261
+
262
+ const randomUrl = getRandomElement(referrerConfig);
263
+ const url = isValidUrl(randomUrl) ? randomUrl : '';
264
+
265
+ if (forceDebug) {
266
+ console.log(formatLogMessage('debug', `${REFERRER_TAG} Selected referrer from array (${referrerConfig.length} options): ${url || 'invalid URL'}`));
267
+ }
268
+
269
+ return url;
270
+ }
271
+
272
+ // Handle object modes
273
+ if (typeof referrerConfig === 'object' && referrerConfig !== null && referrerConfig.mode) {
274
+ switch (referrerConfig.mode) {
275
+ case 'random_search': {
276
+ const searchTerms = referrerConfig.search_terms;
277
+ const context = referrerConfig.context; // Optional context hint
278
+ return generateSearchReferrer(searchTerms, context, forceDebug);
238
279
  }
239
-
240
- const randomUrl = getRandomElement(referrerConfig);
241
- const url = isValidUrl(randomUrl) ? randomUrl : '';
242
-
243
- if (forceDebug) {
244
- console.log(`[debug] Selected referrer from array (${referrerConfig.length} options): ${url || 'invalid URL'}`);
280
+
281
+ case 'social_media': {
282
+ return generateSocialMediaReferrer(forceDebug);
245
283
  }
246
-
247
- return url;
248
- }
249
-
250
- // Handle object modes
251
- if (typeof referrerConfig === 'object' && referrerConfig !== null && referrerConfig.mode) {
252
- switch (referrerConfig.mode) {
253
- case 'random_search': {
254
- const searchTerms = referrerConfig.search_terms;
255
- const context = referrerConfig.context; // Optional context hint
256
- return generateSearchReferrer(searchTerms, context, forceDebug);
257
- }
258
-
259
- case 'social_media': {
260
- return generateSocialMediaReferrer(forceDebug);
261
- }
262
-
263
- case 'news_sites': {
264
- return generateNewsReferrer(forceDebug);
265
- }
266
-
267
- case 'direct_navigation': {
268
- if (forceDebug) console.log(`[debug] Using direct navigation (no referrer)`);
269
- return '';
270
- }
271
-
272
- case 'custom': {
273
- const url = isValidUrl(referrerConfig.url) ? referrerConfig.url : '';
274
- if (forceDebug) {
275
- console.log(`[debug] Using custom referrer URL: ${url || 'invalid URL provided'}`);
276
- }
277
- return url;
278
- }
279
-
280
- case 'mixed': {
281
- // Randomly choose between different referrer types
282
- const modes = ['random_search', 'social_media', 'news_sites'];
283
- const randomMode = getRandomElement(modes);
284
-
285
- if (forceDebug) console.log(`[debug] Mixed mode selected: ${randomMode}`);
286
-
287
- const mixedConfig = { mode: randomMode };
288
- if (randomMode === 'random_search' && referrerConfig.search_terms) {
289
- mixedConfig.search_terms = referrerConfig.search_terms;
290
- mixedConfig.context = referrerConfig.context;
291
- }
292
-
293
- return generateReferrerUrl(mixedConfig, forceDebug);
284
+
285
+ case 'news_sites': {
286
+ return generateNewsReferrer(forceDebug);
287
+ }
288
+
289
+ case 'direct_navigation': {
290
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Using direct navigation (no referrer)`));
291
+ return '';
292
+ }
293
+
294
+ case 'custom': {
295
+ const url = isValidUrl(referrerConfig.url) ? referrerConfig.url : '';
296
+ if (forceDebug) {
297
+ console.log(formatLogMessage('debug', `${REFERRER_TAG} Using custom referrer URL: ${url || 'invalid URL provided'}`));
294
298
  }
295
-
296
- default: {
297
- if (forceDebug) console.log(`[debug] Unknown referrer mode: ${referrerConfig.mode}`);
298
- return '';
299
+ return url;
300
+ }
301
+
302
+ case 'mixed': {
303
+ // Randomly choose between different referrer types
304
+ const modes = ['random_search', 'social_media', 'news_sites'];
305
+ const randomMode = getRandomElement(modes);
306
+
307
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Mixed mode selected: ${randomMode}`));
308
+
309
+ const mixedConfig = { mode: randomMode };
310
+ if (randomMode === 'random_search' && referrerConfig.search_terms) {
311
+ mixedConfig.search_terms = referrerConfig.search_terms;
312
+ mixedConfig.context = referrerConfig.context;
299
313
  }
314
+
315
+ return generateReferrerUrl(mixedConfig, forceDebug);
316
+ }
317
+
318
+ default: {
319
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Unknown referrer mode: ${referrerConfig.mode}`));
320
+ return '';
300
321
  }
301
322
  }
302
-
303
- if (forceDebug) console.log(`[debug] Invalid referrer configuration type: ${typeof referrerConfig}`);
304
- return '';
305
- } catch (err) {
306
- if (forceDebug) console.log(`[debug] Referrer generation failed: ${err.message}`);
307
- return '';
308
323
  }
324
+
325
+ if (forceDebug) console.log(formatLogMessage('debug', `${REFERRER_TAG} Invalid referrer configuration type: ${typeof referrerConfig}`));
326
+ return '';
309
327
  }
310
328
 
311
329
  /**
@@ -350,30 +368,24 @@ function validateReferrerConfig(referrerConfig) {
350
368
  return result;
351
369
  }
352
370
 
353
- // Validate arrays
371
+ // Validate arrays — every item gets checked. The old code spot-checked
372
+ // only first and last when length > 10 "for performance", but config
373
+ // validation runs ONCE at startup and referrer arrays are tiny; the
374
+ // perf savings were imaginary, the correctness gap (items 2..N-1 never
375
+ // validated, typo'd URLs slipping through) was real.
354
376
  if (Array.isArray(referrerConfig)) {
355
377
  if (referrerConfig.length === 0) {
356
378
  result.warnings.push('Empty referrer array will result in no referrer');
357
379
  return result;
358
380
  }
359
-
360
- // Fast validation: check only first and last items if array is large
361
- const itemsToCheck = referrerConfig.length > 10
362
- ? [referrerConfig[0], referrerConfig[referrerConfig.length - 1]]
363
- : referrerConfig;
364
-
365
- itemsToCheck.forEach((url, index) => {
366
- if (!isValidUrl(url)) {
367
- const actualIndex = itemsToCheck === referrerConfig ? index : (index === 0 ? 0 : referrerConfig.length - 1);
368
- result.errors.push(`Array item ${actualIndex} is not a valid HTTP/HTTPS URL: ${url}`);
369
- result.isValid = false;
370
- }
371
- });
372
381
 
373
- if (referrerConfig.length > 10 && itemsToCheck.length < referrerConfig.length) {
374
- result.warnings.push(`Large array (${referrerConfig.length} items): only validated first and last items for performance`);
382
+ for (let i = 0; i < referrerConfig.length; i++) {
383
+ if (!isValidUrl(referrerConfig[i])) {
384
+ result.errors.push(`Array item ${i} is not a valid HTTP/HTTPS URL: ${referrerConfig[i]}`);
385
+ result.isValid = false;
386
+ }
375
387
  }
376
-
388
+
377
389
  return result;
378
390
  }
379
391
 
@@ -406,11 +418,25 @@ function validateReferrerConfig(referrerConfig) {
406
418
  break;
407
419
 
408
420
  case 'random_search':
409
- if (referrerConfig.search_terms && !Array.isArray(referrerConfig.search_terms)) {
410
- result.warnings.push('search_terms should be an array of strings');
411
- }
412
- if (referrerConfig.search_terms && referrerConfig.search_terms.length === 0) {
413
- result.warnings.push('Empty search_terms array will use default terms');
421
+ // Upgrade from warning to error: generateSearchTerm reads
422
+ // customTerms.length, which throws TypeError on a non-array
423
+ // (e.g. a single string the user expected to be auto-wrapped).
424
+ // Letting this slip past validation produces a runtime crash
425
+ // mid-scan instead of a clean config-load failure.
426
+ if (referrerConfig.search_terms !== undefined && !Array.isArray(referrerConfig.search_terms)) {
427
+ result.isValid = false;
428
+ result.errors.push(`search_terms must be an array of strings (got ${typeof referrerConfig.search_terms})`);
429
+ } else if (Array.isArray(referrerConfig.search_terms)) {
430
+ if (referrerConfig.search_terms.length === 0) {
431
+ result.warnings.push('Empty search_terms array will use default terms');
432
+ } else {
433
+ for (let i = 0; i < referrerConfig.search_terms.length; i++) {
434
+ if (typeof referrerConfig.search_terms[i] !== 'string') {
435
+ result.isValid = false;
436
+ result.errors.push(`search_terms[${i}] must be a string (got ${typeof referrerConfig.search_terms[i]})`);
437
+ }
438
+ }
439
+ }
414
440
  }
415
441
  break;
416
442
  }
@@ -464,47 +490,14 @@ function validateReferrerDisable(referrerDisable) {
464
490
  return result;
465
491
  }
466
492
 
467
- /**
468
- * Gets available referrer modes and their descriptions
469
- * @returns {Object} Object containing mode descriptions
470
- */
471
- function getReferrerModes() {
472
- return {
473
- 'random_search': 'Generate random search engine referrers with customizable search terms',
474
- 'social_media': 'Use random social media platform referrers',
475
- 'news_sites': 'Use random news website referrers',
476
- 'direct_navigation': 'No referrer (simulates direct URL entry)',
477
- 'custom': 'Use a specific custom referrer URL',
478
- 'mixed': 'Randomly mix different referrer types for varied traffic simulation'
479
- };
480
- }
481
-
482
- /**
483
- * Gets statistics about available referrer collections
484
- * @returns {Object} Statistics about referrer collections
485
- */
486
- function getReferrerStats() {
487
- return {
488
- searchEngines: REFERRER_COLLECTIONS.SEARCH_ENGINES.length,
489
- socialMedia: REFERRER_COLLECTIONS.SOCIAL_MEDIA.length,
490
- newsSites: REFERRER_COLLECTIONS.NEWS_SITES.length,
491
- defaultSearchTerms: REFERRER_COLLECTIONS.DEFAULT_SEARCH_TERMS.length,
492
- ecommerceTerms: REFERRER_COLLECTIONS.ECOMMERCE_TERMS.length,
493
- techTerms: REFERRER_COLLECTIONS.TECH_TERMS.length
494
- };
495
- }
496
-
493
+ // Public surface used by nwss.js. Internal helpers
494
+ // (generateReferrerUrl, shouldDisableReferrer, generateSearch/Social/News
495
+ // Referrer, isValidUrl, REFERRER_COLLECTIONS) stay module-private
496
+ // the old export list included nine names no caller imported, plus two
497
+ // dead helper functions (getReferrerModes, getReferrerStats) that have
498
+ // been removed entirely.
497
499
  module.exports = {
498
- generateReferrerUrl,
499
500
  getReferrerForUrl,
500
- shouldDisableReferrer,
501
501
  validateReferrerConfig,
502
- validateReferrerDisable,
503
- getReferrerModes,
504
- getReferrerStats,
505
- generateSearchReferrer,
506
- generateSocialMediaReferrer,
507
- generateNewsReferrer,
508
- isValidUrl,
509
- REFERRER_COLLECTIONS
502
+ validateReferrerDisable
510
503
  };