recker 1.0.79 → 1.0.80-next.70ea84d

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 (196) hide show
  1. package/dist/browser/browser/mini.d.ts +2 -2
  2. package/dist/browser/browser/recker-mini.d.ts +8 -8
  3. package/dist/browser/browser/recker.d.ts +11 -8
  4. package/dist/browser/browser/recker.js +54 -6
  5. package/dist/browser/core/client.d.ts +15 -10
  6. package/dist/browser/core/client.js +54 -38
  7. package/dist/browser/index.iife.min.js +129 -130
  8. package/dist/browser/index.min.js +129 -130
  9. package/dist/browser/index.mini.iife.js +5697 -636
  10. package/dist/browser/index.mini.iife.min.js +47 -48
  11. package/dist/browser/index.mini.min.js +47 -48
  12. package/dist/browser/index.mini.umd.js +5697 -636
  13. package/dist/browser/index.mini.umd.min.js +47 -48
  14. package/dist/browser/index.umd.min.js +129 -130
  15. package/dist/browser/mini.d.ts +2 -2
  16. package/dist/browser/plugins/proxy-rotator.d.ts +2 -2
  17. package/dist/browser/plugins/proxy-rotator.js +6 -28
  18. package/dist/browser/recker-mini.d.ts +8 -8
  19. package/dist/browser/recker.d.ts +11 -8
  20. package/dist/browser/recker.js +54 -6
  21. package/dist/browser/scrape/document.js +2 -2
  22. package/dist/browser/scrape/element.js +7 -1
  23. package/dist/browser/scrape/parser/nodes/html.js +1 -1
  24. package/dist/browser/scrape/spider.d.ts +52 -0
  25. package/dist/browser/scrape/spider.js +620 -38
  26. package/dist/browser/scrape/types.d.ts +2 -0
  27. package/dist/browser/search/google.d.ts +26 -1
  28. package/dist/browser/search/google.js +427 -45
  29. package/dist/browser/seo/analyzer.d.ts +1 -0
  30. package/dist/browser/seo/analyzer.js +144 -1
  31. package/dist/browser/seo/index.d.ts +1 -1
  32. package/dist/browser/seo/keyword-campaign-analyzer.d.ts +2 -0
  33. package/dist/browser/seo/keyword-campaign-analyzer.js +538 -0
  34. package/dist/browser/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  35. package/dist/browser/seo/keyword-campaign-seed-advanced.js +269 -0
  36. package/dist/browser/seo/keyword-campaign-seed-core.d.ts +29 -0
  37. package/dist/browser/seo/keyword-campaign-seed-core.js +525 -0
  38. package/dist/browser/seo/keyword-campaign-shared.d.ts +165 -0
  39. package/dist/browser/seo/keyword-campaign-shared.js +59 -0
  40. package/dist/browser/seo/keyword-campaign.d.ts +4 -107
  41. package/dist/browser/seo/keyword-campaign.js +2 -380
  42. package/dist/browser/seo/keywords.js +5 -22
  43. package/dist/browser/seo/types.d.ts +19 -0
  44. package/dist/browser/transport/curl.d.ts +5 -1
  45. package/dist/browser/transport/curl.js +207 -50
  46. package/dist/browser/transport/undici.d.ts +4 -3
  47. package/dist/browser/transport/undici.js +123 -49
  48. package/dist/browser/types/index.d.ts +9 -3
  49. package/dist/browser/utils/binary-manager.js +26 -3
  50. package/dist/browser/utils/block-detector.d.ts +8 -0
  51. package/dist/browser/utils/block-detector.js +542 -7
  52. package/dist/cli/commands/hls-runner.js +5 -4
  53. package/dist/cli/commands/live-runner.js +5 -4
  54. package/dist/cli/commands/loadtest-runner.js +3 -2
  55. package/dist/cli/commands/search.d.ts +2 -0
  56. package/dist/cli/commands/search.js +105 -0
  57. package/dist/cli/commands/seo-runner.js +9 -7
  58. package/dist/cli/commands/seo.js +140 -1
  59. package/dist/cli/commands/serve.js +75 -131
  60. package/dist/cli/commands/server-runner.js +59 -82
  61. package/dist/cli/commands/spider-runner.d.ts +37 -1
  62. package/dist/cli/commands/spider-runner.js +134 -10
  63. package/dist/cli/commands/spider.d.ts +18 -1
  64. package/dist/cli/commands/spider.js +457 -27
  65. package/dist/cli/events/handlers/cli.js +30 -1
  66. package/dist/cli/events/handlers/tui.js +26 -0
  67. package/dist/cli/events/types.d.ts +27 -0
  68. package/dist/cli/handler.d.ts +2 -12
  69. package/dist/cli/handler.js +20 -15
  70. package/dist/cli/handlers/protocols.js +39 -12
  71. package/dist/cli/handlers/search.d.ts +2 -0
  72. package/dist/cli/handlers/search.js +171 -0
  73. package/dist/cli/handlers/seo-analyze.d.ts +1 -0
  74. package/dist/cli/handlers/seo-analyze.js +666 -0
  75. package/dist/cli/handlers/seo-robots.d.ts +1 -0
  76. package/dist/cli/handlers/seo-robots.js +76 -0
  77. package/dist/cli/handlers/seo-serp.d.ts +54 -0
  78. package/dist/cli/handlers/seo-serp.js +243 -0
  79. package/dist/cli/handlers/seo-sitemap.d.ts +1 -0
  80. package/dist/cli/handlers/seo-sitemap.js +55 -0
  81. package/dist/cli/handlers/seo-spider.d.ts +1 -0
  82. package/dist/cli/handlers/seo-spider.js +334 -0
  83. package/dist/cli/handlers/seo.d.ts +8 -4
  84. package/dist/cli/handlers/seo.js +294 -442
  85. package/dist/cli/handlers/spider.js +94 -17
  86. package/dist/cli/handlers/streaming.js +5 -1
  87. package/dist/cli/index.js +11 -2
  88. package/dist/cli/presets.d.ts +1 -1
  89. package/dist/cli/presets.js +15 -4
  90. package/dist/cli/tui/app.js +6 -1
  91. package/dist/cli/tui/components/rich-response.d.ts +72 -0
  92. package/dist/cli/tui/components/rich-response.js +117 -0
  93. package/dist/cli/tui/executor-commands/background.js +30 -24
  94. package/dist/cli/tui/executor-commands/testing.js +3 -2
  95. package/dist/cli/tui/hooks/useDomains.d.ts +17 -0
  96. package/dist/cli/tui/hooks/useHelp.js +15 -2
  97. package/dist/cli/tui/job-manager.d.ts +4 -4
  98. package/dist/cli/tui/job-manager.js +5 -1
  99. package/dist/cli/tui/spider-tui.d.ts +63 -0
  100. package/dist/cli/tui/spider-tui.js +120 -2
  101. package/dist/cli/types.d.ts +12 -0
  102. package/dist/cli/types.js +1 -0
  103. package/dist/cli/utils/option-helpers.d.ts +10 -0
  104. package/dist/cli/utils/option-helpers.js +63 -0
  105. package/dist/cli/utils/score-color.d.ts +11 -0
  106. package/dist/cli/utils/score-color.js +11 -0
  107. package/dist/cli/utils/serp-campaign.d.ts +53 -0
  108. package/dist/cli/utils/serp-campaign.js +53 -0
  109. package/dist/cli/utils/serp-config.d.ts +26 -0
  110. package/dist/cli/utils/serp-config.js +125 -0
  111. package/dist/core/client.d.ts +15 -10
  112. package/dist/core/client.js +54 -38
  113. package/dist/index.d.ts +1 -0
  114. package/dist/index.js +1 -0
  115. package/dist/mcp/cli.js +35 -34
  116. package/dist/mcp/client.d.ts +2 -2
  117. package/dist/mcp/client.js +20 -2
  118. package/dist/mcp/contract.d.ts +1 -1
  119. package/dist/mcp/profiles.js +5 -1
  120. package/dist/mcp/prompts/index.js +8 -4
  121. package/dist/mcp/resources/index.js +46 -23
  122. package/dist/mcp/server.js +9 -6
  123. package/dist/mcp/tools/protocols.js +9 -2
  124. package/dist/mcp/tools/registry.js +13 -3
  125. package/dist/mcp/tools/seo.js +427 -2
  126. package/dist/mcp/types.d.ts +5 -1
  127. package/dist/plugins/proxy-rotator.d.ts +2 -2
  128. package/dist/plugins/proxy-rotator.js +6 -28
  129. package/dist/raffel/client.d.ts +38 -0
  130. package/dist/raffel/client.js +282 -0
  131. package/dist/raffel/index.d.ts +2 -0
  132. package/dist/raffel/index.js +2 -0
  133. package/dist/raffel/types.d.ts +40 -0
  134. package/dist/raffel/types.js +14 -0
  135. package/dist/recker.d.ts +13 -7
  136. package/dist/recker.js +58 -6
  137. package/dist/scrape/document.js +2 -2
  138. package/dist/scrape/element.js +7 -1
  139. package/dist/scrape/parser/nodes/html.js +1 -1
  140. package/dist/scrape/spider.d.ts +52 -0
  141. package/dist/scrape/spider.js +620 -38
  142. package/dist/scrape/types.d.ts +2 -0
  143. package/dist/search/google.d.ts +26 -1
  144. package/dist/search/google.js +427 -45
  145. package/dist/search/index.d.ts +1 -1
  146. package/dist/seo/analyzer.d.ts +1 -0
  147. package/dist/seo/analyzer.js +144 -1
  148. package/dist/seo/index.d.ts +1 -1
  149. package/dist/seo/keyword-campaign-analyzer.d.ts +2 -0
  150. package/dist/seo/keyword-campaign-analyzer.js +538 -0
  151. package/dist/seo/keyword-campaign-seed-advanced.d.ts +17 -0
  152. package/dist/seo/keyword-campaign-seed-advanced.js +269 -0
  153. package/dist/seo/keyword-campaign-seed-core.d.ts +29 -0
  154. package/dist/seo/keyword-campaign-seed-core.js +525 -0
  155. package/dist/seo/keyword-campaign-shared.d.ts +165 -0
  156. package/dist/seo/keyword-campaign-shared.js +59 -0
  157. package/dist/seo/keyword-campaign.d.ts +4 -107
  158. package/dist/seo/keyword-campaign.js +2 -380
  159. package/dist/seo/keywords.js +5 -22
  160. package/dist/seo/types.d.ts +19 -0
  161. package/dist/template/index.d.ts +1 -1
  162. package/dist/template/types.d.ts +0 -2
  163. package/dist/testing/index.d.ts +1 -22
  164. package/dist/testing/index.js +1 -11
  165. package/dist/transport/curl.d.ts +5 -1
  166. package/dist/transport/curl.js +207 -50
  167. package/dist/transport/undici.d.ts +4 -3
  168. package/dist/transport/undici.js +123 -49
  169. package/dist/types/index.d.ts +9 -3
  170. package/dist/utils/binary-manager.js +26 -3
  171. package/dist/utils/block-detector.d.ts +8 -0
  172. package/dist/utils/block-detector.js +542 -7
  173. package/dist/version.js +1 -1
  174. package/package.json +12 -1
  175. package/dist/testing/mock-dns-server.d.ts +0 -69
  176. package/dist/testing/mock-dns-server.js +0 -269
  177. package/dist/testing/mock-ftp-server.d.ts +0 -89
  178. package/dist/testing/mock-ftp-server.js +0 -562
  179. package/dist/testing/mock-hls-server.d.ts +0 -80
  180. package/dist/testing/mock-hls-server.js +0 -381
  181. package/dist/testing/mock-http-server.d.ts +0 -124
  182. package/dist/testing/mock-http-server.js +0 -343
  183. package/dist/testing/mock-proxy-server.d.ts +0 -108
  184. package/dist/testing/mock-proxy-server.js +0 -615
  185. package/dist/testing/mock-sse-server.d.ts +0 -76
  186. package/dist/testing/mock-sse-server.js +0 -291
  187. package/dist/testing/mock-telnet-server.d.ts +0 -59
  188. package/dist/testing/mock-telnet-server.js +0 -274
  189. package/dist/testing/mock-udp-server.d.ts +0 -43
  190. package/dist/testing/mock-udp-server.js +0 -188
  191. package/dist/testing/mock-websocket-server.d.ts +0 -76
  192. package/dist/testing/mock-websocket-server.js +0 -334
  193. package/dist/testing/mock-whois-server.d.ts +0 -56
  194. package/dist/testing/mock-whois-server.js +0 -234
  195. package/dist/testing/proxy-certs.d.ts +0 -19
  196. package/dist/testing/proxy-certs.js +0 -208
