@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,89 @@
1
+ import type { Page } from 'playwright';
2
+
3
+ /**
4
+ * Reads a page as a structured outline (cheap + reliable), the way
5
+ * Playwright MCP / the accessibility tree works — NOT by sending pixels to a
6
+ * vision model. Vision is a fallback for image-only pages (added later).
7
+ *
8
+ * Returns the interactive elements found, which the Screen Reader Agent then
9
+ * enriches with MEANING and saves to the Zeta Core.
10
+ */
11
+ export interface RawElement {
12
+ role: string;
13
+ label: string;
14
+ selectorHint: string;
15
+ expectedData?: string;
16
+ }
17
+
18
+ export async function readScreen(page: Page): Promise<RawElement[]> {
19
+ return page.evaluate(() => {
20
+ const out: { role: string; label: string; selectorHint: string; expectedData?: string }[] = [];
21
+ const seen = new Set<string>();
22
+
23
+ // Noise patterns to skip — cookie banners, footer junk, marketing copy buttons, version strings
24
+ const noiseLabel =
25
+ /^(cookie|consent|privacy policy|terms of use|copyright|©|\bv\d+\.\d+|accept|decline|got it|dismiss|close|skip to|powered by|copy|copied|\s*$)/i;
26
+
27
+ // Select only truly interactive elements (not generic [role] containers)
28
+ const nodes = document.querySelectorAll(
29
+ 'input:not([type=hidden]), select, textarea, button, a[href]'
30
+ );
31
+
32
+ nodes.forEach((el) => {
33
+ const htmlEl = el as HTMLElement;
34
+
35
+ // Skip invisible or zero-size elements
36
+ const style = window.getComputedStyle(htmlEl);
37
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return;
38
+ const rect = el.getBoundingClientRect();
39
+ if (rect.width === 0 && rect.height === 0) return;
40
+
41
+ // Derive role
42
+ const tag = el.tagName.toLowerCase();
43
+ const role =
44
+ el.getAttribute('role') ||
45
+ (tag === 'input' ? ((el as HTMLInputElement).type || 'text') : tag);
46
+
47
+ // Derive the best human-readable label (prefer aria > placeholder > inner text)
48
+ const raw = (
49
+ el.getAttribute('aria-label') ||
50
+ el.getAttribute('placeholder') ||
51
+ (el.getAttribute('title') ?? '') ||
52
+ htmlEl.innerText?.replace(/\s+/g, ' ').trim() ||
53
+ el.getAttribute('name') ||
54
+ ''
55
+ ).substring(0, 80);
56
+
57
+ // Skip empty, noise patterns, or very long labels (likely container text)
58
+ if (!raw || noiseLabel.test(raw) || raw.length > 70) return;
59
+
60
+ // Deduplicate by role+normalised label
61
+ const key = `${role}:${raw.toLowerCase()}`;
62
+ if (seen.has(key)) return;
63
+ seen.add(key);
64
+
65
+ let expectedData: string | undefined;
66
+ if (tag === 'input') {
67
+ const input = el as HTMLInputElement;
68
+ if (input.type === 'email' || /email/i.test(raw)) expectedData = 'valid_email';
69
+ else if (input.type === 'password' || /password/i.test(raw)) expectedData = 'password';
70
+ else if (input.type === 'tel' || /phone|mobile/i.test(raw)) expectedData = 'phone';
71
+ else if (input.type === 'date' || /\bdate\b/i.test(raw)) expectedData = 'date';
72
+ else if (/\b(code|otp|token)\b/i.test(raw)) expectedData = 'one_time_code';
73
+ else if (/\b(search|query)\b/i.test(raw)) expectedData = 'search_term';
74
+ }
75
+
76
+ out.push({ role, label: raw, selectorHint: tag, expectedData });
77
+ });
78
+
79
+ // Prioritise: form fields > buttons > links; within each group keep order
80
+ const priority = (e: typeof out[0]) => {
81
+ if (['email', 'password', 'text', 'tel', 'date', 'number', 'select', 'textarea'].includes(e.role)) return 0;
82
+ if (e.role === 'button' || e.role === 'submit') return 1;
83
+ return 2;
84
+ };
85
+ out.sort((a, b) => priority(a) - priority(b));
86
+
87
+ return out.slice(0, 25); // cap at 25 — enough context, not overwhelming
88
+ });
89
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Screenshot masker — blur or black-box PII regions in captured screenshots.
3
+ *
4
+ * Strategy: inject a CSS overlay into the page before screenshot capture that
5
+ * covers detected PII-likely elements (input[type=email], input[type=password],
6
+ * elements with PII-containing text) with a blur or solid black mask.
7
+ *
8
+ * This is a CSS-based approach (no image processing library needed):
9
+ * 1. Identify likely-PII DOM elements via selectors
10
+ * 2. Add an overlaid <div> with blur/black filter at each element's bounding box
11
+ * 3. Take screenshot
12
+ * 4. Remove overlays
13
+ *
14
+ * Falls back gracefully if elements are not found or overlay injection fails.
15
+ */
16
+
17
+ import type { Page } from 'playwright';
18
+
19
+ export interface MaskingOpts {
20
+ /** Additional CSS selectors to mask (in addition to defaults) */
21
+ extraSelectors?: string[];
22
+ /** Masking style: 'blur' or 'blackout' */
23
+ style?: 'blur' | 'blackout';
24
+ /** Blur strength in px (default 8) */
25
+ blurPx?: number;
26
+ }
27
+
28
+ // Default PII-sensitive selectors
29
+ const DEFAULT_PII_SELECTORS = [
30
+ 'input[type="email"]',
31
+ 'input[type="password"]',
32
+ 'input[type="tel"]',
33
+ 'input[autocomplete="cc-number"]',
34
+ 'input[autocomplete="cc-csc"]',
35
+ 'input[autocomplete="ssn"]',
36
+ '[data-pii="true"]',
37
+ '[data-sensitive="true"]',
38
+ ];
39
+
40
+ export async function maskScreenshot(
41
+ page: Page,
42
+ opts: MaskingOpts = {},
43
+ ): Promise<{ maskedCount: number; screenshot: Buffer }> {
44
+ const { extraSelectors = [], style = 'blur', blurPx = 8 } = opts;
45
+ const selectors = [...DEFAULT_PII_SELECTORS, ...extraSelectors];
46
+
47
+ let maskedCount = 0;
48
+ const overlayIds: string[] = [];
49
+
50
+ // Inject CSS overlays
51
+ try {
52
+ const result = await page.evaluate(({ selectors, style, blurPx }) => {
53
+ const ids: string[] = [];
54
+ for (const sel of selectors) {
55
+ try {
56
+ const elements = Array.from(document.querySelectorAll(sel));
57
+ for (const el of elements) {
58
+ const rect = el.getBoundingClientRect();
59
+ if (rect.width === 0 || rect.height === 0) continue;
60
+ const overlay = document.createElement('div');
61
+ const id = `__zeta_mask_${Math.random().toString(36).slice(2)}`;
62
+ overlay.id = id;
63
+ overlay.style.cssText = [
64
+ 'position: fixed',
65
+ `top: ${rect.top + window.scrollY}px`,
66
+ `left: ${rect.left + window.scrollX}px`,
67
+ `width: ${rect.width}px`,
68
+ `height: ${rect.height}px`,
69
+ 'z-index: 2147483647',
70
+ style === 'blur'
71
+ ? `backdrop-filter: blur(${blurPx}px); -webkit-backdrop-filter: blur(${blurPx}px); background: rgba(0,0,0,0.1);`
72
+ : 'background: #000000;',
73
+ 'pointer-events: none',
74
+ ].join('; ');
75
+ document.body.appendChild(overlay);
76
+ ids.push(id);
77
+ }
78
+ } catch { /* selector error */ }
79
+ }
80
+ return ids;
81
+ }, { selectors, style, blurPx });
82
+
83
+ overlayIds.push(...result);
84
+ maskedCount = result.length;
85
+ } catch { /* non-fatal */ }
86
+
87
+ // Take screenshot with overlays active
88
+ const screenshot = await page.screenshot({ type: 'png', fullPage: false });
89
+
90
+ // Remove overlays
91
+ if (overlayIds.length > 0) {
92
+ await page.evaluate((ids) => {
93
+ for (const id of ids) document.getElementById(id)?.remove();
94
+ }, overlayIds).catch(() => {});
95
+ }
96
+
97
+ return { maskedCount, screenshot };
98
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * SEO indexability checker — determines if a page is crawlable and indexable.
3
+ *
4
+ * Checks:
5
+ * 1. X-Robots-Tag response header (noindex, nofollow, noarchive)
6
+ * 2. <meta name="robots"> tag (noindex, nofollow)
7
+ * 3. Canonical URL (self-canonical vs cross-canonical)
8
+ * 4. rel="nofollow" on outbound links
9
+ * 5. hreflang tags (multilingual)
10
+ * 6. Open Graph completeness (og:title, og:description, og:image)
11
+ * 7. Structured data presence (JSON-LD)
12
+ * 8. Meta description
13
+ * 9. Title tag length (< 60 chars recommended)
14
+ * 10. URL structure (query params, fragments, excessive depth)
15
+ */
16
+
17
+ import type { Page } from 'playwright';
18
+
19
+ export interface SeoIssue {
20
+ rule: string;
21
+ severity: 'critical' | 'warning' | 'info';
22
+ message: string;
23
+ value?: string;
24
+ }
25
+
26
+ export interface SeoCheckResult {
27
+ url: string;
28
+ isIndexable: boolean;
29
+ canonicalUrl?: string;
30
+ isSelfCanonical?: boolean;
31
+ hreflang: Record<string, string>; // lang → url
32
+ issues: SeoIssue[];
33
+ score: number; // 0-100
34
+ checkedAt: string;
35
+ }
36
+
37
+ export async function checkSeo(page: Page, url: string, responseHeaders: Record<string, string> = {}): Promise<SeoCheckResult> {
38
+ const issues: SeoIssue[] = [];
39
+ const checkedAt = new Date().toISOString();
40
+ let isIndexable = true;
41
+
42
+ // 1. X-Robots-Tag
43
+ const robotsHeader = responseHeaders['x-robots-tag'] ?? '';
44
+ if (/noindex/i.test(robotsHeader)) {
45
+ isIndexable = false;
46
+ issues.push({ rule: 'x-robots-noindex', severity: 'critical', message: 'X-Robots-Tag: noindex found in response header', value: robotsHeader });
47
+ }
48
+
49
+ // 2-9. Page-level checks
50
+ const pageData = await page.evaluate(() => {
51
+ const meta = (name: string) => document.querySelector(`meta[name="${name}"]`)?.getAttribute('content') ?? '';
52
+ const og = (prop: string) => document.querySelector(`meta[property="og:${prop}"]`)?.getAttribute('content') ?? '';
53
+ const canonical = document.querySelector('link[rel="canonical"]')?.getAttribute('href') ?? '';
54
+ const hreflang: Record<string, string> = {};
55
+ document.querySelectorAll('link[rel="alternate"][hreflang]').forEach((el: any) => {
56
+ hreflang[el.getAttribute('hreflang')] = el.getAttribute('href') ?? '';
57
+ });
58
+ const jsonLd = Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map((s: any) => s.textContent ?? '');
59
+ return {
60
+ robotsMeta: meta('robots'),
61
+ description: meta('description'),
62
+ title: document.title ?? '',
63
+ canonical,
64
+ hreflang,
65
+ ogTitle: og('title'),
66
+ ogDescription: og('description'),
67
+ ogImage: og('image'),
68
+ hasJsonLd: jsonLd.length > 0,
69
+ jsonLd,
70
+ };
71
+ }).catch(() => ({
72
+ robotsMeta: '', description: '', title: '', canonical: '', hreflang: {},
73
+ ogTitle: '', ogDescription: '', ogImage: '', hasJsonLd: false, jsonLd: [],
74
+ }));
75
+
76
+ // 2. Meta robots
77
+ if (/noindex/i.test(pageData.robotsMeta)) {
78
+ isIndexable = false;
79
+ issues.push({ rule: 'meta-robots-noindex', severity: 'critical', message: 'Meta robots noindex found', value: pageData.robotsMeta });
80
+ }
81
+
82
+ // 3. Canonical
83
+ let isSelfCanonical: boolean | undefined;
84
+ if (pageData.canonical) {
85
+ isSelfCanonical = pageData.canonical === url || pageData.canonical.replace(/\/$/, '') === url.replace(/\/$/, '');
86
+ if (!isSelfCanonical) {
87
+ issues.push({ rule: 'cross-canonical', severity: 'warning', message: 'Cross-canonical points elsewhere', value: pageData.canonical });
88
+ }
89
+ } else {
90
+ issues.push({ rule: 'missing-canonical', severity: 'warning', message: 'No canonical link tag' });
91
+ }
92
+
93
+ // 4. Title
94
+ if (!pageData.title) {
95
+ issues.push({ rule: 'missing-title', severity: 'critical', message: 'Page has no title tag' });
96
+ } else if (pageData.title.length > 60) {
97
+ issues.push({ rule: 'title-too-long', severity: 'warning', message: `Title ${pageData.title.length} chars (>60)`, value: pageData.title });
98
+ }
99
+
100
+ // 5. Meta description
101
+ if (!pageData.description) {
102
+ issues.push({ rule: 'missing-description', severity: 'warning', message: 'Missing meta description' });
103
+ } else if (pageData.description.length > 160) {
104
+ issues.push({ rule: 'description-too-long', severity: 'info', message: `Description ${pageData.description.length} chars (>160)` });
105
+ }
106
+
107
+ // 6. OG tags
108
+ if (!pageData.ogTitle) issues.push({ rule: 'missing-og-title', severity: 'info', message: 'Missing og:title' });
109
+ if (!pageData.ogImage) issues.push({ rule: 'missing-og-image', severity: 'info', message: 'Missing og:image' });
110
+
111
+ // 7. Structured data
112
+ if (!pageData.hasJsonLd) issues.push({ rule: 'no-structured-data', severity: 'info', message: 'No JSON-LD structured data found' });
113
+
114
+ // 8. URL depth
115
+ try {
116
+ const depth = new URL(url).pathname.split('/').filter(Boolean).length;
117
+ if (depth > 5) issues.push({ rule: 'deep-url', severity: 'info', message: `URL depth ${depth} (>5 levels)`, value: url });
118
+ } catch {}
119
+
120
+ const criticals = issues.filter(i => i.severity === 'critical').length;
121
+ const warnings = issues.filter(i => i.severity === 'warning').length;
122
+ const score = Math.max(0, 100 - criticals * 20 - warnings * 5 - issues.filter(i => i.severity === 'info').length * 2);
123
+
124
+ return {
125
+ url,
126
+ isIndexable,
127
+ canonicalUrl: pageData.canonical || undefined,
128
+ isSelfCanonical,
129
+ hreflang: pageData.hreflang,
130
+ issues,
131
+ score,
132
+ checkedAt,
133
+ };
134
+ }
@@ -0,0 +1,325 @@
1
+ import { chromium } from 'playwright';
2
+ import { login } from './auth.js';
3
+ import { generateTOTP } from './job-queue.js';
4
+
5
+ interface CaptureSession {
6
+ browser: any;
7
+ context: any;
8
+ timer: ReturnType<typeof setTimeout>;
9
+ }
10
+
11
+ const activeSessions = new Map<string, CaptureSession>();
12
+
13
+ /**
14
+ * Opens a headed (visible) browser at appUrl and returns a captureId.
15
+ * The user logs in manually, then calls completeSessionCapture() to harvest storageState.
16
+ * Sessions expire after 10 minutes if not completed.
17
+ *
18
+ * Throws 'HEADLESS_ENV' if running in Docker/Linux without a display server.
19
+ * The API route surfaces this as a 503 so the frontend can switch to manual JSON paste.
20
+ */
21
+ export async function startSessionCapture(appUrl: string): Promise<string> {
22
+ const inDocker = process.env.PLAYWRIGHT_IN_DOCKER === 'true';
23
+ const noDisplay = process.platform === 'linux' && !process.env.DISPLAY;
24
+ if (inDocker || noDisplay) {
25
+ throw new Error('HEADLESS_ENV');
26
+ }
27
+
28
+ const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
29
+ const browser = await chromium.launch({ headless: false });
30
+ const context = await browser.newContext();
31
+ const page = await context.newPage();
32
+ await page.goto(appUrl, { waitUntil: 'domcontentloaded' }).catch(() => {});
33
+ const timer = setTimeout(() => cancelSessionCapture(id).catch(() => {}), 10 * 60 * 1000);
34
+ activeSessions.set(id, { browser, context, timer });
35
+ return id;
36
+ }
37
+
38
+ /** Captures storageState from the open browser, closes it, and returns the JSON string. */
39
+ export async function completeSessionCapture(captureId: string): Promise<string> {
40
+ const session = activeSessions.get(captureId);
41
+ if (!session) throw new Error('Capture session not found or expired.');
42
+ clearTimeout(session.timer);
43
+ const state = await session.context.storageState();
44
+ await session.browser.close().catch(() => {});
45
+ activeSessions.delete(captureId);
46
+ return JSON.stringify(state);
47
+ }
48
+
49
+ /** Closes the browser without capturing — called on Cancel. */
50
+ export async function cancelSessionCapture(captureId: string): Promise<void> {
51
+ const session = activeSessions.get(captureId);
52
+ if (!session) return;
53
+ clearTimeout(session.timer);
54
+ await session.browser.close().catch(() => {});
55
+ activeSessions.delete(captureId);
56
+ }
57
+
58
+ /**
59
+ * Post-login exploration: navigates nav links, cycles tabs, advances carousels,
60
+ * opens accordions, scrolls — so the browser accumulates full session state before capture.
61
+ * All best-effort: never throws, never fails the capture.
62
+ */
63
+ async function exploreAppAfterLogin(page: any): Promise<void> {
64
+ const sleep = (ms: number) => page.waitForTimeout(ms).catch(() => {});
65
+
66
+ const isVisible = `
67
+ function isVisible(el) {
68
+ const s = window.getComputedStyle(el);
69
+ if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) < 0.1) return false;
70
+ const r = el.getBoundingClientRect();
71
+ return r.width > 4 && r.height > 4;
72
+ }
73
+ `;
74
+
75
+ try {
76
+ const origin = await page.evaluate(() => location.origin).catch(() => '');
77
+ const startUrl = await page.evaluate(() => location.href).catch(() => '');
78
+
79
+ // ── 1. Expand collapsed nav menus / hamburgers ──────────────────────────
80
+ await page.evaluate(new Function(`
81
+ ${isVisible}
82
+ const inBadCtx = el => {
83
+ let p = el.parentElement;
84
+ while (p) {
85
+ const role = p.getAttribute('role') ?? '';
86
+ if (['dialog','alertdialog','combobox','listbox'].includes(role)) return true;
87
+ p = p.parentElement;
88
+ }
89
+ return false;
90
+ };
91
+ const targets = new Set();
92
+ document.querySelectorAll('[aria-haspopup]:not([aria-expanded="true"])').forEach(el => {
93
+ if (isVisible(el) && !inBadCtx(el)) targets.add(el);
94
+ });
95
+ document.querySelectorAll('[aria-expanded="false"]').forEach(el => {
96
+ if ((el.tagName === 'BUTTON' || el.getAttribute('role') === 'button') && isVisible(el) && !inBadCtx(el)) targets.add(el);
97
+ });
98
+ document.querySelectorAll('[data-state="closed"],[data-headlessui-state="closed"]').forEach(el => {
99
+ if ((el.tagName === 'BUTTON' || el.getAttribute('role') === 'button') && isVisible(el) && !inBadCtx(el)) targets.add(el);
100
+ });
101
+ targets.forEach(el => { try { el.click(); } catch {} });
102
+ `) as () => void).catch(() => {});
103
+ await sleep(600);
104
+
105
+ // ── 2. Collect nav links from the expanded DOM ──────────────────────────
106
+ const navLinks: string[] = await page.evaluate(new Function(`
107
+ ${isVisible}
108
+ const links = new Set();
109
+ const navEls = document.querySelectorAll('nav a, [role="navigation"] a, aside a, header a');
110
+ navEls.forEach(a => {
111
+ const href = a.getAttribute('href');
112
+ if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
113
+ if (!isVisible(a)) return;
114
+ try {
115
+ const url = new URL(href, location.href);
116
+ if (url.origin === location.origin) links.add(url.href);
117
+ } catch {}
118
+ });
119
+ return [...links].slice(0, 10); // cap at 10 nav pages
120
+ `) as () => string[]).catch(() => []);
121
+
122
+ // Dismiss opened menus before navigating
123
+ await page.keyboard?.press('Escape').catch(() => {});
124
+ await sleep(200);
125
+
126
+ // ── 3. Visit each nav link, interact, return ────────────────────────────
127
+ for (const link of navLinks) {
128
+ try {
129
+ if (link === startUrl) continue;
130
+ await page.goto(link, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => {});
131
+ await sleep(400);
132
+ await interactWithCurrentPage(page, isVisible, sleep);
133
+ } catch { /* skip broken page */ }
134
+ }
135
+
136
+ // Return to start page
137
+ if (navLinks.length > 0) {
138
+ await page.goto(startUrl, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => {});
139
+ await sleep(400);
140
+ }
141
+
142
+ // ── 4. Final interaction pass on landing page ───────────────────────────
143
+ await interactWithCurrentPage(page, isVisible, sleep);
144
+
145
+ await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {});
146
+ } catch {
147
+ // Exploration is best-effort
148
+ }
149
+ }
150
+
151
+ /** Interacts with tabs, carousels, accordions, and scrolls on the current page. */
152
+ async function interactWithCurrentPage(page: any, isVisible: string, sleep: (ms: number) => Promise<void>): Promise<void> {
153
+ // Cycle through tabs
154
+ await page.evaluate(new Function(`
155
+ ${isVisible}
156
+ const tabs = [...document.querySelectorAll('[role="tab"]')].filter(isVisible);
157
+ tabs.forEach(tab => { try { tab.click(); } catch {} });
158
+ `) as () => void).catch(() => {});
159
+ await sleep(400);
160
+
161
+ // Advance carousels (next buttons)
162
+ await page.evaluate(new Function(`
163
+ ${isVisible}
164
+ const nextBtns = [...document.querySelectorAll(
165
+ 'button[aria-label*="next" i], button[aria-label*="forward" i], button[aria-label*="right" i], ' +
166
+ '[class*="carousel" i] button, [class*="slider" i] button, [data-direction="next"]'
167
+ )].filter(isVisible);
168
+ // Click next up to 3 times per carousel
169
+ for (let i = 0; i < 3; i++) {
170
+ nextBtns.forEach(btn => { try { btn.click(); } catch {} });
171
+ }
172
+ `) as () => void).catch(() => {});
173
+ await sleep(400);
174
+
175
+ // Open closed accordions / disclosure panels
176
+ await page.evaluate(new Function(`
177
+ ${isVisible}
178
+ document.querySelectorAll(
179
+ '[data-state="closed"] button, [aria-expanded="false"], ' +
180
+ 'details:not([open]) > summary, [class*="accordion" i] button'
181
+ ).forEach(el => {
182
+ if (isVisible(el)) { try { el.click(); } catch {} }
183
+ });
184
+ `) as () => void).catch(() => {});
185
+ await sleep(300);
186
+
187
+ // Scroll to bottom then back to top
188
+ await page.evaluate(() => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' }); }).catch(() => {});
189
+ await sleep(300);
190
+ await page.evaluate(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }).catch(() => {});
191
+ }
192
+
193
+ /**
194
+ * Headless login + session capture — works on Docker/cloud (no display needed).
195
+ * Tries DOM-based login first (fast, free). If it fails and llmConfig is provided,
196
+ * falls back to Stagehand AI-guided login (handles modals, multi-step, marketing pages).
197
+ *
198
+ * Returns { storageState, cookies, localStorage } on success.
199
+ * Throws a human-readable error on failure.
200
+ */
201
+ export async function validateAndCaptureSession(
202
+ appUrl: string,
203
+ creds: {
204
+ username: string; password: string; totpSecret?: string;
205
+ httpBasicUsername?: string; httpBasicPassword?: string;
206
+ },
207
+ llmConfig?: { modelName: string; apiKey: string },
208
+ captchaOpts?: { apiKey?: string; provider?: '2captcha' | 'capsolver' },
209
+ ): Promise<{ storageState: string; cookies: number; localStorage: number }> {
210
+ const browser = await chromium.launch({ headless: true });
211
+ try {
212
+ const contextOpts: any = {};
213
+ if (creds.httpBasicUsername && creds.httpBasicPassword) {
214
+ contextOpts.httpCredentials = { username: creds.httpBasicUsername, password: creds.httpBasicPassword };
215
+ }
216
+ const context = await browser.newContext(contextOpts);
217
+ const page = await context.newPage();
218
+
219
+ const getOneTimeCode = creds.totpSecret
220
+ ? async () => generateTOTP(creds.totpSecret!)
221
+ : undefined;
222
+
223
+ let loggedIn: boolean;
224
+ try {
225
+ loggedIn = await login(page, appUrl, {
226
+ username: creds.username,
227
+ password: creds.password,
228
+ getOneTimeCode,
229
+ });
230
+ } catch (loginErr: any) {
231
+ // login() throws when it can read a real error message from the page
232
+ const shotBuf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false }).catch(() => null);
233
+ throw Object.assign(loginErr, {
234
+ failureScreenshot: shotBuf ? shotBuf.toString('base64') : null,
235
+ });
236
+ }
237
+
238
+ // Stagehand AI fallback when DOM login fails and LLM config is available
239
+ if (!loggedIn && llmConfig) {
240
+ let sh: any = null;
241
+ try {
242
+ const { Stagehand } = await import('@browserbasehq/stagehand');
243
+ const { loginWithStagehand } = await import('./ai-login.js');
244
+ sh = new Stagehand({
245
+ env: 'LOCAL',
246
+ model: { modelName: llmConfig.modelName as any, apiKey: llmConfig.apiKey },
247
+ serverCache: false,
248
+ verbose: 0,
249
+ localBrowserLaunchOptions: { headless: true },
250
+ });
251
+ await sh.init();
252
+ const shPage = await (sh as any).resolvePage();
253
+ await shPage.goto(appUrl, { waitUntil: 'networkidle', timeout: 30_000 }).catch(() => {});
254
+ const shLoggedIn = await loginWithStagehand(
255
+ sh,
256
+ { username: creds.username, password: creds.password, getOneTimeCode },
257
+ appUrl,
258
+ captchaOpts,
259
+ );
260
+ if (shLoggedIn) {
261
+ const shCtx = (sh as any).context ?? (sh as any).browserContext;
262
+ if (shCtx) {
263
+ const shState = await shCtx.storageState();
264
+ const shCookies = shState.cookies?.length ?? 0;
265
+ const shLocalStorage = shState.origins?.reduce((s: number, o: any) => s + (o.localStorage?.length ?? 0), 0) ?? 0;
266
+ if (shCookies > 0 || shLocalStorage > 0) {
267
+ await sh.close().catch(() => {});
268
+ sh = null;
269
+ return { storageState: JSON.stringify(shState), cookies: shCookies, localStorage: shLocalStorage };
270
+ }
271
+ }
272
+ loggedIn = true; // Stagehand succeeded but we'll capture from the plain browser context below
273
+ }
274
+ } catch (shErr: any) {
275
+ const msg = String(shErr?.message ?? shErr);
276
+ if (msg.includes('CAPTCHA') || shErr?.isSSOError) throw shErr;
277
+ // Stagehand failed silently — fall through to "credentials rejected"
278
+ } finally {
279
+ if (sh) await sh.close().catch(() => {});
280
+ }
281
+ }
282
+
283
+ if (!loggedIn) {
284
+ const shotBuf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false }).catch(() => null);
285
+ const challenge = await page.evaluate(() => {
286
+ const text = (document.body?.innerText ?? '').toLowerCase();
287
+ const hasCloudflare = /verify you are human|checking your browser|cloudflare|cf-challenge|turnstile/.test(text) ||
288
+ !!document.querySelector('iframe[src*="challenges.cloudflare.com"], .cf-turnstile, [data-sitekey]');
289
+ const hasCaptcha = hasCloudflare || /captcha|recaptcha|hcaptcha/.test(text) ||
290
+ !!document.querySelector('iframe[src*="recaptcha"], iframe[src*="hcaptcha"], #captcha, .captcha, [id*="captcha"], [class*="captcha"]');
291
+ return { hasCloudflare, hasCaptcha };
292
+ }).catch(() => ({ hasCloudflare: false, hasCaptcha: false }));
293
+ const message = challenge.hasCloudflare
294
+ ? 'Cloudflare human verification blocked headless validation. This cannot be bypassed reliably in code; use Live Browser or manual session capture, configure a CAPTCHA solver, or allowlist the crawler IP.'
295
+ : challenge.hasCaptcha
296
+ ? 'A CAPTCHA blocked headless validation. Configure CAPTCHA Auto-Solve or use Live Browser/manual session capture.'
297
+ : 'Login failed — credentials rejected or the login flow requires a manual/SSO step. Check credentials, saved login instructions, or use Live Browser session capture.';
298
+ const err = Object.assign(new Error(message), {
299
+ failureScreenshot: shotBuf ? shotBuf.toString('base64') : null,
300
+ reason: challenge.hasCloudflare ? 'CLOUDFLARE_CHALLENGE' : challenge.hasCaptcha ? 'CAPTCHA_CHALLENGE' : 'LOGIN_NOT_CONFIRMED',
301
+ });
302
+ throw err;
303
+ }
304
+
305
+ // Wait for post-login navigation to settle before capturing session
306
+ await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});
307
+
308
+ // Post-login exploration: navigate tabs, expand menus, scroll to trigger lazy-loaded state
309
+ await exploreAppAfterLogin(page);
310
+
311
+ const state = await context.storageState();
312
+ const cookies = state.cookies?.length ?? 0;
313
+ const localStorage = state.origins?.reduce(
314
+ (sum, o) => sum + (o.localStorage?.length ?? 0), 0
315
+ ) ?? 0;
316
+
317
+ if (cookies === 0 && localStorage === 0) {
318
+ throw new Error('Login appeared to succeed but no session data was captured — try manual session capture instead');
319
+ }
320
+
321
+ return { storageState: JSON.stringify(state), cookies, localStorage };
322
+ } finally {
323
+ await browser.close().catch(() => {});
324
+ }
325
+ }