@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
package/dist/auth.js ADDED
@@ -0,0 +1,329 @@
1
+ async function firstVisible(page, selector) {
2
+ const loc = page.locator(selector);
3
+ const count = await loc.count().catch(() => 0);
4
+ for (let i = 0; i < count; i += 1) {
5
+ const item = loc.nth(i);
6
+ if (await item.isVisible().catch(() => false)) return item;
7
+ }
8
+ for (const frame of page.frames()) {
9
+ if (frame === page.mainFrame()) continue;
10
+ const floc = frame.locator(selector);
11
+ const fcount = await floc.count().catch(() => 0);
12
+ for (let i = 0; i < fcount; i += 1) {
13
+ const item = floc.nth(i);
14
+ if (await item.isVisible().catch(() => false)) return item;
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ async function reactFill(field, value) {
20
+ if (!field) return;
21
+ await field.click();
22
+ await field.selectText().catch(() => {
23
+ });
24
+ await field.press("Control+a").catch(() => {
25
+ });
26
+ await field.press("Backspace").catch(() => {
27
+ });
28
+ await field.pressSequentially(value, { delay: 30 });
29
+ await field.evaluate((el, v) => {
30
+ const input = el;
31
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
32
+ if (nativeInputValueSetter) nativeInputValueSetter.call(input, v);
33
+ input.dispatchEvent(new Event("input", { bubbles: true }));
34
+ input.dispatchEvent(new Event("change", { bubbles: true }));
35
+ input.dispatchEvent(new Event("blur", { bubbles: true }));
36
+ }, value).catch(() => {
37
+ });
38
+ }
39
+ async function clickLikelySubmit(page) {
40
+ let button = await firstVisible(page, [
41
+ 'button[type="submit"]',
42
+ 'input[type="submit"]',
43
+ 'button:has-text("Next")',
44
+ 'button:has-text("Sign in")',
45
+ 'button:has-text("Log in")',
46
+ 'button:has-text("Login")',
47
+ 'button:has-text("Sign up")'
48
+ ].join(", "));
49
+ if (!button) {
50
+ const candidates = page.locator('button:has-text("Continue")');
51
+ const count = await candidates.count().catch(() => 0);
52
+ for (let i = 0; i < count; i++) {
53
+ const c = candidates.nth(i);
54
+ if (!await c.isVisible().catch(() => false)) continue;
55
+ const text = await c.textContent().catch(() => "") ?? "";
56
+ if (/continue\s+with\s+/i.test(text)) continue;
57
+ button = c;
58
+ break;
59
+ }
60
+ }
61
+ if (button) {
62
+ const handle = await button.elementHandle().catch(() => null);
63
+ if (handle) {
64
+ const deadline = Date.now() + 2e3;
65
+ while (Date.now() < deadline) {
66
+ const disabled = await handle.evaluate((el) => el.disabled).catch(() => false);
67
+ if (!disabled) break;
68
+ await new Promise((r) => setTimeout(r, 100));
69
+ }
70
+ }
71
+ await Promise.all([
72
+ page.waitForLoadState("networkidle").catch(() => {
73
+ }),
74
+ button.click({ timeout: 5e3 }).catch(() => {
75
+ })
76
+ // page may navigate before click completes — ignore
77
+ ]);
78
+ return true;
79
+ }
80
+ await page.keyboard.press("Enter");
81
+ await page.waitForLoadState("networkidle").catch(() => {
82
+ });
83
+ return false;
84
+ }
85
+ async function dismissCookieBanners(page) {
86
+ const acceptSel = [
87
+ 'button:has-text("Accept")',
88
+ 'button:has-text("Accept all")',
89
+ 'button:has-text("Allow all")',
90
+ 'button:has-text("I agree")',
91
+ 'button:has-text("Agree")',
92
+ 'button:has-text("OK")',
93
+ 'button:has-text("Got it")',
94
+ 'button:has-text("Close")',
95
+ '[aria-label*="Accept" i]',
96
+ '[aria-label*="Dismiss" i]',
97
+ '[class*="cookie"] button',
98
+ '[id*="cookie"] button',
99
+ '[class*="consent"] button',
100
+ '[class*="banner"] button[class*="close" i]'
101
+ ].join(", ");
102
+ const btn = await firstVisible(page, acceptSel);
103
+ if (btn) await btn.click().catch(() => {
104
+ });
105
+ await page.waitForTimeout(300).catch(() => {
106
+ });
107
+ }
108
+ async function navigateToLoginPage(page, appUrl) {
109
+ const origin = (() => {
110
+ try {
111
+ return new URL(appUrl).origin;
112
+ } catch {
113
+ return "";
114
+ }
115
+ })();
116
+ const loginPaths = ["/login", "/sign-in", "/signin", "/auth/login", "/auth", "/account/login", "/user/login", "/app/login"];
117
+ const navLoginSel = [
118
+ 'a:has-text("Sign in")',
119
+ 'a:has-text("Sign In")',
120
+ 'a:has-text("Log in")',
121
+ 'a:has-text("Log In")',
122
+ 'a:has-text("Login")',
123
+ 'a:has-text("Get started")',
124
+ 'a[href*="login" i]',
125
+ 'a[href*="sign-in" i]',
126
+ 'a[href*="signin" i]',
127
+ 'a[href*="auth" i]',
128
+ 'button:has-text("Sign in")',
129
+ 'button:has-text("Log in")',
130
+ 'button:has-text("Login")'
131
+ ].join(", ");
132
+ const navLink = await firstVisible(page, navLoginSel);
133
+ if (navLink) {
134
+ const href = await navLink.getAttribute("href").catch(() => null);
135
+ if (href && href.startsWith("http")) {
136
+ await page.goto(href, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
137
+ });
138
+ return true;
139
+ }
140
+ await navLink.click();
141
+ const emailAppeared = await page.waitForSelector(EMAIL_SELECTOR, { timeout: 3e3 }).then(() => true).catch(() => false);
142
+ if (!emailAppeared) {
143
+ await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => {
144
+ });
145
+ }
146
+ return true;
147
+ }
148
+ if (origin) {
149
+ for (const p of loginPaths) {
150
+ const loginUrl = `${origin}${p}`;
151
+ const res = await page.goto(loginUrl, { waitUntil: "networkidle", timeout: 1e4 }).catch(() => null);
152
+ if (res && res.status() < 400) {
153
+ const hasForm = await page.locator(
154
+ 'input[type="email"], input[type="password"], input[name*="email" i], input[name*="user" i]'
155
+ ).count().catch(() => 0) > 0;
156
+ if (hasForm) return true;
157
+ }
158
+ }
159
+ await page.goto(appUrl, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
160
+ });
161
+ }
162
+ return false;
163
+ }
164
+ function isOAuthProviderUrl(url) {
165
+ const u = url.toLowerCase();
166
+ if (u.includes("accounts.google.com")) return "Google";
167
+ if (u.includes("github.com/login")) return "GitHub";
168
+ if (u.includes("login.microsoftonline.com")) return "Microsoft";
169
+ if (u.includes("login.live.com")) return "Microsoft";
170
+ if (u.includes("appleid.apple.com")) return "Apple";
171
+ if (u.includes("okta.com")) return "Okta";
172
+ if (u.includes("auth0.com")) return "Auth0";
173
+ if (u.includes("onelogin.com")) return "OneLogin";
174
+ if (u.includes("pingidentity.com")) return "Ping Identity";
175
+ return null;
176
+ }
177
+ const EMAIL_SELECTOR = 'input[type="email"], input[name*="user" i], input[name*="email" i], input[autocomplete="username"]';
178
+ const PASS_SELECTOR = 'input[type="password"], input[autocomplete="current-password"]';
179
+ async function replayLoginRecipe(page, steps, creds) {
180
+ for (const step of steps) {
181
+ try {
182
+ if (step.action === "wait") {
183
+ await page.waitForTimeout(step.ms ?? 500).catch(() => {
184
+ });
185
+ } else if (step.action === "click") {
186
+ const el = await firstVisible(page, step.selector);
187
+ if (!el) return false;
188
+ await el.click({ timeout: 5e3 }).catch(() => {
189
+ });
190
+ } else if (step.action === "fill") {
191
+ const el = await firstVisible(page, step.selector);
192
+ if (!el) return false;
193
+ const val = (step.value ?? "").replace("{username}", creds.username).replace("{password}", creds.password);
194
+ await reactFill(el, val);
195
+ }
196
+ await page.waitForTimeout(200).catch(() => {
197
+ });
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
203
+ });
204
+ const postUrl = page.url();
205
+ const LOGIN_PATH_RE = /\/(login|signin|sign-in|auth|log-in|sso|oauth)(\/|$|\?)/i;
206
+ if (!LOGIN_PATH_RE.test(new URL(postUrl).pathname)) return true;
207
+ const passCount = await page.locator('input[type="password"]').count().catch(() => 0);
208
+ return passCount === 0;
209
+ }
210
+ async function login(page, appUrl, creds) {
211
+ await page.goto(appUrl, { waitUntil: "networkidle", timeout: 3e4 }).catch(() => {
212
+ });
213
+ if (creds.loginRecipe && creds.loginRecipe.length > 0) {
214
+ const recipeOk = await replayLoginRecipe(page, creds.loginRecipe, creds);
215
+ if (recipeOk) return true;
216
+ await page.goto(appUrl, { waitUntil: "networkidle", timeout: 3e4 }).catch(() => {
217
+ });
218
+ }
219
+ await dismissCookieBanners(page);
220
+ let userField = await firstVisible(page, EMAIL_SELECTOR);
221
+ if (!userField) {
222
+ await navigateToLoginPage(page, appUrl);
223
+ userField = await firstVisible(page, EMAIL_SELECTOR);
224
+ if (!userField) {
225
+ await page.waitForSelector(EMAIL_SELECTOR, { timeout: 5e3 }).catch(() => {
226
+ });
227
+ userField = await firstVisible(page, EMAIL_SELECTOR);
228
+ }
229
+ }
230
+ if (!userField) return false;
231
+ await reactFill(userField, creds.username);
232
+ await page.keyboard.press("Tab");
233
+ let passField = await firstVisible(page, PASS_SELECTOR);
234
+ if (!passField) {
235
+ await clickLikelySubmit(page);
236
+ await page.waitForSelector(PASS_SELECTOR, { timeout: 8e3 }).catch(() => {
237
+ });
238
+ const ssoProvider = isOAuthProviderUrl(page.url());
239
+ if (ssoProvider) {
240
+ throw Object.assign(
241
+ new Error(
242
+ `This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, then click the extension to capture your session.`
243
+ ),
244
+ { isLoginError: true, isSSOError: true, ssoProvider }
245
+ );
246
+ }
247
+ passField = await firstVisible(page, PASS_SELECTOR);
248
+ }
249
+ if (!passField) {
250
+ const ssoProvider = isOAuthProviderUrl(page.url());
251
+ if (ssoProvider) {
252
+ throw Object.assign(
253
+ new Error(
254
+ `This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, then click the extension to capture your session.`
255
+ ),
256
+ { isLoginError: true, isSSOError: true, ssoProvider }
257
+ );
258
+ }
259
+ return false;
260
+ }
261
+ await reactFill(passField, creds.password);
262
+ await page.waitForTimeout(150).catch(() => {
263
+ });
264
+ await Promise.all([
265
+ page.waitForLoadState("networkidle").catch(() => {
266
+ }),
267
+ page.keyboard.press("Enter")
268
+ ]);
269
+ const stillHasPass = await page.locator(PASS_SELECTOR).count().catch(() => 0);
270
+ if (stillHasPass > 0) {
271
+ await page.keyboard.press("Tab");
272
+ await clickLikelySubmit(page);
273
+ }
274
+ const LOGIN_PATH_RE_NAV = /\/(login|signin|sign-in|auth|log-in|sso|oauth)(\/|$|\?)/i;
275
+ if (LOGIN_PATH_RE_NAV.test(new URL(page.url()).pathname)) {
276
+ await page.waitForURL(
277
+ (url) => {
278
+ try {
279
+ return !LOGIN_PATH_RE_NAV.test(new URL(url).pathname);
280
+ } catch {
281
+ return true;
282
+ }
283
+ },
284
+ { timeout: 12e3 }
285
+ ).catch(() => {
286
+ });
287
+ }
288
+ const ssoAfterLogin = isOAuthProviderUrl(page.url());
289
+ if (ssoAfterLogin) {
290
+ throw Object.assign(
291
+ new Error(
292
+ `This app uses ${ssoAfterLogin} SSO \u2014 ZeTa cannot automate OAuth login. Use the ZeTa Capture Chrome Extension instead.`
293
+ ),
294
+ { isLoginError: true, isSSOError: true, ssoProvider: ssoAfterLogin }
295
+ );
296
+ }
297
+ const otpField = await firstVisible(page, [
298
+ 'input[autocomplete="one-time-code"]',
299
+ 'input[name*="otp" i]',
300
+ 'input[name*="code" i]',
301
+ 'input[aria-label*="code" i]',
302
+ 'input[placeholder*="code" i]'
303
+ ].join(", "));
304
+ if (otpField) {
305
+ const code = creds.oneTimeCode ?? await creds.getOneTimeCode?.();
306
+ if (!code) return false;
307
+ await reactFill(otpField, code);
308
+ await clickLikelySubmit(page);
309
+ }
310
+ const postLoginUrl = page.url();
311
+ const LOGIN_PATH_RE = /\/(login|signin|sign-in|auth|log-in|sso|oauth)(\/|$|\?)/i;
312
+ const stillOnLoginPath = LOGIN_PATH_RE.test(new URL(postLoginUrl).pathname);
313
+ const stillOnLogin = await page.locator(PASS_SELECTOR).count();
314
+ if (stillOnLoginPath && stillOnLogin > 0) {
315
+ const errorLoc = page.locator('[role="alert"], .error, [class*="error"], [class*="alert"]');
316
+ await errorLoc.first().waitFor({ state: "visible", timeout: 2e3 }).catch(() => {
317
+ });
318
+ const pageError = await errorLoc.first().textContent().catch(() => null);
319
+ if (pageError?.trim()) {
320
+ throw Object.assign(new Error(pageError.trim()), { isLoginError: true });
321
+ }
322
+ return false;
323
+ }
324
+ return true;
325
+ }
326
+ export {
327
+ login
328
+ };
329
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/auth.ts"],
4
+ "sourcesContent": ["import type { Page } from 'playwright';\n\nexport interface Credentials {\n username: string;\n password: string;\n oneTimeCode?: string;\n getOneTimeCode?: () => Promise<string>;\n}\n\n/**\n * Recorded login step captured by the Chrome extension.\n * {username} and {password} in `value` are substituted at replay time.\n */\nexport interface LoginStep {\n action: 'fill' | 'click' | 'wait';\n selector: string;\n value?: string;\n ms?: number;\n}\n\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function firstVisible(page: Page, selector: string) {\n const loc = page.locator(selector);\n const count = await loc.count().catch(() => 0);\n for (let i = 0; i < count; i += 1) {\n const item = loc.nth(i);\n if (await item.isVisible().catch(() => false)) return item;\n }\n // iframe fallback \u2014 Salesforce, Zendesk, and embedded login widgets hide forms in iframes\n for (const frame of page.frames()) {\n if (frame === page.mainFrame()) continue;\n const floc = frame.locator(selector);\n const fcount = await floc.count().catch(() => 0);\n for (let i = 0; i < fcount; i += 1) {\n const item = floc.nth(i);\n if (await item.isVisible().catch(() => false)) return item;\n }\n }\n return null;\n}\n\n/** Type into framework-controlled inputs \u2014 handles React, Angular, Vue, and plain HTML forms. */\nasync function reactFill(field: Awaited<ReturnType<typeof firstVisible>>, value: string) {\n if (!field) return;\n await field.click();\n await field.selectText().catch(() => {});\n await field.press('Control+a').catch(() => {});\n await field.press('Backspace').catch(() => {});\n await field.pressSequentially(value, { delay: 30 });\n // Fire native input + change events for Angular/Vue/custom frameworks that ignore DOM keypresses.\n // React's synthetic listener catches pressSequentially above; this covers the rest.\n await field.evaluate((el, v) => {\n const input = el as HTMLInputElement;\n const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;\n if (nativeInputValueSetter) nativeInputValueSetter.call(input, v);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n input.dispatchEvent(new Event('change', { bubbles: true }));\n input.dispatchEvent(new Event('blur', { bubbles: true }));\n }, value).catch(() => {});\n}\n\n/**\n * Find and click the most likely submit button.\n * Waits up to 2 s for React's disabled={!email||!password} to clear before clicking.\n */\nasync function clickLikelySubmit(page: Page) {\n // Prefer explicit submit selectors first; \"Continue\" is last resort to avoid\n // accidentally clicking \"Continue with Google/GitHub/Apple\" SSO buttons.\n let button = await firstVisible(page, [\n 'button[type=\"submit\"]',\n 'input[type=\"submit\"]',\n 'button:has-text(\"Next\")',\n 'button:has-text(\"Sign in\")',\n 'button:has-text(\"Log in\")',\n 'button:has-text(\"Login\")',\n 'button:has-text(\"Sign up\")',\n ].join(', '));\n\n if (!button) {\n // Find \"Continue\" buttons but exclude OAuth provider ones (\"Continue with Google\" etc.)\n const candidates = page.locator('button:has-text(\"Continue\")');\n const count = await candidates.count().catch(() => 0);\n for (let i = 0; i < count; i++) {\n const c = candidates.nth(i);\n if (!(await c.isVisible().catch(() => false))) continue;\n const text = (await c.textContent().catch(() => '')) ?? '';\n if (/continue\\s+with\\s+/i.test(text)) continue; // skip \"Continue with Google/GitHub/Apple\"\n button = c;\n break;\n }\n }\n if (button) {\n // Poll for enabled state \u2014 waitForFunction not available on Stagehand V3 proxy\n const handle = await button.elementHandle().catch(() => null);\n if (handle) {\n const deadline = Date.now() + 2_000;\n while (Date.now() < deadline) {\n const disabled = await handle.evaluate((el) => (el as HTMLButtonElement).disabled).catch(() => false);\n if (!disabled) break;\n await new Promise(r => setTimeout(r, 100));\n }\n }\n await Promise.all([\n page.waitForLoadState('networkidle').catch(() => {}),\n button.click({ timeout: 5_000 }).catch(() => {}), // page may navigate before click completes \u2014 ignore\n ]);\n return true;\n }\n await page.keyboard.press('Enter');\n await page.waitForLoadState('networkidle').catch(() => {});\n return false;\n}\n\n/** Dismiss cookie/GDPR consent banners so they don't block form fields. */\nasync function dismissCookieBanners(page: Page) {\n const acceptSel = [\n 'button:has-text(\"Accept\")',\n 'button:has-text(\"Accept all\")',\n 'button:has-text(\"Allow all\")',\n 'button:has-text(\"I agree\")',\n 'button:has-text(\"Agree\")',\n 'button:has-text(\"OK\")',\n 'button:has-text(\"Got it\")',\n 'button:has-text(\"Close\")',\n '[aria-label*=\"Accept\" i]',\n '[aria-label*=\"Dismiss\" i]',\n '[class*=\"cookie\"] button',\n '[id*=\"cookie\"] button',\n '[class*=\"consent\"] button',\n '[class*=\"banner\"] button[class*=\"close\" i]',\n ].join(', ');\n const btn = await firstVisible(page, acceptSel);\n if (btn) await btn.click().catch(() => {});\n await page.waitForTimeout(300).catch(() => {});\n}\n\n/**\n * Look for a Sign In / Log In nav link on a marketing homepage and navigate to the actual login page.\n * Returns true if navigation happened.\n */\nasync function navigateToLoginPage(page: Page, appUrl: string): Promise<boolean> {\n // First try common login URL suffixes\n const origin = (() => { try { return new URL(appUrl).origin; } catch { return ''; } })();\n const loginPaths = ['/login', '/sign-in', '/signin', '/auth/login', '/auth', '/account/login', '/user/login', '/app/login'];\n\n // Look for a \"Sign In\" / \"Login\" / \"Get started\" link in the nav\n const navLoginSel = [\n 'a:has-text(\"Sign in\")',\n 'a:has-text(\"Sign In\")',\n 'a:has-text(\"Log in\")',\n 'a:has-text(\"Log In\")',\n 'a:has-text(\"Login\")',\n 'a:has-text(\"Get started\")',\n 'a[href*=\"login\" i]',\n 'a[href*=\"sign-in\" i]',\n 'a[href*=\"signin\" i]',\n 'a[href*=\"auth\" i]',\n 'button:has-text(\"Sign in\")',\n 'button:has-text(\"Log in\")',\n 'button:has-text(\"Login\")',\n ].join(', ');\n\n const navLink = await firstVisible(page, navLoginSel);\n if (navLink) {\n const href = await navLink.getAttribute('href').catch(() => null);\n // If the link has an href, navigate directly \u2014 avoids modal-only flows\n if (href && href.startsWith('http')) {\n await page.goto(href, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => {});\n return true;\n }\n // Click and immediately check for email field (modal opened) OR wait for navigation\n await navLink.click();\n const emailAppeared = await page.waitForSelector(EMAIL_SELECTOR, { timeout: 3_000 })\n .then(() => true).catch(() => false);\n if (!emailAppeared) {\n // No modal \u2014 likely navigating to a new page\n await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});\n }\n return true;\n }\n\n // Fallback: try common login paths on the same origin\n if (origin) {\n for (const p of loginPaths) {\n const loginUrl = `${origin}${p}`;\n const res = await page.goto(loginUrl, { waitUntil: 'networkidle', timeout: 10_000 }).catch(() => null);\n if (res && res.status() < 400) {\n // Check if this page actually has a login form\n const hasForm = (await page.locator(\n 'input[type=\"email\"], input[type=\"password\"], input[name*=\"email\" i], input[name*=\"user\" i]'\n ).count().catch(() => 0)) > 0;\n if (hasForm) return true;\n }\n }\n // Restore original page\n await page.goto(appUrl, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => {});\n }\n\n return false;\n}\n\n/** Detect if current page redirected to a 3rd-party OAuth provider (SSO flow we can't automate). */\nfunction isOAuthProviderUrl(url: string): string | null {\n const u = url.toLowerCase();\n if (u.includes('accounts.google.com')) return 'Google';\n if (u.includes('github.com/login')) return 'GitHub';\n if (u.includes('login.microsoftonline.com')) return 'Microsoft';\n if (u.includes('login.live.com')) return 'Microsoft';\n if (u.includes('appleid.apple.com')) return 'Apple';\n if (u.includes('okta.com')) return 'Okta';\n if (u.includes('auth0.com')) return 'Auth0';\n if (u.includes('onelogin.com')) return 'OneLogin';\n if (u.includes('pingidentity.com')) return 'Ping Identity';\n return null;\n}\n\nconst EMAIL_SELECTOR = 'input[type=\"email\"], input[name*=\"user\" i], input[name*=\"email\" i], input[autocomplete=\"username\"]';\nconst PASS_SELECTOR = 'input[type=\"password\"], input[autocomplete=\"current-password\"]';\n\n/** Replay recorded login steps from the Chrome extension. Returns true if login succeeded. */\nasync function replayLoginRecipe(page: Page, steps: LoginStep[], creds: { username: string; password: string }): Promise<boolean> {\n for (const step of steps) {\n try {\n if (step.action === 'wait') {\n await page.waitForTimeout(step.ms ?? 500).catch(() => {});\n } else if (step.action === 'click') {\n const el = await firstVisible(page, step.selector);\n if (!el) return false;\n await el.click({ timeout: 5_000 }).catch(() => {});\n } else if (step.action === 'fill') {\n const el = await firstVisible(page, step.selector);\n if (!el) return false;\n const val = (step.value ?? '')\n .replace('{username}', creds.username)\n .replace('{password}', creds.password);\n await reactFill(el, val);\n }\n await page.waitForTimeout(200).catch(() => {});\n } catch { return false; }\n }\n await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {});\n const postUrl = page.url();\n const LOGIN_PATH_RE = /\\/(login|signin|sign-in|auth|log-in|sso|oauth)(\\/|$|\\?)/i;\n if (!LOGIN_PATH_RE.test(new URL(postUrl).pathname)) return true; // navigated away \u2014 success\n const passCount = await page.locator('input[type=\"password\"]').count().catch(() => 0);\n return passCount === 0;\n}\n\n// \u2500\u2500\u2500 Main login function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport async function login(page: Page, appUrl: string, creds: Credentials & { loginRecipe?: LoginStep[] }): Promise<boolean> {\n await page.goto(appUrl, { waitUntil: 'networkidle', timeout: 30_000 }).catch(() => {});\n\n // Recipe replay \u2014 try exact recorded steps first when available\n if (creds.loginRecipe && creds.loginRecipe.length > 0) {\n const recipeOk = await replayLoginRecipe(page, creds.loginRecipe, creds);\n if (recipeOk) return true;\n // Recipe failed \u2014 reload page and fall through to DOM heuristic\n await page.goto(appUrl, { waitUntil: 'networkidle', timeout: 30_000 }).catch(() => {});\n }\n\n // Dismiss cookie banners that might block form interaction\n await dismissCookieBanners(page);\n\n let userField = await firstVisible(page, EMAIL_SELECTOR);\n\n // No email field on the landing page \u2014 this is a marketing homepage.\n // Try to navigate to the actual login page.\n if (!userField) {\n await navigateToLoginPage(page, appUrl);\n // NOTE: do NOT call dismissCookieBanners here \u2014 it can close login modals\n // navigateToLoginPage already waits for email field or networkidle\n userField = await firstVisible(page, EMAIL_SELECTOR);\n if (!userField) {\n // Last attempt: wait up to 5s for form to appear (SPA redirect or slow modal)\n await page.waitForSelector(EMAIL_SELECTOR, { timeout: 5_000 }).catch(() => {});\n userField = await firstVisible(page, EMAIL_SELECTOR);\n }\n }\n\n if (!userField) return false;\n\n await reactFill(userField, creds.username);\n await page.keyboard.press('Tab');\n\n let passField = await firstVisible(page, PASS_SELECTOR);\n if (!passField) {\n // Multi-step form: email-first, then password on next screen\n await clickLikelySubmit(page);\n // Wait for password field to appear (up to 8s) rather than a fixed 2s sleep \u2014\n // slow IdP servers (Okta, Azure) take 3-5s to validate email and render the next step.\n await page.waitForSelector(PASS_SELECTOR, { timeout: 8_000 }).catch(() => {});\n // Check for SSO redirect after email submission\n const ssoProvider = isOAuthProviderUrl(page.url());\n if (ssoProvider) {\n throw Object.assign(\n new Error(\n `This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. ` +\n `Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, ` +\n `then click the extension to capture your session.`\n ),\n { isLoginError: true, isSSOError: true, ssoProvider }\n );\n }\n passField = await firstVisible(page, PASS_SELECTOR);\n }\n\n if (!passField) {\n // Still no password field \u2014 check if we landed on an SSO provider\n const ssoProvider = isOAuthProviderUrl(page.url());\n if (ssoProvider) {\n throw Object.assign(\n new Error(\n `This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. ` +\n `Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, ` +\n `then click the extension to capture your session.`\n ),\n { isLoginError: true, isSSOError: true, ssoProvider }\n );\n }\n return false;\n }\n\n await reactFill(passField, creds.password);\n // Press Enter \u2014 submits regardless of button disabled state\n await page.waitForTimeout(150).catch(() => {});\n await Promise.all([\n page.waitForLoadState('networkidle').catch(() => {}),\n page.keyboard.press('Enter'),\n ]);\n\n // Fallback: if Enter didn't navigate away, try Tab \u2192 click\n const stillHasPass = await page.locator(PASS_SELECTOR).count().catch(() => 0);\n if (stillHasPass > 0) {\n await page.keyboard.press('Tab');\n await clickLikelySubmit(page);\n }\n\n // Wait for post-submit navigation to complete \u2014 waitForLoadState resolves immediately\n // if page is already idle, so use waitForURL to detect actual navigation away from login.\n const LOGIN_PATH_RE_NAV = /\\/(login|signin|sign-in|auth|log-in|sso|oauth)(\\/|$|\\?)/i;\n if (LOGIN_PATH_RE_NAV.test(new URL(page.url()).pathname)) {\n await page.waitForURL(\n (url) => { try { return !LOGIN_PATH_RE_NAV.test(new URL(url).pathname); } catch { return true; } },\n { timeout: 12_000 }\n ).catch(() => {});\n }\n\n // Check for SSO redirect after password submit\n const ssoAfterLogin = isOAuthProviderUrl(page.url());\n if (ssoAfterLogin) {\n throw Object.assign(\n new Error(\n `This app uses ${ssoAfterLogin} SSO \u2014 ZeTa cannot automate OAuth login. ` +\n `Use the ZeTa Capture Chrome Extension instead.`\n ),\n { isLoginError: true, isSSOError: true, ssoProvider: ssoAfterLogin }\n );\n }\n\n // Handle OTP / 2FA\n const otpField = await firstVisible(page, [\n 'input[autocomplete=\"one-time-code\"]',\n 'input[name*=\"otp\" i]',\n 'input[name*=\"code\" i]',\n 'input[aria-label*=\"code\" i]',\n 'input[placeholder*=\"code\" i]',\n ].join(', '));\n if (otpField) {\n const code = creds.oneTimeCode ?? await creds.getOneTimeCode?.();\n if (!code) return false;\n await reactFill(otpField, code);\n await clickLikelySubmit(page);\n }\n\n // Success check: navigated away from login page, and/or no password field remains.\n // Use URL as primary signal \u2014 dashboards can have hidden password fields (profile forms, etc.)\n // that would cause false negatives if we only check input[type=\"password\"].\n const postLoginUrl = page.url();\n const LOGIN_PATH_RE = /\\/(login|signin|sign-in|auth|log-in|sso|oauth)(\\/|$|\\?)/i;\n const stillOnLoginPath = LOGIN_PATH_RE.test(new URL(postLoginUrl).pathname);\n const stillOnLogin = await page.locator(PASS_SELECTOR).count();\n\n // Only treat as failure if BOTH: still on a login-path URL AND password field present\n if (stillOnLoginPath && stillOnLogin > 0) {\n const errorLoc = page.locator('[role=\"alert\"], .error, [class*=\"error\"], [class*=\"alert\"]');\n await errorLoc.first().waitFor({ state: 'visible', timeout: 2_000 }).catch(() => {});\n const pageError = await errorLoc.first().textContent().catch(() => null);\n if (pageError?.trim()) {\n throw Object.assign(new Error(pageError.trim()), { isLoginError: true });\n }\n return false;\n }\n return true;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Browser extension injection support.
3
+ *
4
+ * Chrome extensions require a persistent profile directory — incompatible
5
+ * with headless Docker by default. This module handles:
6
+ * 1. Persistent profile directory per extension set
7
+ * 2. Extension download from .crx URL or load from local path
8
+ * 3. Launch args for Playwright / Stagehand
9
+ *
10
+ * Usage:
11
+ * const profile = await prepareExtensionProfile(['/path/to/ext.crx']);
12
+ * // pass profile.userDataDir and profile.args to browser launch
13
+ */
14
+ export interface ExtensionSource {
15
+ /** Local .crx file path or https:// URL to download from */
16
+ source: string;
17
+ /** Friendly name for logging */
18
+ name?: string;
19
+ }
20
+ export interface ExtensionProfile {
21
+ /** Chrome user-data-dir containing Preferences + extension unpacked dirs */
22
+ userDataDir: string;
23
+ /** Args to append to chromium launch — includes --load-extension and --disable-extensions-except */
24
+ args: string[];
25
+ /** Cleanup the profile directory when done */
26
+ cleanup: () => Promise<void>;
27
+ }
28
+ /**
29
+ * Prepare a Chrome user-data-dir with the given extensions loaded.
30
+ * Returns profile info + cleanup function.
31
+ */
32
+ export declare function prepareExtensionProfile(extensions: ExtensionSource[]): Promise<ExtensionProfile>;
33
+ /**
34
+ * Ad-blocker extension profile using the uBlock Origin CRX.
35
+ * Reduces noise from ad networks during crawls.
36
+ */
37
+ export declare function adBlockerProfile(): Promise<ExtensionProfile>;
@@ -0,0 +1,113 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createWriteStream, existsSync, mkdirSync } from "node:fs";
3
+ import { writeFile, mkdir, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { Readable } from "node:stream";
8
+ const PROFILE_BASE = process.env.EXTENSION_PROFILE_DIR ?? join(tmpdir(), "zeta-ext-profiles");
9
+ async function downloadCrx(url) {
10
+ const dest = join(tmpdir(), `zeta-ext-${randomBytes(6).toString("hex")}.crx`);
11
+ const res = await fetch(url);
12
+ if (!res.ok) throw new Error(`Failed to download extension from ${url}: ${res.status}`);
13
+ const writer = createWriteStream(dest);
14
+ await pipeline(Readable.fromWeb(res.body), writer);
15
+ return dest;
16
+ }
17
+ async function unpackCrx(crxPath, destDir) {
18
+ const { readFile } = await import("node:fs/promises");
19
+ const buf = await readFile(crxPath);
20
+ const magic = buf.readUInt32LE(0);
21
+ if (magic !== 1131557428) {
22
+ await mkdir(destDir, { recursive: true });
23
+ await writeFile(join(destDir, "__raw.crx"), buf);
24
+ return;
25
+ }
26
+ const version = buf.readUInt32LE(4);
27
+ let zipStart;
28
+ if (version === 3) {
29
+ const headerSize = buf.readUInt32LE(8);
30
+ zipStart = 12 + headerSize;
31
+ } else if (version === 2) {
32
+ const pubKeyLen = buf.readUInt32LE(8);
33
+ const sigLen = buf.readUInt32LE(12);
34
+ zipStart = 16 + pubKeyLen + sigLen;
35
+ } else {
36
+ throw new Error(`Unknown CRX version ${version}`);
37
+ }
38
+ const zipBuf = buf.subarray(zipStart);
39
+ const tmpZip = join(tmpdir(), `zeta-ext-${randomBytes(4).toString("hex")}.zip`);
40
+ await writeFile(tmpZip, zipBuf);
41
+ await mkdir(destDir, { recursive: true });
42
+ const unzipper = await import("unzipper").catch(() => null);
43
+ if (unzipper) {
44
+ await unzipper.Open.file(tmpZip).then((d) => d.extract({ path: destDir }));
45
+ } else {
46
+ const { execFile } = await import("node:child_process");
47
+ await new Promise(
48
+ (res, rej) => execFile("unzip", ["-o", tmpZip, "-d", destDir], (err) => err ? rej(err) : res())
49
+ );
50
+ }
51
+ await rm(tmpZip, { force: true });
52
+ }
53
+ async function prepareExtensionProfile(extensions) {
54
+ const key = createHash("md5").update(extensions.map((e) => e.source).sort().join("|")).digest("hex").slice(0, 12);
55
+ const profileDir = join(PROFILE_BASE, key);
56
+ const extensionDirs = [];
57
+ mkdirSync(profileDir, { recursive: true });
58
+ for (const ext of extensions) {
59
+ const extHash = createHash("md5").update(ext.source).digest("hex").slice(0, 8);
60
+ const extDir = join(profileDir, `ext-${extHash}`);
61
+ if (!existsSync(extDir)) {
62
+ let crxPath = ext.source;
63
+ if (ext.source.startsWith("http://") || ext.source.startsWith("https://")) {
64
+ crxPath = await downloadCrx(ext.source);
65
+ await unpackCrx(crxPath, extDir);
66
+ await rm(crxPath, { force: true });
67
+ } else if (ext.source.endsWith(".crx")) {
68
+ await unpackCrx(ext.source, extDir);
69
+ } else {
70
+ extensionDirs.push(ext.source);
71
+ continue;
72
+ }
73
+ }
74
+ extensionDirs.push(extDir);
75
+ }
76
+ const userDataDir = join(profileDir, "user-data");
77
+ mkdirSync(userDataDir, { recursive: true });
78
+ const defaultsDir = join(userDataDir, "Default");
79
+ mkdirSync(defaultsDir, { recursive: true });
80
+ const prefsPath = join(defaultsDir, "Preferences");
81
+ if (!existsSync(prefsPath)) {
82
+ await writeFile(prefsPath, JSON.stringify({
83
+ browser: { has_seen_welcome_page: true },
84
+ extensions: { alerts: { initialized: true } }
85
+ }), "utf8");
86
+ }
87
+ const loadExtensionArg = extensionDirs.join(",");
88
+ const args = [
89
+ `--load-extension=${loadExtensionArg}`,
90
+ `--disable-extensions-except=${loadExtensionArg}`,
91
+ `--user-data-dir=${userDataDir}`,
92
+ "--no-first-run",
93
+ "--no-default-browser-check"
94
+ ];
95
+ return {
96
+ userDataDir,
97
+ args,
98
+ cleanup: async () => {
99
+ await rm(profileDir, { recursive: true, force: true });
100
+ }
101
+ };
102
+ }
103
+ async function adBlockerProfile() {
104
+ return prepareExtensionProfile([{
105
+ name: "uBlock Origin",
106
+ source: "https://github.com/gorhill/uBlock/releases/download/1.57.2/uBlock0_1.57.2.chromium.zip"
107
+ }]);
108
+ }
109
+ export {
110
+ adBlockerProfile,
111
+ prepareExtensionProfile
112
+ };
113
+ //# sourceMappingURL=browser-extension.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/browser-extension.ts"],
4
+ "sourcesContent": ["/**\n * Browser extension injection support.\n *\n * Chrome extensions require a persistent profile directory \u2014 incompatible\n * with headless Docker by default. This module handles:\n * 1. Persistent profile directory per extension set\n * 2. Extension download from .crx URL or load from local path\n * 3. Launch args for Playwright / Stagehand\n *\n * Usage:\n * const profile = await prepareExtensionProfile(['/path/to/ext.crx']);\n * // pass profile.userDataDir and profile.args to browser launch\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport { createWriteStream, existsSync, mkdirSync } from 'node:fs';\nimport { writeFile, mkdir, rm } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { pipeline } from 'node:stream/promises';\nimport { Readable } from 'node:stream';\n\nconst PROFILE_BASE = process.env.EXTENSION_PROFILE_DIR ?? join(tmpdir(), 'zeta-ext-profiles');\n\nexport interface ExtensionSource {\n /** Local .crx file path or https:// URL to download from */\n source: string;\n /** Friendly name for logging */\n name?: string;\n}\n\nexport interface ExtensionProfile {\n /** Chrome user-data-dir containing Preferences + extension unpacked dirs */\n userDataDir: string;\n /** Args to append to chromium launch \u2014 includes --load-extension and --disable-extensions-except */\n args: string[];\n /** Cleanup the profile directory when done */\n cleanup: () => Promise<void>;\n}\n\n/**\n * Download a .crx from a URL to a temp file. Returns path.\n */\nasync function downloadCrx(url: string): Promise<string> {\n const dest = join(tmpdir(), `zeta-ext-${randomBytes(6).toString('hex')}.crx`);\n const res = await fetch(url);\n if (!res.ok) throw new Error(`Failed to download extension from ${url}: ${res.status}`);\n const writer = createWriteStream(dest);\n await pipeline(Readable.fromWeb(res.body as any), writer);\n return dest;\n}\n\n/**\n * Unpack a .crx file to a directory Chrome can load as an unpacked extension.\n * CRX3 format: 4-byte magic + 4-byte version + 4-byte header_size + proto header + zip body\n */\nasync function unpackCrx(crxPath: string, destDir: string): Promise<void> {\n const { readFile } = await import('node:fs/promises');\n const buf = await readFile(crxPath);\n\n const magic = buf.readUInt32LE(0);\n if (magic !== 0x43723234) {\n // Not CRX \u2014 assume it's already a zip (unpacked extension)\n await mkdir(destDir, { recursive: true });\n await writeFile(join(destDir, '__raw.crx'), buf);\n return;\n }\n\n const version = buf.readUInt32LE(4);\n let zipStart: number;\n if (version === 3) {\n const headerSize = buf.readUInt32LE(8);\n zipStart = 12 + headerSize;\n } else if (version === 2) {\n const pubKeyLen = buf.readUInt32LE(8);\n const sigLen = buf.readUInt32LE(12);\n zipStart = 16 + pubKeyLen + sigLen;\n } else {\n throw new Error(`Unknown CRX version ${version}`);\n }\n\n const zipBuf = buf.subarray(zipStart);\n const tmpZip = join(tmpdir(), `zeta-ext-${randomBytes(4).toString('hex')}.zip`);\n await writeFile(tmpZip, zipBuf);\n\n await mkdir(destDir, { recursive: true });\n\n // Extract using unzipper (available in Node without native deps)\n // @ts-ignore \u2013 no types for unzipper\n const unzipper = await import('unzipper').catch(() => null);\n if (unzipper) {\n await unzipper.Open.file(tmpZip).then((d: any) => d.extract({ path: destDir }));\n } else {\n // Fallback: shell unzip\n const { execFile } = await import('node:child_process');\n await new Promise<void>((res, rej) =>\n execFile('unzip', ['-o', tmpZip, '-d', destDir], (err) => err ? rej(err) : res())\n );\n }\n await rm(tmpZip, { force: true });\n}\n\n/**\n * Prepare a Chrome user-data-dir with the given extensions loaded.\n * Returns profile info + cleanup function.\n */\nexport async function prepareExtensionProfile(\n extensions: ExtensionSource[]\n): Promise<ExtensionProfile> {\n // Stable profile key based on extension sources\n const key = createHash('md5')\n .update(extensions.map(e => e.source).sort().join('|'))\n .digest('hex')\n .slice(0, 12);\n\n const profileDir = join(PROFILE_BASE, key);\n const extensionDirs: string[] = [];\n\n mkdirSync(profileDir, { recursive: true });\n\n for (const ext of extensions) {\n const extHash = createHash('md5').update(ext.source).digest('hex').slice(0, 8);\n const extDir = join(profileDir, `ext-${extHash}`);\n\n if (!existsSync(extDir)) {\n let crxPath = ext.source;\n\n if (ext.source.startsWith('http://') || ext.source.startsWith('https://')) {\n crxPath = await downloadCrx(ext.source);\n await unpackCrx(crxPath, extDir);\n await rm(crxPath, { force: true });\n } else if (ext.source.endsWith('.crx')) {\n await unpackCrx(ext.source, extDir);\n } else {\n // Assume already-unpacked extension directory \u2014 symlink or copy\n extensionDirs.push(ext.source);\n continue;\n }\n }\n\n extensionDirs.push(extDir);\n }\n\n const userDataDir = join(profileDir, 'user-data');\n mkdirSync(userDataDir, { recursive: true });\n\n // Write a minimal Preferences file so Chrome doesn't show first-run UI\n const defaultsDir = join(userDataDir, 'Default');\n mkdirSync(defaultsDir, { recursive: true });\n const prefsPath = join(defaultsDir, 'Preferences');\n if (!existsSync(prefsPath)) {\n await writeFile(prefsPath, JSON.stringify({\n browser: { has_seen_welcome_page: true },\n extensions: { alerts: { initialized: true } },\n }), 'utf8');\n }\n\n const loadExtensionArg = extensionDirs.join(',');\n const args: string[] = [\n `--load-extension=${loadExtensionArg}`,\n `--disable-extensions-except=${loadExtensionArg}`,\n `--user-data-dir=${userDataDir}`,\n '--no-first-run',\n '--no-default-browser-check',\n ];\n\n return {\n userDataDir,\n args,\n cleanup: async () => {\n await rm(profileDir, { recursive: true, force: true });\n },\n };\n}\n\n/**\n * Ad-blocker extension profile using the uBlock Origin CRX.\n * Reduces noise from ad networks during crawls.\n */\nexport async function adBlockerProfile(): Promise<ExtensionProfile> {\n // uBlock Origin 1.57.2 from GitHub release (MIT licensed)\n return prepareExtensionProfile([{\n name: 'uBlock Origin',\n source: 'https://github.com/gorhill/uBlock/releases/download/1.57.2/uBlock0_1.57.2.chromium.zip',\n }]);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * CAPTCHA auto-solving via 2captcha or CapSolver APIs.
3
+ * Supports reCAPTCHA v2, hCaptcha, and Cloudflare Turnstile.
4
+ */
5
+ export type CaptchaSolverProvider = '2captcha' | 'capsolver';
6
+ interface SolveResult {
7
+ token: string;
8
+ provider: CaptchaSolverProvider;
9
+ }
10
+ /**
11
+ * Detect CAPTCHA type and sitekey on the page, then solve it.
12
+ * Returns null if no CAPTCHA found or solver not configured.
13
+ * Throws on solve failure.
14
+ */
15
+ export declare function solveCaptcha(page: any, pageUrl: string, apiKey: string, provider?: CaptchaSolverProvider): Promise<SolveResult | null>;
16
+ export {};