@taodigital/crawler 0.0.0-stage → 1.2.1
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.
- package/Dockerfile +82 -0
- package/README.md +10 -2
- package/dist/accessibility-auditor.d.ts +29 -0
- package/dist/accessibility-auditor.js +57 -0
- package/dist/accessibility-auditor.js.map +7 -0
- package/dist/accessibility-tree-crawler.d.ts +44 -0
- package/dist/accessibility-tree-crawler.js +156 -0
- package/dist/accessibility-tree-crawler.js.map +7 -0
- package/dist/advanced-ui-probes.d.ts +47 -0
- package/dist/advanced-ui-probes.js +310 -0
- package/dist/advanced-ui-probes.js.map +7 -0
- package/dist/ai-login.d.ts +32 -0
- package/dist/ai-login.js +250 -0
- package/dist/ai-login.js.map +7 -0
- package/dist/auth-token-rotator.d.ts +47 -0
- package/dist/auth-token-rotator.js +58 -0
- package/dist/auth-token-rotator.js.map +7 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +348 -0
- package/dist/auth.js.map +7 -0
- package/dist/browser-extension.d.ts +37 -0
- package/dist/browser-extension.js +113 -0
- package/dist/browser-extension.js.map +7 -0
- package/dist/captcha-solver.d.ts +16 -0
- package/dist/captcha-solver.js +105 -0
- package/dist/captcha-solver.js.map +7 -0
- package/dist/color-contrast.d.ts +30 -0
- package/dist/color-contrast.js +78 -0
- package/dist/color-contrast.js.map +7 -0
- package/dist/content-chunker.d.ts +18 -0
- package/dist/content-chunker.js +152 -0
- package/dist/content-chunker.js.map +7 -0
- package/dist/content-dedup.d.ts +23 -0
- package/dist/content-dedup.js +60 -0
- package/dist/content-dedup.js.map +7 -0
- package/dist/cookie-consent.d.ts +25 -0
- package/dist/cookie-consent.js +78 -0
- package/dist/cookie-consent.js.map +7 -0
- package/dist/cookie-jar.d.ts +20 -0
- package/dist/cookie-jar.js +61 -0
- package/dist/cookie-jar.js.map +7 -0
- package/dist/crawl-session-manager.d.ts +44 -0
- package/dist/crawl-session-manager.js +145 -0
- package/dist/crawl-session-manager.js.map +7 -0
- package/dist/crawler.d.ts +233 -0
- package/dist/crawler.js +6239 -0
- package/dist/crawler.js.map +7 -0
- package/dist/crud-interaction.d.ts +31 -0
- package/dist/crud-interaction.js +264 -0
- package/dist/crud-interaction.js.map +7 -0
- package/dist/custom-js-injector.d.ts +42 -0
- package/dist/custom-js-injector.js +50 -0
- package/dist/custom-js-injector.js.map +7 -0
- package/dist/dark-mode-capture.d.ts +23 -0
- package/dist/dark-mode-capture.js +75 -0
- package/dist/dark-mode-capture.js.map +7 -0
- package/dist/dead-link-detector.d.ts +37 -0
- package/dist/dead-link-detector.js +50 -0
- package/dist/dead-link-detector.js.map +7 -0
- package/dist/dropdown-enumerator.d.ts +20 -0
- package/dist/dropdown-enumerator.js +230 -0
- package/dist/dropdown-enumerator.js.map +7 -0
- package/dist/element-hash.d.ts +9 -0
- package/dist/element-hash.js +9 -0
- package/dist/element-hash.js.map +7 -0
- package/dist/email-otp.d.ts +42 -0
- package/dist/email-otp.js +177 -0
- package/dist/email-otp.js.map +7 -0
- package/dist/engine-router.d.ts +47 -0
- package/dist/engine-router.js +94 -0
- package/dist/engine-router.js.map +7 -0
- package/dist/export/bulk-export.d.ts +26 -0
- package/dist/export/bulk-export.js +76 -0
- package/dist/export/bulk-export.js.map +7 -0
- package/dist/export/vector-export.d.ts +76 -0
- package/dist/export/vector-export.js +85 -0
- package/dist/export/vector-export.js.map +7 -0
- package/dist/extractors/advanced.d.ts +64 -0
- package/dist/extractors/advanced.js +265 -0
- package/dist/extractors/advanced.js.map +7 -0
- package/dist/extractors/artifact-envelope.d.ts +63 -0
- package/dist/extractors/artifact-envelope.js +63 -0
- package/dist/extractors/artifact-envelope.js.map +7 -0
- package/dist/extractors/branding-extractor.d.ts +46 -0
- package/dist/extractors/branding-extractor.js +104 -0
- package/dist/extractors/branding-extractor.js.map +7 -0
- package/dist/extractors/summary-extractor.d.ts +10 -0
- package/dist/extractors/summary-extractor.js +36 -0
- package/dist/extractors/summary-extractor.js.map +7 -0
- package/dist/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +42 -0
- package/dist/failure-analysis-agent.js.map +7 -0
- package/dist/feature-flag-detector.d.ts +30 -0
- package/dist/feature-flag-detector.js +55 -0
- package/dist/feature-flag-detector.js.map +7 -0
- package/dist/filter-pagination-crawler.d.ts +52 -0
- package/dist/filter-pagination-crawler.js +299 -0
- package/dist/filter-pagination-crawler.js.map +7 -0
- package/dist/form-detector.d.ts +34 -0
- package/dist/form-detector.js +58 -0
- package/dist/form-detector.js.map +7 -0
- package/dist/frontier.d.ts +40 -0
- package/dist/frontier.js +103 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +36 -0
- package/dist/gap-pass.js +115 -0
- package/dist/gap-pass.js.map +7 -0
- package/dist/graphql-detector.d.ts +39 -0
- package/dist/graphql-detector.js +86 -0
- package/dist/graphql-detector.js.map +7 -0
- package/dist/http-engine.d.ts +80 -0
- package/dist/http-engine.js +318 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +116 -0
- package/dist/incremental-diff.js +137 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +7 -0
- package/dist/input-fuzzer.d.ts +30 -0
- package/dist/input-fuzzer.js +105 -0
- package/dist/input-fuzzer.js.map +7 -0
- package/dist/interactive-probe.d.ts +144 -0
- package/dist/interactive-probe.js +865 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +297 -0
- package/dist/job-queue.js +3812 -0
- package/dist/job-queue.js.map +7 -0
- package/dist/js-coverage.d.ts +29 -0
- package/dist/js-coverage.js +56 -0
- package/dist/js-coverage.js.map +7 -0
- package/dist/js-router-detector.d.ts +17 -0
- package/dist/js-router-detector.js +90 -0
- package/dist/js-router-detector.js.map +7 -0
- package/dist/language-detector.d.ts +22 -0
- package/dist/language-detector.js +57 -0
- package/dist/language-detector.js.map +7 -0
- package/dist/link-classifier.d.ts +23 -0
- package/dist/link-classifier.js +40 -0
- package/dist/link-classifier.js.map +7 -0
- package/dist/live-view.d.ts +36 -0
- package/dist/live-view.js +105 -0
- package/dist/live-view.js.map +7 -0
- package/dist/locator-extractor.d.ts +46 -0
- package/dist/locator-extractor.js +220 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +10 -0
- package/dist/logger.js.map +7 -0
- package/dist/markdown-extract.d.ts +8 -0
- package/dist/markdown-extract.js +54 -0
- package/dist/markdown-extract.js.map +7 -0
- package/dist/multi-context-handler.d.ts +55 -0
- package/dist/multi-context-handler.js +182 -0
- package/dist/multi-context-handler.js.map +7 -0
- package/dist/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.js.map +7 -0
- package/dist/nested-window-crawler.d.ts +41 -0
- package/dist/nested-window-crawler.js +172 -0
- package/dist/nested-window-crawler.js.map +7 -0
- package/dist/network-action-correlator.d.ts +47 -0
- package/dist/network-action-correlator.js +209 -0
- package/dist/network-action-correlator.js.map +7 -0
- package/dist/network-observer.d.ts +49 -0
- package/dist/network-observer.js +149 -0
- package/dist/network-observer.js.map +7 -0
- package/dist/network-throttle.d.ts +25 -0
- package/dist/network-throttle.js +34 -0
- package/dist/network-throttle.js.map +7 -0
- package/dist/openapi-generator.d.ts +49 -0
- package/dist/openapi-generator.js +89 -0
- package/dist/openapi-generator.js.map +7 -0
- package/dist/pdf-extractor.d.ts +28 -0
- package/dist/pdf-extractor.js +81 -0
- package/dist/pdf-extractor.js.map +7 -0
- package/dist/performance-metrics-collector.d.ts +50 -0
- package/dist/performance-metrics-collector.js +155 -0
- package/dist/performance-metrics-collector.js.map +7 -0
- package/dist/performance-metrics.d.ts +52 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +50 -0
- package/dist/phash.js +136 -0
- package/dist/phash.js.map +7 -0
- package/dist/pii-scrubber.d.ts +21 -0
- package/dist/pii-scrubber.js +75 -0
- package/dist/pii-scrubber.js.map +7 -0
- package/dist/playwright-recorder.d.ts +39 -0
- package/dist/playwright-recorder.js +123 -0
- package/dist/playwright-recorder.js.map +7 -0
- package/dist/policies/crawl-safety.d.ts +9 -0
- package/dist/policies/crawl-safety.js +41 -0
- package/dist/policies/crawl-safety.js.map +7 -0
- package/dist/profiles/domain-profile.d.ts +19 -0
- package/dist/profiles/domain-profile.js +160 -0
- package/dist/profiles/domain-profile.js.map +7 -0
- package/dist/proxy.d.ts +50 -0
- package/dist/proxy.js +165 -0
- package/dist/proxy.js.map +7 -0
- package/dist/rate-limiter.d.ts +31 -0
- package/dist/rate-limiter.js +64 -0
- package/dist/rate-limiter.js.map +7 -0
- package/dist/redirect-tracker.d.ts +37 -0
- package/dist/redirect-tracker.js +65 -0
- package/dist/redirect-tracker.js.map +7 -0
- package/dist/redis-frontier.d.ts +48 -0
- package/dist/redis-frontier.js +126 -0
- package/dist/redis-frontier.js.map +7 -0
- package/dist/redis-queue.d.ts +48 -0
- package/dist/redis-queue.js +165 -0
- package/dist/redis-queue.js.map +7 -0
- package/dist/response-cache.d.ts +38 -0
- package/dist/response-cache.js +140 -0
- package/dist/response-cache.js.map +7 -0
- package/dist/responsive-detector.d.ts +29 -0
- package/dist/responsive-detector.js +46 -0
- package/dist/responsive-detector.js.map +7 -0
- package/dist/retry-policy.d.ts +20 -0
- package/dist/retry-policy.js +54 -0
- package/dist/retry-policy.js.map +7 -0
- package/dist/robots-txt.d.ts +9 -0
- package/dist/robots-txt.js +77 -0
- package/dist/robots-txt.js.map +7 -0
- package/dist/screen-reader.d.ts +15 -0
- package/dist/screen-reader.js +48 -0
- package/dist/screen-reader.js.map +7 -0
- package/dist/screenshot-masker.d.ts +28 -0
- package/dist/screenshot-masker.js +62 -0
- package/dist/screenshot-masker.js.map +7 -0
- package/dist/seo-checker.d.ts +33 -0
- package/dist/seo-checker.js +91 -0
- package/dist/seo-checker.js.map +7 -0
- package/dist/service-worker-interceptor.d.ts +36 -0
- package/dist/service-worker-interceptor.js +136 -0
- package/dist/service-worker-interceptor.js.map +7 -0
- package/dist/session-capture.d.ts +39 -0
- package/dist/session-capture.js +274 -0
- package/dist/session-capture.js.map +7 -0
- package/dist/session-export.d.ts +49 -0
- package/dist/session-export.js +117 -0
- package/dist/session-export.js.map +7 -0
- package/dist/shadow-dom-crawler.d.ts +35 -0
- package/dist/shadow-dom-crawler.js +132 -0
- package/dist/shadow-dom-crawler.js.map +7 -0
- package/dist/sitemap-writer.d.ts +21 -0
- package/dist/sitemap-writer.js +47 -0
- package/dist/sitemap-writer.js.map +7 -0
- package/dist/sitemap.d.ts +51 -0
- package/dist/sitemap.js +160 -0
- package/dist/sitemap.js.map +7 -0
- package/dist/skyvern.d.ts +56 -0
- package/dist/skyvern.js +129 -0
- package/dist/skyvern.js.map +7 -0
- package/dist/sqs-queue.d.ts +13 -0
- package/dist/sqs-queue.js +115 -0
- package/dist/sqs-queue.js.map +7 -0
- package/dist/sso-token.d.ts +29 -0
- package/dist/sso-token.js +76 -0
- package/dist/sso-token.js.map +7 -0
- package/dist/state-graph.d.ts +59 -0
- package/dist/state-graph.js +193 -0
- package/dist/state-graph.js.map +7 -0
- package/dist/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +202 -0
- package/dist/stealth/human-behavior.js.map +7 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +77 -0
- package/dist/telemetry.js.map +7 -0
- package/dist/test-executor.d.ts +81 -0
- package/dist/test-executor.js +443 -0
- package/dist/test-executor.js.map +7 -0
- package/dist/tls-fingerprint.d.ts +50 -0
- package/dist/tls-fingerprint.js +81 -0
- package/dist/tls-fingerprint.js.map +7 -0
- package/dist/url-pattern-completer.d.ts +28 -0
- package/dist/url-pattern-completer.js +122 -0
- package/dist/url-pattern-completer.js.map +7 -0
- package/dist/visual-diff-verdict.d.ts +40 -0
- package/dist/visual-diff-verdict.js +27 -0
- package/dist/visual-diff-verdict.js.map +7 -0
- package/dist/webhook-notifier.d.ts +30 -0
- package/dist/webhook-notifier.js +76 -0
- package/dist/webhook-notifier.js.map +7 -0
- package/dist/websocket-detector.d.ts +24 -0
- package/dist/websocket-detector.js +60 -0
- package/dist/websocket-detector.js.map +7 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +33 -0
- package/dist/worker.js.map +7 -0
- package/package.json +49 -4
- package/src/accessibility-auditor.ts +102 -0
- package/src/accessibility-tree-crawler.ts +246 -0
- package/src/advanced-ui-probes.ts +470 -0
- package/src/ai-login.ts +298 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +418 -0
- package/src/browser-extension.ts +186 -0
- package/src/captcha-solver.ts +143 -0
- package/src/color-contrast.ts +116 -0
- package/src/content-chunker.ts +170 -0
- package/src/content-dedup.ts +99 -0
- package/src/cookie-consent.ts +106 -0
- package/src/cookie-jar.ts +91 -0
- package/src/crawl-session-manager.ts +231 -0
- package/src/crawler.ts +7123 -0
- package/src/crawler.ts.rej +12 -0
- package/src/crud-interaction.ts +386 -0
- package/src/custom-js-injector.ts +93 -0
- package/src/dark-mode-capture.ts +110 -0
- package/src/dead-link-detector.ts +76 -0
- package/src/dropdown-enumerator.ts +312 -0
- package/src/element-hash.ts +16 -0
- package/src/email-otp.ts +252 -0
- package/src/engine-router.ts +141 -0
- package/src/export/bulk-export.ts +85 -0
- package/src/export/vector-export.ts +160 -0
- package/src/extractors/advanced.ts +309 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/extractors/branding-extractor.ts +159 -0
- package/src/extractors/summary-extractor.ts +49 -0
- package/src/failure-analysis-agent.ts +51 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/filter-pagination-crawler.ts +500 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +144 -0
- package/src/gap-pass.ts +165 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +453 -0
- package/src/incremental-diff.ts +259 -0
- package/src/index.ts +52 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +1136 -0
- package/src/job-queue.test.ts +187 -0
- package/src/job-queue.ts +4504 -0
- package/src/js-coverage.ts +87 -0
- package/src/js-router-detector.ts +112 -0
- package/src/language-detector.ts +82 -0
- package/src/link-classifier.ts +65 -0
- package/src/live-view.ts +146 -0
- package/src/locator-extractor.ts +309 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/multi-context-handler.ts +296 -0
- package/src/native-crawler.ts +1108 -0
- package/src/nested-window-crawler.ts +310 -0
- package/src/network-action-correlator.ts +320 -0
- package/src/network-observer.ts +204 -0
- package/src/network-throttle.ts +69 -0
- package/src/openapi-generator.ts +135 -0
- package/src/pdf-extractor.ts +123 -0
- package/src/performance-metrics-collector.ts +243 -0
- package/src/performance-metrics.ts +134 -0
- package/src/phash.ts +184 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +52 -0
- package/src/profiles/domain-profile.ts +190 -0
- package/src/proxy.ts +238 -0
- package/src/rate-limiter.ts +87 -0
- package/src/redirect-tracker.ts +97 -0
- package/src/redis-frontier.ts +143 -0
- package/src/redis-queue.ts +208 -0
- package/src/response-cache.ts +150 -0
- package/src/responsive-detector.ts +92 -0
- package/src/retry-policy.ts +87 -0
- package/src/robots-txt.ts +96 -0
- package/src/screen-reader.ts +98 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/service-worker-interceptor.ts +185 -0
- package/src/session-capture.ts +335 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/shadow-dom-crawler.ts +198 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +127 -0
- package/src/sitemap.ts +233 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +129 -0
- package/src/sso-token.ts +120 -0
- package/src/state-graph.ts +280 -0
- package/src/stealth/human-behavior.ts +227 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +578 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +161 -0
- package/src/vendor.d.ts +20 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +59 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +50 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -0
- package/types/accessibility-tree-crawler.d.ts +44 -0
- package/types/advanced-ui-probes.d.ts +47 -0
- package/types/ai-login.d.ts +32 -0
- package/types/auth-token-rotator.d.ts +47 -0
- package/types/auth.d.ts +20 -0
- package/types/browser-extension.d.ts +37 -0
- package/types/bull-worker.d.ts +6 -0
- package/types/captcha-solver.d.ts +16 -0
- package/types/color-contrast.d.ts +30 -0
- package/types/content-chunker.d.ts +18 -0
- package/types/content-dedup.d.ts +23 -0
- package/types/cookie-consent.d.ts +25 -0
- package/types/cookie-jar.d.ts +20 -0
- package/types/crawl-session-manager.d.ts +44 -0
- package/types/crawler.d.ts +233 -0
- package/types/crud-interaction.d.ts +31 -0
- package/types/custom-js-injector.d.ts +42 -0
- package/types/dark-mode-capture.d.ts +23 -0
- package/types/dead-link-detector.d.ts +37 -0
- package/types/dropdown-enumerator.d.ts +20 -0
- package/types/element-hash.d.ts +9 -0
- package/types/email-otp.d.ts +42 -0
- package/types/engine-router.d.ts +47 -0
- package/types/export/bulk-export.d.ts +26 -0
- package/types/export/vector-export.d.ts +76 -0
- package/types/extractors/advanced.d.ts +64 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/extractors/branding-extractor.d.ts +46 -0
- package/types/extractors/summary-extractor.d.ts +10 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/filter-pagination-crawler.d.ts +52 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +40 -0
- package/types/gap-pass.d.ts +36 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +116 -0
- package/types/index.d.ts +52 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +144 -0
- package/types/job-queue.d.ts +308 -0
- package/types/js-coverage.d.ts +29 -0
- package/types/js-router-detector.d.ts +17 -0
- package/types/language-detector.d.ts +22 -0
- package/types/link-classifier.d.ts +23 -0
- package/types/live-view.d.ts +36 -0
- package/types/locator-extractor.d.ts +46 -0
- package/types/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/multi-context-handler.d.ts +55 -0
- package/types/native-crawler.d.ts +69 -0
- package/types/nested-window-crawler.d.ts +41 -0
- package/types/network-action-correlator.d.ts +47 -0
- package/types/network-observer.d.ts +49 -0
- package/types/network-throttle.d.ts +25 -0
- package/types/openapi-generator.d.ts +49 -0
- package/types/pdf-extractor.d.ts +28 -0
- package/types/performance-metrics-collector.d.ts +50 -0
- package/types/performance-metrics.d.ts +52 -0
- package/types/phash.d.ts +50 -0
- package/types/pii-scrubber.d.ts +21 -0
- package/types/playwright-recorder.d.ts +39 -0
- package/types/policies/crawl-safety.d.ts +9 -0
- package/types/profiles/domain-profile.d.ts +19 -0
- package/types/proxy.d.ts +50 -0
- package/types/rate-limiter.d.ts +31 -0
- package/types/redirect-tracker.d.ts +37 -0
- package/types/redis-frontier.d.ts +48 -0
- package/types/redis-queue.d.ts +48 -0
- package/types/response-cache.d.ts +38 -0
- package/types/responsive-detector.d.ts +29 -0
- package/types/retry-policy.d.ts +20 -0
- package/types/robots-txt.d.ts +9 -0
- package/types/screen-reader.d.ts +15 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/service-worker-interceptor.d.ts +36 -0
- package/types/session-capture.d.ts +39 -0
- package/types/session-export.d.ts +49 -0
- package/types/shadow-dom-crawler.d.ts +35 -0
- package/types/sitemap-writer.d.ts +21 -0
- package/types/sitemap.d.ts +51 -0
- package/types/skyvern.d.ts +56 -0
- package/types/sqs-queue.d.ts +13 -0
- package/types/sso-token.d.ts +29 -0
- package/types/state-graph.d.ts +59 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +84 -0
- package/types/tls-fingerprint.d.ts +50 -0
- package/types/url-pattern-completer.d.ts +28 -0
- package/types/visual-diff-verdict.d.ts +40 -0
- package/types/webhook-notifier.d.ts +30 -0
- package/types/websocket-detector.d.ts +24 -0
- package/types/worker.d.ts +1 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
|
|
3
|
+
export interface DropdownOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DropdownDef {
|
|
10
|
+
kind: 'native-select' | 'combobox' | 'listbox' | 'multiselect';
|
|
11
|
+
label: string;
|
|
12
|
+
locatorHint: string;
|
|
13
|
+
options: DropdownOption[];
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
cascadesFrom?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DropdownEnumerationResult {
|
|
19
|
+
dropdowns: DropdownDef[];
|
|
20
|
+
conditionalRevealMap: Record<string, string[]>;
|
|
21
|
+
totalOptionsFound: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function enumerateNativeSelects(page: Page): Promise<DropdownDef[]> {
|
|
25
|
+
const selects = await page.$$('select:not([multiple])');
|
|
26
|
+
const limited = selects.slice(0, 50);
|
|
27
|
+
const results: DropdownDef[] = [];
|
|
28
|
+
|
|
29
|
+
for (const handle of limited) {
|
|
30
|
+
try {
|
|
31
|
+
const data = await handle.evaluate((el: HTMLSelectElement) => {
|
|
32
|
+
const name = el.name || el.id || '';
|
|
33
|
+
const ariaLabel = el.getAttribute('aria-label') || '';
|
|
34
|
+
let labelText = ariaLabel;
|
|
35
|
+
if (!labelText && el.id) {
|
|
36
|
+
const lbl = document.querySelector(`label[for="${el.id}"]`);
|
|
37
|
+
if (lbl) labelText = lbl.textContent?.trim() || '';
|
|
38
|
+
}
|
|
39
|
+
if (!labelText && name) labelText = name;
|
|
40
|
+
const options = Array.from(el.options).map((o) => ({
|
|
41
|
+
value: o.value,
|
|
42
|
+
label: o.text.trim(),
|
|
43
|
+
disabled: o.disabled || undefined,
|
|
44
|
+
}));
|
|
45
|
+
const selectedOption = el.options[el.selectedIndex];
|
|
46
|
+
const defaultValue = selectedOption ? selectedOption.value : undefined;
|
|
47
|
+
const locatorHint = name
|
|
48
|
+
? `select[name="${name}"]`
|
|
49
|
+
: el.id
|
|
50
|
+
? `select#${el.id}`
|
|
51
|
+
: 'select';
|
|
52
|
+
return { label: labelText, locatorHint, options, defaultValue };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
results.push({
|
|
56
|
+
kind: 'native-select',
|
|
57
|
+
label: data.label,
|
|
58
|
+
locatorHint: data.locatorHint,
|
|
59
|
+
options: data.options,
|
|
60
|
+
defaultValue: data.defaultValue,
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// skip unresponsive handle
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function enumerateCustomDropdowns(page: Page): Promise<DropdownDef[]> {
|
|
71
|
+
const selectors = [
|
|
72
|
+
'[role="combobox"]',
|
|
73
|
+
'[aria-haspopup="listbox"]:not([role="combobox"])',
|
|
74
|
+
'[data-radix-select-trigger]',
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
const handles: { handle: Awaited<ReturnType<Page['$$']>>[0]; hint: string }[] = [];
|
|
79
|
+
|
|
80
|
+
for (const sel of selectors) {
|
|
81
|
+
const els = await page.$$(sel);
|
|
82
|
+
for (const el of els) {
|
|
83
|
+
const hint = await el
|
|
84
|
+
.evaluate((e: Element) => {
|
|
85
|
+
const id = e.id ? `#${e.id}` : '';
|
|
86
|
+
const aria = e.getAttribute('aria-label') || '';
|
|
87
|
+
return id || aria || e.tagName.toLowerCase();
|
|
88
|
+
})
|
|
89
|
+
.catch(() => '');
|
|
90
|
+
if (!seen.has(hint)) {
|
|
91
|
+
seen.add(hint);
|
|
92
|
+
handles.push({ handle: el, hint });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const limited = handles.slice(0, 20);
|
|
98
|
+
const results: DropdownDef[] = [];
|
|
99
|
+
|
|
100
|
+
for (const { handle, hint } of limited) {
|
|
101
|
+
try {
|
|
102
|
+
const ariaLabel = await handle
|
|
103
|
+
.evaluate((e: Element) => e.getAttribute('aria-label') || e.textContent?.trim() || '')
|
|
104
|
+
.catch(() => '');
|
|
105
|
+
|
|
106
|
+
await handle.click();
|
|
107
|
+
await page.waitForSelector('[role="listbox"]', { timeout: 2000 });
|
|
108
|
+
|
|
109
|
+
const options = await page.$$eval('[role="listbox"] [role="option"]', (els) =>
|
|
110
|
+
els.map((el) => ({
|
|
111
|
+
value: el.getAttribute('data-value') || el.getAttribute('value') || el.textContent?.trim() || '',
|
|
112
|
+
label: el.textContent?.trim() || '',
|
|
113
|
+
disabled: el.getAttribute('aria-disabled') === 'true' || undefined,
|
|
114
|
+
}))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
await page.keyboard.press('Escape');
|
|
118
|
+
await page.waitForTimeout(300);
|
|
119
|
+
|
|
120
|
+
results.push({
|
|
121
|
+
kind: 'combobox',
|
|
122
|
+
label: ariaLabel,
|
|
123
|
+
locatorHint: hint,
|
|
124
|
+
options,
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
try {
|
|
128
|
+
await page.keyboard.press('Escape');
|
|
129
|
+
await page.waitForTimeout(300);
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore cleanup failure
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function enumerateMultiSelects(page: Page): Promise<DropdownDef[]> {
|
|
140
|
+
const results: DropdownDef[] = [];
|
|
141
|
+
|
|
142
|
+
const nativeMulti = await page.$$('select[multiple]');
|
|
143
|
+
for (const handle of nativeMulti) {
|
|
144
|
+
try {
|
|
145
|
+
const data = await handle.evaluate((el: HTMLSelectElement) => {
|
|
146
|
+
const name = el.name || el.id || '';
|
|
147
|
+
const ariaLabel = el.getAttribute('aria-label') || '';
|
|
148
|
+
let labelText = ariaLabel;
|
|
149
|
+
if (!labelText && el.id) {
|
|
150
|
+
const lbl = document.querySelector(`label[for="${el.id}"]`);
|
|
151
|
+
if (lbl) labelText = lbl.textContent?.trim() || '';
|
|
152
|
+
}
|
|
153
|
+
if (!labelText && name) labelText = name;
|
|
154
|
+
const options = Array.from(el.options).map((o) => ({
|
|
155
|
+
value: o.value,
|
|
156
|
+
label: o.text.trim(),
|
|
157
|
+
disabled: o.disabled || undefined,
|
|
158
|
+
}));
|
|
159
|
+
const locatorHint = name
|
|
160
|
+
? `select[multiple][name="${name}"]`
|
|
161
|
+
: el.id
|
|
162
|
+
? `select[multiple]#${el.id}`
|
|
163
|
+
: 'select[multiple]';
|
|
164
|
+
return { label: labelText, locatorHint, options };
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
results.push({
|
|
168
|
+
kind: 'multiselect',
|
|
169
|
+
label: data.label,
|
|
170
|
+
locatorHint: data.locatorHint,
|
|
171
|
+
options: data.options,
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
// skip
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ariaMulti = await page.$$('[role="listbox"][aria-multiselectable="true"]');
|
|
179
|
+
for (const handle of ariaMulti) {
|
|
180
|
+
try {
|
|
181
|
+
const data = await handle.evaluate((el: Element) => {
|
|
182
|
+
const ariaLabel = el.getAttribute('aria-label') || el.id || '';
|
|
183
|
+
const locatorHint = el.id
|
|
184
|
+
? `[role="listbox"]#${el.id}`
|
|
185
|
+
: '[role="listbox"][aria-multiselectable="true"]';
|
|
186
|
+
const optionEls = el.querySelectorAll('[role="option"]');
|
|
187
|
+
const options = Array.from(optionEls).map((o) => ({
|
|
188
|
+
value: o.getAttribute('data-value') || o.getAttribute('value') || o.textContent?.trim() || '',
|
|
189
|
+
label: o.textContent?.trim() || '',
|
|
190
|
+
disabled: o.getAttribute('aria-disabled') === 'true' || undefined,
|
|
191
|
+
}));
|
|
192
|
+
return { label: ariaLabel, locatorHint, options };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
results.push({
|
|
196
|
+
kind: 'multiselect',
|
|
197
|
+
label: data.label,
|
|
198
|
+
locatorHint: data.locatorHint,
|
|
199
|
+
options: data.options,
|
|
200
|
+
});
|
|
201
|
+
} catch {
|
|
202
|
+
// skip
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function detectCascading(
|
|
210
|
+
page: Page,
|
|
211
|
+
dropdowns: DropdownDef[]
|
|
212
|
+
): Promise<Record<string, string[]>> {
|
|
213
|
+
const cascadeMap: Record<string, string[]> = {};
|
|
214
|
+
|
|
215
|
+
const nativeParents = dropdowns.filter(
|
|
216
|
+
(d) => d.kind === 'native-select' && d.options.length <= 8
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
for (const parent of nativeParents) {
|
|
220
|
+
const parentName = parent.locatorHint.match(/name="([^"]+)"/)?.[1];
|
|
221
|
+
if (!parentName) continue;
|
|
222
|
+
|
|
223
|
+
const otherNatives = dropdowns.filter(
|
|
224
|
+
(d) => d.kind === 'native-select' && d.locatorHint !== parent.locatorHint
|
|
225
|
+
);
|
|
226
|
+
if (otherNatives.length === 0) continue;
|
|
227
|
+
|
|
228
|
+
const baselineCounts = await page
|
|
229
|
+
.$$eval('select:not([multiple])', (els) =>
|
|
230
|
+
(els as HTMLSelectElement[]).map((el) => ({ name: el.name || el.id, count: el.options.length }))
|
|
231
|
+
)
|
|
232
|
+
.catch(() => [] as { name: string; count: number }[]);
|
|
233
|
+
|
|
234
|
+
let foundChildren: string[] = [];
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < parent.options.length; i++) {
|
|
237
|
+
try {
|
|
238
|
+
await page.selectOption(`select[name="${parentName}"]`, { index: i });
|
|
239
|
+
await page.waitForTimeout(500);
|
|
240
|
+
|
|
241
|
+
const newCounts = await page
|
|
242
|
+
.$$eval('select:not([multiple])', (els) =>
|
|
243
|
+
(els as HTMLSelectElement[]).map((el) => ({ name: el.name || el.id, count: el.options.length }))
|
|
244
|
+
)
|
|
245
|
+
.catch(() => [] as { name: string; count: number }[]);
|
|
246
|
+
|
|
247
|
+
for (const curr of newCounts) {
|
|
248
|
+
const base = baselineCounts.find((b) => b.name === curr.name);
|
|
249
|
+
if (base && base.count !== curr.count && curr.name !== parentName) {
|
|
250
|
+
const childHint = `select[name="${curr.name}"]`;
|
|
251
|
+
const childDef = dropdowns.find((d) => d.locatorHint === childHint);
|
|
252
|
+
if (childDef && !foundChildren.includes(childHint)) {
|
|
253
|
+
foundChildren.push(childHint);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// skip option that fails
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// reset to index 0
|
|
263
|
+
try {
|
|
264
|
+
await page.selectOption(`select[name="${parentName}"]`, { index: 0 });
|
|
265
|
+
} catch {
|
|
266
|
+
// ignore reset failure
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (foundChildren.length > 0) {
|
|
270
|
+
cascadeMap[parent.locatorHint] = foundChildren;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return cascadeMap;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function enumerateDropdowns(page: Page): Promise<DropdownEnumerationResult> {
|
|
278
|
+
try {
|
|
279
|
+
const [native, custom, multi] = await Promise.all([
|
|
280
|
+
enumerateNativeSelects(page).catch(() => [] as DropdownDef[]),
|
|
281
|
+
enumerateCustomDropdowns(page).catch(() => [] as DropdownDef[]),
|
|
282
|
+
enumerateMultiSelects(page).catch(() => [] as DropdownDef[]),
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
const seen = new Set<string>();
|
|
286
|
+
const dropdowns: DropdownDef[] = [];
|
|
287
|
+
|
|
288
|
+
for (const d of [...native, ...custom, ...multi]) {
|
|
289
|
+
if (!seen.has(d.locatorHint)) {
|
|
290
|
+
seen.add(d.locatorHint);
|
|
291
|
+
dropdowns.push(d);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const conditionalRevealMap = await detectCascading(page, dropdowns).catch(
|
|
296
|
+
() => ({} as Record<string, string[]>)
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
for (const [parent, children] of Object.entries(conditionalRevealMap)) {
|
|
300
|
+
for (const childHint of children) {
|
|
301
|
+
const child = dropdowns.find((d) => d.locatorHint === childHint);
|
|
302
|
+
if (child) child.cascadesFrom = parent;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const totalOptionsFound = dropdowns.reduce((sum, d) => sum + d.options.length, 0);
|
|
307
|
+
|
|
308
|
+
return { dropdowns, conditionalRevealMap, totalOptionsFound };
|
|
309
|
+
} catch {
|
|
310
|
+
return { dropdowns: [], conditionalRevealMap: {}, totalOptionsFound: 0 };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute a stable fingerprint of a screen's element set.
|
|
5
|
+
* Sorted so insertion-order changes (React re-renders) don't produce false positives.
|
|
6
|
+
* Returns first 16 hex chars of SHA-256.
|
|
7
|
+
*/
|
|
8
|
+
export function computeElementHash(
|
|
9
|
+
elements: Array<{ role: string; meaning: string }>,
|
|
10
|
+
): string {
|
|
11
|
+
const pairs = elements
|
|
12
|
+
.map((e) => `${e.role.toLowerCase()}:${e.meaning.toLowerCase().trim()}`)
|
|
13
|
+
.sort()
|
|
14
|
+
.join('|');
|
|
15
|
+
return createHash('sha256').update(pairs).digest('hex').slice(0, 16);
|
|
16
|
+
}
|
package/src/email-otp.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
|
|
11
|
+
export type EmailOTPProvider = 'mailslurp' | 'imap' | 'mailpit';
|
|
12
|
+
|
|
13
|
+
export interface EmailContent {
|
|
14
|
+
/** 4-8 digit OTP code, if found. */
|
|
15
|
+
otp?: string;
|
|
16
|
+
/** Magic link URL to navigate to, if found. */
|
|
17
|
+
magicLink?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EmailOTPConfig {
|
|
21
|
+
provider: EmailOTPProvider;
|
|
22
|
+
// MailSlurp
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
inboxId?: string;
|
|
25
|
+
// IMAP (Gmail app-password, Outlook, custom IMAP)
|
|
26
|
+
imapHost?: string;
|
|
27
|
+
imapPort?: number;
|
|
28
|
+
imapUser?: string;
|
|
29
|
+
imapPass?: string;
|
|
30
|
+
imapTls?: boolean;
|
|
31
|
+
// Mailpit (REST API at baseUrl)
|
|
32
|
+
mailpitBaseUrl?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
36
|
+
const MAX_WAIT_MS = 60_000;
|
|
37
|
+
|
|
38
|
+
/** Extract OTP code from email body text. Prefers 6-digit, falls back to 4-8 digit. */
|
|
39
|
+
function extractOTP(text: string): string | null {
|
|
40
|
+
const m6 = text.match(/\b(\d{6})\b/);
|
|
41
|
+
if (m6) return m6[1];
|
|
42
|
+
const m48 = text.match(/\b(\d{4,8})\b/);
|
|
43
|
+
if (m48) return m48[1];
|
|
44
|
+
const magic = text.match(/\b([A-Z0-9]{6,10})\b/);
|
|
45
|
+
if (magic) return magic[1];
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Extract a magic-link URL from email body text. Matches /verify, /magic, /auth/callback, ?token= etc. */
|
|
50
|
+
function extractMagicLink(text: string): string | null {
|
|
51
|
+
const magicPath = /\/(verify|magic|login|auth|confirm|sign-in)[/?#]/i;
|
|
52
|
+
const magicQuery = /[?&](token|magic_token|login_token|auth_token|key)=/i;
|
|
53
|
+
const urlMatches = text.match(/https:\/\/[^\s<>"')\]]+/g);
|
|
54
|
+
if (!urlMatches) return null;
|
|
55
|
+
for (const raw of urlMatches) {
|
|
56
|
+
try {
|
|
57
|
+
const u = new URL(raw.replace(/[>'")\].,]+$/, ''));
|
|
58
|
+
if (magicPath.test(u.pathname) || magicQuery.test(u.search)) return u.toString();
|
|
59
|
+
} catch { /* skip malformed URLs */ }
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseEmailContent(text: string): EmailContent {
|
|
65
|
+
const magicLink = extractMagicLink(text);
|
|
66
|
+
if (magicLink) return { magicLink };
|
|
67
|
+
const otp = extractOTP(text);
|
|
68
|
+
if (otp) return { otp };
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── MailSlurp ────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const MAILSLURP_BASE = 'https://api.mailslurp.com';
|
|
75
|
+
|
|
76
|
+
async function waitForContentMailSlurp(apiKey: string, inboxId: string, since: string): Promise<EmailContent | null> {
|
|
77
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
80
|
+
try {
|
|
81
|
+
const listRes = await fetch(
|
|
82
|
+
`${MAILSLURP_BASE}/inboxes/${inboxId}/emails?sort=DESC&limit=5&since=${encodeURIComponent(since)}`,
|
|
83
|
+
{ headers: { 'x-api-key': apiKey } }
|
|
84
|
+
);
|
|
85
|
+
if (!listRes.ok) continue;
|
|
86
|
+
const emails = await listRes.json() as Array<{ id: string }>;
|
|
87
|
+
if (!emails.length) continue;
|
|
88
|
+
const bodyRes = await fetch(`${MAILSLURP_BASE}/emails/${emails[0].id}`, { headers: { 'x-api-key': apiKey } });
|
|
89
|
+
if (!bodyRes.ok) continue;
|
|
90
|
+
const email = await bodyRes.json() as { body?: string; subject?: string };
|
|
91
|
+
const content = parseEmailContent((email.body ?? '') + ' ' + (email.subject ?? ''));
|
|
92
|
+
if (content.otp || content.magicLink) return content;
|
|
93
|
+
} catch { /* retry */ }
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function getInboxEmailMailSlurp(apiKey: string, inboxId: string): Promise<string | null> {
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`${MAILSLURP_BASE}/inboxes/${inboxId}`, { headers: { 'x-api-key': apiKey } });
|
|
101
|
+
if (!res.ok) return null;
|
|
102
|
+
const data = await res.json() as { emailAddress?: string };
|
|
103
|
+
return data.emailAddress ?? null;
|
|
104
|
+
} catch { return null; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Mailpit (SMTP trap REST API) ──────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
async function waitForContentMailpit(baseUrl: string, since: Date): Promise<EmailContent | null> {
|
|
110
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
111
|
+
const sinceMs = since.getTime();
|
|
112
|
+
while (Date.now() < deadline) {
|
|
113
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${baseUrl}/api/v1/messages?limit=10`);
|
|
116
|
+
if (!res.ok) continue;
|
|
117
|
+
const data = await res.json() as { messages?: Array<{ ID: string; Created: string; Snippet: string; Subject: string }> };
|
|
118
|
+
const recent = (data.messages ?? []).filter(m => new Date(m.Created).getTime() > sinceMs);
|
|
119
|
+
for (const msg of recent) {
|
|
120
|
+
const snippet = parseEmailContent(msg.Snippet + ' ' + msg.Subject);
|
|
121
|
+
if (snippet.otp || snippet.magicLink) return snippet;
|
|
122
|
+
// Fetch full body — snippet may truncate the magic link
|
|
123
|
+
const bodyRes = await fetch(`${baseUrl}/api/v1/message/${msg.ID}`);
|
|
124
|
+
if (!bodyRes.ok) continue;
|
|
125
|
+
const body = await bodyRes.json() as { Text?: { Body?: string }; Subject?: string };
|
|
126
|
+
const full = parseEmailContent((body.Text?.Body ?? '') + ' ' + (body.Subject ?? ''));
|
|
127
|
+
if (full.otp || full.magicLink) return full;
|
|
128
|
+
}
|
|
129
|
+
} catch { /* retry */ }
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── IMAP (any mailbox) ────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async function waitForContentImap(
|
|
137
|
+
cfg: { host: string; port: number; user: string; pass: string; tls: boolean },
|
|
138
|
+
since: Date
|
|
139
|
+
): Promise<EmailContent | null> {
|
|
140
|
+
let ImapFlow: any;
|
|
141
|
+
try {
|
|
142
|
+
const mod = await import('imapflow');
|
|
143
|
+
ImapFlow = mod.ImapFlow;
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error('IMAP provider requires imapflow package. Add it to dependencies.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const client = new ImapFlow({
|
|
149
|
+
host: cfg.host,
|
|
150
|
+
port: cfg.port,
|
|
151
|
+
secure: cfg.tls,
|
|
152
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
153
|
+
logger: false,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await client.connect();
|
|
158
|
+
const lock = await client.getMailboxLock('INBOX');
|
|
159
|
+
try {
|
|
160
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
161
|
+
while (Date.now() < deadline) {
|
|
162
|
+
const uids = await client.search({ seen: false, since });
|
|
163
|
+
if (uids.length) {
|
|
164
|
+
for await (const msg of client.fetch(uids.slice(-5), { source: true })) {
|
|
165
|
+
const { simpleParser } = await import('mailparser');
|
|
166
|
+
const parsed = await simpleParser(msg.source);
|
|
167
|
+
const text = (parsed.text ?? '') + ' ' + (parsed.subject ?? '');
|
|
168
|
+
const content = parseEmailContent(text);
|
|
169
|
+
if (content.otp || content.magicLink) return content;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
} finally {
|
|
176
|
+
lock.release();
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
await client.logout().catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Wait for an email and return its OTP code or magic link URL.
|
|
187
|
+
* Prefer this over waitForEmailOTP when the app may send passwordless magic links.
|
|
188
|
+
*/
|
|
189
|
+
export async function waitForEmailContent(
|
|
190
|
+
config: EmailOTPConfig,
|
|
191
|
+
since: string | Date
|
|
192
|
+
): Promise<EmailContent | null> {
|
|
193
|
+
const sinceStr = typeof since === 'string' ? since : since.toISOString();
|
|
194
|
+
const sinceDate = typeof since === 'string' ? new Date(since) : since;
|
|
195
|
+
|
|
196
|
+
switch (config.provider) {
|
|
197
|
+
case 'mailslurp':
|
|
198
|
+
if (!config.apiKey || !config.inboxId) throw new Error('mailslurp requires apiKey + inboxId');
|
|
199
|
+
return waitForContentMailSlurp(config.apiKey, config.inboxId, sinceStr);
|
|
200
|
+
|
|
201
|
+
case 'mailpit':
|
|
202
|
+
return waitForContentMailpit(config.mailpitBaseUrl ?? 'http://mailpit:8025', sinceDate);
|
|
203
|
+
|
|
204
|
+
case 'imap':
|
|
205
|
+
if (!config.imapHost || !config.imapUser || !config.imapPass) {
|
|
206
|
+
throw new Error('imap requires imapHost, imapUser, imapPass');
|
|
207
|
+
}
|
|
208
|
+
return waitForContentImap({
|
|
209
|
+
host: config.imapHost,
|
|
210
|
+
port: config.imapPort ?? 993,
|
|
211
|
+
user: config.imapUser,
|
|
212
|
+
pass: config.imapPass,
|
|
213
|
+
tls: config.imapTls !== false,
|
|
214
|
+
}, sinceDate);
|
|
215
|
+
|
|
216
|
+
default:
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Wait for an OTP email and extract the code.
|
|
223
|
+
* @deprecated Use waitForEmailContent which also handles magic links.
|
|
224
|
+
*/
|
|
225
|
+
export async function waitForEmailOTP(
|
|
226
|
+
config: EmailOTPConfig,
|
|
227
|
+
since: string | Date
|
|
228
|
+
): Promise<string | null> {
|
|
229
|
+
const content = await waitForEmailContent(config, since);
|
|
230
|
+
return content?.otp ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get the email address for a MailSlurp inbox (used to display in UI).
|
|
235
|
+
* Returns null for IMAP/Mailpit providers — use imapUser directly.
|
|
236
|
+
*/
|
|
237
|
+
export async function getInboxEmail(
|
|
238
|
+
configOrApiKey: EmailOTPConfig | string,
|
|
239
|
+
inboxId?: string
|
|
240
|
+
): Promise<string | null> {
|
|
241
|
+
// Legacy call: getInboxEmail(apiKey, inboxId)
|
|
242
|
+
if (typeof configOrApiKey === 'string') {
|
|
243
|
+
return getInboxEmailMailSlurp(configOrApiKey, inboxId ?? '');
|
|
244
|
+
}
|
|
245
|
+
if (configOrApiKey.provider === 'mailslurp' && configOrApiKey.apiKey && configOrApiKey.inboxId) {
|
|
246
|
+
return getInboxEmailMailSlurp(configOrApiKey.apiKey, configOrApiKey.inboxId);
|
|
247
|
+
}
|
|
248
|
+
if (configOrApiKey.provider === 'imap') {
|
|
249
|
+
return configOrApiKey.imapUser ?? null;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|