@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,42 @@
1
+ /**
2
+ * Custom JS injector — run user-provided JavaScript in the context of each crawled page.
3
+ *
4
+ * Use cases:
5
+ * - Extract custom data points not captured by standard extractors
6
+ * - Set localStorage/sessionStorage values before crawl
7
+ * - Override fetch() to intercept API calls
8
+ * - Simulate user events (keydown, focus, etc.)
9
+ * - Bypass specific anti-bot checks
10
+ *
11
+ * Security: scripts run in the page context (sandboxed by Chromium's renderer).
12
+ * They cannot access the Node.js process or file system.
13
+ * ZeTa validates scripts are strings before injection.
14
+ */
15
+ import type { Page } from 'playwright';
16
+ export interface JsInjectionOpts {
17
+ /** Script to run on every page BEFORE navigation (via page.addInitScript) */
18
+ initScript?: string;
19
+ /** Script to run on every page AFTER navigation completes (via page.evaluate) */
20
+ postLoadScript?: string;
21
+ /** Key-value pairs to set in localStorage before page load */
22
+ localStorage?: Record<string, string>;
23
+ /** Key-value pairs to set in sessionStorage before page load */
24
+ sessionStorage?: Record<string, string>;
25
+ /** Cookie key-value pairs to add before page load */
26
+ cookies?: Array<{
27
+ name: string;
28
+ value: string;
29
+ domain?: string;
30
+ path?: string;
31
+ }>;
32
+ }
33
+ export interface JsInjectionResult {
34
+ initScriptApplied: boolean;
35
+ postLoadResult: unknown;
36
+ storageSet: boolean;
37
+ cookiesSet: number;
38
+ error?: string;
39
+ }
40
+ export declare function injectCustomJs(page: Page, opts: JsInjectionOpts): Promise<JsInjectionResult>;
41
+ /** Safe string validator — ensures script is a string, not an object/function reference */
42
+ export declare function validateScript(script: unknown): string;
@@ -0,0 +1,50 @@
1
+ async function injectCustomJs(page, opts) {
2
+ const result = {
3
+ initScriptApplied: false,
4
+ postLoadResult: null,
5
+ storageSet: false,
6
+ cookiesSet: 0
7
+ };
8
+ if (opts.cookies?.length) {
9
+ try {
10
+ await page.context().addCookies(
11
+ opts.cookies.map((c) => ({ name: c.name, value: c.value, domain: c.domain ?? "", path: c.path ?? "/", url: page.url() || void 0 }))
12
+ );
13
+ result.cookiesSet = opts.cookies.length;
14
+ } catch (e) {
15
+ result.error = `Cookie injection failed: ${e.message}`;
16
+ }
17
+ }
18
+ if (opts.initScript || opts.localStorage || opts.sessionStorage) {
19
+ const storageSetup = [
20
+ opts.localStorage ? `Object.entries(${JSON.stringify(opts.localStorage)}).forEach(([k,v])=>localStorage.setItem(k,v));` : "",
21
+ opts.sessionStorage ? `Object.entries(${JSON.stringify(opts.sessionStorage)}).forEach(([k,v])=>sessionStorage.setItem(k,v));` : ""
22
+ ].filter(Boolean).join("\n");
23
+ const combinedInit = [storageSetup, opts.initScript ?? ""].filter(Boolean).join("\n");
24
+ try {
25
+ await page.addInitScript(combinedInit);
26
+ result.initScriptApplied = true;
27
+ result.storageSet = !!(opts.localStorage || opts.sessionStorage);
28
+ } catch (e) {
29
+ result.error = `Init script failed: ${e.message}`;
30
+ }
31
+ }
32
+ if (opts.postLoadScript) {
33
+ try {
34
+ result.postLoadResult = await page.evaluate(opts.postLoadScript);
35
+ } catch (e) {
36
+ result.error = `Post-load script failed: ${e.message}`;
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ function validateScript(script) {
42
+ if (typeof script !== "string") throw new Error("Script must be a string");
43
+ if (script.length > 5e4) throw new Error("Script too large (>50KB)");
44
+ return script;
45
+ }
46
+ export {
47
+ injectCustomJs,
48
+ validateScript
49
+ };
50
+ //# sourceMappingURL=custom-js-injector.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/custom-js-injector.ts"],
4
+ "sourcesContent": ["/**\n * Custom JS injector \u2014 run user-provided JavaScript in the context of each crawled page.\n *\n * Use cases:\n * - Extract custom data points not captured by standard extractors\n * - Set localStorage/sessionStorage values before crawl\n * - Override fetch() to intercept API calls\n * - Simulate user events (keydown, focus, etc.)\n * - Bypass specific anti-bot checks\n *\n * Security: scripts run in the page context (sandboxed by Chromium's renderer).\n * They cannot access the Node.js process or file system.\n * ZeTa validates scripts are strings before injection.\n */\n\nimport type { Page } from 'playwright';\n\nexport interface JsInjectionOpts {\n /** Script to run on every page BEFORE navigation (via page.addInitScript) */\n initScript?: string;\n /** Script to run on every page AFTER navigation completes (via page.evaluate) */\n postLoadScript?: string;\n /** Key-value pairs to set in localStorage before page load */\n localStorage?: Record<string, string>;\n /** Key-value pairs to set in sessionStorage before page load */\n sessionStorage?: Record<string, string>;\n /** Cookie key-value pairs to add before page load */\n cookies?: Array<{ name: string; value: string; domain?: string; path?: string }>;\n}\n\nexport interface JsInjectionResult {\n initScriptApplied: boolean;\n postLoadResult: unknown;\n storageSet: boolean;\n cookiesSet: number;\n error?: string;\n}\n\nexport async function injectCustomJs(page: Page, opts: JsInjectionOpts): Promise<JsInjectionResult> {\n const result: JsInjectionResult = {\n initScriptApplied: false,\n postLoadResult: null,\n storageSet: false,\n cookiesSet: 0,\n };\n\n // Set cookies via Playwright context\n if (opts.cookies?.length) {\n try {\n await page.context().addCookies(\n opts.cookies.map(c => ({ name: c.name, value: c.value, domain: c.domain ?? '', path: c.path ?? '/', url: page.url() || undefined }))\n );\n result.cookiesSet = opts.cookies.length;\n } catch (e: any) {\n result.error = `Cookie injection failed: ${e.message}`;\n }\n }\n\n // Init script (runs before page load on every navigation)\n if (opts.initScript || opts.localStorage || opts.sessionStorage) {\n const storageSetup = [\n opts.localStorage ? `Object.entries(${JSON.stringify(opts.localStorage)}).forEach(([k,v])=>localStorage.setItem(k,v));` : '',\n opts.sessionStorage ? `Object.entries(${JSON.stringify(opts.sessionStorage)}).forEach(([k,v])=>sessionStorage.setItem(k,v));` : '',\n ].filter(Boolean).join('\\n');\n\n const combinedInit = [storageSetup, opts.initScript ?? ''].filter(Boolean).join('\\n');\n try {\n await page.addInitScript(combinedInit);\n result.initScriptApplied = true;\n result.storageSet = !!(opts.localStorage || opts.sessionStorage);\n } catch (e: any) {\n result.error = `Init script failed: ${e.message}`;\n }\n }\n\n // Post-load script (runs after navigation)\n if (opts.postLoadScript) {\n try {\n result.postLoadResult = await page.evaluate(opts.postLoadScript);\n } catch (e: any) {\n result.error = `Post-load script failed: ${e.message}`;\n }\n }\n\n return result;\n}\n\n/** Safe string validator \u2014 ensures script is a string, not an object/function reference */\nexport function validateScript(script: unknown): string {\n if (typeof script !== 'string') throw new Error('Script must be a string');\n if (script.length > 50_000) throw new Error('Script too large (>50KB)');\n return script;\n}\n"],
5
+ "mappings": "AAsCA,eAAsB,eAAe,MAAY,MAAmD;AAClG,QAAM,SAA4B;AAAA,IAChC,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAGA,MAAI,KAAK,SAAS,QAAQ;AACxB,QAAI;AACF,YAAM,KAAK,QAAQ,EAAE;AAAA,QACnB,KAAK,QAAQ,IAAI,QAAM,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,QAAQ,EAAE,UAAU,IAAI,MAAM,EAAE,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,OAAU,EAAE;AAAA,MACrI;AACA,aAAO,aAAa,KAAK,QAAQ;AAAA,IACnC,SAAS,GAAQ;AACf,aAAO,QAAQ,4BAA4B,EAAE,OAAO;AAAA,IACtD;AAAA,EACF;AAGA,MAAI,KAAK,cAAc,KAAK,gBAAgB,KAAK,gBAAgB;AAC/D,UAAM,eAAe;AAAA,MACnB,KAAK,eAAe,kBAAkB,KAAK,UAAU,KAAK,YAAY,CAAC,mDAAmD;AAAA,MAC1H,KAAK,iBAAiB,kBAAkB,KAAK,UAAU,KAAK,cAAc,CAAC,qDAAqD;AAAA,IAClI,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAE3B,UAAM,eAAe,CAAC,cAAc,KAAK,cAAc,EAAE,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AACpF,QAAI;AACF,YAAM,KAAK,cAAc,YAAY;AACrC,aAAO,oBAAoB;AAC3B,aAAO,aAAa,CAAC,EAAE,KAAK,gBAAgB,KAAK;AAAA,IACnD,SAAS,GAAQ;AACf,aAAO,QAAQ,uBAAuB,EAAE,OAAO;AAAA,IACjD;AAAA,EACF;AAGA,MAAI,KAAK,gBAAgB;AACvB,QAAI;AACF,aAAO,iBAAiB,MAAM,KAAK,SAAS,KAAK,cAAc;AAAA,IACjE,SAAS,GAAQ;AACf,aAAO,QAAQ,4BAA4B,EAAE,OAAO;AAAA,IACtD;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,eAAe,QAAyB;AACtD,MAAI,OAAO,WAAW,SAAU,OAAM,IAAI,MAAM,yBAAyB;AACzE,MAAI,OAAO,SAAS,IAAQ,OAAM,IAAI,MAAM,0BAA0B;AACtE,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Dark mode capture — screenshot pages in both light and dark color schemes.
3
+ *
4
+ * Uses Playwright's page.emulateMedia({ colorScheme: 'dark' }) to force dark mode,
5
+ * then captures a screenshot. Compares with light mode to determine if dark mode
6
+ * is supported (significant visual difference).
7
+ *
8
+ * Also detects prefers-color-scheme media query support in CSS.
9
+ */
10
+ import type { Page } from 'playwright';
11
+ export interface DarkModeCapture {
12
+ url: string;
13
+ lightScreenshot?: Buffer;
14
+ darkScreenshot?: Buffer;
15
+ hasDarkMode: boolean;
16
+ hasCssDarkMode: boolean;
17
+ backgroundColorLight?: string;
18
+ backgroundColorDark?: string;
19
+ capturedAt: string;
20
+ }
21
+ export declare function captureDarkMode(page: Page, url: string, opts?: {
22
+ takeScreenshots?: boolean;
23
+ }): Promise<DarkModeCapture>;
@@ -0,0 +1,75 @@
1
+ async function getBackgroundColor(page) {
2
+ try {
3
+ return await page.evaluate(() => {
4
+ const body = document.body;
5
+ return window.getComputedStyle(body).backgroundColor;
6
+ });
7
+ } catch {
8
+ return "unknown";
9
+ }
10
+ }
11
+ function colorsAreDifferent(a, b) {
12
+ if (a === b) return false;
13
+ if (a === "unknown" || b === "unknown") return false;
14
+ const parse = (c) => {
15
+ const m = c.match(/\d+/g);
16
+ return m ? m.map(Number) : [0, 0, 0];
17
+ };
18
+ const [ar, ag, ab] = parse(a);
19
+ const [br, bg, bb] = parse(b);
20
+ const lumA = 0.299 * ar + 0.587 * ag + 0.114 * ab;
21
+ const lumB = 0.299 * br + 0.587 * bg + 0.114 * bb;
22
+ return Math.abs(lumA - lumB) > 30;
23
+ }
24
+ async function hasCssDarkModeSupport(page) {
25
+ try {
26
+ return await page.evaluate(() => {
27
+ const sheets = Array.from(document.styleSheets);
28
+ for (const sheet of sheets) {
29
+ try {
30
+ const rules = Array.from(sheet.cssRules ?? []);
31
+ for (const rule of rules) {
32
+ if (rule instanceof CSSMediaRule && rule.conditionText?.includes("prefers-color-scheme")) {
33
+ return true;
34
+ }
35
+ }
36
+ } catch {
37
+ }
38
+ }
39
+ return false;
40
+ });
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ async function captureDarkMode(page, url, opts = {}) {
46
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
47
+ const { takeScreenshots = false } = opts;
48
+ await page.emulateMedia({ colorScheme: "light" }).catch(() => {
49
+ });
50
+ const backgroundColorLight = await getBackgroundColor(page);
51
+ const hasCssDark = await hasCssDarkModeSupport(page);
52
+ const lightScreenshot = takeScreenshots ? await page.screenshot({ type: "jpeg", quality: 60 }).catch(() => void 0) : void 0;
53
+ await page.emulateMedia({ colorScheme: "dark" }).catch(() => {
54
+ });
55
+ await page.waitForTimeout(300);
56
+ const backgroundColorDark = await getBackgroundColor(page);
57
+ const darkScreenshot = takeScreenshots ? await page.screenshot({ type: "jpeg", quality: 60 }).catch(() => void 0) : void 0;
58
+ await page.emulateMedia({ colorScheme: "light" }).catch(() => {
59
+ });
60
+ const hasDarkMode = colorsAreDifferent(backgroundColorLight, backgroundColorDark) || hasCssDark;
61
+ return {
62
+ url,
63
+ lightScreenshot,
64
+ darkScreenshot,
65
+ hasDarkMode,
66
+ hasCssDarkMode: hasCssDark,
67
+ backgroundColorLight,
68
+ backgroundColorDark,
69
+ capturedAt
70
+ };
71
+ }
72
+ export {
73
+ captureDarkMode
74
+ };
75
+ //# sourceMappingURL=dark-mode-capture.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/dark-mode-capture.ts"],
4
+ "sourcesContent": ["/**\n * Dark mode capture \u2014 screenshot pages in both light and dark color schemes.\n *\n * Uses Playwright's page.emulateMedia({ colorScheme: 'dark' }) to force dark mode,\n * then captures a screenshot. Compares with light mode to determine if dark mode\n * is supported (significant visual difference).\n *\n * Also detects prefers-color-scheme media query support in CSS.\n */\n\nimport type { Page } from 'playwright';\n\nexport interface DarkModeCapture {\n url: string;\n lightScreenshot?: Buffer;\n darkScreenshot?: Buffer;\n hasDarkMode: boolean;\n hasCssDarkMode: boolean; // @media (prefers-color-scheme: dark) detected in stylesheets\n backgroundColorLight?: string;\n backgroundColorDark?: string;\n capturedAt: string;\n}\n\nasync function getBackgroundColor(page: Page): Promise<string> {\n try {\n return await page.evaluate(() => {\n const body = document.body;\n return window.getComputedStyle(body).backgroundColor;\n });\n } catch {\n return 'unknown';\n }\n}\n\nfunction colorsAreDifferent(a: string, b: string): boolean {\n if (a === b) return false;\n if (a === 'unknown' || b === 'unknown') return false;\n // Parse rgb(r, g, b) and compare luminance\n const parse = (c: string) => {\n const m = c.match(/\\d+/g);\n return m ? m.map(Number) : [0, 0, 0];\n };\n const [ar, ag, ab] = parse(a);\n const [br, bg, bb] = parse(b);\n const lumA = 0.299 * ar + 0.587 * ag + 0.114 * ab;\n const lumB = 0.299 * br + 0.587 * bg + 0.114 * bb;\n return Math.abs(lumA - lumB) > 30; // significant luminance difference\n}\n\nasync function hasCssDarkModeSupport(page: Page): Promise<boolean> {\n try {\n return await page.evaluate(() => {\n const sheets = Array.from(document.styleSheets);\n for (const sheet of sheets) {\n try {\n const rules = Array.from(sheet.cssRules ?? []);\n for (const rule of rules) {\n if (rule instanceof CSSMediaRule && rule.conditionText?.includes('prefers-color-scheme')) {\n return true;\n }\n }\n } catch { /* cross-origin stylesheet \u2014 skip */ }\n }\n return false;\n });\n } catch {\n return false;\n }\n}\n\nexport async function captureDarkMode(\n page: Page,\n url: string,\n opts: { takeScreenshots?: boolean } = {},\n): Promise<DarkModeCapture> {\n const capturedAt = new Date().toISOString();\n const { takeScreenshots = false } = opts;\n\n // Light mode\n await page.emulateMedia({ colorScheme: 'light' }).catch(() => {});\n const backgroundColorLight = await getBackgroundColor(page);\n const hasCssDark = await hasCssDarkModeSupport(page);\n const lightScreenshot = takeScreenshots\n ? await page.screenshot({ type: 'jpeg', quality: 60 }).catch(() => undefined)\n : undefined;\n\n // Dark mode\n await page.emulateMedia({ colorScheme: 'dark' }).catch(() => {});\n await page.waitForTimeout(300); // allow CSS transitions\n const backgroundColorDark = await getBackgroundColor(page);\n const darkScreenshot = takeScreenshots\n ? await page.screenshot({ type: 'jpeg', quality: 60 }).catch(() => undefined)\n : undefined;\n\n // Restore light mode\n await page.emulateMedia({ colorScheme: 'light' }).catch(() => {});\n\n const hasDarkMode = colorsAreDifferent(backgroundColorLight, backgroundColorDark) || hasCssDark;\n\n return {\n url,\n lightScreenshot,\n darkScreenshot,\n hasDarkMode,\n hasCssDarkMode: hasCssDark,\n backgroundColorLight,\n backgroundColorDark,\n capturedAt,\n };\n}\n"],
5
+ "mappings": "AAuBA,eAAe,mBAAmB,MAA6B;AAC7D,MAAI;AACF,WAAO,MAAM,KAAK,SAAS,MAAM;AAC/B,YAAM,OAAO,SAAS;AACtB,aAAO,OAAO,iBAAiB,IAAI,EAAE;AAAA,IACvC,CAAC;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,mBAAmB,GAAW,GAAoB;AACzD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,MAAM,aAAa,MAAM,UAAW,QAAO;AAE/C,QAAM,QAAQ,CAAC,MAAc;AAC3B,UAAM,IAAI,EAAE,MAAM,MAAM;AACxB,WAAO,IAAI,EAAE,IAAI,MAAM,IAAI,CAAC,GAAG,GAAG,CAAC;AAAA,EACrC;AACA,QAAM,CAAC,IAAI,IAAI,EAAE,IAAI,MAAM,CAAC;AAC5B,QAAM,CAAC,IAAI,IAAI,EAAE,IAAI,MAAM,CAAC;AAC5B,QAAM,OAAO,QAAQ,KAAK,QAAQ,KAAK,QAAQ;AAC/C,QAAM,OAAO,QAAQ,KAAK,QAAQ,KAAK,QAAQ;AAC/C,SAAO,KAAK,IAAI,OAAO,IAAI,IAAI;AACjC;AAEA,eAAe,sBAAsB,MAA8B;AACjE,MAAI;AACF,WAAO,MAAM,KAAK,SAAS,MAAM;AAC/B,YAAM,SAAS,MAAM,KAAK,SAAS,WAAW;AAC9C,iBAAW,SAAS,QAAQ;AAC1B,YAAI;AACF,gBAAM,QAAQ,MAAM,KAAK,MAAM,YAAY,CAAC,CAAC;AAC7C,qBAAW,QAAQ,OAAO;AACxB,gBAAI,gBAAgB,gBAAgB,KAAK,eAAe,SAAS,sBAAsB,GAAG;AACxF,qBAAO;AAAA,YACT;AAAA,UACF;AAAA,QACF,QAAQ;AAAA,QAAuC;AAAA,MACjD;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,gBACpB,MACA,KACA,OAAsC,CAAC,GACb;AAC1B,QAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAC1C,QAAM,EAAE,kBAAkB,MAAM,IAAI;AAGpC,QAAM,KAAK,aAAa,EAAE,aAAa,QAAQ,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AAChE,QAAM,uBAAuB,MAAM,mBAAmB,IAAI;AAC1D,QAAM,aAAa,MAAM,sBAAsB,IAAI;AACnD,QAAM,kBAAkB,kBACpB,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,GAAG,CAAC,EAAE,MAAM,MAAM,MAAS,IAC1E;AAGJ,QAAM,KAAK,aAAa,EAAE,aAAa,OAAO,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AAC/D,QAAM,KAAK,eAAe,GAAG;AAC7B,QAAM,sBAAsB,MAAM,mBAAmB,IAAI;AACzD,QAAM,iBAAiB,kBACnB,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,GAAG,CAAC,EAAE,MAAM,MAAM,MAAS,IAC1E;AAGJ,QAAM,KAAK,aAAa,EAAE,aAAa,QAAQ,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AAEhE,QAAM,cAAc,mBAAmB,sBAAsB,mBAAmB,KAAK;AAErF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Dead link detector — tracks broken links discovered during crawl.
3
+ *
4
+ * A "dead link" is a URL that:
5
+ * - Returns 404, 410, or other 4xx/5xx responses
6
+ * - Times out after maxRetries
7
+ * - Redirects to an error page (detected by title heuristic)
8
+ *
9
+ * Results: list of broken links with source page, status code, and anchor text.
10
+ * Used for: SEO audit export, test report enrichment.
11
+ */
12
+ export interface DeadLink {
13
+ url: string;
14
+ statusCode: number;
15
+ sourceUrl?: string;
16
+ anchorText?: string;
17
+ checkedAt: string;
18
+ reason: 'http-error' | 'timeout' | 'dns-error';
19
+ }
20
+ export declare class DeadLinkTracker {
21
+ private deadLinks;
22
+ private checked;
23
+ record(url: string, statusCode: number, source?: {
24
+ sourceUrl?: string;
25
+ anchorText?: string;
26
+ }): void;
27
+ recordTimeout(url: string, sourceUrl?: string): void;
28
+ getDeadLinks(): DeadLink[];
29
+ isKnownBroken(url: string): boolean;
30
+ stats(): {
31
+ total: number;
32
+ by404: number;
33
+ by5xx: number;
34
+ byTimeout: number;
35
+ };
36
+ toCsv(): string;
37
+ }
@@ -0,0 +1,50 @@
1
+ class DeadLinkTracker {
2
+ deadLinks = [];
3
+ checked = /* @__PURE__ */ new Map();
4
+ // url → statusCode
5
+ record(url, statusCode, source) {
6
+ if (this.checked.has(url)) return;
7
+ this.checked.set(url, statusCode);
8
+ if (statusCode >= 400) {
9
+ this.deadLinks.push({
10
+ url,
11
+ statusCode,
12
+ sourceUrl: source?.sourceUrl,
13
+ anchorText: source?.anchorText,
14
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
15
+ reason: statusCode === 0 ? "timeout" : "http-error"
16
+ });
17
+ }
18
+ }
19
+ recordTimeout(url, sourceUrl) {
20
+ if (this.checked.has(url)) return;
21
+ this.checked.set(url, 0);
22
+ this.deadLinks.push({ url, statusCode: 0, sourceUrl, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), reason: "timeout" });
23
+ }
24
+ getDeadLinks() {
25
+ return [...this.deadLinks];
26
+ }
27
+ isKnownBroken(url) {
28
+ const code = this.checked.get(url);
29
+ return code !== void 0 && code >= 400;
30
+ }
31
+ stats() {
32
+ return {
33
+ total: this.deadLinks.length,
34
+ by404: this.deadLinks.filter((l) => l.statusCode === 404).length,
35
+ by5xx: this.deadLinks.filter((l) => l.statusCode >= 500).length,
36
+ byTimeout: this.deadLinks.filter((l) => l.reason === "timeout").length
37
+ };
38
+ }
39
+ toCsv() {
40
+ const rows = [["URL", "Status", "Source URL", "Anchor Text", "Checked At"]];
41
+ for (const l of this.deadLinks) {
42
+ rows.push([l.url, String(l.statusCode), l.sourceUrl ?? "", l.anchorText ?? "", l.checkedAt]);
43
+ }
44
+ return rows.map((r) => r.map((c) => `"${c.replace(/"/g, '""')}"`).join(",")).join("\n");
45
+ }
46
+ }
47
+ export {
48
+ DeadLinkTracker
49
+ };
50
+ //# sourceMappingURL=dead-link-detector.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/dead-link-detector.ts"],
4
+ "sourcesContent": ["/**\n * Dead link detector \u2014 tracks broken links discovered during crawl.\n *\n * A \"dead link\" is a URL that:\n * - Returns 404, 410, or other 4xx/5xx responses\n * - Times out after maxRetries\n * - Redirects to an error page (detected by title heuristic)\n *\n * Results: list of broken links with source page, status code, and anchor text.\n * Used for: SEO audit export, test report enrichment.\n */\n\nexport interface DeadLink {\n url: string;\n statusCode: number;\n sourceUrl?: string;\n anchorText?: string;\n checkedAt: string;\n reason: 'http-error' | 'timeout' | 'dns-error';\n}\n\nexport class DeadLinkTracker {\n private deadLinks: DeadLink[] = [];\n private checked = new Map<string, number>(); // url \u2192 statusCode\n\n record(\n url: string,\n statusCode: number,\n source?: { sourceUrl?: string; anchorText?: string },\n ): void {\n if (this.checked.has(url)) return;\n this.checked.set(url, statusCode);\n if (statusCode >= 400) {\n this.deadLinks.push({\n url,\n statusCode,\n sourceUrl: source?.sourceUrl,\n anchorText: source?.anchorText,\n checkedAt: new Date().toISOString(),\n reason: statusCode === 0 ? 'timeout' : 'http-error',\n });\n }\n }\n\n recordTimeout(url: string, sourceUrl?: string): void {\n if (this.checked.has(url)) return;\n this.checked.set(url, 0);\n this.deadLinks.push({ url, statusCode: 0, sourceUrl, checkedAt: new Date().toISOString(), reason: 'timeout' });\n }\n\n getDeadLinks(): DeadLink[] {\n return [...this.deadLinks];\n }\n\n isKnownBroken(url: string): boolean {\n const code = this.checked.get(url);\n return code !== undefined && code >= 400;\n }\n\n stats(): { total: number; by404: number; by5xx: number; byTimeout: number } {\n return {\n total: this.deadLinks.length,\n by404: this.deadLinks.filter(l => l.statusCode === 404).length,\n by5xx: this.deadLinks.filter(l => l.statusCode >= 500).length,\n byTimeout: this.deadLinks.filter(l => l.reason === 'timeout').length,\n };\n }\n\n toCsv(): string {\n const rows = [['URL', 'Status', 'Source URL', 'Anchor Text', 'Checked At']];\n for (const l of this.deadLinks) {\n rows.push([l.url, String(l.statusCode), l.sourceUrl ?? '', l.anchorText ?? '', l.checkedAt]);\n }\n return rows.map(r => r.map(c => `\"${c.replace(/\"/g, '\"\"')}\"`).join(',')).join('\\n');\n }\n}\n"],
5
+ "mappings": "AAqBO,MAAM,gBAAgB;AAAA,EACnB,YAAwB,CAAC;AAAA,EACzB,UAAU,oBAAI,IAAoB;AAAA;AAAA,EAE1C,OACE,KACA,YACA,QACM;AACN,QAAI,KAAK,QAAQ,IAAI,GAAG,EAAG;AAC3B,SAAK,QAAQ,IAAI,KAAK,UAAU;AAChC,QAAI,cAAc,KAAK;AACrB,WAAK,UAAU,KAAK;AAAA,QAClB;AAAA,QACA;AAAA,QACA,WAAW,QAAQ;AAAA,QACnB,YAAY,QAAQ;AAAA,QACpB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,QAAQ,eAAe,IAAI,YAAY;AAAA,MACzC,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,cAAc,KAAa,WAA0B;AACnD,QAAI,KAAK,QAAQ,IAAI,GAAG,EAAG;AAC3B,SAAK,QAAQ,IAAI,KAAK,CAAC;AACvB,SAAK,UAAU,KAAK,EAAE,KAAK,YAAY,GAAG,WAAW,YAAW,oBAAI,KAAK,GAAE,YAAY,GAAG,QAAQ,UAAU,CAAC;AAAA,EAC/G;AAAA,EAEA,eAA2B;AACzB,WAAO,CAAC,GAAG,KAAK,SAAS;AAAA,EAC3B;AAAA,EAEA,cAAc,KAAsB;AAClC,UAAM,OAAO,KAAK,QAAQ,IAAI,GAAG;AACjC,WAAO,SAAS,UAAa,QAAQ;AAAA,EACvC;AAAA,EAEA,QAA4E;AAC1E,WAAO;AAAA,MACL,OAAO,KAAK,UAAU;AAAA,MACtB,OAAO,KAAK,UAAU,OAAO,OAAK,EAAE,eAAe,GAAG,EAAE;AAAA,MACxD,OAAO,KAAK,UAAU,OAAO,OAAK,EAAE,cAAc,GAAG,EAAE;AAAA,MACvD,WAAW,KAAK,UAAU,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE;AAAA,IAChE;AAAA,EACF;AAAA,EAEA,QAAgB;AACd,UAAM,OAAO,CAAC,CAAC,OAAO,UAAU,cAAc,eAAe,YAAY,CAAC;AAC1E,eAAW,KAAK,KAAK,WAAW;AAC9B,WAAK,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE,UAAU,GAAG,EAAE,aAAa,IAAI,EAAE,cAAc,IAAI,EAAE,SAAS,CAAC;AAAA,IAC7F;AACA,WAAO,KAAK,IAAI,OAAK,EAAE,IAAI,OAAK,IAAI,EAAE,QAAQ,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI;AAAA,EACpF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compute a stable fingerprint of a screen's element set.
3
+ * Sorted so insertion-order changes (React re-renders) don't produce false positives.
4
+ * Returns first 16 hex chars of SHA-256.
5
+ */
6
+ export declare function computeElementHash(elements: Array<{
7
+ role: string;
8
+ meaning: string;
9
+ }>): string;
@@ -0,0 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+ function computeElementHash(elements) {
3
+ const pairs = elements.map((e) => `${e.role.toLowerCase()}:${e.meaning.toLowerCase().trim()}`).sort().join("|");
4
+ return createHash("sha256").update(pairs).digest("hex").slice(0, 16);
5
+ }
6
+ export {
7
+ computeElementHash
8
+ };
9
+ //# sourceMappingURL=element-hash.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/element-hash.ts"],
4
+ "sourcesContent": ["import { createHash } from 'node:crypto';\n\n/**\n * Compute a stable fingerprint of a screen's element set.\n * Sorted so insertion-order changes (React re-renders) don't produce false positives.\n * Returns first 16 hex chars of SHA-256.\n */\nexport function computeElementHash(\n elements: Array<{ role: string; meaning: string }>,\n): string {\n const pairs = elements\n .map((e) => `${e.role.toLowerCase()}:${e.meaning.toLowerCase().trim()}`)\n .sort()\n .join('|');\n return createHash('sha256').update(pairs).digest('hex').slice(0, 16);\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAOpB,SAAS,mBACd,UACQ;AACR,QAAM,QAAQ,SACX,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,YAAY,CAAC,IAAI,EAAE,QAAQ,YAAY,EAAE,KAAK,CAAC,EAAE,EACtE,KAAK,EACL,KAAK,GAAG;AACX,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AACrE;",
6
+ "names": []
7
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Email OTP retrieval — supports 3 providers:
3
+ * mailslurp — managed SaaS (existing, no self-hosting)
4
+ * imap — any real IMAP mailbox (Gmail, Outlook, custom domain catch-all)
5
+ * mailpit — self-hosted SMTP trap (dev/staging only)
6
+ *
7
+ * Use mailslurp or imap for production apps.
8
+ * Use mailpit only when you can point the app's SMTP at the trap server.
9
+ */
10
+ export type EmailOTPProvider = 'mailslurp' | 'imap' | 'mailpit';
11
+ export interface EmailContent {
12
+ /** 4-8 digit OTP code, if found. */
13
+ otp?: string;
14
+ /** Magic link URL to navigate to, if found. */
15
+ magicLink?: string;
16
+ }
17
+ export interface EmailOTPConfig {
18
+ provider: EmailOTPProvider;
19
+ apiKey?: string;
20
+ inboxId?: string;
21
+ imapHost?: string;
22
+ imapPort?: number;
23
+ imapUser?: string;
24
+ imapPass?: string;
25
+ imapTls?: boolean;
26
+ mailpitBaseUrl?: string;
27
+ }
28
+ /**
29
+ * Wait for an email and return its OTP code or magic link URL.
30
+ * Prefer this over waitForEmailOTP when the app may send passwordless magic links.
31
+ */
32
+ export declare function waitForEmailContent(config: EmailOTPConfig, since: string | Date): Promise<EmailContent | null>;
33
+ /**
34
+ * Wait for an OTP email and extract the code.
35
+ * @deprecated Use waitForEmailContent which also handles magic links.
36
+ */
37
+ export declare function waitForEmailOTP(config: EmailOTPConfig, since: string | Date): Promise<string | null>;
38
+ /**
39
+ * Get the email address for a MailSlurp inbox (used to display in UI).
40
+ * Returns null for IMAP/Mailpit providers — use imapUser directly.
41
+ */
42
+ export declare function getInboxEmail(configOrApiKey: EmailOTPConfig | string, inboxId?: string): Promise<string | null>;
@@ -0,0 +1,177 @@
1
+ const POLL_INTERVAL_MS = 2e3;
2
+ const MAX_WAIT_MS = 6e4;
3
+ function extractOTP(text) {
4
+ const m6 = text.match(/\b(\d{6})\b/);
5
+ if (m6) return m6[1];
6
+ const m48 = text.match(/\b(\d{4,8})\b/);
7
+ if (m48) return m48[1];
8
+ const magic = text.match(/\b([A-Z0-9]{6,10})\b/);
9
+ if (magic) return magic[1];
10
+ return null;
11
+ }
12
+ function extractMagicLink(text) {
13
+ const magicPath = /\/(verify|magic|login|auth|confirm|sign-in)[/?#]/i;
14
+ const magicQuery = /[?&](token|magic_token|login_token|auth_token|key)=/i;
15
+ const urlMatches = text.match(/https:\/\/[^\s<>"')\]]+/g);
16
+ if (!urlMatches) return null;
17
+ for (const raw of urlMatches) {
18
+ try {
19
+ const u = new URL(raw.replace(/[>'")\].,]+$/, ""));
20
+ if (magicPath.test(u.pathname) || magicQuery.test(u.search)) return u.toString();
21
+ } catch {
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function parseEmailContent(text) {
27
+ const magicLink = extractMagicLink(text);
28
+ if (magicLink) return { magicLink };
29
+ const otp = extractOTP(text);
30
+ if (otp) return { otp };
31
+ return {};
32
+ }
33
+ const MAILSLURP_BASE = "https://api.mailslurp.com";
34
+ async function waitForContentMailSlurp(apiKey, inboxId, since) {
35
+ const deadline = Date.now() + MAX_WAIT_MS;
36
+ while (Date.now() < deadline) {
37
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
38
+ try {
39
+ const listRes = await fetch(
40
+ `${MAILSLURP_BASE}/inboxes/${inboxId}/emails?sort=DESC&limit=5&since=${encodeURIComponent(since)}`,
41
+ { headers: { "x-api-key": apiKey } }
42
+ );
43
+ if (!listRes.ok) continue;
44
+ const emails = await listRes.json();
45
+ if (!emails.length) continue;
46
+ const bodyRes = await fetch(`${MAILSLURP_BASE}/emails/${emails[0].id}`, { headers: { "x-api-key": apiKey } });
47
+ if (!bodyRes.ok) continue;
48
+ const email = await bodyRes.json();
49
+ const content = parseEmailContent((email.body ?? "") + " " + (email.subject ?? ""));
50
+ if (content.otp || content.magicLink) return content;
51
+ } catch {
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ async function getInboxEmailMailSlurp(apiKey, inboxId) {
57
+ try {
58
+ const res = await fetch(`${MAILSLURP_BASE}/inboxes/${inboxId}`, { headers: { "x-api-key": apiKey } });
59
+ if (!res.ok) return null;
60
+ const data = await res.json();
61
+ return data.emailAddress ?? null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ async function waitForContentMailpit(baseUrl, since) {
67
+ const deadline = Date.now() + MAX_WAIT_MS;
68
+ const sinceMs = since.getTime();
69
+ while (Date.now() < deadline) {
70
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
71
+ try {
72
+ const res = await fetch(`${baseUrl}/api/v1/messages?limit=10`);
73
+ if (!res.ok) continue;
74
+ const data = await res.json();
75
+ const recent = (data.messages ?? []).filter((m) => new Date(m.Created).getTime() > sinceMs);
76
+ for (const msg of recent) {
77
+ const snippet = parseEmailContent(msg.Snippet + " " + msg.Subject);
78
+ if (snippet.otp || snippet.magicLink) return snippet;
79
+ const bodyRes = await fetch(`${baseUrl}/api/v1/message/${msg.ID}`);
80
+ if (!bodyRes.ok) continue;
81
+ const body = await bodyRes.json();
82
+ const full = parseEmailContent((body.Text?.Body ?? "") + " " + (body.Subject ?? ""));
83
+ if (full.otp || full.magicLink) return full;
84
+ }
85
+ } catch {
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ async function waitForContentImap(cfg, since) {
91
+ let ImapFlow;
92
+ try {
93
+ const mod = await import("imapflow");
94
+ ImapFlow = mod.ImapFlow;
95
+ } catch {
96
+ throw new Error("IMAP provider requires imapflow package. Add it to dependencies.");
97
+ }
98
+ const client = new ImapFlow({
99
+ host: cfg.host,
100
+ port: cfg.port,
101
+ secure: cfg.tls,
102
+ auth: { user: cfg.user, pass: cfg.pass },
103
+ logger: false
104
+ });
105
+ try {
106
+ await client.connect();
107
+ const lock = await client.getMailboxLock("INBOX");
108
+ try {
109
+ const deadline = Date.now() + MAX_WAIT_MS;
110
+ while (Date.now() < deadline) {
111
+ const uids = await client.search({ seen: false, since });
112
+ if (uids.length) {
113
+ for await (const msg of client.fetch(uids.slice(-5), { source: true })) {
114
+ const { simpleParser } = await import("mailparser");
115
+ const parsed = await simpleParser(msg.source);
116
+ const text = (parsed.text ?? "") + " " + (parsed.subject ?? "");
117
+ const content = parseEmailContent(text);
118
+ if (content.otp || content.magicLink) return content;
119
+ }
120
+ }
121
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
122
+ }
123
+ return null;
124
+ } finally {
125
+ lock.release();
126
+ }
127
+ } finally {
128
+ await client.logout().catch(() => {
129
+ });
130
+ }
131
+ }
132
+ async function waitForEmailContent(config, since) {
133
+ const sinceStr = typeof since === "string" ? since : since.toISOString();
134
+ const sinceDate = typeof since === "string" ? new Date(since) : since;
135
+ switch (config.provider) {
136
+ case "mailslurp":
137
+ if (!config.apiKey || !config.inboxId) throw new Error("mailslurp requires apiKey + inboxId");
138
+ return waitForContentMailSlurp(config.apiKey, config.inboxId, sinceStr);
139
+ case "mailpit":
140
+ return waitForContentMailpit(config.mailpitBaseUrl ?? "http://mailpit:8025", sinceDate);
141
+ case "imap":
142
+ if (!config.imapHost || !config.imapUser || !config.imapPass) {
143
+ throw new Error("imap requires imapHost, imapUser, imapPass");
144
+ }
145
+ return waitForContentImap({
146
+ host: config.imapHost,
147
+ port: config.imapPort ?? 993,
148
+ user: config.imapUser,
149
+ pass: config.imapPass,
150
+ tls: config.imapTls !== false
151
+ }, sinceDate);
152
+ default:
153
+ return null;
154
+ }
155
+ }
156
+ async function waitForEmailOTP(config, since) {
157
+ const content = await waitForEmailContent(config, since);
158
+ return content?.otp ?? null;
159
+ }
160
+ async function getInboxEmail(configOrApiKey, inboxId) {
161
+ if (typeof configOrApiKey === "string") {
162
+ return getInboxEmailMailSlurp(configOrApiKey, inboxId ?? "");
163
+ }
164
+ if (configOrApiKey.provider === "mailslurp" && configOrApiKey.apiKey && configOrApiKey.inboxId) {
165
+ return getInboxEmailMailSlurp(configOrApiKey.apiKey, configOrApiKey.inboxId);
166
+ }
167
+ if (configOrApiKey.provider === "imap") {
168
+ return configOrApiKey.imapUser ?? null;
169
+ }
170
+ return null;
171
+ }
172
+ export {
173
+ getInboxEmail,
174
+ waitForEmailContent,
175
+ waitForEmailOTP
176
+ };
177
+ //# sourceMappingURL=email-otp.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/email-otp.ts"],
4
+ "sourcesContent": ["/**\n * Email OTP retrieval \u2014 supports 3 providers:\n * mailslurp \u2014 managed SaaS (existing, no self-hosting)\n * imap \u2014 any real IMAP mailbox (Gmail, Outlook, custom domain catch-all)\n * mailpit \u2014 self-hosted SMTP trap (dev/staging only)\n *\n * Use mailslurp or imap for production apps.\n * Use mailpit only when you can point the app's SMTP at the trap server.\n */\n\nexport type EmailOTPProvider = 'mailslurp' | 'imap' | 'mailpit';\n\nexport interface EmailContent {\n /** 4-8 digit OTP code, if found. */\n otp?: string;\n /** Magic link URL to navigate to, if found. */\n magicLink?: string;\n}\n\nexport interface EmailOTPConfig {\n provider: EmailOTPProvider;\n // MailSlurp\n apiKey?: string;\n inboxId?: string;\n // IMAP (Gmail app-password, Outlook, custom IMAP)\n imapHost?: string;\n imapPort?: number;\n imapUser?: string;\n imapPass?: string;\n imapTls?: boolean;\n // Mailpit (REST API at baseUrl)\n mailpitBaseUrl?: string;\n}\n\nconst POLL_INTERVAL_MS = 2_000;\nconst MAX_WAIT_MS = 60_000;\n\n/** Extract OTP code from email body text. Prefers 6-digit, falls back to 4-8 digit. */\nfunction extractOTP(text: string): string | null {\n const m6 = text.match(/\\b(\\d{6})\\b/);\n if (m6) return m6[1];\n const m48 = text.match(/\\b(\\d{4,8})\\b/);\n if (m48) return m48[1];\n const magic = text.match(/\\b([A-Z0-9]{6,10})\\b/);\n if (magic) return magic[1];\n return null;\n}\n\n/** Extract a magic-link URL from email body text. Matches /verify, /magic, /auth/callback, ?token= etc. */\nfunction extractMagicLink(text: string): string | null {\n const magicPath = /\\/(verify|magic|login|auth|confirm|sign-in)[/?#]/i;\n const magicQuery = /[?&](token|magic_token|login_token|auth_token|key)=/i;\n const urlMatches = text.match(/https:\\/\\/[^\\s<>\"')\\]]+/g);\n if (!urlMatches) return null;\n for (const raw of urlMatches) {\n try {\n const u = new URL(raw.replace(/[>'\")\\].,]+$/, ''));\n if (magicPath.test(u.pathname) || magicQuery.test(u.search)) return u.toString();\n } catch { /* skip malformed URLs */ }\n }\n return null;\n}\n\nfunction parseEmailContent(text: string): EmailContent {\n const magicLink = extractMagicLink(text);\n if (magicLink) return { magicLink };\n const otp = extractOTP(text);\n if (otp) return { otp };\n return {};\n}\n\n// \u2500\u2500 MailSlurp \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\nconst MAILSLURP_BASE = 'https://api.mailslurp.com';\n\nasync function waitForContentMailSlurp(apiKey: string, inboxId: string, since: string): Promise<EmailContent | null> {\n const deadline = Date.now() + MAX_WAIT_MS;\n while (Date.now() < deadline) {\n await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));\n try {\n const listRes = await fetch(\n `${MAILSLURP_BASE}/inboxes/${inboxId}/emails?sort=DESC&limit=5&since=${encodeURIComponent(since)}`,\n { headers: { 'x-api-key': apiKey } }\n );\n if (!listRes.ok) continue;\n const emails = await listRes.json() as Array<{ id: string }>;\n if (!emails.length) continue;\n const bodyRes = await fetch(`${MAILSLURP_BASE}/emails/${emails[0].id}`, { headers: { 'x-api-key': apiKey } });\n if (!bodyRes.ok) continue;\n const email = await bodyRes.json() as { body?: string; subject?: string };\n const content = parseEmailContent((email.body ?? '') + ' ' + (email.subject ?? ''));\n if (content.otp || content.magicLink) return content;\n } catch { /* retry */ }\n }\n return null;\n}\n\nasync function getInboxEmailMailSlurp(apiKey: string, inboxId: string): Promise<string | null> {\n try {\n const res = await fetch(`${MAILSLURP_BASE}/inboxes/${inboxId}`, { headers: { 'x-api-key': apiKey } });\n if (!res.ok) return null;\n const data = await res.json() as { emailAddress?: string };\n return data.emailAddress ?? null;\n } catch { return null; }\n}\n\n// \u2500\u2500 Mailpit (SMTP trap REST API) \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 waitForContentMailpit(baseUrl: string, since: Date): Promise<EmailContent | null> {\n const deadline = Date.now() + MAX_WAIT_MS;\n const sinceMs = since.getTime();\n while (Date.now() < deadline) {\n await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));\n try {\n const res = await fetch(`${baseUrl}/api/v1/messages?limit=10`);\n if (!res.ok) continue;\n const data = await res.json() as { messages?: Array<{ ID: string; Created: string; Snippet: string; Subject: string }> };\n const recent = (data.messages ?? []).filter(m => new Date(m.Created).getTime() > sinceMs);\n for (const msg of recent) {\n const snippet = parseEmailContent(msg.Snippet + ' ' + msg.Subject);\n if (snippet.otp || snippet.magicLink) return snippet;\n // Fetch full body \u2014 snippet may truncate the magic link\n const bodyRes = await fetch(`${baseUrl}/api/v1/message/${msg.ID}`);\n if (!bodyRes.ok) continue;\n const body = await bodyRes.json() as { Text?: { Body?: string }; Subject?: string };\n const full = parseEmailContent((body.Text?.Body ?? '') + ' ' + (body.Subject ?? ''));\n if (full.otp || full.magicLink) return full;\n }\n } catch { /* retry */ }\n }\n return null;\n}\n\n// \u2500\u2500 IMAP (any mailbox) \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 waitForContentImap(\n cfg: { host: string; port: number; user: string; pass: string; tls: boolean },\n since: Date\n): Promise<EmailContent | null> {\n let ImapFlow: any;\n try {\n const mod = await import('imapflow');\n ImapFlow = mod.ImapFlow;\n } catch {\n throw new Error('IMAP provider requires imapflow package. Add it to dependencies.');\n }\n\n const client = new ImapFlow({\n host: cfg.host,\n port: cfg.port,\n secure: cfg.tls,\n auth: { user: cfg.user, pass: cfg.pass },\n logger: false,\n });\n\n try {\n await client.connect();\n const lock = await client.getMailboxLock('INBOX');\n try {\n const deadline = Date.now() + MAX_WAIT_MS;\n while (Date.now() < deadline) {\n const uids = await client.search({ seen: false, since });\n if (uids.length) {\n for await (const msg of client.fetch(uids.slice(-5), { source: true })) {\n const { simpleParser } = await import('mailparser');\n const parsed = await simpleParser(msg.source);\n const text = (parsed.text ?? '') + ' ' + (parsed.subject ?? '');\n const content = parseEmailContent(text);\n if (content.otp || content.magicLink) return content;\n }\n }\n await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));\n }\n return null;\n } finally {\n lock.release();\n }\n } finally {\n await client.logout().catch(() => {});\n }\n}\n\n// \u2500\u2500 Public API \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\n/**\n * Wait for an email and return its OTP code or magic link URL.\n * Prefer this over waitForEmailOTP when the app may send passwordless magic links.\n */\nexport async function waitForEmailContent(\n config: EmailOTPConfig,\n since: string | Date\n): Promise<EmailContent | null> {\n const sinceStr = typeof since === 'string' ? since : since.toISOString();\n const sinceDate = typeof since === 'string' ? new Date(since) : since;\n\n switch (config.provider) {\n case 'mailslurp':\n if (!config.apiKey || !config.inboxId) throw new Error('mailslurp requires apiKey + inboxId');\n return waitForContentMailSlurp(config.apiKey, config.inboxId, sinceStr);\n\n case 'mailpit':\n return waitForContentMailpit(config.mailpitBaseUrl ?? 'http://mailpit:8025', sinceDate);\n\n case 'imap':\n if (!config.imapHost || !config.imapUser || !config.imapPass) {\n throw new Error('imap requires imapHost, imapUser, imapPass');\n }\n return waitForContentImap({\n host: config.imapHost,\n port: config.imapPort ?? 993,\n user: config.imapUser,\n pass: config.imapPass,\n tls: config.imapTls !== false,\n }, sinceDate);\n\n default:\n return null;\n }\n}\n\n/**\n * Wait for an OTP email and extract the code.\n * @deprecated Use waitForEmailContent which also handles magic links.\n */\nexport async function waitForEmailOTP(\n config: EmailOTPConfig,\n since: string | Date\n): Promise<string | null> {\n const content = await waitForEmailContent(config, since);\n return content?.otp ?? null;\n}\n\n/**\n * Get the email address for a MailSlurp inbox (used to display in UI).\n * Returns null for IMAP/Mailpit providers \u2014 use imapUser directly.\n */\nexport async function getInboxEmail(\n configOrApiKey: EmailOTPConfig | string,\n inboxId?: string\n): Promise<string | null> {\n // Legacy call: getInboxEmail(apiKey, inboxId)\n if (typeof configOrApiKey === 'string') {\n return getInboxEmailMailSlurp(configOrApiKey, inboxId ?? '');\n }\n if (configOrApiKey.provider === 'mailslurp' && configOrApiKey.apiKey && configOrApiKey.inboxId) {\n return getInboxEmailMailSlurp(configOrApiKey.apiKey, configOrApiKey.inboxId);\n }\n if (configOrApiKey.provider === 'imap') {\n return configOrApiKey.imapUser ?? null;\n }\n return null;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }