@detiq/crawler 0.1.3

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 (412) hide show
  1. package/Dockerfile +49 -0
  2. package/README.md +11 -0
  3. package/dist/accessibility-auditor.d.ts +29 -0
  4. package/dist/accessibility-auditor.js +57 -0
  5. package/dist/accessibility-auditor.js.map +7 -0
  6. package/dist/ai-login.d.ts +32 -0
  7. package/dist/ai-login.js +177 -0
  8. package/dist/ai-login.js.map +7 -0
  9. package/dist/auth-token-rotator.d.ts +47 -0
  10. package/dist/auth-token-rotator.js +58 -0
  11. package/dist/auth-token-rotator.js.map +7 -0
  12. package/dist/auth.d.ts +20 -0
  13. package/dist/auth.js +329 -0
  14. package/dist/auth.js.map +7 -0
  15. package/dist/browser-extension.d.ts +37 -0
  16. package/dist/browser-extension.js +113 -0
  17. package/dist/browser-extension.js.map +7 -0
  18. package/dist/captcha-solver.d.ts +16 -0
  19. package/dist/captcha-solver.js +105 -0
  20. package/dist/captcha-solver.js.map +7 -0
  21. package/dist/color-contrast.d.ts +30 -0
  22. package/dist/color-contrast.js +78 -0
  23. package/dist/color-contrast.js.map +7 -0
  24. package/dist/content-chunker.d.ts +18 -0
  25. package/dist/content-chunker.js +152 -0
  26. package/dist/content-chunker.js.map +7 -0
  27. package/dist/content-dedup.d.ts +23 -0
  28. package/dist/content-dedup.js +60 -0
  29. package/dist/content-dedup.js.map +7 -0
  30. package/dist/cookie-consent.d.ts +25 -0
  31. package/dist/cookie-consent.js +78 -0
  32. package/dist/cookie-consent.js.map +7 -0
  33. package/dist/cookie-jar.d.ts +20 -0
  34. package/dist/cookie-jar.js +61 -0
  35. package/dist/cookie-jar.js.map +7 -0
  36. package/dist/crawler.d.ts +179 -0
  37. package/dist/crawler.js +3672 -0
  38. package/dist/crawler.js.map +7 -0
  39. package/dist/custom-js-injector.d.ts +42 -0
  40. package/dist/custom-js-injector.js +50 -0
  41. package/dist/custom-js-injector.js.map +7 -0
  42. package/dist/dark-mode-capture.d.ts +23 -0
  43. package/dist/dark-mode-capture.js +75 -0
  44. package/dist/dark-mode-capture.js.map +7 -0
  45. package/dist/dead-link-detector.d.ts +37 -0
  46. package/dist/dead-link-detector.js +50 -0
  47. package/dist/dead-link-detector.js.map +7 -0
  48. package/dist/element-hash.d.ts +9 -0
  49. package/dist/element-hash.js +9 -0
  50. package/dist/element-hash.js.map +7 -0
  51. package/dist/email-otp.d.ts +42 -0
  52. package/dist/email-otp.js +177 -0
  53. package/dist/email-otp.js.map +7 -0
  54. package/dist/engine-router.d.ts +47 -0
  55. package/dist/engine-router.js +94 -0
  56. package/dist/engine-router.js.map +7 -0
  57. package/dist/export/bulk-export.d.ts +26 -0
  58. package/dist/export/bulk-export.js +76 -0
  59. package/dist/export/bulk-export.js.map +7 -0
  60. package/dist/export/vector-export.d.ts +76 -0
  61. package/dist/export/vector-export.js +85 -0
  62. package/dist/export/vector-export.js.map +7 -0
  63. package/dist/extractors/advanced.d.ts +54 -0
  64. package/dist/extractors/advanced.js +245 -0
  65. package/dist/extractors/advanced.js.map +7 -0
  66. package/dist/extractors/artifact-envelope.d.ts +63 -0
  67. package/dist/extractors/artifact-envelope.js +63 -0
  68. package/dist/extractors/artifact-envelope.js.map +7 -0
  69. package/dist/failure-analysis-agent.d.ts +16 -0
  70. package/dist/failure-analysis-agent.js +40 -0
  71. package/dist/failure-analysis-agent.js.map +7 -0
  72. package/dist/feature-flag-detector.d.ts +30 -0
  73. package/dist/feature-flag-detector.js +55 -0
  74. package/dist/feature-flag-detector.js.map +7 -0
  75. package/dist/form-detector.d.ts +34 -0
  76. package/dist/form-detector.js +58 -0
  77. package/dist/form-detector.js.map +7 -0
  78. package/dist/frontier.d.ts +32 -0
  79. package/dist/frontier.js +83 -0
  80. package/dist/frontier.js.map +7 -0
  81. package/dist/gap-pass.d.ts +35 -0
  82. package/dist/gap-pass.js +81 -0
  83. package/dist/gap-pass.js.map +7 -0
  84. package/dist/graphql-detector.d.ts +39 -0
  85. package/dist/graphql-detector.js +86 -0
  86. package/dist/graphql-detector.js.map +7 -0
  87. package/dist/http-engine.d.ts +80 -0
  88. package/dist/http-engine.js +304 -0
  89. package/dist/http-engine.js.map +7 -0
  90. package/dist/incremental-diff.d.ts +59 -0
  91. package/dist/incremental-diff.js +61 -0
  92. package/dist/incremental-diff.js.map +7 -0
  93. package/dist/index.d.ts +35 -0
  94. package/dist/index.js +61 -0
  95. package/dist/index.js.map +7 -0
  96. package/dist/input-fuzzer.d.ts +30 -0
  97. package/dist/input-fuzzer.js +105 -0
  98. package/dist/input-fuzzer.js.map +7 -0
  99. package/dist/interactive-probe.d.ts +104 -0
  100. package/dist/interactive-probe.js +442 -0
  101. package/dist/interactive-probe.js.map +7 -0
  102. package/dist/job-queue.d.ts +171 -0
  103. package/dist/job-queue.js +2256 -0
  104. package/dist/job-queue.js.map +7 -0
  105. package/dist/js-coverage.d.ts +29 -0
  106. package/dist/js-coverage.js +56 -0
  107. package/dist/js-coverage.js.map +7 -0
  108. package/dist/js-router-detector.d.ts +17 -0
  109. package/dist/js-router-detector.js +90 -0
  110. package/dist/js-router-detector.js.map +7 -0
  111. package/dist/language-detector.d.ts +22 -0
  112. package/dist/language-detector.js +57 -0
  113. package/dist/language-detector.js.map +7 -0
  114. package/dist/link-classifier.d.ts +23 -0
  115. package/dist/link-classifier.js +40 -0
  116. package/dist/link-classifier.js.map +7 -0
  117. package/dist/live-view.d.ts +36 -0
  118. package/dist/live-view.js +100 -0
  119. package/dist/live-view.js.map +7 -0
  120. package/dist/logger.d.ts +2 -0
  121. package/dist/logger.js +10 -0
  122. package/dist/logger.js.map +7 -0
  123. package/dist/markdown-extract.d.ts +8 -0
  124. package/dist/markdown-extract.js +54 -0
  125. package/dist/markdown-extract.js.map +7 -0
  126. package/dist/native-crawler.d.ts +69 -0
  127. package/dist/native-crawler.js +839 -0
  128. package/dist/native-crawler.js.map +7 -0
  129. package/dist/network-observer.d.ts +49 -0
  130. package/dist/network-observer.js +149 -0
  131. package/dist/network-observer.js.map +7 -0
  132. package/dist/network-throttle.d.ts +25 -0
  133. package/dist/network-throttle.js +34 -0
  134. package/dist/network-throttle.js.map +7 -0
  135. package/dist/openapi-generator.d.ts +49 -0
  136. package/dist/openapi-generator.js +89 -0
  137. package/dist/openapi-generator.js.map +7 -0
  138. package/dist/pdf-extractor.d.ts +28 -0
  139. package/dist/pdf-extractor.js +81 -0
  140. package/dist/pdf-extractor.js.map +7 -0
  141. package/dist/performance-metrics.d.ts +53 -0
  142. package/dist/performance-metrics.js +83 -0
  143. package/dist/performance-metrics.js.map +7 -0
  144. package/dist/phash.d.ts +38 -0
  145. package/dist/phash.js +115 -0
  146. package/dist/phash.js.map +7 -0
  147. package/dist/pii-scrubber.d.ts +21 -0
  148. package/dist/pii-scrubber.js +75 -0
  149. package/dist/pii-scrubber.js.map +7 -0
  150. package/dist/playwright-recorder.d.ts +39 -0
  151. package/dist/playwright-recorder.js +123 -0
  152. package/dist/playwright-recorder.js.map +7 -0
  153. package/dist/policies/crawl-safety.d.ts +9 -0
  154. package/dist/policies/crawl-safety.js +37 -0
  155. package/dist/policies/crawl-safety.js.map +7 -0
  156. package/dist/profiles/domain-profile.d.ts +19 -0
  157. package/dist/profiles/domain-profile.js +140 -0
  158. package/dist/profiles/domain-profile.js.map +7 -0
  159. package/dist/proxy.d.ts +50 -0
  160. package/dist/proxy.js +165 -0
  161. package/dist/proxy.js.map +7 -0
  162. package/dist/rate-limiter.d.ts +31 -0
  163. package/dist/rate-limiter.js +64 -0
  164. package/dist/rate-limiter.js.map +7 -0
  165. package/dist/redirect-tracker.d.ts +37 -0
  166. package/dist/redirect-tracker.js +65 -0
  167. package/dist/redirect-tracker.js.map +7 -0
  168. package/dist/redis-frontier.d.ts +48 -0
  169. package/dist/redis-frontier.js +126 -0
  170. package/dist/redis-frontier.js.map +7 -0
  171. package/dist/redis-queue.d.ts +48 -0
  172. package/dist/redis-queue.js +165 -0
  173. package/dist/redis-queue.js.map +7 -0
  174. package/dist/response-cache.d.ts +38 -0
  175. package/dist/response-cache.js +140 -0
  176. package/dist/response-cache.js.map +7 -0
  177. package/dist/responsive-detector.d.ts +29 -0
  178. package/dist/responsive-detector.js +46 -0
  179. package/dist/responsive-detector.js.map +7 -0
  180. package/dist/retry-policy.d.ts +20 -0
  181. package/dist/retry-policy.js +54 -0
  182. package/dist/retry-policy.js.map +7 -0
  183. package/dist/robots-txt.d.ts +9 -0
  184. package/dist/robots-txt.js +77 -0
  185. package/dist/robots-txt.js.map +7 -0
  186. package/dist/screen-reader.d.ts +16 -0
  187. package/dist/screen-reader.js +46 -0
  188. package/dist/screen-reader.js.map +7 -0
  189. package/dist/screenshot-masker.d.ts +28 -0
  190. package/dist/screenshot-masker.js +62 -0
  191. package/dist/screenshot-masker.js.map +7 -0
  192. package/dist/seo-checker.d.ts +33 -0
  193. package/dist/seo-checker.js +91 -0
  194. package/dist/seo-checker.js.map +7 -0
  195. package/dist/session-capture.d.ts +35 -0
  196. package/dist/session-capture.js +267 -0
  197. package/dist/session-capture.js.map +7 -0
  198. package/dist/session-export.d.ts +49 -0
  199. package/dist/session-export.js +117 -0
  200. package/dist/session-export.js.map +7 -0
  201. package/dist/sitemap-writer.d.ts +21 -0
  202. package/dist/sitemap-writer.js +47 -0
  203. package/dist/sitemap-writer.js.map +7 -0
  204. package/dist/sitemap.d.ts +51 -0
  205. package/dist/sitemap.js +152 -0
  206. package/dist/sitemap.js.map +7 -0
  207. package/dist/skyvern.d.ts +56 -0
  208. package/dist/skyvern.js +129 -0
  209. package/dist/skyvern.js.map +7 -0
  210. package/dist/sqs-queue.d.ts +2 -0
  211. package/dist/sqs-queue.js +47 -0
  212. package/dist/sqs-queue.js.map +7 -0
  213. package/dist/sso-token.d.ts +29 -0
  214. package/dist/sso-token.js +76 -0
  215. package/dist/sso-token.js.map +7 -0
  216. package/dist/stealth/human-behavior.d.ts +20 -0
  217. package/dist/stealth/human-behavior.js +168 -0
  218. package/dist/stealth/human-behavior.js.map +7 -0
  219. package/dist/telemetry.d.ts +34 -0
  220. package/dist/telemetry.js +77 -0
  221. package/dist/telemetry.js.map +7 -0
  222. package/dist/test-executor.d.ts +67 -0
  223. package/dist/test-executor.js +311 -0
  224. package/dist/test-executor.js.map +7 -0
  225. package/dist/tls-fingerprint.d.ts +50 -0
  226. package/dist/tls-fingerprint.js +81 -0
  227. package/dist/tls-fingerprint.js.map +7 -0
  228. package/dist/url-pattern-completer.d.ts +28 -0
  229. package/dist/url-pattern-completer.js +87 -0
  230. package/dist/url-pattern-completer.js.map +7 -0
  231. package/dist/visual-diff-verdict.d.ts +40 -0
  232. package/dist/visual-diff-verdict.js +25 -0
  233. package/dist/visual-diff-verdict.js.map +7 -0
  234. package/dist/webhook-notifier.d.ts +30 -0
  235. package/dist/webhook-notifier.js +76 -0
  236. package/dist/webhook-notifier.js.map +7 -0
  237. package/dist/websocket-detector.d.ts +24 -0
  238. package/dist/websocket-detector.js +60 -0
  239. package/dist/websocket-detector.js.map +7 -0
  240. package/dist/worker.d.ts +1 -0
  241. package/dist/worker.js +33 -0
  242. package/dist/worker.js.map +7 -0
  243. package/package.json +45 -0
  244. package/src/accessibility-auditor.ts +102 -0
  245. package/src/ai-login.ts +218 -0
  246. package/src/auth-token-rotator.ts +89 -0
  247. package/src/auth.ts +396 -0
  248. package/src/browser-extension.ts +186 -0
  249. package/src/captcha-solver.ts +143 -0
  250. package/src/color-contrast.ts +116 -0
  251. package/src/content-chunker.ts +170 -0
  252. package/src/content-dedup.ts +99 -0
  253. package/src/cookie-consent.ts +106 -0
  254. package/src/cookie-jar.ts +91 -0
  255. package/src/crawler.ts +4305 -0
  256. package/src/custom-js-injector.ts +93 -0
  257. package/src/dark-mode-capture.ts +110 -0
  258. package/src/dead-link-detector.ts +76 -0
  259. package/src/element-hash.ts +16 -0
  260. package/src/email-otp.ts +252 -0
  261. package/src/engine-router.ts +141 -0
  262. package/src/export/bulk-export.ts +85 -0
  263. package/src/export/vector-export.ts +160 -0
  264. package/src/extractors/advanced.ts +283 -0
  265. package/src/extractors/artifact-envelope.ts +111 -0
  266. package/src/failure-analysis-agent.ts +49 -0
  267. package/src/feature-flag-detector.ts +97 -0
  268. package/src/form-detector.ts +103 -0
  269. package/src/frontier.test.ts +64 -0
  270. package/src/frontier.ts +112 -0
  271. package/src/gap-pass.ts +122 -0
  272. package/src/graphql-detector.ts +129 -0
  273. package/src/http-engine.ts +433 -0
  274. package/src/incremental-diff.ts +121 -0
  275. package/src/index.ts +35 -0
  276. package/src/input-fuzzer.ts +129 -0
  277. package/src/interactive-probe.ts +623 -0
  278. package/src/job-queue.test.ts +120 -0
  279. package/src/job-queue.ts +2701 -0
  280. package/src/js-coverage.ts +87 -0
  281. package/src/js-router-detector.ts +112 -0
  282. package/src/language-detector.ts +82 -0
  283. package/src/link-classifier.ts +65 -0
  284. package/src/live-view.ts +141 -0
  285. package/src/logger.ts +9 -0
  286. package/src/markdown-extract.test.ts +73 -0
  287. package/src/markdown-extract.ts +76 -0
  288. package/src/native-crawler.ts +1106 -0
  289. package/src/network-observer.ts +204 -0
  290. package/src/network-throttle.ts +69 -0
  291. package/src/openapi-generator.ts +135 -0
  292. package/src/pdf-extractor.ts +123 -0
  293. package/src/performance-metrics.ts +134 -0
  294. package/src/phash.ts +151 -0
  295. package/src/pii-scrubber.ts +99 -0
  296. package/src/playwright-recorder.ts +170 -0
  297. package/src/policies/crawl-safety.ts +46 -0
  298. package/src/profiles/domain-profile.ts +167 -0
  299. package/src/proxy.ts +238 -0
  300. package/src/rate-limiter.ts +87 -0
  301. package/src/redirect-tracker.ts +97 -0
  302. package/src/redis-frontier.ts +143 -0
  303. package/src/redis-queue.ts +208 -0
  304. package/src/response-cache.ts +150 -0
  305. package/src/responsive-detector.ts +92 -0
  306. package/src/retry-policy.ts +87 -0
  307. package/src/robots-txt.ts +96 -0
  308. package/src/screen-reader.ts +89 -0
  309. package/src/screenshot-masker.ts +98 -0
  310. package/src/seo-checker.ts +134 -0
  311. package/src/session-capture.ts +325 -0
  312. package/src/session-export.test.ts +75 -0
  313. package/src/session-export.ts +172 -0
  314. package/src/sitemap-writer.ts +65 -0
  315. package/src/sitemap.test.ts +131 -0
  316. package/src/sitemap.ts +224 -0
  317. package/src/skyvern.ts +199 -0
  318. package/src/sqs-queue.ts +49 -0
  319. package/src/sso-token.ts +120 -0
  320. package/src/stealth/human-behavior.ts +191 -0
  321. package/src/telemetry.ts +122 -0
  322. package/src/test-executor.test.ts +72 -0
  323. package/src/test-executor.ts +424 -0
  324. package/src/tls-fingerprint.ts +111 -0
  325. package/src/url-pattern-completer.ts +114 -0
  326. package/src/visual-diff-verdict.test.ts +80 -0
  327. package/src/visual-diff-verdict.ts +57 -0
  328. package/src/webhook-notifier.ts +109 -0
  329. package/src/websocket-detector.ts +83 -0
  330. package/src/worker.ts +48 -0
  331. package/tsconfig.json +13 -0
  332. package/types/accessibility-auditor.d.ts +29 -0
  333. package/types/ai-login.d.ts +32 -0
  334. package/types/auth-token-rotator.d.ts +47 -0
  335. package/types/auth.d.ts +20 -0
  336. package/types/browser-extension.d.ts +37 -0
  337. package/types/bull-worker.d.ts +6 -0
  338. package/types/captcha-solver.d.ts +16 -0
  339. package/types/color-contrast.d.ts +30 -0
  340. package/types/content-chunker.d.ts +18 -0
  341. package/types/content-dedup.d.ts +23 -0
  342. package/types/cookie-consent.d.ts +25 -0
  343. package/types/cookie-jar.d.ts +20 -0
  344. package/types/crawler.d.ts +179 -0
  345. package/types/custom-js-injector.d.ts +42 -0
  346. package/types/dark-mode-capture.d.ts +23 -0
  347. package/types/dead-link-detector.d.ts +37 -0
  348. package/types/element-hash.d.ts +9 -0
  349. package/types/email-otp.d.ts +42 -0
  350. package/types/engine-router.d.ts +47 -0
  351. package/types/export/bulk-export.d.ts +26 -0
  352. package/types/export/vector-export.d.ts +76 -0
  353. package/types/extractors/advanced.d.ts +54 -0
  354. package/types/extractors/artifact-envelope.d.ts +63 -0
  355. package/types/failure-analysis-agent.d.ts +16 -0
  356. package/types/feature-flag-detector.d.ts +30 -0
  357. package/types/form-detector.d.ts +34 -0
  358. package/types/frontier.d.ts +32 -0
  359. package/types/gap-pass.d.ts +35 -0
  360. package/types/graphql-detector.d.ts +39 -0
  361. package/types/http-engine.d.ts +80 -0
  362. package/types/incremental-diff.d.ts +59 -0
  363. package/types/index.d.ts +35 -0
  364. package/types/input-fuzzer.d.ts +30 -0
  365. package/types/interactive-probe.d.ts +104 -0
  366. package/types/job-queue.d.ts +171 -0
  367. package/types/js-coverage.d.ts +29 -0
  368. package/types/js-router-detector.d.ts +17 -0
  369. package/types/language-detector.d.ts +22 -0
  370. package/types/link-classifier.d.ts +23 -0
  371. package/types/live-view.d.ts +36 -0
  372. package/types/logger.d.ts +2 -0
  373. package/types/markdown-extract.d.ts +8 -0
  374. package/types/native-crawler.d.ts +69 -0
  375. package/types/network-observer.d.ts +49 -0
  376. package/types/network-throttle.d.ts +25 -0
  377. package/types/openapi-generator.d.ts +49 -0
  378. package/types/pdf-extractor.d.ts +28 -0
  379. package/types/performance-metrics.d.ts +53 -0
  380. package/types/phash.d.ts +38 -0
  381. package/types/pii-scrubber.d.ts +21 -0
  382. package/types/playwright-recorder.d.ts +39 -0
  383. package/types/policies/crawl-safety.d.ts +9 -0
  384. package/types/profiles/domain-profile.d.ts +19 -0
  385. package/types/proxy.d.ts +50 -0
  386. package/types/rate-limiter.d.ts +31 -0
  387. package/types/redirect-tracker.d.ts +37 -0
  388. package/types/redis-frontier.d.ts +48 -0
  389. package/types/redis-queue.d.ts +48 -0
  390. package/types/response-cache.d.ts +38 -0
  391. package/types/responsive-detector.d.ts +29 -0
  392. package/types/retry-policy.d.ts +20 -0
  393. package/types/robots-txt.d.ts +9 -0
  394. package/types/screen-reader.d.ts +16 -0
  395. package/types/screenshot-masker.d.ts +28 -0
  396. package/types/seo-checker.d.ts +33 -0
  397. package/types/session-capture.d.ts +38 -0
  398. package/types/session-export.d.ts +49 -0
  399. package/types/sitemap-writer.d.ts +21 -0
  400. package/types/sitemap.d.ts +51 -0
  401. package/types/skyvern.d.ts +56 -0
  402. package/types/sqs-queue.d.ts +2 -0
  403. package/types/sso-token.d.ts +29 -0
  404. package/types/stealth/human-behavior.d.ts +20 -0
  405. package/types/telemetry.d.ts +34 -0
  406. package/types/test-executor.d.ts +67 -0
  407. package/types/tls-fingerprint.d.ts +50 -0
  408. package/types/url-pattern-completer.d.ts +28 -0
  409. package/types/visual-diff-verdict.d.ts +40 -0
  410. package/types/webhook-notifier.d.ts +30 -0
  411. package/types/websocket-detector.d.ts +24 -0
  412. package/types/worker.d.ts +1 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * CAPTCHA auto-solving via 2captcha or CapSolver APIs.
