@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,304 @@
1
+ import { Readability } from "@mozilla/readability";
2
+ import TurndownService from "turndown";
3
+ import { parseHTML } from "linkedom";
4
+ import { RobotsCache } from "./robots-txt.js";
5
+ import { ResponseCache } from "./response-cache.js";
6
+ import { extractMetadata, extractTables, extractImages } from "./extractors/advanced.js";
7
+ import { CHROME_CIPHERS, CHROME_CURVES } from "./tls-fingerprint.js";
8
+ import { renderingPredictor } from "./engine-router.js";
9
+ let chromeFetchDispatcher;
10
+ async function getChromeFetchDispatcher() {
11
+ if (!chromeFetchDispatcher) {
12
+ try {
13
+ const impitMod = await import("impit");
14
+ const ImpitClass = impitMod.Impit ?? impitMod.default;
15
+ if (ImpitClass) {
16
+ chromeFetchDispatcher = new ImpitClass({ browser: "chrome" });
17
+ return chromeFetchDispatcher;
18
+ }
19
+ } catch {
20
+ }
21
+ const { Agent } = await import("undici");
22
+ chromeFetchDispatcher = new Agent({
23
+ connect: {
24
+ ciphers: CHROME_CIPHERS,
25
+ ecdhCurve: CHROME_CURVES,
26
+ minVersion: "TLSv1.2",
27
+ maxVersion: "TLSv1.3"
28
+ },
29
+ keepAliveTimeout: 1e4,
30
+ keepAliveMaxTimeout: 3e4
31
+ });
32
+ }
33
+ return chromeFetchDispatcher;
34
+ }
35
+ const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
36
+ const SKIP_PATH = /\/(terms|privacy|dpa|legal|cookie|changelog|blog|help|docs|about|contact|careers|press|imprint|gdpr|compliance|acceptable-use|security-policy|accessibility)([-/]|$)/i;
37
+ function canonicalize(url) {
38
+ try {
39
+ const u = new URL(url);
40
+ u.hash = "";
41
+ for (const p of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "fbclid", "gclid", "ref", "referrer"]) {
42
+ u.searchParams.delete(p);
43
+ }
44
+ return u.href.toLowerCase().replace(/\/$/, "");
45
+ } catch {
46
+ return url.toLowerCase();
47
+ }
48
+ }
49
+ function extractLinks(html, baseUrl) {
50
+ const links = [];
51
+ const seen = /* @__PURE__ */ new Set();
52
+ try {
53
+ const base = new URL(baseUrl);
54
+ const { document } = parseHTML(html);
55
+ const anchors = Array.from(document.querySelectorAll("a[href]"));
56
+ for (const a of anchors) {
57
+ const href = a.getAttribute("href") ?? "";
58
+ if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("javascript:")) continue;
59
+ try {
60
+ const resolved = new URL(href, base).href;
61
+ if (new URL(resolved).origin !== base.origin) continue;
62
+ const c = canonicalize(resolved);
63
+ if (!seen.has(c)) {
64
+ seen.add(c);
65
+ links.push(resolved);
66
+ }
67
+ } catch {
68
+ }
69
+ }
70
+ } catch {
71
+ }
72
+ return links;
73
+ }
74
+ function extractMarkdown(html, url) {
75
+ try {
76
+ const { document } = parseHTML(html);
77
+ for (const tag of Array.from(document.querySelectorAll("script, style, noscript"))) tag.remove();
78
+ const reader = new Readability(document, { charThreshold: 50 });
79
+ const article = reader.parse();
80
+ if (article && article.textContent && article.textContent.split(/\s+/).length >= 30) {
81
+ const md = turndown.turndown(article.content ?? html);
82
+ return { markdown: md, title: article.title ?? null };
83
+ }
84
+ } catch {
85
+ }
86
+ try {
87
+ const { document } = parseHTML(html);
88
+ for (const tag of Array.from(document.querySelectorAll("script, style, noscript, nav, footer"))) tag.remove();
89
+ const bodyHtml = document.body?.innerHTML ?? html;
90
+ const md = turndown.turndown(bodyHtml);
91
+ const titleEl = document.querySelector("title");
92
+ return { markdown: md, title: titleEl?.textContent?.trim() ?? null };
93
+ } catch {
94
+ return { markdown: "", title: null };
95
+ }
96
+ }
97
+ async function fetchWithBackoff(url, cache, opts, retryCount = 0) {
98
+ const t0 = performance.now();
99
+ if (cache.isFresh(url)) {
100
+ const cached = cache.get(url);
101
+ return { body: cached.body, statusCode: 200, contentType: cached.contentType, fromCache: true, durationMs: 0 };
102
+ }
103
+ const validationHeaders = cache.getValidationHeaders(url);
104
+ const headers = {
105
+ "User-Agent": opts.userAgent,
106
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
107
+ "Accept-Language": "en-US,en;q=0.9",
108
+ "Accept-Encoding": "gzip, deflate, br, zstd",
109
+ // Chrome-like security headers reduce bot-detection fingerprint
110
+ "sec-ch-ua": '"Google Chrome";v="120", "Chromium";v="120", "Not-A.Brand";v="99"',
111
+ "sec-ch-ua-mobile": "?0",
112
+ "sec-ch-ua-platform": '"Windows"',
113
+ "Sec-Fetch-Dest": "document",
114
+ "Sec-Fetch-Mode": "navigate",
115
+ "Sec-Fetch-Site": "none",
116
+ "Sec-Fetch-User": "?1",
117
+ "Upgrade-Insecure-Requests": "1",
118
+ ...validationHeaders,
119
+ ...opts.extraHeaders ?? {}
120
+ };
121
+ let response;
122
+ try {
123
+ const dispatcher = await getChromeFetchDispatcher();
124
+ response = await fetch(url, {
125
+ headers,
126
+ signal: AbortSignal.timeout(opts.timeoutMs),
127
+ redirect: "follow",
128
+ dispatcher
129
+ });
130
+ } catch (e) {
131
+ throw new Error(`[http-engine] fetch failed for ${url}: ${e.message}`);
132
+ }
133
+ const durationMs = performance.now() - t0;
134
+ if (response.status === 304) {
135
+ const cached = cache.get(url);
136
+ if (cached) {
137
+ cache.markHit(url);
138
+ return { body: cached.body, statusCode: 200, contentType: cached.contentType, fromCache: true, durationMs };
139
+ }
140
+ }
141
+ if (response.status === 429 || response.status === 503) {
142
+ if (retryCount >= 5) throw new Error(`[http-engine] rate-limited after 5 retries: ${url}`);
143
+ const retryAfter = parseInt(response.headers.get("retry-after") ?? "0") || 0;
144
+ const baseDelay = retryAfter > 0 ? retryAfter * 1e3 : Math.min(6e4, 1e3 * Math.pow(2, retryCount));
145
+ const jitter = Math.random() * 1e3;
146
+ console.log(`[http-engine] ${response.status} on ${url} \u2014 backing off ${Math.round(baseDelay + jitter)}ms (retry ${retryCount + 1}/5)`);
147
+ await new Promise((r) => setTimeout(r, baseDelay + jitter));
148
+ return fetchWithBackoff(url, cache, opts, retryCount + 1);
149
+ }
150
+ if (response.status >= 500) {
151
+ if (retryCount < 3) {
152
+ await new Promise((r) => setTimeout(r, 2e3 * (retryCount + 1)));
153
+ return fetchWithBackoff(url, cache, opts, retryCount + 1);
154
+ }
155
+ if (cache.staleIfError(url)) {
156
+ const cached = cache.get(url);
157
+ return { body: cached.body, statusCode: 200, contentType: cached.contentType, fromCache: true, durationMs: performance.now() - t0 };
158
+ }
159
+ }
160
+ const contentType = response.headers.get("content-type") ?? "";
161
+ const body = await response.text();
162
+ const etag = response.headers.get("etag") ?? void 0;
163
+ const lastModified = response.headers.get("last-modified") ?? void 0;
164
+ const cacheControl = response.headers.get("cache-control") ?? void 0;
165
+ if (response.ok && contentType.includes("html")) {
166
+ cache.set(url, { url, etag, lastModified, cacheControl, body, contentType, statusCode: response.status });
167
+ }
168
+ return { body, statusCode: response.status, contentType, fromCache: false, durationMs, etag, lastModified };
169
+ }
170
+ async function httpCrawl(opts) {
171
+ const startTime = performance.now();
172
+ const maxScreens = opts.maxScreens ?? 100;
173
+ const cacheDir = opts.cacheDir ?? process.env.CRAWLER_CACHE_DIR ?? "/tmp/zeta-cache";
174
+ const timeoutMs = opts.timeoutMs ?? 15e3;
175
+ const userAgent = opts.userAgent ?? "ZeTa-Crawler/1.0 (+https://zetaai.dpdns.org)";
176
+ const cache = new ResponseCache(cacheDir);
177
+ const robots = opts.respectRobotsTxt !== false ? new RobotsCache() : null;
178
+ const urlFilterRe = opts.urlFilter ? (() => {
179
+ try {
180
+ return new RegExp(opts.urlFilter, "i");
181
+ } catch {
182
+ return null;
183
+ }
184
+ })() : null;
185
+ const visited = /* @__PURE__ */ new Set();
186
+ const queue = [opts.appUrl];
187
+ const queued = /* @__PURE__ */ new Set([canonicalize(opts.appUrl)]);
188
+ const screens = [];
189
+ let cachedCount = 0;
190
+ let errorCount = 0;
191
+ let rateLimitedCount = 0;
192
+ const batchFetch = async (urls) => {
193
+ await Promise.all(urls.map(async (url) => {
194
+ const canonUrl = canonicalize(url);
195
+ if (visited.has(canonUrl)) return;
196
+ visited.add(canonUrl);
197
+ if (robots && !await robots.isAllowed(url).catch(() => true)) {
198
+ console.log(`[http-engine] robots.txt blocked: ${url}`);
199
+ return;
200
+ }
201
+ try {
202
+ const { pathname } = new URL(url);
203
+ if (SKIP_PATH.test(pathname)) return;
204
+ } catch {
205
+ }
206
+ if (urlFilterRe) {
207
+ try {
208
+ const { pathname, search } = new URL(url);
209
+ if (!urlFilterRe.test(pathname + search)) {
210
+ console.log(`[http-engine] filtered out: ${url}`);
211
+ return;
212
+ }
213
+ } catch {
214
+ }
215
+ }
216
+ let fetchResult;
217
+ try {
218
+ fetchResult = await fetchWithBackoff(url, cache, { userAgent, timeoutMs, extraHeaders: opts.headers });
219
+ if (fetchResult.statusCode === 429 || fetchResult.statusCode === 503) rateLimitedCount++;
220
+ } catch (err) {
221
+ console.error(`[http-engine] error fetching ${url}: ${err.message}`);
222
+ errorCount++;
223
+ return;
224
+ }
225
+ if (!fetchResult.contentType.includes("html")) return;
226
+ if (fetchResult.statusCode >= 400) return;
227
+ if (fetchResult.fromCache) cachedCount++;
228
+ const { markdown, title } = extractMarkdown(fetchResult.body, url);
229
+ const fullMeta = extractMetadata(fetchResult.body, url);
230
+ const links = extractLinks(fetchResult.body, url);
231
+ const tables = extractTables(fetchResult.body);
232
+ const images = extractImages(fetchResult.body, url);
233
+ const screen = {
234
+ url,
235
+ title: title ?? fullMeta.title,
236
+ markdown,
237
+ html: opts.includeRawHtml ? fetchResult.body : void 0,
238
+ metadata: {
239
+ description: fullMeta.description,
240
+ ogTitle: fullMeta.ogTitle,
241
+ ogDescription: fullMeta.ogDescription,
242
+ ogImage: fullMeta.ogImage,
243
+ canonical: fullMeta.canonical,
244
+ schemaTypes: fullMeta.schemaTypes,
245
+ lang: fullMeta.lang
246
+ },
247
+ tables,
248
+ links,
249
+ images: images.map((i) => ({ src: i.src, alt: i.alt })),
250
+ statusCode: fetchResult.statusCode,
251
+ fromCache: fetchResult.fromCache,
252
+ requiresBrowser: renderingPredictor.getCached(url) === "browser",
253
+ durationMs: fetchResult.durationMs
254
+ };
255
+ screens.push(screen);
256
+ renderingPredictor.predict(url, fetchResult.body, fetchResult.statusCode);
257
+ opts.onProgress?.({ url, screensFound: screens.length, total: maxScreens, cached: fetchResult.fromCache });
258
+ for (const link of links) {
259
+ const c = canonicalize(link);
260
+ if (!queued.has(c) && !visited.has(c) && screens.length + queue.length < maxScreens * 2) {
261
+ queued.add(c);
262
+ if (opts.traversalStrategy === "DFS") queue.unshift(link);
263
+ else queue.push(link);
264
+ }
265
+ }
266
+ }));
267
+ };
268
+ const concurrency = opts.concurrency ?? 5;
269
+ while (queue.length > 0 && screens.length < maxScreens) {
270
+ const batch = queue.splice(0, concurrency);
271
+ await batchFetch(batch);
272
+ }
273
+ let jsonLines;
274
+ let csv;
275
+ const formats = opts.outputFormats ?? [];
276
+ if (formats.includes("jsonlines")) {
277
+ jsonLines = screens.map((s) => JSON.stringify({ url: s.url, title: s.title, markdown: s.markdown, metadata: s.metadata })).join("\n");
278
+ }
279
+ if (formats.includes("csv")) {
280
+ const header = "url,title,description,ogTitle,schemaTypes,statusCode,fromCache,durationMs";
281
+ const rows = screens.map(
282
+ (s) => [s.url, s.title ?? "", s.metadata.description ?? "", s.metadata.ogTitle ?? "", s.metadata.schemaTypes.join("|"), s.statusCode, s.fromCache, Math.round(s.durationMs)].map((v) => `"${String(v).replace(/"/g, '""')}"`).join(",")
283
+ );
284
+ csv = [header, ...rows].join("\n");
285
+ }
286
+ const cacheStats = cache.stats();
287
+ return {
288
+ screens,
289
+ stats: {
290
+ total: screens.length,
291
+ cached: cachedCount,
292
+ errors: errorCount,
293
+ rateLimited: rateLimitedCount,
294
+ durationMs: performance.now() - startTime,
295
+ cacheHitRate: cacheStats.hitRate
296
+ },
297
+ jsonLines,
298
+ csv
299
+ };
300
+ }
301
+ export {
302
+ httpCrawl
303
+ };
304
+ //# sourceMappingURL=http-engine.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/http-engine.ts"],
4
+ "sourcesContent": ["/**\n * HTTP-only crawl engine \u2014 10-50x faster than browser for SSR/static sites.\n *\n * Uses native Node.js fetch (HTTP/2), linkedom, Readability, and Turndown.\n * No browser overhead. Falls back to browser engine on JS-heavy sites.\n *\n * Features:\n * - HTTP/2 native (Node 20 built-in fetch)\n * - ETag / Last-Modified response caching\n * - 429 / 503 exponential backoff with jitter\n * - BFS / DFS traversal\n * - URL regex filtering\n * - Robots.txt compliance\n * - Link discovery (href, action, src)\n * - Markdown extraction (Readability + Turndown)\n * - OG / schema.org / meta extraction\n * - Table \u2192 JSON extraction\n * - JSON Lines, CSV, raw HTML output formats\n * - Streaming progress via onProgress callback\n */\n\nimport { createHash } from 'node:crypto';\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { Readability } from '@mozilla/readability';\nimport TurndownService from 'turndown';\nimport { parseHTML } from 'linkedom';\nimport { RobotsCache } from './robots-txt.js';\nimport { ResponseCache } from './response-cache.js';\nimport { extractMetadata, extractTables, extractImages } from './extractors/advanced.js';\nimport { CHROME_CIPHERS, CHROME_CURVES } from './tls-fingerprint.js';\nimport { renderingPredictor } from './engine-router.js';\n\n// Chrome-level TLS dispatcher: impit (true JA3/JA4 BoringSSL match) with undici fallback\nlet chromeFetchDispatcher: any;\nasync function getChromeFetchDispatcher() {\n if (!chromeFetchDispatcher) {\n try {\n // impit provides native Chrome BoringSSL TLS fingerprint \u2014 true JA3/JA4 match\n // @ts-ignore \u2014 impit is an optional dependency\n const impitMod = await import('impit');\n const ImpitClass = impitMod.Impit ?? impitMod.default;\n if (ImpitClass) {\n chromeFetchDispatcher = new ImpitClass({ browser: 'chrome' });\n return chromeFetchDispatcher;\n }\n } catch { /* impit not installed \u2014 fall through to undici */ }\n const { Agent } = await import('undici');\n chromeFetchDispatcher = new Agent({\n connect: {\n ciphers: CHROME_CIPHERS,\n ecdhCurve: CHROME_CURVES,\n minVersion: 'TLSv1.2' as any,\n maxVersion: 'TLSv1.3' as any,\n },\n keepAliveTimeout: 10_000,\n keepAliveMaxTimeout: 30_000,\n });\n }\n return chromeFetchDispatcher;\n}\n\nconst turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });\n\n// Pages unlikely to have testable UI \u2014 skip from HTTP crawl queue\nconst SKIP_PATH = /\\/(terms|privacy|dpa|legal|cookie|changelog|blog|help|docs|about|contact|careers|press|imprint|gdpr|compliance|acceptable-use|security-policy|accessibility)([-/]|$)/i;\n\nexport type HttpOutputFormat = 'json' | 'jsonlines' | 'csv' | 'markdown' | 'html';\n\nexport interface HttpScreen {\n url: string;\n title: string | null;\n markdown: string;\n html?: string;\n metadata: {\n description: string | null;\n ogTitle: string | null;\n ogDescription: string | null;\n ogImage: string | null;\n canonical: string | null;\n schemaTypes: string[];\n lang: string | null;\n };\n tables: Array<Record<string, string>[]>;\n links: string[];\n images: Array<{ src: string; alt: string }>;\n statusCode: number;\n fromCache: boolean;\n requiresBrowser?: boolean;\n durationMs: number;\n}\n\nexport interface HttpCrawlOpts {\n appUrl: string;\n maxScreens?: number; // default 100\n urlFilter?: string; // regex pattern \u2014 only crawl matching URLs\n traversalStrategy?: 'BFS' | 'DFS'; // default BFS\n outputFormats?: HttpOutputFormat[];\n cacheDir?: string; // default process.env.CRAWLER_CACHE_DIR || '/tmp/zeta-cache'\n userAgent?: string;\n headers?: Record<string, string>;\n respectRobotsTxt?: boolean; // default true\n includeRawHtml?: boolean;\n timeoutMs?: number; // per-request timeout, default 15000\n concurrency?: number; // default 5 parallel fetches\n onProgress?: (update: { url: string; screensFound: number; total: number; cached: boolean }) => void;\n}\n\nexport interface HttpCrawlResult {\n screens: HttpScreen[];\n stats: {\n total: number;\n cached: number;\n errors: number;\n rateLimited: number;\n durationMs: number;\n cacheHitRate: number;\n };\n jsonLines?: string;\n csv?: string;\n}\n\nfunction canonicalize(url: string): string {\n try {\n const u = new URL(url);\n u.hash = '';\n // Strip tracking params\n for (const p of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'fbclid', 'gclid', 'ref', 'referrer']) {\n u.searchParams.delete(p);\n }\n return u.href.toLowerCase().replace(/\\/$/, '');\n } catch {\n return url.toLowerCase();\n }\n}\n\nfunction extractLinks(html: string, baseUrl: string): string[] {\n const links: string[] = [];\n const seen = new Set<string>();\n try {\n const base = new URL(baseUrl);\n const { document } = parseHTML(html);\n const anchors = Array.from(document.querySelectorAll('a[href]'));\n for (const a of anchors) {\n const href = (a as any).getAttribute('href') ?? '';\n if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:')) continue;\n try {\n const resolved = new URL(href, base).href;\n // Same origin only\n if (new URL(resolved).origin !== base.origin) continue;\n const c = canonicalize(resolved);\n if (!seen.has(c)) { seen.add(c); links.push(resolved); }\n } catch { /* skip */ }\n }\n } catch { /* non-fatal */ }\n return links;\n}\n\nfunction extractMarkdown(html: string, url: string): { markdown: string; title: string | null } {\n try {\n const { document } = parseHTML(html);\n // Remove script/style tags before Readability\n for (const tag of Array.from(document.querySelectorAll('script, style, noscript'))) (tag as any).remove();\n const reader = new Readability(document as any, { charThreshold: 50 });\n const article = reader.parse();\n if (article && article.textContent && article.textContent.split(/\\s+/).length >= 30) {\n const md = turndown.turndown(article.content ?? html);\n return { markdown: md, title: article.title ?? null };\n }\n } catch { /* fall through */ }\n // Full-page fallback\n try {\n const { document } = parseHTML(html);\n for (const tag of Array.from(document.querySelectorAll('script, style, noscript, nav, footer'))) (tag as any).remove();\n const bodyHtml = (document as any).body?.innerHTML ?? html;\n const md = turndown.turndown(bodyHtml);\n const titleEl = (document as any).querySelector('title');\n return { markdown: md, title: titleEl?.textContent?.trim() ?? null };\n } catch {\n return { markdown: '', title: null };\n }\n}\n\ninterface FetchResult {\n body: string;\n statusCode: number;\n contentType: string;\n fromCache: boolean;\n durationMs: number;\n etag?: string;\n lastModified?: string;\n}\n\nasync function fetchWithBackoff(\n url: string,\n cache: ResponseCache,\n opts: { userAgent: string; timeoutMs: number; extraHeaders?: Record<string, string> },\n retryCount = 0\n): Promise<FetchResult> {\n const t0 = performance.now();\n\n if (cache.isFresh(url)) {\n const cached = cache.get(url)!;\n return { body: cached.body, statusCode: 200, contentType: cached.contentType, fromCache: true, durationMs: 0 };\n }\n\n const validationHeaders = cache.getValidationHeaders(url);\n\n const headers: Record<string, string> = {\n 'User-Agent': opts.userAgent,\n 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',\n 'Accept-Language': 'en-US,en;q=0.9',\n 'Accept-Encoding': 'gzip, deflate, br, zstd',\n // Chrome-like security headers reduce bot-detection fingerprint\n 'sec-ch-ua': '\"Google Chrome\";v=\"120\", \"Chromium\";v=\"120\", \"Not-A.Brand\";v=\"99\"',\n 'sec-ch-ua-mobile': '?0',\n 'sec-ch-ua-platform': '\"Windows\"',\n 'Sec-Fetch-Dest': 'document',\n 'Sec-Fetch-Mode': 'navigate',\n 'Sec-Fetch-Site': 'none',\n 'Sec-Fetch-User': '?1',\n 'Upgrade-Insecure-Requests': '1',\n ...validationHeaders,\n ...(opts.extraHeaders ?? {}),\n };\n\n let response: Response;\n try {\n const dispatcher = await getChromeFetchDispatcher();\n response = await (fetch as any)(url, {\n headers,\n signal: AbortSignal.timeout(opts.timeoutMs),\n redirect: 'follow',\n dispatcher,\n });\n } catch (e: any) {\n throw new Error(`[http-engine] fetch failed for ${url}: ${e.message}`);\n }\n\n const durationMs = performance.now() - t0;\n\n // 304 Not Modified \u2014 use cached body\n if (response.status === 304) {\n const cached = cache.get(url);\n if (cached) {\n cache.markHit(url);\n return { body: cached.body, statusCode: 200, contentType: cached.contentType, fromCache: true, durationMs };\n }\n }\n\n // 429 / 503 \u2014 exponential backoff with jitter\n if (response.status === 429 || response.status === 503) {\n if (retryCount >= 5) throw new Error(`[http-engine] rate-limited after 5 retries: ${url}`);\n const retryAfter = parseInt(response.headers.get('retry-after') ?? '0') || 0;\n const baseDelay = retryAfter > 0 ? retryAfter * 1000 : Math.min(60_000, 1_000 * Math.pow(2, retryCount));\n const jitter = Math.random() * 1000;\n console.log(`[http-engine] ${response.status} on ${url} \u2014 backing off ${Math.round(baseDelay + jitter)}ms (retry ${retryCount + 1}/5)`);\n await new Promise(r => setTimeout(r, baseDelay + jitter));\n return fetchWithBackoff(url, cache, opts, retryCount + 1);\n }\n\n // 5xx \u2014 retry up to 3 times\n if (response.status >= 500) {\n if (retryCount < 3) {\n await new Promise(r => setTimeout(r, 2_000 * (retryCount + 1)));\n return fetchWithBackoff(url, cache, opts, retryCount + 1);\n }\n if (cache.staleIfError(url)) {\n const cached = cache.get(url)!;\n return { body: cached.body, statusCode: 200, contentType: cached.contentType, fromCache: true, durationMs: performance.now() - t0 };\n }\n }\n\n const contentType = response.headers.get('content-type') ?? '';\n const body = await response.text();\n const etag = response.headers.get('etag') ?? undefined;\n const lastModified = response.headers.get('last-modified') ?? undefined;\n const cacheControl = response.headers.get('cache-control') ?? undefined;\n\n // Cache successful HTML responses\n if (response.ok && contentType.includes('html')) {\n cache.set(url, { url, etag, lastModified, cacheControl, body, contentType, statusCode: response.status });\n }\n\n return { body, statusCode: response.status, contentType, fromCache: false, durationMs, etag, lastModified };\n}\n\nexport async function httpCrawl(opts: HttpCrawlOpts): Promise<HttpCrawlResult> {\n const startTime = performance.now();\n const maxScreens = opts.maxScreens ?? 100;\n const cacheDir = opts.cacheDir ?? process.env.CRAWLER_CACHE_DIR ?? '/tmp/zeta-cache';\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const userAgent = opts.userAgent ?? 'ZeTa-Crawler/1.0 (+https://zetaai.dpdns.org)';\n const cache = new ResponseCache(cacheDir);\n const robots = opts.respectRobotsTxt !== false ? new RobotsCache() : null;\n const urlFilterRe = opts.urlFilter ? (() => { try { return new RegExp(opts.urlFilter!, 'i'); } catch { return null; } })() : null;\n\n const visited = new Set<string>();\n const queue: string[] = [opts.appUrl];\n const queued = new Set<string>([canonicalize(opts.appUrl)]);\n const screens: HttpScreen[] = [];\n let cachedCount = 0;\n let errorCount = 0;\n let rateLimitedCount = 0;\n\n const batchFetch = async (urls: string[]): Promise<void> => {\n await Promise.all(urls.map(async (url) => {\n const canonUrl = canonicalize(url);\n if (visited.has(canonUrl)) return;\n visited.add(canonUrl);\n\n // Robots.txt check\n if (robots && !(await robots.isAllowed(url).catch(() => true))) {\n console.log(`[http-engine] robots.txt blocked: ${url}`);\n return;\n }\n\n // Skip content/legal pages\n try {\n const { pathname } = new URL(url);\n if (SKIP_PATH.test(pathname)) return;\n } catch { /* skip */ }\n\n // URL filter\n if (urlFilterRe) {\n try {\n const { pathname, search } = new URL(url);\n if (!urlFilterRe.test(pathname + search)) {\n console.log(`[http-engine] filtered out: ${url}`);\n return;\n }\n } catch { /* skip */ }\n }\n\n let fetchResult: FetchResult;\n try {\n fetchResult = await fetchWithBackoff(url, cache, { userAgent, timeoutMs, extraHeaders: opts.headers });\n if (fetchResult.statusCode === 429 || fetchResult.statusCode === 503) rateLimitedCount++;\n } catch (err: any) {\n console.error(`[http-engine] error fetching ${url}: ${err.message}`);\n errorCount++;\n return;\n }\n\n if (!fetchResult.contentType.includes('html')) return; // skip non-HTML\n if (fetchResult.statusCode >= 400) return;\n if (fetchResult.fromCache) cachedCount++;\n\n const { markdown, title } = extractMarkdown(fetchResult.body, url);\n const fullMeta = extractMetadata(fetchResult.body, url);\n const links = extractLinks(fetchResult.body, url);\n const tables = extractTables(fetchResult.body);\n const images = extractImages(fetchResult.body, url);\n\n const screen: HttpScreen = {\n url,\n title: title ?? fullMeta.title,\n markdown,\n html: opts.includeRawHtml ? fetchResult.body : undefined,\n metadata: {\n description: fullMeta.description,\n ogTitle: fullMeta.ogTitle,\n ogDescription: fullMeta.ogDescription,\n ogImage: fullMeta.ogImage,\n canonical: fullMeta.canonical,\n schemaTypes: fullMeta.schemaTypes,\n lang: fullMeta.lang,\n },\n tables,\n links,\n images: images.map(i => ({ src: i.src, alt: i.alt })),\n statusCode: fetchResult.statusCode,\n fromCache: fetchResult.fromCache,\n requiresBrowser: renderingPredictor.getCached(url) === 'browser',\n durationMs: fetchResult.durationMs,\n };\n screens.push(screen);\n\n // Record rendering prediction: SPA signals \u2192 browser needed; content-rich \u2192 http engine works\n renderingPredictor.predict(url, fetchResult.body, fetchResult.statusCode);\n\n opts.onProgress?.({ url, screensFound: screens.length, total: maxScreens, cached: fetchResult.fromCache });\n\n // Enqueue discovered links\n for (const link of links) {\n const c = canonicalize(link);\n if (!queued.has(c) && !visited.has(c) && screens.length + queue.length < maxScreens * 2) {\n queued.add(c);\n if (opts.traversalStrategy === 'DFS') queue.unshift(link);\n else queue.push(link);\n }\n }\n }));\n };\n\n const concurrency = opts.concurrency ?? 5;\n\n while (queue.length > 0 && screens.length < maxScreens) {\n const batch = queue.splice(0, concurrency);\n await batchFetch(batch);\n }\n\n // Output formats\n let jsonLines: string | undefined;\n let csv: string | undefined;\n\n const formats = opts.outputFormats ?? [];\n if (formats.includes('jsonlines')) {\n jsonLines = screens.map(s => JSON.stringify({ url: s.url, title: s.title, markdown: s.markdown, metadata: s.metadata })).join('\\n');\n }\n if (formats.includes('csv')) {\n const header = 'url,title,description,ogTitle,schemaTypes,statusCode,fromCache,durationMs';\n const rows = screens.map(s =>\n [s.url, s.title ?? '', s.metadata.description ?? '', s.metadata.ogTitle ?? '', s.metadata.schemaTypes.join('|'), s.statusCode, s.fromCache, Math.round(s.durationMs)].map(v => `\"${String(v).replace(/\"/g, '\"\"')}\"`).join(',')\n );\n csv = [header, ...rows].join('\\n');\n }\n\n const cacheStats = cache.stats();\n return {\n screens,\n stats: {\n total: screens.length,\n cached: cachedCount,\n errors: errorCount,\n rateLimited: rateLimitedCount,\n durationMs: performance.now() - startTime,\n cacheHitRate: cacheStats.hitRate,\n },\n jsonLines,\n csv,\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Incremental crawl diff — compute stable content hashes and skip unchanged screens.
3
+ *
4
+ * Each crawled page gets a content fingerprint (murmurhash-free, pure JS).
5
+ * On the next crawl of the same project, pages with the same fingerprint are
6
+ * marked SKIPPED instead of re-processed, drastically reducing crawl time and
7
+ * LLM token spend for sites with mostly-static content.
8
+ *
9
+ * Design: "same content hash" means the meaningful text hasn't changed.
10
+ * Layout/CSS/JS changes that don't affect visible text are ignored intentionally.
11
+ */
12
+ export interface ContentHash {
13
+ url: string;
14
+ hash: string;
15
+ textLength: number;
16
+ computedAt: string;
17
+ }
18
+ export interface DiffResult {
19
+ url: string;
20
+ changed: boolean;
21
+ previousHash?: string;
22
+ currentHash: string;
23
+ reason: 'new' | 'changed' | 'unchanged';
24
+ }
25
+ /**
26
+ * Compute a stable 64-bit-ish hash from text content.
27
+ * djb2 variant — fast, no deps, collision-resistant enough for content fingerprinting.
28
+ */
29
+ export declare function hashContent(text: string): string;
30
+ /**
31
+ * Normalize text before hashing — strips whitespace-only lines, trims each line,
32
+ * collapses runs of blank lines. Makes hash stable across minor formatting changes.
33
+ */
34
+ export declare function normalizeText(text: string): string;
35
+ export declare function computeContentHash(url: string, markdownText: string): ContentHash;
36
+ /**
37
+ * Compare a page's current content against prior hashes.
38
+ * Returns { changed: false } for unchanged pages — caller should skip re-processing.
39
+ */
40
+ export declare function diffContent(url: string, currentMarkdown: string, priorHashes: Map<string, string>): DiffResult;
41
+ /**
42
+ * Build a hash map from prior screen records (from DB query result).
43
+ * screenRecords: Array<{ url: string; elementHash?: string | null }>
44
+ */
45
+ export declare function buildPriorHashMap(screenRecords: Array<{
46
+ url: string;
47
+ elementHash?: string | null;
48
+ }>): Map<string, string>;
49
+ /**
50
+ * Filter a batch of discovered URLs against prior hashes.
51
+ * Returns { toProcess, toSkip } — caller enqueues toProcess, logs toSkip count.
52
+ *
53
+ * Note: filtering at URL level only works if the prior crawl already visited the URL.
54
+ * For new URLs, they're always in toProcess.
55
+ */
56
+ export declare function partitionByChange(urls: string[], priorHashes: Map<string, string>): {
57
+ toProcess: string[];
58
+ toSkip: string[];
59
+ };
@@ -0,0 +1,61 @@
1
+ function hashContent(text) {
2
+ let h1 = 5381;
3
+ let h2 = 52711;
4
+ for (let i = 0; i < text.length; i++) {
5
+ const c = text.charCodeAt(i);
6
+ h1 = (Math.imul(h1, 33) ^ c) >>> 0;
7
+ h2 = (Math.imul(h2, 31) ^ c) >>> 0;
8
+ }
9
+ return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
10
+ }
11
+ function normalizeText(text) {
12
+ return text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).join("\n");
13
+ }
14
+ function computeContentHash(url, markdownText) {
15
+ const normalized = normalizeText(markdownText);
16
+ return {
17
+ url,
18
+ hash: hashContent(normalized),
19
+ textLength: normalized.length,
20
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
21
+ };
22
+ }
23
+ function diffContent(url, currentMarkdown, priorHashes) {
24
+ const current = computeContentHash(url, currentMarkdown);
25
+ const prior = priorHashes.get(url);
26
+ if (!prior) {
27
+ return { url, changed: true, currentHash: current.hash, reason: "new" };
28
+ }
29
+ if (prior === current.hash) {
30
+ return { url, changed: false, previousHash: prior, currentHash: current.hash, reason: "unchanged" };
31
+ }
32
+ return { url, changed: true, previousHash: prior, currentHash: current.hash, reason: "changed" };
33
+ }
34
+ function buildPriorHashMap(screenRecords) {
35
+ const map = /* @__PURE__ */ new Map();
36
+ for (const s of screenRecords) {
37
+ if (s.url && s.elementHash) map.set(s.url, s.elementHash);
38
+ }
39
+ return map;
40
+ }
41
+ function partitionByChange(urls, priorHashes) {
42
+ const toProcess = [];
43
+ const toSkip = [];
44
+ for (const url of urls) {
45
+ if (priorHashes.has(url)) {
46
+ toProcess.push(url);
47
+ } else {
48
+ toProcess.push(url);
49
+ }
50
+ }
51
+ return { toProcess, toSkip };
52
+ }
53
+ export {
54
+ buildPriorHashMap,
55
+ computeContentHash,
56
+ diffContent,
57
+ hashContent,
58
+ normalizeText,
59
+ partitionByChange
60
+ };
61
+ //# sourceMappingURL=incremental-diff.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/incremental-diff.ts"],
4
+ "sourcesContent": ["/**\n * Incremental crawl diff \u2014 compute stable content hashes and skip unchanged screens.\n *\n * Each crawled page gets a content fingerprint (murmurhash-free, pure JS).\n * On the next crawl of the same project, pages with the same fingerprint are\n * marked SKIPPED instead of re-processed, drastically reducing crawl time and\n * LLM token spend for sites with mostly-static content.\n *\n * Design: \"same content hash\" means the meaningful text hasn't changed.\n * Layout/CSS/JS changes that don't affect visible text are ignored intentionally.\n */\n\nexport interface ContentHash {\n url: string;\n hash: string;\n textLength: number;\n computedAt: string; // ISO timestamp\n}\n\nexport interface DiffResult {\n url: string;\n changed: boolean;\n previousHash?: string;\n currentHash: string;\n reason: 'new' | 'changed' | 'unchanged';\n}\n\n/**\n * Compute a stable 64-bit-ish hash from text content.\n * djb2 variant \u2014 fast, no deps, collision-resistant enough for content fingerprinting.\n */\nexport function hashContent(text: string): string {\n let h1 = 5381;\n let h2 = 52711;\n for (let i = 0; i < text.length; i++) {\n const c = text.charCodeAt(i);\n h1 = (Math.imul(h1, 33) ^ c) >>> 0;\n h2 = (Math.imul(h2, 31) ^ c) >>> 0;\n }\n return (h1 >>> 0).toString(16).padStart(8, '0') + (h2 >>> 0).toString(16).padStart(8, '0');\n}\n\n/**\n * Normalize text before hashing \u2014 strips whitespace-only lines, trims each line,\n * collapses runs of blank lines. Makes hash stable across minor formatting changes.\n */\nexport function normalizeText(text: string): string {\n return text\n .split('\\n')\n .map(l => l.trim())\n .filter(l => l.length > 0)\n .join('\\n');\n}\n\nexport function computeContentHash(url: string, markdownText: string): ContentHash {\n const normalized = normalizeText(markdownText);\n return {\n url,\n hash: hashContent(normalized),\n textLength: normalized.length,\n computedAt: new Date().toISOString(),\n };\n}\n\n/**\n * Compare a page's current content against prior hashes.\n * Returns { changed: false } for unchanged pages \u2014 caller should skip re-processing.\n */\nexport function diffContent(\n url: string,\n currentMarkdown: string,\n priorHashes: Map<string, string>, // url \u2192 hash\n): DiffResult {\n const current = computeContentHash(url, currentMarkdown);\n const prior = priorHashes.get(url);\n\n if (!prior) {\n return { url, changed: true, currentHash: current.hash, reason: 'new' };\n }\n if (prior === current.hash) {\n return { url, changed: false, previousHash: prior, currentHash: current.hash, reason: 'unchanged' };\n }\n return { url, changed: true, previousHash: prior, currentHash: current.hash, reason: 'changed' };\n}\n\n/**\n * Build a hash map from prior screen records (from DB query result).\n * screenRecords: Array<{ url: string; elementHash?: string | null }>\n */\nexport function buildPriorHashMap(screenRecords: Array<{ url: string; elementHash?: string | null }>): Map<string, string> {\n const map = new Map<string, string>();\n for (const s of screenRecords) {\n if (s.url && s.elementHash) map.set(s.url, s.elementHash);\n }\n return map;\n}\n\n/**\n * Filter a batch of discovered URLs against prior hashes.\n * Returns { toProcess, toSkip } \u2014 caller enqueues toProcess, logs toSkip count.\n *\n * Note: filtering at URL level only works if the prior crawl already visited the URL.\n * For new URLs, they're always in toProcess.\n */\nexport function partitionByChange(\n urls: string[],\n priorHashes: Map<string, string>,\n): { toProcess: string[]; toSkip: string[] } {\n const toProcess: string[] = [];\n const toSkip: string[] = [];\n for (const url of urls) {\n if (priorHashes.has(url)) {\n // Can't know if changed without fetching \u2014 still needs fetch, but hash after fetch\n // will determine skip. Include all \u2014 hash check happens post-fetch.\n toProcess.push(url);\n } else {\n toProcess.push(url);\n }\n }\n return { toProcess, toSkip };\n}\n"],
5
+ "mappings": "AA+BO,SAAS,YAAY,MAAsB;AAChD,MAAI,KAAK;AACT,MAAI,KAAK;AACT,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,KAAK,WAAW,CAAC;AAC3B,UAAM,KAAK,KAAK,IAAI,EAAE,IAAI,OAAO;AACjC,UAAM,KAAK,KAAK,IAAI,EAAE,IAAI,OAAO;AAAA,EACnC;AACA,UAAQ,OAAO,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,KAAK,OAAO,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC3F;AAMO,SAAS,cAAc,MAAsB;AAClD,SAAO,KACJ,MAAM,IAAI,EACV,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAK,EAAE,SAAS,CAAC,EACxB,KAAK,IAAI;AACd;AAEO,SAAS,mBAAmB,KAAa,cAAmC;AACjF,QAAM,aAAa,cAAc,YAAY;AAC7C,SAAO;AAAA,IACL;AAAA,IACA,MAAM,YAAY,UAAU;AAAA,IAC5B,YAAY,WAAW;AAAA,IACvB,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,EACrC;AACF;AAMO,SAAS,YACd,KACA,iBACA,aACY;AACZ,QAAM,UAAU,mBAAmB,KAAK,eAAe;AACvD,QAAM,QAAQ,YAAY,IAAI,GAAG;AAEjC,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,KAAK,SAAS,MAAM,aAAa,QAAQ,MAAM,QAAQ,MAAM;AAAA,EACxE;AACA,MAAI,UAAU,QAAQ,MAAM;AAC1B,WAAO,EAAE,KAAK,SAAS,OAAO,cAAc,OAAO,aAAa,QAAQ,MAAM,QAAQ,YAAY;AAAA,EACpG;AACA,SAAO,EAAE,KAAK,SAAS,MAAM,cAAc,OAAO,aAAa,QAAQ,MAAM,QAAQ,UAAU;AACjG;AAMO,SAAS,kBAAkB,eAAyF;AACzH,QAAM,MAAM,oBAAI,IAAoB;AACpC,aAAW,KAAK,eAAe;AAC7B,QAAI,EAAE,OAAO,EAAE,YAAa,KAAI,IAAI,EAAE,KAAK,EAAE,WAAW;AAAA,EAC1D;AACA,SAAO;AACT;AASO,SAAS,kBACd,MACA,aAC2C;AAC3C,QAAM,YAAsB,CAAC;AAC7B,QAAM,SAAmB,CAAC;AAC1B,aAAW,OAAO,MAAM;AACtB,QAAI,YAAY,IAAI,GAAG,GAAG;AAGxB,gBAAU,KAAK,GAAG;AAAA,IACpB,OAAO;AACL,gBAAU,KAAK,GAAG;AAAA,IACpB;AAAA,EACF;AACA,SAAO,EAAE,WAAW,OAAO;AAC7B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,35 @@
1
+ export * from './crawler.js';
2
+ export * from './visual-diff-verdict.js';
3
+ export * from './auth.js';
4
+ export * from './screen-reader.js';
5
+ export * from './job-queue.js';
6
+ export { startSqsWorker } from './sqs-queue.js';
7
+ export * from './session-capture.js';
8
+ export * from './session-export.js';
9
+ export * from './test-executor.js';
10
+ export { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';
11
+ export { waitForEmailOTP, getInboxEmail } from './email-otp.js';
12
+ export { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';
13
+ export type { SsoConfig, SsoProvider } from './sso-token.js';
14
+ export * from './engine-router.js';
15
+ export * from './profiles/domain-profile.js';
16
+ export * from './policies/crawl-safety.js';
17
+ export * from './extractors/artifact-envelope.js';
18
+ export * from './network-observer.js';
19
+ export { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';
20
+ export type { TlsFingerprintOptions } from './tls-fingerprint.js';
21
+ export { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';
22
+ export type { ExtensionSource, ExtensionProfile } from './browser-extension.js';
23
+ export { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';
24
+ export type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';
25
+ export { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';
26
+ export type { UrlCandidate } from './sitemap.js';
27
+ export { httpCrawl } from './http-engine.js';
28
+ export { extractMarkdown } from './markdown-extract.js';
29
+ export type { ExtractedMarkdown } from './markdown-extract.js';
30
+ export type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';
31
+ export { TieredProxyManager } from './proxy.js';
32
+ export type { ProxyTier, ProxyTierConfig } from './proxy.js';
33
+ export { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';
34
+ export { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';
35
+ export type { Span, FinishedSpan } from './telemetry.js';
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ export * from "./crawler.js";
2
+ export * from "./visual-diff-verdict.js";
3
+ export * from "./auth.js";
4
+ export * from "./screen-reader.js";
5
+ export * from "./job-queue.js";
6
+ import { startSqsWorker } from "./sqs-queue.js";
7
+ export * from "./session-capture.js";
8
+ export * from "./session-export.js";
9
+ export * from "./test-executor.js";
10
+ import { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from "./live-view.js";
11
+ import { waitForEmailOTP, getInboxEmail } from "./email-otp.js";
12
+ import { getSsoToken, getSsoClientCredentialsToken } from "./sso-token.js";
13
+ export * from "./engine-router.js";
14
+ export * from "./profiles/domain-profile.js";
15
+ export * from "./policies/crawl-safety.js";
16
+ export * from "./extractors/artifact-envelope.js";
17
+ export * from "./network-observer.js";
18
+ import { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from "./tls-fingerprint.js";
19
+ import { prepareExtensionProfile, adBlockerProfile } from "./browser-extension.js";
20
+ import { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from "./redis-queue.js";
21
+ import { discoverProjectUrls, discoverSitemapUrls } from "./sitemap.js";
22
+ import { httpCrawl } from "./http-engine.js";
23
+ import { extractMarkdown } from "./markdown-extract.js";
24
+ import { TieredProxyManager } from "./proxy.js";
25
+ import { RedisCrawlFrontier, isRedisFrontierEnabled } from "./redis-frontier.js";
26
+ import { startSpan, endSpan, withSpan, increment, flushMetrics } from "./telemetry.js";
27
+ export {
28
+ CHROME_TLS_HEADERS,
29
+ RedisCrawlFrontier,
30
+ TieredProxyManager,
31
+ adBlockerProfile,
32
+ chromeTlsAgent,
33
+ closeLiveView,
34
+ completeLiveView,
35
+ createChromeTlsAgent,
36
+ createRedisQueue,
37
+ discoverProjectUrls,
38
+ discoverSitemapUrls,
39
+ endSpan,
40
+ extractMarkdown,
41
+ flushMetrics,
42
+ getFrame,
43
+ getInboxEmail,
44
+ getSsoClientCredentialsToken,
45
+ getSsoToken,
46
+ httpCrawl,
47
+ increment,
48
+ isRedisFrontierEnabled,
49
+ isRedisQueueEnabled,
50
+ liveViewNavigate,
51
+ maybeCreateRedisQueue,
52
+ prepareExtensionProfile,
53
+ probeTlsFingerprint,
54
+ sendInput,
55
+ startLiveView,
56
+ startSpan,
57
+ startSqsWorker,
58
+ waitForEmailOTP,
59
+ withSpan
60
+ };
61
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["export * from './crawler.js';\nexport * from './visual-diff-verdict.js';\nexport * from './auth.js';\nexport * from './screen-reader.js';\nexport * from './job-queue.js';\nexport { startSqsWorker } from './sqs-queue.js';\nexport * from './session-capture.js';\nexport * from './session-export.js';\nexport * from './test-executor.js';\nexport { startLiveView, getFrame, liveViewNavigate, completeLiveView, closeLiveView, sendInput } from './live-view.js';\nexport { waitForEmailOTP, getInboxEmail } from './email-otp.js';\nexport { getSsoToken, getSsoClientCredentialsToken } from './sso-token.js';\nexport type { SsoConfig, SsoProvider } from './sso-token.js';\nexport * from './engine-router.js';\nexport * from './profiles/domain-profile.js';\nexport * from './policies/crawl-safety.js';\nexport * from './extractors/artifact-envelope.js';\nexport * from './network-observer.js';\nexport { createChromeTlsAgent, probeTlsFingerprint, chromeTlsAgent, CHROME_TLS_HEADERS } from './tls-fingerprint.js';\nexport type { TlsFingerprintOptions } from './tls-fingerprint.js';\nexport { prepareExtensionProfile, adBlockerProfile } from './browser-extension.js';\nexport type { ExtensionSource, ExtensionProfile } from './browser-extension.js';\nexport { createRedisQueue, maybeCreateRedisQueue, isRedisQueueEnabled } from './redis-queue.js';\nexport type { RedisQueueJob, RedisQueueClient } from './redis-queue.js';\nexport { discoverProjectUrls, discoverSitemapUrls } from './sitemap.js';\nexport type { UrlCandidate } from './sitemap.js';\nexport { httpCrawl } from './http-engine.js';\nexport { extractMarkdown } from './markdown-extract.js';\nexport type { ExtractedMarkdown } from './markdown-extract.js';\nexport type { HttpScreen, HttpCrawlOpts, HttpCrawlResult, HttpOutputFormat } from './http-engine.js';\nexport { TieredProxyManager } from './proxy.js';\nexport type { ProxyTier, ProxyTierConfig } from './proxy.js';\nexport { RedisCrawlFrontier, isRedisFrontierEnabled } from './redis-frontier.js';\nexport { startSpan, endSpan, withSpan, increment, flushMetrics } from './telemetry.js';\nexport type { Span, FinishedSpan } from './telemetry.js';\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB;AAC/B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,eAAe,UAAU,kBAAkB,kBAAkB,eAAe,iBAAiB;AACtG,SAAS,iBAAiB,qBAAqB;AAC/C,SAAS,aAAa,oCAAoC;AAE1D,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,sBAAsB,qBAAqB,gBAAgB,0BAA0B;AAE9F,SAAS,yBAAyB,wBAAwB;AAE1D,SAAS,kBAAkB,uBAAuB,2BAA2B;AAE7E,SAAS,qBAAqB,2BAA2B;AAEzD,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAGhC,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB,8BAA8B;AAC3D,SAAS,WAAW,SAAS,UAAU,WAAW,oBAAoB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Input fuzzer — generate test data for form fields.
3
+ *
4
+ * For each field type, produces a set of test values covering:
5
+ * - Happy path (valid, normal input)
6
+ * - Boundary (min/max length, min/max value)
7
+ * - Edge cases (empty, very long, special chars, SQL injection probe, XSS probe)
8
+ * - Type-invalid (wrong type for the field)
9
+ *
10
+ * This feeds ZeTa's test generator to automatically create form validation tests.
11
+ * Values are safe probes (NOT actual exploits) — they test whether the app
12
+ * properly rejects/accepts boundary inputs.
13
+ */
14
+ import type { FormField } from './form-detector.js';
15
+ export type InputCase = {
16
+ label: string;
17
+ value: string;
18
+ expectedValid: boolean;
19
+ category: 'happy' | 'boundary' | 'edge' | 'invalid';
20
+ };
21
+ export declare function generateFieldCases(field: FormField): InputCase[];
22
+ export interface FuzzedForm {
23
+ formIndex: number;
24
+ purpose: string;
25
+ fieldCases: Array<{
26
+ field: FormField;
27
+ cases: InputCase[];
28
+ }>;
29
+ }
30
+ export declare function fuzzForm(form: import('./form-detector.js').DetectedForm): FuzzedForm;