@@ -2,9 +2,8 @@ import { createClient } from '../core/client.js';
2
2
  import { ValidationError } from '../core/errors.js';
3
3
  import { HttpRequest } from '../core/request.js';
4
4
  import { ScrapeDocument } from '../scrape/document.js';
5
- import { detectBlock } from '../utils/block-detector.js';
5
+ import { detectBlock, detectCaptcha, } from '../utils/block-detector.js';
6
6
  import { getRandomUserAgent } from '../utils/user-agent.js';
7
- const GOOGLE_SEARCH_BASE_URL = 'https://www.google.com/search';
8
7
  const GOOGLE_SEARCH_ORIGIN = 'https://www.google.com';
9
8
  const GOOGLE_RESULT_SNIPPET_SELECTOR_ORDER = [
10
9
  '[data-sncf="1"]',
@@ -23,6 +22,18 @@ const GOOGLE_RESULT_LINK_SELECTORS = [
23
22
  'a[href^="http://www.google.com/url?"]',
24
23
  ];
25
24
  const GOOGLE_RESULT_CONTAINER_SELECTORS = '[data-hveid], [data-ved], div[class*="g"], div[class*="MjjY"], div[class*="tF2Cxc"], [class*="xpd"]';
25
+ const GOOGLE_BROWSER_PROFILE_GS_LCRP_VALUES = [
26
+ 'EgZjaHJvbWUyBggAEEUYOdIBCDIwNzlqMGoxqAIAsAIA',
27
+ 'EgZjaHJvbWUyBggAEEUYOdIBCDIwMjlqMGoxqAIAsAIA',
28
+ 'EgZjaHJvbWUyBggAEEUYOdIBCDIwMzNqMGoxqAIAsAIA',
29
+ ];
30
+ const GOOGLE_BROWSER_PROFILE_AQS_VALUES = [
31
+ 'EgNQQxIEGAM%3D',
32
+ 'EgZjaHJvbWUyBggAEEUYOdIB',
33
+ 'EgYIARABGBQyGAM%3D',
34
+ ];
35
+ const GOOGLE_SEARCH_PARAM_IE_VALUES = ['UTF-8', 'utf-8', 'UTF-8', 'utf-8'];
36
+ const GOOGLE_BROWSER_SEI_LENGTH = 16;
26
37
  const GOOGLE_AD_CONTAINER_CLASS_HINTS = [
27
38
  'ad',
28
39
  'ads',
@@ -47,6 +58,47 @@ const GOOGLE_AD_TEXT_HINTS = [
47
58
  'anunciante',
48
59
  ];
49
60
  const COUNTRY_CODE_PATTERN = /^[a-z]{2}$/;
61
+ const GOOGLE_SEARCH_HOST_OVERRIDES = {
62
+ br: 'google.com.br',
63
+ de: 'google.de',
64
+ fr: 'google.fr',
65
+ es: 'google.es',
66
+ it: 'google.it',
67
+ in: 'google.co.in',
68
+ uk: 'google.co.uk',
69
+ gb: 'google.co.uk',
70
+ us: 'google.com',
71
+ au: 'google.com.au',
72
+ ca: 'google.ca',
73
+ mx: 'google.com.mx',
74
+ ar: 'google.com.ar',
75
+ };
76
+ const GOOGLE_DEFAULT_LANGUAGE_BY_COUNTRY = {
77
+ br: 'pt-BR',
78
+ pt: 'pt-PT',
79
+ pt_br: 'pt-BR',
80
+ de: 'de',
81
+ fr: 'fr',
82
+ es: 'es',
83
+ it: 'it',
84
+ in: 'en-IN',
85
+ mx: 'es',
86
+ ar: 'es',
87
+ au: 'en',
88
+ ca: 'en',
89
+ gb: 'en',
90
+ uk: 'en',
91
+ };
92
+ const SERP_BLOCK_CONFIDENCE = 0.6;
93
+ const URL_DOMAIN_MARKET_MAP = {
94
+ 'co.uk': 'uk',
95
+ 'com.br': 'br',
96
+ 'co.br': 'br',
97
+ 'com.au': 'au',
98
+ 'co.in': 'in',
99
+ 'com.ar': 'ar',
100
+ 'com.mx': 'mx',
101
+ };
50
102
  const COUNTRY_ALIASES = {
51
103
  us: 'us',
52
104
  usa: 'us',
@@ -85,6 +137,39 @@ const COUNTRY_ALIASES_NORMALIZED = Object.entries(COUNTRY_ALIASES).reduce((acc,
85
137
  acc[key.toLowerCase().replace(/[^a-z]/g, '_')] = value;
86
138
  return acc;
87
139
  }, {});
140
+ const SERP_SEARCH_SOURCES = ['google'];
141
+ function normalizeNumberTiming(value) {
142
+ if (typeof value !== 'number')
143
+ return undefined;
144
+ if (!Number.isFinite(value))
145
+ return undefined;
146
+ if (value <= 0)
147
+ return undefined;
148
+ return Math.round(value);
149
+ }
150
+ function toSearchTimings(timings) {
151
+ if (!timings || typeof timings !== 'object')
152
+ return undefined;
153
+ const parsed = timings;
154
+ const normalized = {
155
+ dns: normalizeNumberTiming(parsed.dns),
156
+ tcp: normalizeNumberTiming(parsed.tcp),
157
+ tls: normalizeNumberTiming(parsed.tls),
158
+ ttfb: normalizeNumberTiming(parsed.firstByte),
159
+ content: normalizeNumberTiming(parsed.content),
160
+ total: normalizeNumberTiming(parsed.total),
161
+ };
162
+ return Object.values(normalized).some(value => value !== undefined) ? normalized : undefined;
163
+ }
164
+ function detectSearchCaptcha(status, headers, body) {
165
+ const result = detectCaptcha({ status, headers }, body);
166
+ return {
167
+ detected: result.detected,
168
+ confidence: result.confidence,
169
+ provider: result.provider,
170
+ description: result.description,
171
+ };
172
+ }
88
173
  function cleanText(value) {
89
174
  return value.replace(/\s+/g, ' ').trim();
90
175
  }
@@ -99,6 +184,41 @@ function toParamValue(value) {
99
184
  const trimmed = String(value).trim();
100
185
  return trimmed.length > 0 ? trimmed : undefined;
101
186
  }
187
+ function pickRandomProfileValue(values) {
188
+ if (values.length === 0)
189
+ return '';
190
+ const index = Math.floor(Math.random() * values.length);
191
+ return values[index];
192
+ }
193
+ function generateSeiHint() {
194
+ const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
195
+ const chars = [];
196
+ for (let i = 0; i < GOOGLE_BROWSER_SEI_LENGTH; i++) {
197
+ chars.push(alphabet[Math.floor(Math.random() * alphabet.length)] ?? 'a');
198
+ }
199
+ return chars.join('');
200
+ }
201
+ function normalizeHumanSearchProfile(value) {
202
+ if (typeof value !== 'string') {
203
+ return 'off';
204
+ }
205
+ const normalized = value.trim().toLowerCase();
206
+ if (!normalized || normalized === 'off' || normalized === 'false' || normalized === '0') {
207
+ return 'off';
208
+ }
209
+ if (normalized === 'chrome' || normalized === 'browser' || normalized === 'human') {
210
+ return 'chrome';
211
+ }
212
+ return 'off';
213
+ }
214
+ function normalizeSearchSource(value) {
215
+ if (typeof value !== 'string')
216
+ return 'google';
217
+ const normalized = value.trim().toLowerCase();
218
+ return SERP_SEARCH_SOURCES.includes(normalized)
219
+ ? normalized
220
+ : 'google';
221
+ }
102
222
  function pick(...values) {
103
223
  for (const value of values) {
104
224
  if (value !== undefined) {
@@ -138,6 +258,84 @@ function resolveCountryCode(country, legacyGl) {
138
258
  }
139
259
  return legacyGl ? legacyGl.trim().toLowerCase() : '';
140
260
  }
261
+ function parseCountryFromHostname(hostname) {
262
+ const normalized = hostname
263
+ .toLowerCase()
264
+ .replace(/^www\./, '')
265
+ .trim();
266
+ const parts = normalized.split('.');
267
+ if (parts.length < 2)
268
+ return undefined;
269
+ const suffix2 = parts.slice(-2).join('.');
270
+ const mapped2 = URL_DOMAIN_MARKET_MAP[suffix2];
271
+ if (mapped2)
272
+ return mapped2;
273
+ const last = parts[parts.length - 1];
274
+ if (COUNTRY_CODE_PATTERN.test(last))
275
+ return last;
276
+ return undefined;
277
+ }
278
+ function resolveGoogleSearchOrigin(gl) {
279
+ if (!gl)
280
+ return GOOGLE_SEARCH_ORIGIN;
281
+ const normalized = gl.toLowerCase();
282
+ const host = GOOGLE_SEARCH_HOST_OVERRIDES[normalized] ?? `google.${normalized}`;
283
+ return `https://www.${host}`;
284
+ }
285
+ function normalizeSearchOrigin(origin) {
286
+ try {
287
+ const parsed = new URL(origin);
288
+ return `${parsed.protocol}//${parsed.host}`;
289
+ }
290
+ catch {
291
+ if (origin.startsWith('http://') || origin.startsWith('https://'))
292
+ return origin;
293
+ return `https://www.google.com`;
294
+ }
295
+ }
296
+ function pickGoogleSearchOrigins(primary, country) {
297
+ const unique = [];
298
+ const push = (value) => {
299
+ if (!value)
300
+ return;
301
+ if (!unique.includes(value))
302
+ unique.push(value);
303
+ };
304
+ const normalizedPrimary = normalizeSearchOrigin(primary);
305
+ const canonical = 'https://www.google.com';
306
+ push(normalizedPrimary);
307
+ push(canonical);
308
+ const countryHost = country ? GOOGLE_SEARCH_HOST_OVERRIDES[country.toLowerCase()] : '';
309
+ if (countryHost) {
310
+ push(`https://www.${countryHost}`);
311
+ }
312
+ if (countryHost && `https://www.${countryHost}` !== canonical) {
313
+ push('https://www.google.com');
314
+ }
315
+ if (normalizedPrimary.startsWith('https://')) {
316
+ push(normalizedPrimary.replace(/^https:/, 'http:'));
317
+ }
318
+ return unique;
319
+ }
320
+ export function inferSearchLocaleFromUrl(targetUrl) {
321
+ try {
322
+ const parsed = new URL(targetUrl);
323
+ const country = parseCountryFromHostname(parsed.hostname);
324
+ if (!country)
325
+ return undefined;
326
+ const resolved = normalizeCountryCode(country);
327
+ if (!resolved)
328
+ return undefined;
329
+ return {
330
+ country: resolved,
331
+ gl: resolved,
332
+ hl: GOOGLE_DEFAULT_LANGUAGE_BY_COUNTRY[resolved],
333
+ };
334
+ }
335
+ catch {
336
+ return undefined;
337
+ }
338
+ }
141
339
  function normalizeOptions(query, options = {}) {
142
340
  const normalizedQuery = cleanText(query);
143
341
  if (!normalizedQuery) {
@@ -147,6 +345,9 @@ function normalizeOptions(query, options = {}) {
147
345
  });
148
346
  }
149
347
  const resolvedTransport = (options.transport ?? 'auto');
348
+ const gl = resolveCountryCode(options.country, options.gl);
349
+ const searchOrigin = resolveGoogleSearchOrigin(gl);
350
+ const source = normalizeSearchSource(options.source);
150
351
  return {
151
352
  ...options,
152
353
  as_q: pick(options.as_q, options.asQ) ?? '',
@@ -159,16 +360,29 @@ function normalizeOptions(query, options = {}) {
159
360
  as_rights: pick(options.as_rights, options.asRights) ?? '',
160
361
  as_nlo: toParamValue(pick(options.as_nlo, options.asNlo)) ?? '',
161
362
  as_nhi: toParamValue(pick(options.as_nhi, options.asNhi)) ?? '',
162
- gl: resolveCountryCode(options.country, options.gl),
363
+ gl,
364
+ humanProfile: normalizeHumanSearchProfile(options.humanProfile),
365
+ source,
163
366
  transport: resolvedTransport,
164
367
  includeRawHtml: options.includeRawHtml ?? false,
368
+ searchOrigin,
369
+ searchBaseUrl: `${searchOrigin}/search`,
165
370
  };
166
371
  }
167
372
  function buildSearchUrl(query, options) {
168
373
  const params = new URLSearchParams();
169
374
  params.set('q', query);
170
- params.set('ie', 'UTF-8');
171
- params.set('oe', 'UTF-8');
375
+ params.set('oq', query);
376
+ params.set('sourceid', 'chrome');
377
+ params.set('ie', pickRandomProfileValue(GOOGLE_SEARCH_PARAM_IE_VALUES));
378
+ params.set('oe', pickRandomProfileValue(GOOGLE_SEARCH_PARAM_IE_VALUES));
379
+ if (options.humanProfile === 'chrome') {
380
+ const qs = query.trim().toLowerCase();
381
+ params.set('aqs', pickRandomProfileValue(GOOGLE_BROWSER_PROFILE_AQS_VALUES));
382
+ params.set('gs_lcrp', pickRandomProfileValue(GOOGLE_BROWSER_PROFILE_GS_LCRP_VALUES));
383
+ params.set('sei', generateSeiHint());
384
+ params.set('client', 'chrome');
385
+ }
172
386
  if (options.as_q)
173
387
  params.set('as_q', options.as_q);
174
388
  if (options.asEpq)
@@ -221,17 +435,26 @@ function buildSearchUrl(query, options) {
221
435
  }
222
436
  }
223
437
  }
224
- return `${GOOGLE_SEARCH_BASE_URL}?${params.toString()}`;
438
+ return `${options.searchBaseUrl}?${params.toString()}`;
225
439
  }
226
- function normalizeRequestHeaders(inputHeaders, userAgent) {
440
+ function normalizeRequestHeaders(inputHeaders, userAgent, referer, locale) {
441
+ const normalizedLocale = (locale || 'en-US').replace('_', '-');
442
+ const acceptLanguage = `${normalizedLocale},en-US;q=0.8,en;q=0.6`;
227
443
  const headers = new Headers({
228
- accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
229
- 'accept-language': 'en-US,en;q=0.9',
444
+ accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
445
+ 'accept-language': acceptLanguage,
230
446
  'cache-control': 'max-age=0',
231
- 'sec-ch-ua-mobile': '?0',
447
+ 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="132", "Google Chrome";v="132"',
232
448
  'sec-ch-ua-platform': '"Windows"',
449
+ 'sec-ch-ua-mobile': '?0',
450
+ 'sec-fetch-dest': 'document',
451
+ 'sec-fetch-mode': 'navigate',
452
+ 'sec-fetch-site': 'same-origin',
453
+ 'sec-fetch-user': '?1',
454
+ 'upgrade-insecure-requests': '1',
233
455
  'user-agent': userAgent,
234
- referer: GOOGLE_SEARCH_ORIGIN,
456
+ referer,
457
+ cookie: 'SOCS=CAI',
235
458
  });
236
459
  if (inputHeaders) {
237
460
  const incoming = new Headers(inputHeaders);
@@ -243,6 +466,24 @@ function normalizeRequestHeaders(inputHeaders, userAgent) {
243
466
  });
244
467
  return merged;
245
468
  }
469
+ function isUsefulSearchResponse(parsed, fetchResult) {
470
+ if (parsed.results.length > 0)
471
+ return true;
472
+ if (fetchResult.block?.blocked && fetchResult.block.confidence >= SERP_BLOCK_CONFIDENCE)
473
+ return false;
474
+ if (fetchResult.captcha?.detected && fetchResult.captcha.confidence >= SERP_BLOCK_CONFIDENCE)
475
+ return false;
476
+ return false;
477
+ }
478
+ function isHardSearchBlock(parsed, fetchResult) {
479
+ if (fetchResult.captcha?.detected && fetchResult.captcha.confidence >= SERP_BLOCK_CONFIDENCE)
480
+ return true;
481
+ if (fetchResult.block?.blocked && fetchResult.block.confidence >= SERP_BLOCK_CONFIDENCE)
482
+ return true;
483
+ if (parsed.results.length === 0 && fetchResult.block?.reason === 'captcha')
484
+ return true;
485
+ return false;
486
+ }
246
487
  async function hasImpersonateBinary() {
247
488
  try {
248
489
  const { hasImpersonate } = await import('../utils/binary-manager.js');
@@ -254,6 +495,7 @@ async function hasImpersonateBinary() {
254
495
  }
255
496
  async function fetchWithCurl(url, headers, timeout) {
256
497
  const { CurlTransport } = await import('../transport/curl.js');
498
+ const requestStart = performance.now();
257
499
  const transport = new CurlTransport();
258
500
  const request = new HttpRequest(url, {
259
501
  method: 'GET',
@@ -261,22 +503,32 @@ async function fetchWithCurl(url, headers, timeout) {
261
503
  timeout,
262
504
  });
263
505
  const response = await transport.dispatch(request);
506
+ const requestEnd = performance.now();
264
507
  const html = await response.text();
265
- return { html, status: response.status };
508
+ const parsedTimings = toSearchTimings(response.timings);
509
+ const timings = parsedTimings ?? {
510
+ total: Math.round(requestEnd - requestStart),
511
+ };
512
+ return {
513
+ html,
514
+ status: response.status,
515
+ responseHeaders: response.headers,
516
+ timings,
517
+ };
266
518
  }
267
519
  async function fetchSearchResults(url, options) {
268
- const headers = normalizeRequestHeaders(options.headers, options.userAgent ?? getRandomUserAgent('desktop.chrome'));
520
+ const headers = normalizeRequestHeaders(options.headers, options.userAgent ?? getRandomUserAgent('desktop.chrome'), options.searchOrigin, options.hl);
269
521
  const requestTimeout = options.timeout;
270
- const impersonateAvailable = options.transport !== 'undici' && (await hasImpersonateBinary());
271
- if (options.transport === 'curl' && !impersonateAvailable) {
272
- throw new ValidationError('Transport "curl" requires curl-impersonate; install it with `rek setup`', {
273
- field: 'transport',
274
- value: options.transport,
275
- });
276
- }
522
+ const impersonateAvailable = await hasImpersonateBinary();
277
523
  if (options.transport === 'curl') {
524
+ if (!impersonateAvailable) {
525
+ throw new ValidationError('Transport "curl" requires curl-impersonate; install it with `rek setup`', {
526
+ field: 'transport',
527
+ value: options.transport,
528
+ });
529
+ }
278
530
  const directResponse = await fetchWithCurl(url, headers, requestTimeout);
279
- const directBlock = detectBlock({ status: directResponse.status, headers: new Headers() }, directResponse.html);
531
+ const directBlock = detectBlock({ status: directResponse.status, headers: directResponse.responseHeaders }, directResponse.html);
280
532
  return {
281
533
  html: directResponse.html,
282
534
  status: directResponse.status,
@@ -284,13 +536,19 @@ async function fetchSearchResults(url, options) {
284
536
  fallbackUsed: false,
285
537
  impersonateAvailable,
286
538
  block: directBlock,
539
+ captcha: detectSearchCaptcha(directResponse.status, directResponse.responseHeaders, directResponse.html),
540
+ timings: directResponse.timings,
287
541
  };
288
542
  }
289
543
  const client = createClient({ timeout: requestTimeout });
290
544
  const performUndiciRequest = async () => {
291
- const response = await client.get(url, { headers });
545
+ const response = await client.get(url, {
546
+ headers,
547
+ throwHttpErrors: false,
548
+ });
292
549
  const html = await response.text();
293
550
  const block = detectBlock({ status: response.status, headers: response.headers }, html);
551
+ const captcha = detectSearchCaptcha(response.status, response.headers, html);
294
552
  return {
295
553
  html,
296
554
  status: response.status,
@@ -298,18 +556,39 @@ async function fetchSearchResults(url, options) {
298
556
  fallbackUsed: false,
299
557
  impersonateAvailable,
300
558
  block,
559
+ captcha,
560
+ timings: toSearchTimings(response.timings),
301
561
  };
302
562
  };
303
- if (options.transport === 'undici') {
304
- return performUndiciRequest();
305
- }
306
563
  if (!impersonateAvailable) {
307
564
  return performUndiciRequest();
308
565
  }
566
+ const shouldFallbackToUndici = (block, captcha) => {
567
+ if (block.blocked && block.confidence >= SERP_BLOCK_CONFIDENCE)
568
+ return true;
569
+ if (captcha.detected && captcha.confidence >= SERP_BLOCK_CONFIDENCE)
570
+ return true;
571
+ if (block.reason === 'captcha' || block.reason === 'rate-limit')
572
+ return true;
573
+ return false;
574
+ };
575
+ const getSearchResponseRisk = (response) => ((response.block?.confidence ?? 0) + (response.captcha?.confidence ?? 0));
576
+ const shouldKeepPrimaryImpersonateResponse = (primary, fallback) => (getSearchResponseRisk(primary) <= getSearchResponseRisk(fallback));
309
577
  try {
310
578
  const primaryImpersonateResponse = await fetchWithCurl(url, headers, requestTimeout);
311
- const primaryImpersonateBlock = detectBlock({ status: primaryImpersonateResponse.status, headers: new Headers() }, primaryImpersonateResponse.html);
312
- if (!primaryImpersonateBlock.blocked || primaryImpersonateBlock.confidence <= 0.7) {
579
+ const primaryImpersonateBlock = detectBlock({ status: primaryImpersonateResponse.status, headers: primaryImpersonateResponse.responseHeaders }, primaryImpersonateResponse.html);
580
+ const primaryImpersonateCaptcha = detectSearchCaptcha(primaryImpersonateResponse.status, primaryImpersonateResponse.responseHeaders, primaryImpersonateResponse.html);
581
+ const primaryResult = {
582
+ html: primaryImpersonateResponse.html,
583
+ status: primaryImpersonateResponse.status,
584
+ transport: 'curl',
585
+ fallbackUsed: false,
586
+ impersonateAvailable,
587
+ block: primaryImpersonateBlock,
588
+ captcha: primaryImpersonateCaptcha,
589
+ timings: primaryImpersonateResponse.timings,
590
+ };
591
+ if (!shouldFallbackToUndici(primaryImpersonateBlock, primaryImpersonateCaptcha)) {
313
592
  return {
314
593
  html: primaryImpersonateResponse.html,
315
594
  status: primaryImpersonateResponse.status,
@@ -317,8 +596,20 @@ async function fetchSearchResults(url, options) {
317
596
  fallbackUsed: false,
318
597
  impersonateAvailable,
319
598
  block: primaryImpersonateBlock,
599
+ captcha: primaryImpersonateCaptcha,
600
+ timings: primaryImpersonateResponse.timings,
601
+ };
602
+ }
603
+ const fallback = await performUndiciRequest();
604
+ if (!shouldKeepPrimaryImpersonateResponse(primaryResult, fallback)) {
605
+ return {
606
+ ...fallback,
607
+ transport: fallback.transport,
608
+ fallbackUsed: true,
609
+ impersonateAvailable,
320
610
  };
321
611
  }
612
+ return primaryResult;
322
613
  }
323
614
  catch {
324
615
  }
@@ -330,19 +621,36 @@ async function fetchSearchResults(url, options) {
330
621
  impersonateAvailable,
331
622
  };
332
623
  }
333
- function resolveSearchResultUrl(rawHref) {
624
+ function isGoogleOwnHost(hostname) {
625
+ const lower = hostname.toLowerCase();
626
+ return (lower === 'www.google.com' ||
627
+ lower === 'google.com' ||
628
+ lower.endsWith('.google.com') ||
629
+ lower.endsWith('.googleapis.com') ||
630
+ lower.endsWith('.googleusercontent.com') ||
631
+ lower.endsWith('.googlesyndication.com') ||
632
+ lower.endsWith('.doubleclick.net') ||
633
+ lower.endsWith('.gstatic.com') ||
634
+ lower.startsWith('www.google.') ||
635
+ /^([a-z0-9-]+\.)*google(\.[a-z]{2,})+$/.test(lower));
636
+ }
637
+ function resolveSearchResultUrl(rawHref, searchOrigin) {
334
638
  try {
335
639
  const normalized = rawHref.startsWith('//') ? `https:${rawHref}` : rawHref;
336
- const parsed = new URL(normalized, GOOGLE_SEARCH_ORIGIN);
640
+ const parsed = new URL(normalized, searchOrigin);
337
641
  const rawResultUrl = parsed.searchParams.get('q') ?? parsed.searchParams.get('url');
338
- if (!rawResultUrl)
339
- return null;
340
- const candidate = new URL(decodeURIComponent(rawResultUrl), GOOGLE_SEARCH_ORIGIN);
341
- if (!candidate.protocol.startsWith('http'))
342
- return null;
343
- if (candidate.hostname === 'www.google.com' && candidate.pathname === '/search')
344
- return null;
345
- return candidate.toString();
642
+ if (rawResultUrl) {
643
+ const candidate = new URL(decodeURIComponent(rawResultUrl), searchOrigin);
644
+ if (!candidate.protocol.startsWith('http'))
645
+ return null;
646
+ if (candidate.pathname === '/search' && candidate.hostname.includes('google.'))
647
+ return null;
648
+ return candidate.toString();
649
+ }
650
+ if (parsed.protocol.startsWith('http') && !isGoogleOwnHost(parsed.hostname)) {
651
+ return parsed.toString();
652
+ }
653
+ return null;
346
654
  }
347
655
  catch {
348
656
  return null;
@@ -490,8 +798,8 @@ function parseResultStats(text) {
490
798
  const parsed = Number.parseInt(match[1], 10);
491
799
  return Number.isFinite(parsed) ? parsed : undefined;
492
800
  }
493
- function parseSearchPage(html, options) {
494
- const doc = ScrapeDocument.createSync(html, { baseUrl: GOOGLE_SEARCH_ORIGIN });
801
+ function parseSearchPage(html, options, searchOrigin) {
802
+ const doc = ScrapeDocument.createSync(html, { baseUrl: searchOrigin });
495
803
  const results = [];
496
804
  const seen = new Set();
497
805
  const maxResults = options.maxResults ? Number(options.maxResults) : undefined;
@@ -501,7 +809,7 @@ function parseSearchPage(html, options) {
501
809
  const rawHref = anchor.attr('href');
502
810
  if (!rawHref)
503
811
  continue;
504
- const resultUrl = resolveSearchResultUrl(rawHref);
812
+ const resultUrl = resolveSearchResultUrl(rawHref, searchOrigin);
505
813
  if (!resultUrl || seen.has(resultUrl))
506
814
  continue;
507
815
  const titleText = (() => {
@@ -551,6 +859,49 @@ function parseSearchPage(html, options) {
551
859
  break;
552
860
  }
553
861
  }
862
+ for (const anchor of doc.selectAll('a[href^="https://"], a[href^="http://"]')) {
863
+ if (typeof maxResults === 'number' && Number.isFinite(maxResults) && results.length >= maxResults)
864
+ break;
865
+ const rawHref = anchor.attr('href');
866
+ if (!rawHref)
867
+ continue;
868
+ const resultUrl = resolveSearchResultUrl(rawHref, searchOrigin);
869
+ if (!resultUrl || seen.has(resultUrl))
870
+ continue;
871
+ const titleText = cleanText(anchor.find('h3').text());
872
+ if (!titleText)
873
+ continue;
874
+ const resultContainer = anchor.parents(GOOGLE_RESULT_CONTAINER_SELECTORS).first();
875
+ const placement = detectResultPlacement(anchor, resultContainer);
876
+ const snippet = (() => {
877
+ for (const selector of GOOGLE_RESULT_SNIPPET_SELECTOR_ORDER) {
878
+ const snippetNode = resultContainer.find(selector).first();
879
+ const snippetText = cleanText(snippetNode.text());
880
+ if (looksLikeSnippet(snippetText, titleText))
881
+ return snippetText;
882
+ }
883
+ const fallbackElements = resultContainer.find('span,div').toArray();
884
+ for (const fallbackEl of fallbackElements) {
885
+ const fallbackText = cleanText(fallbackEl.text());
886
+ if (looksLikeSnippet(fallbackText, titleText))
887
+ return fallbackText;
888
+ }
889
+ const rootFallback = cleanText(resultContainer.text());
890
+ if (looksLikeSnippet(rootFallback, titleText))
891
+ return rootFallback.slice(0, 240);
892
+ return undefined;
893
+ })();
894
+ results.push({
895
+ rank: results.length + 1,
896
+ title: titleText,
897
+ url: resultUrl,
898
+ snippet,
899
+ displayedUrl: extractDisplayedUrl(resultUrl, anchor.text()),
900
+ placement: placement.placement,
901
+ placementHint: placement.placementHint,
902
+ });
903
+ seen.add(resultUrl);
904
+ }
554
905
  const nextPageRaw = (() => {
555
906
  const candidate = doc.selectFirst('a#pnnext, a[aria-label="Next"], a[id="pnnext"]').first();
556
907
  if (candidate && candidate.length) {
@@ -558,7 +909,7 @@ function parseSearchPage(html, options) {
558
909
  if (!href)
559
910
  return undefined;
560
911
  try {
561
- return new URL(href, GOOGLE_SEARCH_ORIGIN).toString();
912
+ return new URL(href, searchOrigin).toString();
562
913
  }
563
914
  catch {
564
915
  return undefined;
@@ -591,12 +942,41 @@ function parseSearchPage(html, options) {
591
942
  }
592
943
  export async function searchGoogleAdvanced(query, options = {}) {
593
944
  const normalized = normalizeOptions(query, options);
594
- const searchUrl = buildSearchUrl(query, normalized);
595
- const fetchResult = await fetchSearchResults(searchUrl, normalized);
596
- const parsed = parseSearchPage(fetchResult.html, normalized);
945
+ const originCandidates = pickGoogleSearchOrigins(normalized.searchOrigin, normalized.gl);
946
+ let fetchResult;
947
+ let parsed;
948
+ let selectedSearchUrl = '';
949
+ for (let index = 0; index < originCandidates.length; index += 1) {
950
+ const origin = originCandidates[index];
951
+ const isFinalCandidate = index === originCandidates.length - 1;
952
+ const candidateOptions = {
953
+ ...normalized,
954
+ searchOrigin: origin,
955
+ searchBaseUrl: `${origin}/search`,
956
+ };
957
+ const candidateUrl = buildSearchUrl(query, candidateOptions);
958
+ const candidateResult = await fetchSearchResults(candidateUrl, candidateOptions);
959
+ const candidateParsed = parseSearchPage(candidateResult.html, candidateOptions, candidateOptions.searchOrigin);
960
+ fetchResult = candidateResult;
961
+ parsed = candidateParsed;
962
+ selectedSearchUrl = candidateUrl;
963
+ if (isUsefulSearchResponse(candidateParsed, candidateResult) || isFinalCandidate) {
964
+ break;
965
+ }
966
+ if (isHardSearchBlock(candidateParsed, candidateResult)) {
967
+ break;
968
+ }
969
+ if (candidateParsed.results.length === 0) {
970
+ continue;
971
+ }
972
+ }
973
+ if (!fetchResult || !parsed) {
974
+ throw new ValidationError('Failed to execute Google search attempts');
975
+ }
597
976
  const response = {
598
977
  query: normalized.query,
599
- searchUrl,
978
+ source: normalized.source,
979
+ searchUrl: selectedSearchUrl,
600
980
  results: parsed.results,
601
981
  transport: {
602
982
  requested: normalized.transport,
@@ -605,7 +985,9 @@ export async function searchGoogleAdvanced(query, options = {}) {
605
985
  impersonateAvailable: fetchResult.impersonateAvailable,
606
986
  },
607
987
  status: fetchResult.status,
988
+ captcha: fetchResult.captcha,
608
989
  block: fetchResult.block,
990
+ timings: fetchResult.timings,
609
991
  nextPageUrl: parsed.nextPageUrl,
610
992
  nextPageStart: parsed.nextPageStart,
611
993
  resultStats: parsed.resultStats,
@@ -1,3 +1,3 @@
1
1
  export { searchGoogleAdvanced } from './google.js';
2
- export type { GoogleSearchAdvancedOptions, GoogleSearchResult, GoogleSearchResultPlacement, GoogleSearchResponse, SearchTransportDetails, } from './google.js';
2
+ export type { GoogleSearchAdvancedOptions, GoogleSearchResult, GoogleSearchResultPlacement, GoogleSearchResponse, SerpSearchSource, SearchTransportDetails, } from './google.js';
3
3
  export type { SearchTransport } from './google.js';
@@ -13,6 +13,7 @@ export declare class SeoAnalyzer {
13
13
  analyze(): SeoReport;
14
14
  private getMainBody;
15
15
  private detectPageType;
16
+ private extractContentSectionSignals;
16
17
  private getVisibleText;
17
18
  private buildRuleContext;
18
19
  private analyzeUrlQuality;