3
+ * Supports reCAPTCHA v2, hCaptcha, and Cloudflare Turnstile.
4
+ */
5
+
6
+ export type CaptchaSolverProvider = '2captcha' | 'capsolver';
7
+
8
+ interface SolveResult {
9
+ token: string;
10
+ provider: CaptchaSolverProvider;
11
+ }
12
+
13
+ // Poll interval and max attempts
14
+ const POLL_INTERVAL_MS = 3_000;
15
+ const MAX_ATTEMPTS = 40; // 2 min max
16
+
17
+ /**
18
+ * Detect CAPTCHA type and sitekey on the page, then solve it.
19
+ * Returns null if no CAPTCHA found or solver not configured.
20
+ * Throws on solve failure.
21
+ */
22
+ export async function solveCaptcha(
23
+ page: any,
24
+ pageUrl: string,
25
+ apiKey: string,
26
+ provider: CaptchaSolverProvider = '2captcha'
27
+ ): Promise<SolveResult | null> {
28
+ // Detect CAPTCHA type
29
+ const captchaInfo = await page.evaluate(() => {
30
+ // reCAPTCHA v2
31
+ const recaptchaEl = document.querySelector('.g-recaptcha, [data-sitekey]') as HTMLElement | null;
32
+ if (recaptchaEl) {
33
+ const sitekey = recaptchaEl.getAttribute('data-sitekey') || '';
34
+ return { type: 'recaptcha', sitekey };
35
+ }
36
+ // hCaptcha
37
+ const hcaptchaEl = document.querySelector('.h-captcha, [data-hcaptcha-sitekey]') as HTMLElement | null;
38
+ if (hcaptchaEl) {
39
+ const sitekey = hcaptchaEl.getAttribute('data-sitekey') || hcaptchaEl.getAttribute('data-hcaptcha-sitekey') || '';
40
+ return { type: 'hcaptcha', sitekey };
41
+ }
42
+ // Cloudflare Turnstile
43
+ const turnstileEl = document.querySelector('.cf-turnstile') as HTMLElement | null;
44
+ if (turnstileEl) {
45
+ const sitekey = turnstileEl.getAttribute('data-sitekey') || '';
46
+ return { type: 'turnstile', sitekey };
47
+ }
48
+ return null;
49
+ });
50
+
51
+ if (!captchaInfo?.sitekey) return null;
52
+
53
+ const token = provider === 'capsolver'
54
+ ? await solveWithCapSolver(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey)
55
+ : await solveWith2Captcha(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey);
56
+
57
+ // Inject the token into the page
58
+ await page.evaluate((t: string, type: string) => {
59
+ // reCAPTCHA
60
+ if (type === 'recaptcha') {
61
+ const el = document.querySelector('textarea.g-recaptcha-response') as HTMLTextAreaElement | null;
62
+ if (el) { el.value = t; }
63
+ // Also trigger the callback if registered
64
+ if ((window as any).grecaptcha?.getResponse && document.querySelector('[data-callback]')) {
65
+ const cb = (document.querySelector('[data-callback]') as HTMLElement)?.getAttribute('data-callback');
66
+ if (cb && (window as any)[cb]) (window as any)[cb](t);
67
+ }
68
+ }
69
+ // hCaptcha
70
+ if (type === 'hcaptcha') {
71
+ const el = document.querySelector('textarea[name="h-captcha-response"]') as HTMLTextAreaElement | null;
72
+ if (el) { el.value = t; }
73
+ }
74
+ // Turnstile
75
+ if (type === 'turnstile') {
76
+ const el = document.querySelector('[name="cf-turnstile-response"]') as HTMLInputElement | null;
77
+ if (el) { el.value = t; }
78
+ }
79
+ }, token, captchaInfo.type);
80
+
81
+ return { token, provider };
82
+ }
83
+
84
+ async function solveWith2Captcha(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise<string> {
85
+ const method = type === 'hcaptcha' ? 'hcaptcha' : type === 'turnstile' ? 'turnstile' : 'userrecaptcha';
86
+
87
+ const submitRes = await fetch('http://2captcha.com/in.php', {
88
+ method: 'POST',
89
+ body: new URLSearchParams({
90
+ key: apiKey,
91
+ method,
92
+ googlekey: sitekey,
93
+ pageurl: pageUrl,
94
+ json: '1',
95
+ }),
96
+ });
97
+ const submitData = await submitRes.json() as { status: number; request: string };
98
+ if (submitData.status !== 1) throw new Error(`2captcha submit failed: ${submitData.request}`);
99
+ const taskId = submitData.request;
100
+
101
+ // Poll for result
102
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
103
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
104
+ const pollRes = await fetch(`http://2captcha.com/res.php?key=${apiKey}&action=get&id=${taskId}&json=1`);
105
+ const pollData = await pollRes.json() as { status: number; request: string };
106
+ if (pollData.status === 1) return pollData.request;
107
+ if (pollData.request !== 'CAPCHA_NOT_READY') throw new Error(`2captcha error: ${pollData.request}`);
108
+ }
109
+ throw new Error('2captcha timed out after 2 minutes');
110
+ }
111
+
112
+ async function solveWithCapSolver(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise<string> {
113
+ const taskType = type === 'hcaptcha' ? 'HCaptchaTaskProxyless'
114
+ : type === 'turnstile' ? 'AntiTurnstileTaskProxyless'
115
+ : 'ReCaptchaV2TaskProxyless';
116
+
117
+ const createRes = await fetch('https://api.capsolver.com/createTask', {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({
121
+ clientKey: apiKey,
122
+ task: { type: taskType, websiteURL: pageUrl, websiteKey: sitekey },
123
+ }),
124
+ });
125
+ const createData = await createRes.json() as { errorId: number; taskId?: string; errorDescription?: string };
126
+ if (createData.errorId !== 0) throw new Error(`CapSolver create failed: ${createData.errorDescription}`);
127
+ const taskId = createData.taskId!;
128
+
129
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
130
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
131
+ const pollRes = await fetch('https://api.capsolver.com/getTaskResult', {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify({ clientKey: apiKey, taskId }),
135
+ });
136
+ const pollData = await pollRes.json() as { errorId: number; status: string; solution?: { gRecaptchaResponse?: string; token?: string }; errorDescription?: string };
137
+ if (pollData.errorId !== 0) throw new Error(`CapSolver error: ${pollData.errorDescription}`);
138
+ if (pollData.status === 'ready') {
139
+ return pollData.solution?.gRecaptchaResponse ?? pollData.solution?.token ?? '';
140
+ }
141
+ }
142
+ throw new Error('CapSolver timed out after 2 minutes');
143
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * WCAG color contrast checker.
3
+ *
4
+ * Evaluates text contrast ratios against backgrounds for all visible text elements.
5
+ * Reports elements that fail WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large text)
6
+ * or AAA (7:1 for normal, 4.5:1 for large text) requirements.
7
+ *
8
+ * Uses computedStyle to get actual rendered colors — accounts for CSS variables
9
+ * and inheritance.
10
+ */
11
+
12
+ import type { Page } from 'playwright';
13
+
14
+ export interface ContrastViolation {
15
+ selector: string;
16
+ text: string;
17
+ foreground: string; // rgb(r, g, b)
18
+ background: string; // rgb(r, g, b)
19
+ ratio: number; // actual contrast ratio
20
+ required: number; // required ratio (4.5 or 3.0)
21
+ level: 'AA' | 'AAA';
22
+ isLargeText: boolean; // font-size >= 18px or bold >= 14px
23
+ severity: 'fail' | 'warn'; // fail = below AA, warn = below AAA
24
+ }
25
+
26
+ export interface ContrastAuditResult {
27
+ url: string;
28
+ violations: ContrastViolation[];
29
+ passCount: number;
30
+ score: number; // 0-100
31
+ auditedAt: string;
32
+ }
33
+
34
+ export async function auditColorContrast(page: Page, url: string): Promise<ContrastAuditResult> {
35
+ const auditedAt = new Date().toISOString();
36
+
37
+ const violations: ContrastViolation[] = await page.evaluate(() => {
38
+ function parseRgb(color: string): [number, number, number, number] | null {
39
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
40
+ return m ? [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1] : null;
41
+ }
42
+ function luminance([r, g, b]: [number, number, number, number]): number {
43
+ const toLinear = (c: number) => {
44
+ const n = c / 255;
45
+ return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
46
+ };
47
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
48
+ }
49
+ function contrastRatio(fg: [number,number,number,number], bg: [number,number,number,number]): number {
50
+ const l1 = luminance(fg);
51
+ const l2 = luminance(bg);
52
+ const lighter = Math.max(l1, l2);
53
+ const darker = Math.min(l1, l2);
54
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
55
+ }
56
+
57
+ const violations: any[] = [];
58
+ const elements = Array.from(document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button'));
59
+ let checked = 0;
60
+
61
+ for (const el of elements as HTMLElement[]) {
62
+ if (checked >= 100) break; // limit to avoid perf hit
63
+ const text = (el.textContent ?? '').trim().slice(0, 60);
64
+ if (!text) continue;
65
+
66
+ const style = window.getComputedStyle(el);
67
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
68
+
69
+ const fg = parseRgb(style.color);
70
+ const bg = parseRgb(style.backgroundColor);
71
+ if (!fg || !bg || bg[3] === 0) continue; // transparent bg — can't check
72
+
73
+ const fontSize = parseFloat(style.fontSize);
74
+ const fontWeight = style.fontWeight;
75
+ const isLargeText = fontSize >= 18 || (fontSize >= 14 && (fontWeight === 'bold' || +fontWeight >= 700));
76
+ const ratio = contrastRatio(fg, bg);
77
+ const aaRequired = isLargeText ? 3.0 : 4.5;
78
+ const aaaRequired = isLargeText ? 4.5 : 7.0;
79
+
80
+ if (ratio < aaRequired) {
81
+ violations.push({
82
+ selector: el.tagName.toLowerCase(),
83
+ text: text.slice(0, 50),
84
+ foreground: style.color,
85
+ background: style.backgroundColor,
86
+ ratio,
87
+ required: aaRequired,
88
+ level: 'AA',
89
+ isLargeText,
90
+ severity: 'fail',
91
+ });
92
+ } else if (ratio < aaaRequired) {
93
+ violations.push({
94
+ selector: el.tagName.toLowerCase(),
95
+ text: text.slice(0, 50),
96
+ foreground: style.color,
97
+ background: style.backgroundColor,
98
+ ratio,
99
+ required: aaaRequired,
100
+ level: 'AAA',
101
+ isLargeText,
102
+ severity: 'warn',
103
+ });
104
+ } else {
105
+ checked++;
106
+ }
107
+ }
108
+ return violations;
109
+ }).catch(() => [] as ContrastViolation[]);
110
+
111
+ const passCount = Math.max(0, 100 - violations.length);
112
+ const criticals = violations.filter(v => v.severity === 'fail').length;
113
+ const score = Math.max(0, 100 - criticals * 15 - violations.filter(v => v.severity === 'warn').length * 3);
114
+
115
+ return { url, violations, passCount, score, auditedAt };
116
+ }
@@ -0,0 +1,170 @@
1
+ export type ChunkingStrategy = 'fixed' | 'overlap' | 'sentence' | 'paragraph' | 'markdown-heading' | 'semantic' | 'recursive';
2
+
3
+ export interface ChunkOpts {
4
+ strategy: ChunkingStrategy;
5
+ chunkSize?: number; // token estimate target (1 token ≈ 4 chars). Default 2000.
6
+ overlap?: number; // overlap tokens for 'overlap' strategy. Default 200.
7
+ separators?: string[]; // for 'recursive' strategy. Default ['\\n\\n', '\\n', '. ', ' ']
8
+ }
9
+
10
+ export interface Chunk {
11
+ index: number;
12
+ text: string;
13
+ tokenEstimate: number;
14
+ startChar: number;
15
+ endChar: number;
16
+ heading?: string;
17
+ }
18
+
19
+ export function estimateTokens(text: string): number {
20
+ return Math.ceil(text.length / 4);
21
+ }
22
+
23
+ export function chunkContent(text: string, opts: ChunkOpts): Chunk[] {
24
+ const size = opts.chunkSize ?? 2000;
25
+ const charSize = size * 4;
26
+ const overlap = opts.overlap ?? 200;
27
+ const overlapChars = overlap * 4;
28
+
29
+ switch (opts.strategy) {
30
+ case 'fixed': return fixedChunk(text, charSize);
31
+ case 'overlap': return overlapChunk(text, charSize, overlapChars);
32
+ case 'sentence': return sentenceChunk(text, charSize);
33
+ case 'paragraph': return paragraphChunk(text, charSize);
34
+ case 'markdown-heading': return markdownHeadingChunk(text, charSize);
35
+ case 'semantic': return semanticChunk(text, charSize);
36
+ case 'recursive': return recursiveChunk(text, charSize, opts.separators ?? ['\n\n', '\n', '. ', ' ']);
37
+ default: return fixedChunk(text, charSize);
38
+ }
39
+ }
40
+
41
+ function makeChunk(text: string, index: number, startChar: number, heading?: string): Chunk {
42
+ return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
43
+ }
44
+
45
+ function fixedChunk(text: string, charSize: number): Chunk[] {
46
+ const chunks: Chunk[] = [];
47
+ let i = 0;
48
+ while (i < text.length) {
49
+ const slice = text.slice(i, i + charSize);
50
+ chunks.push(makeChunk(slice, chunks.length, i));
51
+ i += charSize;
52
+ }
53
+ return chunks;
54
+ }
55
+
56
+ function overlapChunk(text: string, charSize: number, overlapChars: number): Chunk[] {
57
+ const chunks: Chunk[] = [];
58
+ let i = 0;
59
+ while (i < text.length) {
60
+ const slice = text.slice(i, i + charSize);
61
+ chunks.push(makeChunk(slice, chunks.length, i));
62
+ i += charSize - overlapChars;
63
+ if (i <= 0) break;
64
+ }
65
+ return chunks;
66
+ }
67
+
68
+ function sentenceChunk(text: string, charSize: number): Chunk[] {
69
+ const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
70
+ return groupIntoChunks(sentences, charSize);
71
+ }
72
+
73
+ function paragraphChunk(text: string, charSize: number): Chunk[] {
74
+ const paras = text.split(/\n\n+/).filter(p => p.trim());
75
+ return groupIntoChunks(paras, charSize);
76
+ }
77
+
78
+ function markdownHeadingChunk(text: string, charSize: number): Chunk[] {
79
+ const chunks: Chunk[] = [];
80
+ const headingRe = /^(#{1,3}\s.+)$/m;
81
+ const sections = text.split(/(?=^#{1,3}\s)/m).filter(s => s.trim());
82
+ let pos = 0;
83
+ for (const section of sections) {
84
+ const headingMatch = section.match(headingRe);
85
+ const heading = headingMatch ? headingMatch[1].replace(/^#+\s*/, '').trim() : undefined;
86
+ if (section.length <= charSize) {
87
+ chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });
88
+ } else {
89
+ // Section too large — sub-chunk by paragraph
90
+ const sub = paragraphChunk(section, charSize);
91
+ for (const s of sub) {
92
+ chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });
93
+ }
94
+ }
95
+ pos += section.length;
96
+ }
97
+ return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
98
+ }
99
+
100
+ function semanticChunk(text: string, charSize: number): Chunk[] {
101
+ // Like paragraph but merges short paragraphs with the next
102
+ const paras = text.split(/\n\n+/).filter(p => p.trim());
103
+ const merged: string[] = [];
104
+ let current = '';
105
+ for (const para of paras) {
106
+ if (current.length + para.length + 2 <= charSize) {
107
+ current = current ? current + '\n\n' + para : para;
108
+ } else {
109
+ if (current) merged.push(current);
110
+ current = para;
111
+ }
112
+ }
113
+ if (current) merged.push(current);
114
+ return groupIntoChunks(merged, charSize);
115
+ }
116
+
117
+ function recursiveChunk(text: string, charSize: number, separators: string[]): Chunk[] {
118
+ if (text.length <= charSize) return [makeChunk(text, 0, 0)];
119
+ const [sep, ...rest] = separators;
120
+ if (!sep) return fixedChunk(text, charSize);
121
+ const parts = text.split(sep).filter(p => p);
122
+ const chunks: Chunk[] = [];
123
+ let pos = 0;
124
+ for (const part of parts) {
125
+ if (part.length <= charSize) {
126
+ chunks.push(makeChunk(part, chunks.length, pos));
127
+ } else {
128
+ const sub = recursiveChunk(part, charSize, rest);
129
+ for (const s of sub) chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });
130
+ }
131
+ pos += part.length + sep.length;
132
+ }
133
+ return chunks;
134
+ }
135
+
136
+ function groupIntoChunks(parts: string[], charSize: number): Chunk[] {
137
+ const chunks: Chunk[] = [];
138
+ let current = '';
139
+ let startChar = 0;
140
+ let pos = 0;
141
+ for (const part of parts) {
142
+ if (current.length + part.length + 1 > charSize && current) {
143
+ chunks.push(makeChunk(current.trim(), chunks.length, startChar));
144
+ startChar = pos;
145
+ current = part;
146
+ } else {
147
+ current = current ? current + ' ' + part : part;
148
+ }
149
+ pos += part.length + 1;
150
+ }
151
+ if (current.trim()) chunks.push(makeChunk(current.trim(), chunks.length, startChar));
152
+ return chunks;
153
+ }
154
+
155
+ export function mergeSmallChunks(chunks: Chunk[], minTokens: number): Chunk[] {
156
+ const minChars = minTokens * 4;
157
+ const result: Chunk[] = [];
158
+ let i = 0;
159
+ while (i < chunks.length) {
160
+ if (chunks[i].text.length < minChars && i + 1 < chunks.length) {
161
+ const merged = chunks[i].text + '\n\n' + chunks[i + 1].text;
162
+ result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));
163
+ i += 2;
164
+ } else {
165
+ result.push({ ...chunks[i], index: result.length });
166
+ i++;
167
+ }
168
+ }
169
+ return result;
170
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Content deduplication — detect near-duplicate pages during crawl.
3
+ *
4
+ * Uses MinHash-inspired approach: instead of full MinHash (needs many hash functions),
5
+ * we use 8 structural fingerprints extracted from the markdown to detect pages that
6
+ * share the same layout/template even if the data differs.
7
+ *
8
+ * Typical duplicates: /products?page=1 vs /products?page=2 (same template, different data).
9
+ * These waste crawl budget and generate redundant test cases.
10
+ */
11
+
12
+ /** Extract structural fingerprints from markdown — layout-sensitive, data-insensitive */
13
+ function extractFingerprints(markdown: string): string[] {
14
+ const lines = markdown.split('\n').filter(l => l.trim().length > 0);
15
+ const fps: string[] = [];
16
+
17
+ // FP1: heading structure (H1-H6 count distribution)
18
+ const headings = lines.filter(l => /^#{1,6} /.test(l));
19
+ const hCounts = [0, 0, 0, 0, 0, 0];
20
+ for (const h of headings) {
21
+ const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;
22
+ hCounts[lvl - 1]++;
23
+ }
24
+ fps.push('h:' + hCounts.join(','));
25
+
26
+ // FP2: link count (grouped into buckets)
27
+ const linkCount = (markdown.match(/\[.+?\]\(.+?\)/g) ?? []).length;
28
+ fps.push('lk:' + Math.floor(linkCount / 5));
29
+
30
+ // FP3: list item count (grouped)
31
+ const listCount = lines.filter(l => /^[\-\*\+] |^\d+\. /.test(l)).length;
32
+ fps.push('li:' + Math.floor(listCount / 3));
33
+
34
+ // FP4: code block presence
35
+ fps.push('code:' + (markdown.includes('```') ? '1' : '0'));
36
+
37
+ // FP5: table presence
38
+ fps.push('tbl:' + (markdown.includes('|') ? '1' : '0'));
39
+
40
+ // FP6: approximate paragraph count
41
+ const paraCount = markdown.split(/\n{2,}/).filter(p => p.trim().length > 30).length;
42
+ fps.push('p:' + Math.floor(paraCount / 2));
43
+
44
+ // FP7: first heading text (normalized)
45
+ const firstH = headings[0]?.replace(/^#{1,6} /, '').toLowerCase().replace(/[^a-z0-9]/g, '') ?? '';
46
+ fps.push('fh:' + firstH.slice(0, 20));
47
+
48
+ // FP8: word count bucket
49
+ const wordCount = markdown.split(/\s+/).length;
50
+ fps.push('wc:' + Math.floor(wordCount / 50));
51
+
52
+ return fps;
53
+ }
54
+
55
+ function similarity(a: string[], b: string[]): number {
56
+ if (a.length !== b.length) return 0;
57
+ let matches = 0;
58
+ for (let i = 0; i < a.length; i++) {
59
+ if (a[i] === b[i]) matches++;
60
+ }
61
+ return matches / a.length;
62
+ }
63
+
64
+ export interface DedupResult {
65
+ isDuplicate: boolean;
66
+ similarUrl?: string;
67
+ similarityScore: number;
68
+ }
69
+
70
+ export class ContentDeduplicator {
71
+ private seen = new Map<string, { fps: string[]; url: string }>();
72
+ private readonly threshold: number;
73
+
74
+ constructor(threshold = 0.85) {
75
+ this.threshold = threshold;
76
+ }
77
+
78
+ check(url: string, markdown: string): DedupResult {
79
+ const fps = extractFingerprints(markdown);
80
+
81
+ for (const [key, entry] of this.seen) {
82
+ const score = similarity(fps, entry.fps);
83
+ if (score >= this.threshold) {
84
+ return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };
85
+ }
86
+ }
87
+
88
+ this.seen.set(url, { fps, url });
89
+ return { isDuplicate: false, similarityScore: 1.0 };
90
+ }
91
+
92
+ size(): number {
93
+ return this.seen.size;
94
+ }
95
+
96
+ reset(): void {
97
+ this.seen.clear();
98
+ }
99
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Cookie consent auto-acceptor.
3
+ *
4
+ * Detects and clicks "Accept" on common cookie consent banners before the
5
+ * crawler proceeds. Without this, many EU sites show a consent wall that
6
+ * blocks content or shows a different layout, making crawl results unreliable.
7
+ *
8
+ * Supports: Cookiebot, OneTrust, TrustArc, Usercentrics, GDPR Cookie Compliance,
9
+ * generic "Accept All" / "Accept Cookies" buttons.
10
+ *
11
+ * Strategy: try known selectors first, then fall back to text-based button search.
12
+ * Non-blocking — if no banner is found or click fails, crawl proceeds normally.
13
+ */
14
+
15
+ import type { Page } from 'playwright';
16
+
17
+ export interface ConsentResult {
18
+ found: boolean;
19
+ clicked: boolean;
20
+ method?: string; // which selector/strategy matched
21
+ error?: string;
22
+ }
23
+
24
+ // Known consent platform selectors
25
+ const CONSENT_SELECTORS = [
26
+ // Cookiebot
27
+ '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
28
+ '#CybotCookiebotDialogBodyButtonAccept',
29
+ // OneTrust
30
+ '#onetrust-accept-btn-handler',
31
+ '.ot-sdk-accept-btn',
32
+ // TrustArc
33
+ '.truste_popframe [title*="Accept"]',
34
+ '#truste-consent-button',
35
+ // Usercentrics
36
+ '[data-testid="uc-accept-all-button"]',
37
+ // Generic high-confidence
38
+ '[id*="accept"][id*="cookie"]',
39
+ '[class*="accept"][class*="cookie"]',
40
+ '[class*="cookie"][class*="accept"]',
41
+ // GDPR Cookie Compliance
42
+ '.gdpr-cookie-notice-accept',
43
+ // Quantcast
44
+ '.qc-cmp2-summary-buttons button:first-child',
45
+ ];
46
+
47
+ // Fallback: button text matching
48
+ const ACCEPT_TEXT_PATTERNS = [
49
+ /^accept all$/i,
50
+ /^accept all cookies$/i,
51
+ /^allow all$/i,
52
+ /^i accept$/i,
53
+ /^agree$/i,
54
+ /^ok$/i,
55
+ /^got it$/i,
56
+ /^allow cookies$/i,
57
+ ];
58
+
59
+ export async function acceptCookieConsent(page: Page, opts: { waitMs?: number } = {}): Promise<ConsentResult> {
60
+ const { waitMs = 1_500 } = opts;
61
+
62
+ // Wait briefly for consent banners to appear
63
+ await page.waitForTimeout(waitMs).catch(() => {});
64
+
65
+ // Try known selectors first
66
+ for (const selector of CONSENT_SELECTORS) {
67
+ try {
68
+ const el = await page.$(selector);
69
+ if (el && await el.isVisible()) {
70
+ await el.click({ timeout: 2_000 });
71
+ await page.waitForTimeout(500);
72
+ return { found: true, clicked: true, method: `selector: ${selector}` };
73
+ }
74
+ } catch { /* try next */ }
75
+ }
76
+
77
+ // Fallback: find buttons with accept-like text
78
+ try {
79
+ const buttons = await page.$$('button, [role="button"], a[class*="cookie"]');
80
+ for (const btn of buttons) {
81
+ const text = ((await btn.textContent()) ?? '').trim();
82
+ if (ACCEPT_TEXT_PATTERNS.some(re => re.test(text))) {
83
+ if (await btn.isVisible()) {
84
+ await btn.click({ timeout: 2_000 });
85
+ await page.waitForTimeout(500);
86
+ return { found: true, clicked: true, method: `text: "${text}"` };
87
+ }
88
+ }
89
+ }
90
+ } catch { /* non-fatal */ }
91
+
92
+ return { found: false, clicked: false };
93
+ }
94
+
95
+ /** Check if a consent banner is still visible after attempted dismissal */
96
+ export async function isConsentBannerVisible(page: Page): Promise<boolean> {
97
+ try {
98
+ for (const sel of CONSENT_SELECTORS.slice(0, 5)) {
99
+ const el = await page.$(sel);
100
+ if (el && await el.isVisible()) return true;
101
+ }
102
+ return false;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }