@taodigital/crawler 0.0.0-stage → 1.2.0
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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/ai-login.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod';\nimport type { Stagehand } from '@browserbasehq/stagehand';\nimport { AppBrain } from '@detiq/app-brain';\nimport { solveCaptcha } from './captcha-solver.js';\nimport { waitForEmailContent } from './email-otp.js';\nimport { logger } from './logger.js';\nimport type { Credentials } from './auth.js';\nimport { simulateMouseMovement } from './stealth/human-behavior.js';\n\nconst CLICK_INTENT = /\\b(click|tap|press|submit|select|choose|toggle)\\b/i;\n\n/** Stagehand V3 enforces elementId as /^\\d+-\\d+$/ \u2014 LLM sometimes returns bare integers.\n * Catch the resulting AI_NoObjectGeneratedError so callers can fall back to DOM. */\nfunction isNoObjectGeneratedError(err: any): boolean {\n const name = String(err?.name ?? '');\n const msg = String(err?.message ?? '');\n return name.includes('NoObjectGenerated') || msg.includes('NoObjectGenerated') || msg.includes('No object generated');\n}\n\n/** Simulate human mouse movement before click-type actions, then call stagehand.act(). */\nasync function actWithMouseSim(stagehand: Stagehand, instruction: string): Promise<any> {\n if (CLICK_INTENT.test(instruction)) {\n try {\n const pg = await (stagehand as any).resolvePage();\n const viewport = pg.viewportSize() ?? { width: 1280, height: 720 };\n // Move mouse toward center of page with some randomness before Stagehand clicks\n const targetX = viewport.width * 0.3 + Math.random() * viewport.width * 0.4;\n const targetY = viewport.height * 0.2 + Math.random() * viewport.height * 0.5;\n await simulateMouseMovement(pg, targetX, targetY);\n } catch { /* non-fatal \u2014 don't block the action */ }\n }\n return stagehand.act(instruction);\n}\n\n/**\n * Recorded login step from the Chrome extension's step recorder.\n * {username} and {password} in `value` are substituted at replay time.\n */\nexport interface LoginStep {\n action: 'fill' | 'click' | 'wait';\n selector: string;\n value?: string;\n ms?: number;\n}\n\nconst TwoFASchema = z.object({\n has2FA: z.boolean(),\n fieldLabel: z.string().optional(),\n});\n\nconst CAPTCHA_SELECTORS = [\n 'iframe[src*=\"recaptcha\"]',\n 'iframe[src*=\"hcaptcha\"]',\n 'iframe[src*=\"challenges.cloudflare.com\"]',\n '.cf-turnstile',\n '[data-sitekey]',\n '#captcha',\n '.captcha',\n '[id*=\"captcha\"]',\n '[class*=\"captcha\"]',\n];\n\n/**\n * AI-guided login using Stagehand act/extract.\n *\n * @param loginInstructions - Optional natural language description of the login flow.\n * When provided, an initial stagehand.act() is called with these instructions to\n * navigate the UI (e.g. \"click Sign In in the nav, fill email, click Continue\").\n * The standard 3-step credential fill follows immediately after.\n * @param opts - Optional tenant/project/job IDs for writing AUTH_STEP CrawlEvidence.\n */\nexport async function loginWithStagehand(\n stagehand: Stagehand,\n creds: Credentials,\n startUrl: string,\n captchaOpts?: { apiKey?: string; provider?: '2captcha' | 'capsolver' },\n emailOtpOpts?: { apiKey?: string; inboxId?: string },\n loginInstructions?: string,\n opts?: { tenantId?: string; projectId?: string; jobId?: string },\n): Promise<boolean> {\n async function logStep(step: string, fn: () => Promise<any>): Promise<{ success: boolean; detail: string }> {\n const t0 = Date.now();\n try {\n const r = await fn();\n const result = { success: true, detail: r?.description ?? r?.message ?? step };\n if (opts?.tenantId && opts?.projectId) {\n AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {\n jobId: opts?.jobId,\n type: 'AUTH_STEP',\n payload: { step, ...result, ms: Date.now() - t0 },\n }).catch(() => {});\n }\n return result;\n } catch (e: any) {\n const result = { success: false, detail: e.message ?? 'failed' };\n if (opts?.tenantId && opts?.projectId) {\n AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {\n jobId: opts?.jobId,\n type: 'AUTH_STEP',\n payload: { step, ...result, ms: Date.now() - t0 },\n }).catch(() => {});\n }\n throw e;\n }\n }\n\n try {\n const pg = await (stagehand as any).resolvePage();\n\n // CAPTCHA detection\n for (const sel of CAPTCHA_SELECTORS) {\n const count = await pg.locator(sel).count().catch(() => 0);\n if (count > 0) {\n logger.warn({ selector: sel }, '[crawler] CAPTCHA detected on login page');\n if (captchaOpts?.apiKey) {\n logger.info({ provider: captchaOpts.provider ?? '2captcha' }, '[crawler] Attempting auto-solve');\n try {\n const result = await solveCaptcha(pg, startUrl, captchaOpts.apiKey, captchaOpts.provider ?? '2captcha');\n if (result) {\n logger.info({ provider: result.provider }, '[crawler] CAPTCHA solved \u2014 proceeding with login');\n break;\n }\n } catch (solveErr) {\n logger.warn({ err: solveErr }, '[crawler] CAPTCHA solve failed \u2014 falling back to manual error');\n throw new Error(`CAPTCHA_DETECTED: Auto-solve failed (${String(solveErr)}). Use session capture instead.`);\n }\n } else {\n throw new Error(\n 'CAPTCHA_DETECTED: Login page has a CAPTCHA. Use \"Session Capture \u2192 Launch Browser\" to log in manually, then capture the session. Alternatively, disable CAPTCHA for the ZeTa crawler IP.'\n );\n }\n break;\n }\n }\n\n // If caller provided natural language login instructions, use them to navigate to the login form\n const MAX_LOGIN_INSTRUCTIONS = 2000;\n if (loginInstructions && loginInstructions.length > MAX_LOGIN_INSTRUCTIONS) {\n throw new Error(`loginInstructions exceeds ${MAX_LOGIN_INSTRUCTIONS} character limit`);\n }\n if (loginInstructions) {\n await logStep('custom_recipe', async () => {\n try {\n return await actWithMouseSim(stagehand, loginInstructions);\n } catch (err: any) {\n if (!isNoObjectGeneratedError(err)) throw err;\n logger.warn('[crawler] custom_recipe act() failed \u2014 Stagehand V3 elementId schema error, skipping to standard fill');\n return { description: 'custom_recipe skipped (schema error)' };\n }\n });\n await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(1500));\n }\n\n await logStep('fill_username', async () => {\n try {\n return await actWithMouseSim(stagehand, `Type \"${creds.username}\" into the username or email input field`);\n } catch (err: any) {\n if (!isNoObjectGeneratedError(err)) throw err;\n logger.warn('[crawler] fill_username act() failed \u2014 falling back to DOM');\n const sel = 'input[type=\"email\"], input[autocomplete=\"email\"], input[autocomplete=\"username\"], input[name*=\"email\" i], input[name*=\"user\" i], input[placeholder*=\"email\" i], input[placeholder*=\"user\" i], input[id*=\"email\" i], input[id*=\"user\" i]';\n await pg.locator(sel).first().fill(creds.username);\n return { description: 'fill_username via DOM fallback' };\n }\n });\n // Don't send password to LLM \u2014 use direct Playwright fill to avoid\n // leaking credentials into the Stagehand/LLM provider's request logs.\n await logStep('fill_password', async () => {\n const observations = await stagehand.observe('Find the password input field');\n const selector = observations[0]?.selector || 'input[type=\"password\"]';\n try {\n await pg.locator(selector).fill(creds.password);\n } catch {\n // Stagehand locator failed (e.g. form re-rendered, proxy can't locate element).\n // Fall back to direct DOM fill via evaluate so the password never touches the LLM.\n await pg.evaluate((pwd: string) => {\n const input = document.querySelector('input[type=\"password\"]') as HTMLInputElement | null;\n if (!input) throw new Error('No password field found on page');\n const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;\n setter?.call(input, pwd);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n input.dispatchEvent(new Event('change', { bubbles: true }));\n }, creds.password);\n }\n });\n await logStep('click_submit', async () => {\n try {\n return await actWithMouseSim(stagehand, 'Click the sign in, log in, login, or continue button to submit credentials');\n } catch (err: any) {\n if (!isNoObjectGeneratedError(err)) throw err;\n logger.warn('[crawler] click_submit act() failed \u2014 falling back to DOM');\n const sel = 'button[type=\"submit\"], input[type=\"submit\"], button:has-text(\"Sign in\"), button:has-text(\"Log in\"), button:has-text(\"Login\"), button:has-text(\"Continue\"), button:has-text(\"Next\"), button:has-text(\"Sign In\"), [role=\"button\"]:has-text(\"Sign in\"), [role=\"button\"]:has-text(\"Login\")';\n await pg.locator(sel).first().click();\n await pg.waitForTimeout(500);\n return { description: 'click_submit via DOM fallback' };\n }\n });\n await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(2000));\n\n const twoFA = await stagehand.extract(\n 'Is there a two-factor authentication, OTP, or verification code input visible right now?',\n TwoFASchema as any\n );\n\n if (twoFA.has2FA) {\n if (opts?.tenantId && opts?.projectId) {\n AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {\n jobId: opts?.jobId,\n type: 'AUTH_STEP',\n payload: { step: '2fa_detect', success: true, detail: twoFA.fieldLabel ?? '2FA detected', ms: 0 },\n }).catch(() => {});\n }\n\n let code = creds.oneTimeCode ?? await creds.getOneTimeCode?.();\n\n if (!code && emailOtpOpts?.apiKey && emailOtpOpts?.inboxId) {\n const since = new Date().toISOString();\n logger.info('[crawler] 2FA detected \u2014 waiting for email OTP or magic link via MailSlurp');\n const emailContent = await waitForEmailContent(\n { provider: 'mailslurp', apiKey: emailOtpOpts.apiKey, inboxId: emailOtpOpts.inboxId },\n since\n );\n if (emailContent?.magicLink) {\n logger.info({ url: emailContent.magicLink }, '[crawler] Magic link received \u2014 navigating');\n await pg.goto(emailContent.magicLink, { waitUntil: 'domcontentloaded' });\n await pg.waitForLoadState('networkidle').catch(() => {});\n return true; // magic link navigation = login complete\n }\n code = emailContent?.otp ?? undefined;\n if (code) {\n logger.info('[crawler] Email OTP received');\n } else {\n logger.warn('[crawler] Email OTP timed out after 60s');\n }\n }\n\n if (!code) {\n console.warn('[crawler] 2FA detected but no oneTimeCode provided');\n return false;\n }\n await logStep('2fa_inject', async () => {\n try {\n return await actWithMouseSim(stagehand, `Enter \"${code}\" into the ${twoFA.fieldLabel ?? 'verification code'} field`);\n } catch (err: any) {\n if (!isNoObjectGeneratedError(err)) throw err;\n logger.warn('[crawler] 2fa_inject act() failed \u2014 falling back to DOM');\n const sel = 'input[autocomplete=\"one-time-code\"], input[name*=\"otp\" i], input[name*=\"code\" i], input[name*=\"token\" i], input[inputmode=\"numeric\"], input[type=\"number\"]';\n await pg.locator(sel).first().fill(code as string);\n return { description: '2fa_inject via DOM fallback' };\n }\n });\n await logStep('2fa_submit', async () => {\n try {\n return await actWithMouseSim(stagehand, 'Click verify or confirm to submit the verification code');\n } catch (err: any) {\n if (!isNoObjectGeneratedError(err)) throw err;\n logger.warn('[crawler] 2fa_submit act() failed \u2014 falling back to DOM');\n const sel = 'button[type=\"submit\"], button:has-text(\"Verify\"), button:has-text(\"Confirm\"), button:has-text(\"Submit\"), button:has-text(\"Continue\")';\n await pg.locator(sel).first().click();\n return { description: '2fa_submit via DOM fallback' };\n }\n });\n await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(2000));\n }\n\n // URL-based success check\n const currentUrl = pg.url();\n const loginPathPattern = /\\/(login|signin|sign-in|auth|authenticate|log-in|account\\/login)(\\?|$|\\/)/i;\n try {\n const { pathname } = new URL(currentUrl);\n const wasRedirected = currentUrl !== startUrl;\n const stillOnLoginPath = loginPathPattern.test(pathname);\n if (wasRedirected && !stillOnLoginPath) {\n logger.info({ url: currentUrl }, 'Login confirmed via URL change');\n return true;\n }\n } catch { /* invalid URL \u2014 fall through */ }\n\n const passwordCount = await pg.locator('input[type=\"password\"]').count().catch(() => 1);\n const bodyText = await pg.evaluate(() => (document as any).body?.innerText?.trim()?.length ?? 0).catch(() => 0);\n if (passwordCount === 0 && bodyText > 30) {\n logger.info('Login confirmed: password field no longer visible');\n return true;\n }\n\n logger.warn({ url: currentUrl }, 'Login appears failed');\n try {\n const screenshotPath = `/tmp/zeta-auth-failure-${Date.now()}.jpg`;\n await pg.screenshot({ path: screenshotPath, type: 'jpeg', quality: 70, fullPage: false });\n logger.warn({ screenshotPath }, '[crawler] Auth failure screenshot saved');\n } catch { /* non-fatal */ }\n return false;\n } catch (err) {\n const msg = String((err as Error).message ?? err);\n if (msg.includes('CAPTCHA_DETECTED')) throw err;\n logger.error({ err }, 'Login error');\n return false;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,cAAc;AAEvB,SAAS,6BAA6B;AAEtC,MAAM,eAAe;AAIrB,SAAS,yBAAyB,KAAmB;AACnD,QAAM,OAAO,OAAO,KAAK,QAAQ,EAAE;AACnC,QAAM,MAAM,OAAO,KAAK,WAAW,EAAE;AACrC,SAAO,KAAK,SAAS,mBAAmB,KAAK,IAAI,SAAS,mBAAmB,KAAK,IAAI,SAAS,qBAAqB;AACtH;AAGA,eAAe,gBAAgB,WAAsB,aAAmC;AACtF,MAAI,aAAa,KAAK,WAAW,GAAG;AAClC,QAAI;AACF,YAAM,KAAK,MAAO,UAAkB,YAAY;AAChD,YAAM,WAAW,GAAG,aAAa,KAAK,EAAE,OAAO,MAAM,QAAQ,IAAI;AAEjE,YAAM,UAAU,SAAS,QAAQ,MAAM,KAAK,OAAO,IAAI,SAAS,QAAQ;AACxE,YAAM,UAAU,SAAS,SAAS,MAAM,KAAK,OAAO,IAAI,SAAS,SAAS;AAC1E,YAAM,sBAAsB,IAAI,SAAS,OAAO;AAAA,IAClD,QAAQ;AAAA,IAA2C;AAAA,EACrD;AACA,SAAO,UAAU,IAAI,WAAW;AAClC;AAaA,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,QAAQ,EAAE,QAAQ;AAAA,EAClB,YAAY,EAAE,OAAO,EAAE,SAAS;AAClC,CAAC;AAED,MAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAWA,eAAsB,mBACpB,WACA,OACA,UACA,aACA,cACA,mBACA,MACkB;AAClB,iBAAe,QAAQ,MAAc,IAAuE;AAC1G,UAAM,KAAK,KAAK,IAAI;AACpB,QAAI;AACF,YAAM,IAAI,MAAM,GAAG;AACnB,YAAM,SAAS,EAAE,SAAS,MAAM,QAAQ,GAAG,eAAe,GAAG,WAAW,KAAK;AAC7E,UAAI,MAAM,YAAY,MAAM,WAAW;AACrC,iBAAS,kBAAkB,KAAK,UAAU,KAAK,WAAW;AAAA,UACxD,OAAO,MAAM;AAAA,UACb,MAAM;AAAA,UACN,SAAS,EAAE,MAAM,GAAG,QAAQ,IAAI,KAAK,IAAI,IAAI,GAAG;AAAA,QAClD,CAAC,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACnB;AACA,aAAO;AAAA,IACT,SAAS,GAAQ;AACf,YAAM,SAAS,EAAE,SAAS,OAAO,QAAQ,EAAE,WAAW,SAAS;AAC/D,UAAI,MAAM,YAAY,MAAM,WAAW;AACrC,iBAAS,kBAAkB,KAAK,UAAU,KAAK,WAAW;AAAA,UACxD,OAAO,MAAM;AAAA,UACb,MAAM;AAAA,UACN,SAAS,EAAE,MAAM,GAAG,QAAQ,IAAI,KAAK,IAAI,IAAI,GAAG;AAAA,QAClD,CAAC,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACnB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI;AACF,UAAM,KAAK,MAAO,UAAkB,YAAY;AAGhD,eAAW,OAAO,mBAAmB;AACnC,YAAM,QAAQ,MAAM,GAAG,QAAQ,GAAG,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACzD,UAAI,QAAQ,GAAG;AACb,eAAO,KAAK,EAAE,UAAU,IAAI,GAAG,0CAA0C;AACzE,YAAI,aAAa,QAAQ;AACvB,iBAAO,KAAK,EAAE,UAAU,YAAY,YAAY,WAAW,GAAG,iCAAiC;AAC/F,cAAI;AACF,kBAAM,SAAS,MAAM,aAAa,IAAI,UAAU,YAAY,QAAQ,YAAY,YAAY,UAAU;AACtG,gBAAI,QAAQ;AACV,qBAAO,KAAK,EAAE,UAAU,OAAO,SAAS,GAAG,uDAAkD;AAC7F;AAAA,YACF;AAAA,UACF,SAAS,UAAU;AACjB,mBAAO,KAAK,EAAE,KAAK,SAAS,GAAG,oEAA+D;AAC9F,kBAAM,IAAI,MAAM,wCAAwC,OAAO,QAAQ,CAAC,iCAAiC;AAAA,UAC3G;AAAA,QACF,OAAO;AACL,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AACA;AAAA,MACF;AAAA,IACF;AAGA,UAAM,yBAAyB;AAC/B,QAAI,qBAAqB,kBAAkB,SAAS,wBAAwB;AAC1E,YAAM,IAAI,MAAM,6BAA6B,sBAAsB,kBAAkB;AAAA,IACvF;AACA,QAAI,mBAAmB;AACrB,YAAM,QAAQ,iBAAiB,YAAY;AACzC,YAAI;AACF,iBAAO,MAAM,gBAAgB,WAAW,iBAAiB;AAAA,QAC3D,SAAS,KAAU;AACjB,cAAI,CAAC,yBAAyB,GAAG,EAAG,OAAM;AAC1C,iBAAO,KAAK,4GAAuG;AACnH,iBAAO,EAAE,aAAa,uCAAuC;AAAA,QAC/D;AAAA,MACF,CAAC;AACD,YAAM,GAAG,iBAAiB,aAAa,EAAE,MAAM,MAAM,GAAG,eAAe,IAAI,CAAC;AAAA,IAC9E;AAEA,UAAM,QAAQ,iBAAiB,YAAY;AACzC,UAAI;AACF,eAAO,MAAM,gBAAgB,WAAW,SAAS,MAAM,QAAQ,0CAA0C;AAAA,MAC3G,SAAS,KAAU;AACjB,YAAI,CAAC,yBAAyB,GAAG,EAAG,OAAM;AAC1C,eAAO,KAAK,iEAA4D;AACxE,cAAM,MAAM;AACZ,cAAM,GAAG,QAAQ,GAAG,EAAE,MAAM,EAAE,KAAK,MAAM,QAAQ;AACjD,eAAO,EAAE,aAAa,iCAAiC;AAAA,MACzD;AAAA,IACF,CAAC;AAGD,UAAM,QAAQ,iBAAiB,YAAY;AACzC,YAAM,eAAe,MAAM,UAAU,QAAQ,+BAA+B;AAC5E,YAAM,WAAW,aAAa,CAAC,GAAG,YAAY;AAC9C,UAAI;AACF,cAAM,GAAG,QAAQ,QAAQ,EAAE,KAAK,MAAM,QAAQ;AAAA,MAChD,QAAQ;AAGN,cAAM,GAAG,SAAS,CAAC,QAAgB;AACjC,gBAAM,QAAQ,SAAS,cAAc,wBAAwB;AAC7D,cAAI,CAAC,MAAO,OAAM,IAAI,MAAM,iCAAiC;AAC7D,gBAAM,SAAS,OAAO,yBAAyB,OAAO,iBAAiB,WAAW,OAAO,GAAG;AAC5F,kBAAQ,KAAK,OAAO,GAAG;AACvB,gBAAM,cAAc,IAAI,MAAM,SAAS,EAAE,SAAS,KAAK,CAAC,CAAC;AACzD,gBAAM,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;AAAA,QAC5D,GAAG,MAAM,QAAQ;AAAA,MACnB;AAAA,IACF,CAAC;AACD,UAAM,QAAQ,gBAAgB,YAAY;AACxC,UAAI;AACF,eAAO,MAAM,gBAAgB,WAAW,4EAA4E;AAAA,MACtH,SAAS,KAAU;AACjB,YAAI,CAAC,yBAAyB,GAAG,EAAG,OAAM;AAC1C,eAAO,KAAK,gEAA2D;AACvE,cAAM,MAAM;AACZ,cAAM,GAAG,QAAQ,GAAG,EAAE,MAAM,EAAE,MAAM;AACpC,cAAM,GAAG,eAAe,GAAG;AAC3B,eAAO,EAAE,aAAa,gCAAgC;AAAA,MACxD;AAAA,IACF,CAAC;AACD,UAAM,GAAG,iBAAiB,aAAa,EAAE,MAAM,MAAM,GAAG,eAAe,GAAI,CAAC;AAE5E,UAAM,QAAQ,MAAM,UAAU;AAAA,MAC5B;AAAA,MACA;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ;AAChB,UAAI,MAAM,YAAY,MAAM,WAAW;AACrC,iBAAS,kBAAkB,KAAK,UAAU,KAAK,WAAW;AAAA,UACxD,OAAO,MAAM;AAAA,UACb,MAAM;AAAA,UACN,SAAS,EAAE,MAAM,cAAc,SAAS,MAAM,QAAQ,MAAM,cAAc,gBAAgB,IAAI,EAAE;AAAA,QAClG,CAAC,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACnB;AAEA,UAAI,OAAO,MAAM,eAAe,MAAM,MAAM,iBAAiB;AAE7D,UAAI,CAAC,QAAQ,cAAc,UAAU,cAAc,SAAS;AAC1D,cAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY;AACrC,eAAO,KAAK,iFAA4E;AACxF,cAAM,eAAe,MAAM;AAAA,UACzB,EAAE,UAAU,aAAa,QAAQ,aAAa,QAAQ,SAAS,aAAa,QAAQ;AAAA,UACpF;AAAA,QACF;AACA,YAAI,cAAc,WAAW;AAC3B,iBAAO,KAAK,EAAE,KAAK,aAAa,UAAU,GAAG,iDAA4C;AACzF,gBAAM,GAAG,KAAK,aAAa,WAAW,EAAE,WAAW,mBAAmB,CAAC;AACvE,gBAAM,GAAG,iBAAiB,aAAa,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AACvD,iBAAO;AAAA,QACT;AACA,eAAO,cAAc,OAAO;AAC5B,YAAI,MAAM;AACR,iBAAO,KAAK,8BAA8B;AAAA,QAC5C,OAAO;AACL,iBAAO,KAAK,yCAAyC;AAAA,QACvD;AAAA,MACF;AAEA,UAAI,CAAC,MAAM;AACT,gBAAQ,KAAK,oDAAoD;AACjE,eAAO;AAAA,MACT;AACA,YAAM,QAAQ,cAAc,YAAY;AACtC,YAAI;AACF,iBAAO,MAAM,gBAAgB,WAAW,UAAU,IAAI,cAAc,MAAM,cAAc,mBAAmB,QAAQ;AAAA,QACrH,SAAS,KAAU;AACjB,cAAI,CAAC,yBAAyB,GAAG,EAAG,OAAM;AAC1C,iBAAO,KAAK,8DAAyD;AACrE,gBAAM,MAAM;AACZ,gBAAM,GAAG,QAAQ,GAAG,EAAE,MAAM,EAAE,KAAK,IAAc;AACjD,iBAAO,EAAE,aAAa,8BAA8B;AAAA,QACtD;AAAA,MACF,CAAC;AACD,YAAM,QAAQ,cAAc,YAAY;AACtC,YAAI;AACF,iBAAO,MAAM,gBAAgB,WAAW,yDAAyD;AAAA,QACnG,SAAS,KAAU;AACjB,cAAI,CAAC,yBAAyB,GAAG,EAAG,OAAM;AAC1C,iBAAO,KAAK,8DAAyD;AACrE,gBAAM,MAAM;AACZ,gBAAM,GAAG,QAAQ,GAAG,EAAE,MAAM,EAAE,MAAM;AACpC,iBAAO,EAAE,aAAa,8BAA8B;AAAA,QACtD;AAAA,MACF,CAAC;AACD,YAAM,GAAG,iBAAiB,aAAa,EAAE,MAAM,MAAM,GAAG,eAAe,GAAI,CAAC;AAAA,IAC9E;AAGA,UAAM,aAAa,GAAG,IAAI;AAC1B,UAAM,mBAAmB;AACzB,QAAI;AACF,YAAM,EAAE,SAAS,IAAI,IAAI,IAAI,UAAU;AACvC,YAAM,gBAAgB,eAAe;AACrC,YAAM,mBAAmB,iBAAiB,KAAK,QAAQ;AACvD,UAAI,iBAAiB,CAAC,kBAAkB;AACtC,eAAO,KAAK,EAAE,KAAK,WAAW,GAAG,gCAAgC;AACjE,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AAAA,IAAmC;AAE3C,UAAM,gBAAgB,MAAM,GAAG,QAAQ,wBAAwB,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACtF,UAAM,WAAW,MAAM,GAAG,SAAS,MAAO,SAAiB,MAAM,WAAW,KAAK,GAAG,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC;AAC9G,QAAI,kBAAkB,KAAK,WAAW,IAAI;AACxC,aAAO,KAAK,mDAAmD;AAC/D,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,EAAE,KAAK,WAAW,GAAG,sBAAsB;AACvD,QAAI;AACF,YAAM,iBAAiB,0BAA0B,KAAK,IAAI,CAAC;AAC3D,YAAM,GAAG,WAAW,EAAE,MAAM,gBAAgB,MAAM,QAAQ,SAAS,IAAI,UAAU,MAAM,CAAC;AACxF,aAAO,KAAK,EAAE,eAAe,GAAG,yCAAyC;AAAA,IAC3E,QAAQ;AAAA,IAAkB;AAC1B,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,UAAM,MAAM,OAAQ,IAAc,WAAW,GAAG;AAChD,QAAI,IAAI,SAAS,kBAAkB,EAAG,OAAM;AAC5C,WAAO,MAAM,EAAE,IAAI,GAAG,aAAa;AACnC,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth token rotator — manage a pool of authenticated sessions for multi-account crawls.
|
|
3
|
+
*
|
|
4
|
+
* Use case: crawl the same app as admin+user+guest in one crawl job to discover
|
|
5
|
+
* role-specific screens that only appear when authenticated with different privileges.
|
|
6
|
+
* Each session is a Playwright storageState JSON string.
|
|
7
|
+
*
|
|
8
|
+
* Sessions are checked out (locked) before use and returned after the page is done.
|
|
9
|
+
* Round-robin assignment with stale-session eviction after maxAgeMs.
|
|
10
|
+
*/
|
|
11
|
+
export interface SessionEntry {
|
|
12
|
+
id: string;
|
|
13
|
+
role: string;
|
|
14
|
+
storageState: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
checkedOutAt: number | null;
|
|
17
|
+
requestCount: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SessionPoolOpts {
|
|
20
|
+
sessions: Array<{
|
|
21
|
+
role: string;
|
|
22
|
+
storageState: string;
|
|
23
|
+
}>;
|
|
24
|
+
maxAgeMs?: number;
|
|
25
|
+
maxRequestsPerSession?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare class AuthTokenRotator {
|
|
28
|
+
private pool;
|
|
29
|
+
private readonly maxAgeMs;
|
|
30
|
+
private readonly maxRequests;
|
|
31
|
+
constructor(opts: SessionPoolOpts);
|
|
32
|
+
/** Get the next available session. Returns null if all are checked out. */
|
|
33
|
+
checkout(): SessionEntry | null;
|
|
34
|
+
/** Return a session to the pool after use. */
|
|
35
|
+
checkin(sessionId: string, updatedStorageState?: string): void;
|
|
36
|
+
/** Get all available sessions (for parallel crawl setup). */
|
|
37
|
+
getAll(): ReadonlyArray<SessionEntry>;
|
|
38
|
+
/** Stats for monitoring. */
|
|
39
|
+
stats(): {
|
|
40
|
+
total: number;
|
|
41
|
+
available: number;
|
|
42
|
+
checkedOut: number;
|
|
43
|
+
stale: number;
|
|
44
|
+
};
|
|
45
|
+
/** Add a new session to the pool at runtime. */
|
|
46
|
+
addSession(role: string, storageState: string): string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
class AuthTokenRotator {
|
|
2
|
+
pool;
|
|
3
|
+
maxAgeMs;
|
|
4
|
+
maxRequests;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.maxAgeMs = opts.maxAgeMs ?? 4 * 60 * 60 * 1e3;
|
|
7
|
+
this.maxRequests = opts.maxRequestsPerSession ?? 100;
|
|
8
|
+
this.pool = opts.sessions.map((s, i) => ({
|
|
9
|
+
id: `session-${i}-${s.role}`,
|
|
10
|
+
role: s.role,
|
|
11
|
+
storageState: s.storageState,
|
|
12
|
+
createdAt: Date.now(),
|
|
13
|
+
checkedOutAt: null,
|
|
14
|
+
requestCount: 0
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
/** Get the next available session. Returns null if all are checked out. */
|
|
18
|
+
checkout() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const available = this.pool.filter(
|
|
21
|
+
(s) => !s.checkedOutAt && now - s.createdAt < this.maxAgeMs && s.requestCount < this.maxRequests
|
|
22
|
+
);
|
|
23
|
+
if (available.length === 0) return null;
|
|
24
|
+
available.sort((a, b) => a.requestCount - b.requestCount);
|
|
25
|
+
const session = available[0];
|
|
26
|
+
session.checkedOutAt = now;
|
|
27
|
+
return session;
|
|
28
|
+
}
|
|
29
|
+
/** Return a session to the pool after use. */
|
|
30
|
+
checkin(sessionId, updatedStorageState) {
|
|
31
|
+
const s = this.pool.find((e) => e.id === sessionId);
|
|
32
|
+
if (!s) return;
|
|
33
|
+
s.checkedOutAt = null;
|
|
34
|
+
s.requestCount++;
|
|
35
|
+
if (updatedStorageState) s.storageState = updatedStorageState;
|
|
36
|
+
}
|
|
37
|
+
/** Get all available sessions (for parallel crawl setup). */
|
|
38
|
+
getAll() {
|
|
39
|
+
return this.pool;
|
|
40
|
+
}
|
|
41
|
+
/** Stats for monitoring. */
|
|
42
|
+
stats() {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const stale = this.pool.filter((s) => now - s.createdAt >= this.maxAgeMs || s.requestCount >= this.maxRequests).length;
|
|
45
|
+
const checkedOut = this.pool.filter((s) => !!s.checkedOutAt).length;
|
|
46
|
+
return { total: this.pool.length, available: this.pool.length - checkedOut - stale, checkedOut, stale };
|
|
47
|
+
}
|
|
48
|
+
/** Add a new session to the pool at runtime. */
|
|
49
|
+
addSession(role, storageState) {
|
|
50
|
+
const id = `session-${this.pool.length}-${role}`;
|
|
51
|
+
this.pool.push({ id, role, storageState, createdAt: Date.now(), checkedOutAt: null, requestCount: 0 });
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
AuthTokenRotator
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=auth-token-rotator.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/auth-token-rotator.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Auth token rotator \u2014 manage a pool of authenticated sessions for multi-account crawls.\n *\n * Use case: crawl the same app as admin+user+guest in one crawl job to discover\n * role-specific screens that only appear when authenticated with different privileges.\n * Each session is a Playwright storageState JSON string.\n *\n * Sessions are checked out (locked) before use and returned after the page is done.\n * Round-robin assignment with stale-session eviction after maxAgeMs.\n */\n\nexport interface SessionEntry {\n id: string;\n role: string; // 'admin' | 'user' | 'guest' | custom label\n storageState: string; // Playwright StorageState JSON\n createdAt: number; // Date.now()\n checkedOutAt: number | null;\n requestCount: number;\n}\n\nexport interface SessionPoolOpts {\n sessions: Array<{ role: string; storageState: string }>;\n maxAgeMs?: number; // evict sessions older than this (default: 4h)\n maxRequestsPerSession?: number; // rotate after N requests (default: 100)\n}\n\nexport class AuthTokenRotator {\n private pool: SessionEntry[];\n private readonly maxAgeMs: number;\n private readonly maxRequests: number;\n\n constructor(opts: SessionPoolOpts) {\n this.maxAgeMs = opts.maxAgeMs ?? 4 * 60 * 60 * 1000;\n this.maxRequests = opts.maxRequestsPerSession ?? 100;\n this.pool = opts.sessions.map((s, i) => ({\n id: `session-${i}-${s.role}`,\n role: s.role,\n storageState: s.storageState,\n createdAt: Date.now(),\n checkedOutAt: null,\n requestCount: 0,\n }));\n }\n\n /** Get the next available session. Returns null if all are checked out. */\n checkout(): SessionEntry | null {\n const now = Date.now();\n const available = this.pool.filter(s =>\n !s.checkedOutAt &&\n now - s.createdAt < this.maxAgeMs &&\n s.requestCount < this.maxRequests,\n );\n if (available.length === 0) return null;\n // Round-robin: pick lowest requestCount\n available.sort((a, b) => a.requestCount - b.requestCount);\n const session = available[0];\n session.checkedOutAt = now;\n return session;\n }\n\n /** Return a session to the pool after use. */\n checkin(sessionId: string, updatedStorageState?: string): void {\n const s = this.pool.find(e => e.id === sessionId);\n if (!s) return;\n s.checkedOutAt = null;\n s.requestCount++;\n if (updatedStorageState) s.storageState = updatedStorageState;\n }\n\n /** Get all available sessions (for parallel crawl setup). */\n getAll(): ReadonlyArray<SessionEntry> {\n return this.pool;\n }\n\n /** Stats for monitoring. */\n stats(): { total: number; available: number; checkedOut: number; stale: number } {\n const now = Date.now();\n const stale = this.pool.filter(s => now - s.createdAt >= this.maxAgeMs || s.requestCount >= this.maxRequests).length;\n const checkedOut = this.pool.filter(s => !!s.checkedOutAt).length;\n return { total: this.pool.length, available: this.pool.length - checkedOut - stale, checkedOut, stale };\n }\n\n /** Add a new session to the pool at runtime. */\n addSession(role: string, storageState: string): string {\n const id = `session-${this.pool.length}-${role}`;\n this.pool.push({ id, role, storageState, createdAt: Date.now(), checkedOutAt: null, requestCount: 0 });\n return id;\n }\n}\n"],
|
|
5
|
+
"mappings": "AA0BO,MAAM,iBAAiB;AAAA,EACpB;AAAA,EACS;AAAA,EACA;AAAA,EAEjB,YAAY,MAAuB;AACjC,SAAK,WAAW,KAAK,YAAY,IAAI,KAAK,KAAK;AAC/C,SAAK,cAAc,KAAK,yBAAyB;AACjD,SAAK,OAAO,KAAK,SAAS,IAAI,CAAC,GAAG,OAAO;AAAA,MACvC,IAAI,WAAW,CAAC,IAAI,EAAE,IAAI;AAAA,MAC1B,MAAM,EAAE;AAAA,MACR,cAAc,EAAE;AAAA,MAChB,WAAW,KAAK,IAAI;AAAA,MACpB,cAAc;AAAA,MACd,cAAc;AAAA,IAChB,EAAE;AAAA,EACJ;AAAA;AAAA,EAGA,WAAgC;AAC9B,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,YAAY,KAAK,KAAK;AAAA,MAAO,OACjC,CAAC,EAAE,gBACH,MAAM,EAAE,YAAY,KAAK,YACzB,EAAE,eAAe,KAAK;AAAA,IACxB;AACA,QAAI,UAAU,WAAW,EAAG,QAAO;AAEnC,cAAU,KAAK,CAAC,GAAG,MAAM,EAAE,eAAe,EAAE,YAAY;AACxD,UAAM,UAAU,UAAU,CAAC;AAC3B,YAAQ,eAAe;AACvB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,QAAQ,WAAmB,qBAAoC;AAC7D,UAAM,IAAI,KAAK,KAAK,KAAK,OAAK,EAAE,OAAO,SAAS;AAChD,QAAI,CAAC,EAAG;AACR,MAAE,eAAe;AACjB,MAAE;AACF,QAAI,oBAAqB,GAAE,eAAe;AAAA,EAC5C;AAAA;AAAA,EAGA,SAAsC;AACpC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,QAAiF;AAC/E,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,QAAQ,KAAK,KAAK,OAAO,OAAK,MAAM,EAAE,aAAa,KAAK,YAAY,EAAE,gBAAgB,KAAK,WAAW,EAAE;AAC9G,UAAM,aAAa,KAAK,KAAK,OAAO,OAAK,CAAC,CAAC,EAAE,YAAY,EAAE;AAC3D,WAAO,EAAE,OAAO,KAAK,KAAK,QAAQ,WAAW,KAAK,KAAK,SAAS,aAAa,OAAO,YAAY,MAAM;AAAA,EACxG;AAAA;AAAA,EAGA,WAAW,MAAc,cAA8B;AACrD,UAAM,KAAK,WAAW,KAAK,KAAK,MAAM,IAAI,IAAI;AAC9C,SAAK,KAAK,KAAK,EAAE,IAAI,MAAM,cAAc,WAAW,KAAK,IAAI,GAAG,cAAc,MAAM,cAAc,EAAE,CAAC;AACrG,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export interface Credentials {
|
|
3
|
+
username: string;
|
|
4
|
+
password: string;
|
|
5
|
+
oneTimeCode?: string;
|
|
6
|
+
getOneTimeCode?: () => Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Recorded login step captured by the Chrome extension.
|
|
10
|
+
* {username} and {password} in `value` are substituted at replay time.
|
|
11
|
+
*/
|
|
12
|
+
export interface LoginStep {
|
|
13
|
+
action: 'fill' | 'click' | 'wait';
|
|
14
|
+
selector: string;
|
|
15
|
+
value?: string;
|
|
16
|
+
ms?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function login(page: Page, appUrl: string, creds: Credentials & {
|
|
19
|
+
loginRecipe?: LoginStep[];
|
|
20
|
+
}): Promise<boolean>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
async function firstVisible(page, selector) {
|
|
2
|
+
const loc = page.locator(selector);
|
|
3
|
+
const count = await loc.count().catch(() => 0);
|
|
4
|
+
for (let i = 0; i < count; i += 1) {
|
|
5
|
+
const item = loc.nth(i);
|
|
6
|
+
if (await item.isVisible().catch(() => false)) return item;
|
|
7
|
+
}
|
|
8
|
+
for (const frame of page.frames()) {
|
|
9
|
+
if (frame === page.mainFrame()) continue;
|
|
10
|
+
const floc = frame.locator(selector);
|
|
11
|
+
const fcount = await floc.count().catch(() => 0);
|
|
12
|
+
for (let i = 0; i < fcount; i += 1) {
|
|
13
|
+
const item = floc.nth(i);
|
|
14
|
+
if (await item.isVisible().catch(() => false)) return item;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
async function reactFill(field, value) {
|
|
20
|
+
if (!field) return;
|
|
21
|
+
await field.click();
|
|
22
|
+
await field.selectText().catch(() => {
|
|
23
|
+
});
|
|
24
|
+
await field.press("Control+a").catch(() => {
|
|
25
|
+
});
|
|
26
|
+
await field.press("Backspace").catch(() => {
|
|
27
|
+
});
|
|
28
|
+
await field.pressSequentially(value, { delay: 30 });
|
|
29
|
+
await field.evaluate((el, v) => {
|
|
30
|
+
const input = el;
|
|
31
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
|
32
|
+
if (nativeInputValueSetter) nativeInputValueSetter.call(input, v);
|
|
33
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
34
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
35
|
+
input.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
36
|
+
}, value).catch(() => {
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async function clickLikelySubmit(page) {
|
|
40
|
+
let button = await firstVisible(page, [
|
|
41
|
+
'button[type="submit"]',
|
|
42
|
+
'input[type="submit"]',
|
|
43
|
+
'button:has-text("Next")',
|
|
44
|
+
'button:has-text("Sign in")',
|
|
45
|
+
'button:has-text("Log in")',
|
|
46
|
+
'button:has-text("Login")',
|
|
47
|
+
'button:has-text("Sign up")'
|
|
48
|
+
].join(", "));
|
|
49
|
+
if (!button) {
|
|
50
|
+
const candidates = page.locator('button:has-text("Continue")');
|
|
51
|
+
const count = await candidates.count().catch(() => 0);
|
|
52
|
+
for (let i = 0; i < count; i++) {
|
|
53
|
+
const c = candidates.nth(i);
|
|
54
|
+
if (!await c.isVisible().catch(() => false)) continue;
|
|
55
|
+
const text = await c.textContent().catch(() => "") ?? "";
|
|
56
|
+
if (/continue\s+with\s+/i.test(text)) continue;
|
|
57
|
+
button = c;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (button) {
|
|
62
|
+
const handle = await button.elementHandle().catch(() => null);
|
|
63
|
+
if (handle) {
|
|
64
|
+
const deadline = Date.now() + 2e3;
|
|
65
|
+
while (Date.now() < deadline) {
|
|
66
|
+
const disabled = await handle.evaluate((el) => el.disabled).catch(() => false);
|
|
67
|
+
if (!disabled) break;
|
|
68
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await Promise.all([
|
|
72
|
+
page.waitForLoadState("networkidle").catch(() => {
|
|
73
|
+
}),
|
|
74
|
+
button.click({ timeout: 5e3 }).catch(() => {
|
|
75
|
+
})
|
|
76
|
+
// page may navigate before click completes — ignore
|
|
77
|
+
]);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
await page.keyboard.press("Enter");
|
|
81
|
+
await page.waitForLoadState("networkidle").catch(() => {
|
|
82
|
+
});
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
async function dismissCookieBanners(page) {
|
|
86
|
+
const acceptSel = [
|
|
87
|
+
'button:has-text("Accept")',
|
|
88
|
+
'button:has-text("Accept all")',
|
|
89
|
+
'button:has-text("Allow all")',
|
|
90
|
+
'button:has-text("I agree")',
|
|
91
|
+
'button:has-text("Agree")',
|
|
92
|
+
'button:has-text("OK")',
|
|
93
|
+
'button:has-text("Got it")',
|
|
94
|
+
'button:has-text("Close")',
|
|
95
|
+
'[aria-label*="Accept" i]',
|
|
96
|
+
'[aria-label*="Dismiss" i]',
|
|
97
|
+
'[class*="cookie"] button',
|
|
98
|
+
'[id*="cookie"] button',
|
|
99
|
+
'[class*="consent"] button',
|
|
100
|
+
'[class*="banner"] button[class*="close" i]'
|
|
101
|
+
].join(", ");
|
|
102
|
+
const btn = await firstVisible(page, acceptSel);
|
|
103
|
+
if (btn) await btn.click().catch(() => {
|
|
104
|
+
});
|
|
105
|
+
await page.waitForTimeout(300).catch(() => {
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function navigateToLoginPage(page, appUrl) {
|
|
109
|
+
const origin = (() => {
|
|
110
|
+
try {
|
|
111
|
+
return new URL(appUrl).origin;
|
|
112
|
+
} catch {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
const loginPaths = ["/login", "/sign-in", "/signin", "/auth/login", "/auth", "/account/login", "/user/login", "/app/login"];
|
|
117
|
+
const navLoginSel = [
|
|
118
|
+
'a:has-text("Sign in")',
|
|
119
|
+
'a:has-text("Sign In")',
|
|
120
|
+
'a:has-text("Log in")',
|
|
121
|
+
'a:has-text("Log In")',
|
|
122
|
+
'a:has-text("Login")',
|
|
123
|
+
'a:has-text("Get started")',
|
|
124
|
+
'a[href*="login" i]',
|
|
125
|
+
'a[href*="sign-in" i]',
|
|
126
|
+
'a[href*="signin" i]',
|
|
127
|
+
'a[href*="auth" i]',
|
|
128
|
+
'button:has-text("Sign in")',
|
|
129
|
+
'button:has-text("Log in")',
|
|
130
|
+
'button:has-text("Login")'
|
|
131
|
+
].join(", ");
|
|
132
|
+
const navLink = await firstVisible(page, navLoginSel);
|
|
133
|
+
if (navLink) {
|
|
134
|
+
const href = await navLink.getAttribute("href").catch(() => null);
|
|
135
|
+
if (href && href.startsWith("http")) {
|
|
136
|
+
await page.goto(href, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
|
|
137
|
+
});
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
await navLink.click();
|
|
141
|
+
const emailAppeared = await page.waitForSelector(EMAIL_SELECTOR, { timeout: 3e3 }).then(() => true).catch(() => false);
|
|
142
|
+
if (!emailAppeared) {
|
|
143
|
+
await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => {
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (origin) {
|
|
149
|
+
for (const p of loginPaths) {
|
|
150
|
+
const loginUrl = `${origin}${p}`;
|
|
151
|
+
const res = await page.goto(loginUrl, { waitUntil: "networkidle", timeout: 1e4 }).catch(() => null);
|
|
152
|
+
if (res && res.status() < 400) {
|
|
153
|
+
const hasForm = await page.locator(
|
|
154
|
+
'input[type="email"], input[type="password"], input[name*="email" i], input[name*="user" i]'
|
|
155
|
+
).count().catch(() => 0) > 0;
|
|
156
|
+
if (hasForm) return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await page.goto(appUrl, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
function isOAuthProviderUrl(url) {
|
|
165
|
+
const u = url.toLowerCase();
|
|
166
|
+
if (u.includes("accounts.google.com")) return "Google";
|
|
167
|
+
if (u.includes("github.com/login")) return "GitHub";
|
|
168
|
+
if (u.includes("login.microsoftonline.com")) return "Microsoft";
|
|
169
|
+
if (u.includes("login.live.com")) return "Microsoft";
|
|
170
|
+
if (u.includes("appleid.apple.com")) return "Apple";
|
|
171
|
+
if (u.includes("okta.com")) return "Okta";
|
|
172
|
+
if (u.includes("auth0.com")) return "Auth0";
|
|
173
|
+
if (u.includes("onelogin.com")) return "OneLogin";
|
|
174
|
+
if (u.includes("pingidentity.com")) return "Ping Identity";
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const EMAIL_SELECTOR = 'input[type="email"], input[name*="user" i], input[name*="email" i], input[autocomplete="username"]';
|
|
178
|
+
const PASS_SELECTOR = 'input[type="password"], input[autocomplete="current-password"]';
|
|
179
|
+
async function findOtpField(page) {
|
|
180
|
+
const strong = await firstVisible(page, 'input[autocomplete="one-time-code"], input[name*="otp" i]');
|
|
181
|
+
if (strong) return strong;
|
|
182
|
+
const weak = page.locator('input[name*="code" i], input[aria-label*="code" i], input[placeholder*="code" i]');
|
|
183
|
+
const count = await weak.count().catch(() => 0);
|
|
184
|
+
for (let i = 0; i < count; i += 1) {
|
|
185
|
+
const item = weak.nth(i);
|
|
186
|
+
if (!await item.isVisible().catch(() => false)) continue;
|
|
187
|
+
const labels = await item.evaluate(
|
|
188
|
+
(el) => [el.name, el.id, el.placeholder, el.getAttribute("aria-label")].filter(Boolean)
|
|
189
|
+
).catch(() => []);
|
|
190
|
+
if (labels.some((l) => /^\s*code\s*$/i.test(l) || /verif|one[-\s]?time|2fa|mfa|totp|auth|security|passcode|sms/i.test(l))) return item;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
async function replayLoginRecipe(page, steps, creds) {
|
|
195
|
+
for (const step of steps) {
|
|
196
|
+
try {
|
|
197
|
+
if (step.action === "wait") {
|
|
198
|
+
await page.waitForTimeout(step.ms ?? 500).catch(() => {
|
|
199
|
+
});
|
|
200
|
+
} else if (step.action === "click") {
|
|
201
|
+
const el = await firstVisible(page, step.selector);
|
|
202
|
+
if (!el) return false;
|
|
203
|
+
await el.click({ timeout: 5e3 }).catch(() => {
|
|
204
|
+
});
|
|
205
|
+
} else if (step.action === "fill") {
|
|
206
|
+
const el = await firstVisible(page, step.selector);
|
|
207
|
+
if (!el) return false;
|
|
208
|
+
const val = (step.value ?? "").replace("{username}", creds.username).replace("{password}", creds.password);
|
|
209
|
+
await reactFill(el, val);
|
|
210
|
+
}
|
|
211
|
+
await page.waitForTimeout(200).catch(() => {
|
|
212
|
+
});
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
218
|
+
});
|
|
219
|
+
const postUrl = page.url();
|
|
220
|
+
const LOGIN_PATH_RE = /\/(login|signin|sign-in|auth|log-in|sso|oauth)(\.[a-z]+)?(\/|$|\?)/i;
|
|
221
|
+
if (!LOGIN_PATH_RE.test(new URL(postUrl).pathname)) return true;
|
|
222
|
+
const passCount = await page.locator('input[type="password"]').count().catch(() => 0);
|
|
223
|
+
return passCount === 0;
|
|
224
|
+
}
|
|
225
|
+
async function login(page, appUrl, creds) {
|
|
226
|
+
const gotoResponse = await page.goto(appUrl, { waitUntil: "networkidle", timeout: 3e4 }).catch(() => null);
|
|
227
|
+
if (gotoResponse) {
|
|
228
|
+
const status = gotoResponse.status();
|
|
229
|
+
if (status === 403 || status === 401) {
|
|
230
|
+
throw Object.assign(
|
|
231
|
+
new Error(
|
|
232
|
+
`Login page returned HTTP ${status} \u2014 the server is blocking access from this IP address. Use a proxy URL in Crawl Settings, or ask the server admin to allowlist the crawler server IP.`
|
|
233
|
+
),
|
|
234
|
+
{ isLoginError: true, reason: status === 403 ? "IP_BLOCKED_403" : "IP_BLOCKED_401" }
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (creds.loginRecipe && creds.loginRecipe.length > 0) {
|
|
239
|
+
const recipeOk = await replayLoginRecipe(page, creds.loginRecipe, creds);
|
|
240
|
+
if (recipeOk) return true;
|
|
241
|
+
await page.goto(appUrl, { waitUntil: "networkidle", timeout: 3e4 }).catch(() => {
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
await dismissCookieBanners(page);
|
|
245
|
+
let userField = await firstVisible(page, EMAIL_SELECTOR);
|
|
246
|
+
if (!userField) {
|
|
247
|
+
await navigateToLoginPage(page, appUrl);
|
|
248
|
+
userField = await firstVisible(page, EMAIL_SELECTOR);
|
|
249
|
+
if (!userField) {
|
|
250
|
+
await page.waitForSelector(EMAIL_SELECTOR, { timeout: 5e3 }).catch(() => {
|
|
251
|
+
});
|
|
252
|
+
userField = await firstVisible(page, EMAIL_SELECTOR);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!userField) return false;
|
|
256
|
+
await reactFill(userField, creds.username);
|
|
257
|
+
await page.keyboard.press("Tab");
|
|
258
|
+
let passField = await firstVisible(page, PASS_SELECTOR);
|
|
259
|
+
if (!passField) {
|
|
260
|
+
await clickLikelySubmit(page);
|
|
261
|
+
await page.waitForSelector(PASS_SELECTOR, { timeout: 8e3 }).catch(() => {
|
|
262
|
+
});
|
|
263
|
+
const ssoProvider = isOAuthProviderUrl(page.url());
|
|
264
|
+
if (ssoProvider) {
|
|
265
|
+
throw Object.assign(
|
|
266
|
+
new Error(
|
|
267
|
+
`This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, then click the extension to capture your session.`
|
|
268
|
+
),
|
|
269
|
+
{ isLoginError: true, isSSOError: true, ssoProvider }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
passField = await firstVisible(page, PASS_SELECTOR);
|
|
273
|
+
}
|
|
274
|
+
if (!passField) {
|
|
275
|
+
const ssoProvider = isOAuthProviderUrl(page.url());
|
|
276
|
+
if (ssoProvider) {
|
|
277
|
+
throw Object.assign(
|
|
278
|
+
new Error(
|
|
279
|
+
`This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, then click the extension to capture your session.`
|
|
280
|
+
),
|
|
281
|
+
{ isLoginError: true, isSSOError: true, ssoProvider }
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
await reactFill(passField, creds.password);
|
|
287
|
+
await page.waitForTimeout(150).catch(() => {
|
|
288
|
+
});
|
|
289
|
+
await Promise.all([
|
|
290
|
+
page.waitForLoadState("networkidle").catch(() => {
|
|
291
|
+
}),
|
|
292
|
+
page.keyboard.press("Enter")
|
|
293
|
+
]);
|
|
294
|
+
const stillHasPass = await page.locator(PASS_SELECTOR).count().catch(() => 0);
|
|
295
|
+
if (stillHasPass > 0) {
|
|
296
|
+
await page.keyboard.press("Tab");
|
|
297
|
+
await clickLikelySubmit(page);
|
|
298
|
+
}
|
|
299
|
+
const LOGIN_PATH_RE_NAV = /\/(login|signin|sign-in|auth|log-in|sso|oauth)(\.[a-z]+)?(\/|$|\?)/i;
|
|
300
|
+
if (LOGIN_PATH_RE_NAV.test(new URL(page.url()).pathname)) {
|
|
301
|
+
await page.waitForURL(
|
|
302
|
+
(url) => {
|
|
303
|
+
try {
|
|
304
|
+
return !LOGIN_PATH_RE_NAV.test(new URL(url).pathname);
|
|
305
|
+
} catch {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{ timeout: 12e3 }
|
|
310
|
+
).catch(() => {
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const ssoAfterLogin = isOAuthProviderUrl(page.url());
|
|
314
|
+
if (ssoAfterLogin) {
|
|
315
|
+
throw Object.assign(
|
|
316
|
+
new Error(
|
|
317
|
+
`This app uses ${ssoAfterLogin} SSO \u2014 ZeTa cannot automate OAuth login. Use the ZeTa Capture Chrome Extension instead.`
|
|
318
|
+
),
|
|
319
|
+
{ isLoginError: true, isSSOError: true, ssoProvider: ssoAfterLogin }
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const otpField = await findOtpField(page);
|
|
323
|
+
if (otpField) {
|
|
324
|
+
const code = creds.oneTimeCode ?? await creds.getOneTimeCode?.();
|
|
325
|
+
if (!code) return false;
|
|
326
|
+
await reactFill(otpField, code);
|
|
327
|
+
await clickLikelySubmit(page);
|
|
328
|
+
}
|
|
329
|
+
const postLoginUrl = page.url();
|
|
330
|
+
const LOGIN_PATH_RE = /\/(login|signin|sign-in|auth|log-in|sso|oauth)(\.[a-z]+)?(\/|$|\?)/i;
|
|
331
|
+
const stillOnLoginPath = LOGIN_PATH_RE.test(new URL(postLoginUrl).pathname);
|
|
332
|
+
const stillOnLogin = await page.locator(PASS_SELECTOR).count();
|
|
333
|
+
if (stillOnLoginPath && stillOnLogin > 0) {
|
|
334
|
+
const errorLoc = page.locator('[role="alert"], .error, [class*="error"], [class*="alert"]');
|
|
335
|
+
await errorLoc.first().waitFor({ state: "visible", timeout: 2e3 }).catch(() => {
|
|
336
|
+
});
|
|
337
|
+
const pageError = await errorLoc.first().textContent().catch(() => null);
|
|
338
|
+
if (pageError?.trim()) {
|
|
339
|
+
throw Object.assign(new Error(pageError.trim()), { isLoginError: true });
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
export {
|
|
346
|
+
login
|
|
347
|
+
};
|
|
348
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/auth.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Page } from 'playwright';\n\nexport interface Credentials {\n username: string;\n password: string;\n oneTimeCode?: string;\n getOneTimeCode?: () => Promise<string>;\n}\n\n/**\n * Recorded login step captured by the Chrome extension.\n * {username} and {password} in `value` are substituted at replay time.\n */\nexport interface LoginStep {\n action: 'fill' | 'click' | 'wait';\n selector: string;\n value?: string;\n ms?: number;\n}\n\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function firstVisible(page: Page, selector: string) {\n const loc = page.locator(selector);\n const count = await loc.count().catch(() => 0);\n for (let i = 0; i < count; i += 1) {\n const item = loc.nth(i);\n if (await item.isVisible().catch(() => false)) return item;\n }\n // iframe fallback \u2014 Salesforce, Zendesk, and embedded login widgets hide forms in iframes\n for (const frame of page.frames()) {\n if (frame === page.mainFrame()) continue;\n const floc = frame.locator(selector);\n const fcount = await floc.count().catch(() => 0);\n for (let i = 0; i < fcount; i += 1) {\n const item = floc.nth(i);\n if (await item.isVisible().catch(() => false)) return item;\n }\n }\n return null;\n}\n\n/** Type into framework-controlled inputs \u2014 handles React, Angular, Vue, and plain HTML forms. */\nasync function reactFill(field: Awaited<ReturnType<typeof firstVisible>>, value: string) {\n if (!field) return;\n await field.click();\n await field.selectText().catch(() => {});\n await field.press('Control+a').catch(() => {});\n await field.press('Backspace').catch(() => {});\n await field.pressSequentially(value, { delay: 30 });\n // Fire native input + change events for Angular/Vue/custom frameworks that ignore DOM keypresses.\n // React's synthetic listener catches pressSequentially above; this covers the rest.\n await field.evaluate((el, v) => {\n const input = el as HTMLInputElement;\n const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;\n if (nativeInputValueSetter) nativeInputValueSetter.call(input, v);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n input.dispatchEvent(new Event('change', { bubbles: true }));\n input.dispatchEvent(new Event('blur', { bubbles: true }));\n }, value).catch(() => {});\n}\n\n/**\n * Find and click the most likely submit button.\n * Waits up to 2 s for React's disabled={!email||!password} to clear before clicking.\n */\nasync function clickLikelySubmit(page: Page) {\n // Prefer explicit submit selectors first; \"Continue\" is last resort to avoid\n // accidentally clicking \"Continue with Google/GitHub/Apple\" SSO buttons.\n let button = await firstVisible(page, [\n 'button[type=\"submit\"]',\n 'input[type=\"submit\"]',\n 'button:has-text(\"Next\")',\n 'button:has-text(\"Sign in\")',\n 'button:has-text(\"Log in\")',\n 'button:has-text(\"Login\")',\n 'button:has-text(\"Sign up\")',\n ].join(', '));\n\n if (!button) {\n // Find \"Continue\" buttons but exclude OAuth provider ones (\"Continue with Google\" etc.)\n const candidates = page.locator('button:has-text(\"Continue\")');\n const count = await candidates.count().catch(() => 0);\n for (let i = 0; i < count; i++) {\n const c = candidates.nth(i);\n if (!(await c.isVisible().catch(() => false))) continue;\n const text = (await c.textContent().catch(() => '')) ?? '';\n if (/continue\\s+with\\s+/i.test(text)) continue; // skip \"Continue with Google/GitHub/Apple\"\n button = c;\n break;\n }\n }\n if (button) {\n // Poll for enabled state \u2014 waitForFunction not available on Stagehand V3 proxy\n const handle = await button.elementHandle().catch(() => null);\n if (handle) {\n const deadline = Date.now() + 2_000;\n while (Date.now() < deadline) {\n const disabled = await handle.evaluate((el) => (el as HTMLButtonElement).disabled).catch(() => false);\n if (!disabled) break;\n await new Promise(r => setTimeout(r, 100));\n }\n }\n await Promise.all([\n page.waitForLoadState('networkidle').catch(() => {}),\n button.click({ timeout: 5_000 }).catch(() => {}), // page may navigate before click completes \u2014 ignore\n ]);\n return true;\n }\n await page.keyboard.press('Enter');\n await page.waitForLoadState('networkidle').catch(() => {});\n return false;\n}\n\n/** Dismiss cookie/GDPR consent banners so they don't block form fields. */\nasync function dismissCookieBanners(page: Page) {\n const acceptSel = [\n 'button:has-text(\"Accept\")',\n 'button:has-text(\"Accept all\")',\n 'button:has-text(\"Allow all\")',\n 'button:has-text(\"I agree\")',\n 'button:has-text(\"Agree\")',\n 'button:has-text(\"OK\")',\n 'button:has-text(\"Got it\")',\n 'button:has-text(\"Close\")',\n '[aria-label*=\"Accept\" i]',\n '[aria-label*=\"Dismiss\" i]',\n '[class*=\"cookie\"] button',\n '[id*=\"cookie\"] button',\n '[class*=\"consent\"] button',\n '[class*=\"banner\"] button[class*=\"close\" i]',\n ].join(', ');\n const btn = await firstVisible(page, acceptSel);\n if (btn) await btn.click().catch(() => {});\n await page.waitForTimeout(300).catch(() => {});\n}\n\n/**\n * Look for a Sign In / Log In nav link on a marketing homepage and navigate to the actual login page.\n * Returns true if navigation happened.\n */\nasync function navigateToLoginPage(page: Page, appUrl: string): Promise<boolean> {\n // First try common login URL suffixes\n const origin = (() => { try { return new URL(appUrl).origin; } catch { return ''; } })();\n const loginPaths = ['/login', '/sign-in', '/signin', '/auth/login', '/auth', '/account/login', '/user/login', '/app/login'];\n\n // Look for a \"Sign In\" / \"Login\" / \"Get started\" link in the nav\n const navLoginSel = [\n 'a:has-text(\"Sign in\")',\n 'a:has-text(\"Sign In\")',\n 'a:has-text(\"Log in\")',\n 'a:has-text(\"Log In\")',\n 'a:has-text(\"Login\")',\n 'a:has-text(\"Get started\")',\n 'a[href*=\"login\" i]',\n 'a[href*=\"sign-in\" i]',\n 'a[href*=\"signin\" i]',\n 'a[href*=\"auth\" i]',\n 'button:has-text(\"Sign in\")',\n 'button:has-text(\"Log in\")',\n 'button:has-text(\"Login\")',\n ].join(', ');\n\n const navLink = await firstVisible(page, navLoginSel);\n if (navLink) {\n const href = await navLink.getAttribute('href').catch(() => null);\n // If the link has an href, navigate directly \u2014 avoids modal-only flows\n if (href && href.startsWith('http')) {\n await page.goto(href, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => {});\n return true;\n }\n // Click and immediately check for email field (modal opened) OR wait for navigation\n await navLink.click();\n const emailAppeared = await page.waitForSelector(EMAIL_SELECTOR, { timeout: 3_000 })\n .then(() => true).catch(() => false);\n if (!emailAppeared) {\n // No modal \u2014 likely navigating to a new page\n await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => {});\n }\n return true;\n }\n\n // Fallback: try common login paths on the same origin\n if (origin) {\n for (const p of loginPaths) {\n const loginUrl = `${origin}${p}`;\n const res = await page.goto(loginUrl, { waitUntil: 'networkidle', timeout: 10_000 }).catch(() => null);\n if (res && res.status() < 400) {\n // Check if this page actually has a login form\n const hasForm = (await page.locator(\n 'input[type=\"email\"], input[type=\"password\"], input[name*=\"email\" i], input[name*=\"user\" i]'\n ).count().catch(() => 0)) > 0;\n if (hasForm) return true;\n }\n }\n // Restore original page\n await page.goto(appUrl, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => {});\n }\n\n return false;\n}\n\n/** Detect if current page redirected to a 3rd-party OAuth provider (SSO flow we can't automate). */\nfunction isOAuthProviderUrl(url: string): string | null {\n const u = url.toLowerCase();\n if (u.includes('accounts.google.com')) return 'Google';\n if (u.includes('github.com/login')) return 'GitHub';\n if (u.includes('login.microsoftonline.com')) return 'Microsoft';\n if (u.includes('login.live.com')) return 'Microsoft';\n if (u.includes('appleid.apple.com')) return 'Apple';\n if (u.includes('okta.com')) return 'Okta';\n if (u.includes('auth0.com')) return 'Auth0';\n if (u.includes('onelogin.com')) return 'OneLogin';\n if (u.includes('pingidentity.com')) return 'Ping Identity';\n return null;\n}\n\nconst EMAIL_SELECTOR = 'input[type=\"email\"], input[name*=\"user\" i], input[name*=\"email\" i], input[autocomplete=\"username\"]';\nconst PASS_SELECTOR = 'input[type=\"password\"], input[autocomplete=\"current-password\"]';\n\nasync function findOtpField(page: Page) {\n const strong = await firstVisible(page, 'input[autocomplete=\"one-time-code\"], input[name*=\"otp\" i]');\n if (strong) return strong;\n const weak = page.locator('input[name*=\"code\" i], input[aria-label*=\"code\" i], input[placeholder*=\"code\" i]');\n const count = await weak.count().catch(() => 0);\n for (let i = 0; i < count; i += 1) {\n const item = weak.nth(i);\n if (!await item.isVisible().catch(() => false)) continue;\n const labels = await item.evaluate((el: HTMLInputElement) =>\n [el.name, el.id, el.placeholder, el.getAttribute('aria-label')].filter(Boolean) as string[]\n ).catch(() => [] as string[]);\n if (labels.some((l) => /^\\s*code\\s*$/i.test(l) || /verif|one[-\\s]?time|2fa|mfa|totp|auth|security|passcode|sms/i.test(l))) return item;\n }\n return null;\n}\n\n/** Replay recorded login steps from the Chrome extension. Returns true if login succeeded. */\nasync function replayLoginRecipe(page: Page, steps: LoginStep[], creds: { username: string; password: string }): Promise<boolean> {\n for (const step of steps) {\n try {\n if (step.action === 'wait') {\n await page.waitForTimeout(step.ms ?? 500).catch(() => {});\n } else if (step.action === 'click') {\n const el = await firstVisible(page, step.selector);\n if (!el) return false;\n await el.click({ timeout: 5_000 }).catch(() => {});\n } else if (step.action === 'fill') {\n const el = await firstVisible(page, step.selector);\n if (!el) return false;\n const val = (step.value ?? '')\n .replace('{username}', creds.username)\n .replace('{password}', creds.password);\n await reactFill(el, val);\n }\n await page.waitForTimeout(200).catch(() => {});\n } catch { return false; }\n }\n await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {});\n const postUrl = page.url();\n const LOGIN_PATH_RE = /\\/(login|signin|sign-in|auth|log-in|sso|oauth)(\\.[a-z]+)?(\\/|$|\\?)/i;\n if (!LOGIN_PATH_RE.test(new URL(postUrl).pathname)) return true; // navigated away \u2014 success\n const passCount = await page.locator('input[type=\"password\"]').count().catch(() => 0);\n return passCount === 0;\n}\n\n// \u2500\u2500\u2500 Main login function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport async function login(page: Page, appUrl: string, creds: Credentials & { loginRecipe?: LoginStep[] }): Promise<boolean> {\n const gotoResponse = await page.goto(appUrl, { waitUntil: 'networkidle', timeout: 30_000 }).catch(() => null);\n if (gotoResponse) {\n const status = gotoResponse.status();\n if (status === 403 || status === 401) {\n throw Object.assign(\n new Error(\n `Login page returned HTTP ${status} \u2014 the server is blocking access from this IP address. ` +\n `Use a proxy URL in Crawl Settings, or ask the server admin to allowlist the crawler server IP.`\n ),\n { isLoginError: true, reason: status === 403 ? 'IP_BLOCKED_403' : 'IP_BLOCKED_401' }\n );\n }\n }\n\n // Recipe replay \u2014 try exact recorded steps first when available\n if (creds.loginRecipe && creds.loginRecipe.length > 0) {\n const recipeOk = await replayLoginRecipe(page, creds.loginRecipe, creds);\n if (recipeOk) return true;\n // Recipe failed \u2014 reload page and fall through to DOM heuristic\n await page.goto(appUrl, { waitUntil: 'networkidle', timeout: 30_000 }).catch(() => {});\n }\n\n // Dismiss cookie banners that might block form interaction\n await dismissCookieBanners(page);\n\n let userField = await firstVisible(page, EMAIL_SELECTOR);\n\n // No email field on the landing page \u2014 this is a marketing homepage.\n // Try to navigate to the actual login page.\n if (!userField) {\n await navigateToLoginPage(page, appUrl);\n // NOTE: do NOT call dismissCookieBanners here \u2014 it can close login modals\n // navigateToLoginPage already waits for email field or networkidle\n userField = await firstVisible(page, EMAIL_SELECTOR);\n if (!userField) {\n // Last attempt: wait up to 5s for form to appear (SPA redirect or slow modal)\n await page.waitForSelector(EMAIL_SELECTOR, { timeout: 5_000 }).catch(() => {});\n userField = await firstVisible(page, EMAIL_SELECTOR);\n }\n }\n\n if (!userField) return false;\n\n await reactFill(userField, creds.username);\n await page.keyboard.press('Tab');\n\n let passField = await firstVisible(page, PASS_SELECTOR);\n if (!passField) {\n // Multi-step form: email-first, then password on next screen\n await clickLikelySubmit(page);\n // Wait for password field to appear (up to 8s) rather than a fixed 2s sleep \u2014\n // slow IdP servers (Okta, Azure) take 3-5s to validate email and render the next step.\n await page.waitForSelector(PASS_SELECTOR, { timeout: 8_000 }).catch(() => {});\n // Check for SSO redirect after email submission\n const ssoProvider = isOAuthProviderUrl(page.url());\n if (ssoProvider) {\n throw Object.assign(\n new Error(\n `This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. ` +\n `Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, ` +\n `then click the extension to capture your session.`\n ),\n { isLoginError: true, isSSOError: true, ssoProvider }\n );\n }\n passField = await firstVisible(page, PASS_SELECTOR);\n }\n\n if (!passField) {\n // Still no password field \u2014 check if we landed on an SSO provider\n const ssoProvider = isOAuthProviderUrl(page.url());\n if (ssoProvider) {\n throw Object.assign(\n new Error(\n `This app uses ${ssoProvider} SSO \u2014 ZeTa cannot automate OAuth login. ` +\n `Use the ZeTa Capture Chrome Extension instead: log in normally in Chrome, ` +\n `then click the extension to capture your session.`\n ),\n { isLoginError: true, isSSOError: true, ssoProvider }\n );\n }\n return false;\n }\n\n await reactFill(passField, creds.password);\n // Press Enter \u2014 submits regardless of button disabled state\n await page.waitForTimeout(150).catch(() => {});\n await Promise.all([\n page.waitForLoadState('networkidle').catch(() => {}),\n page.keyboard.press('Enter'),\n ]);\n\n // Fallback: if Enter didn't navigate away, try Tab \u2192 click\n const stillHasPass = await page.locator(PASS_SELECTOR).count().catch(() => 0);\n if (stillHasPass > 0) {\n await page.keyboard.press('Tab');\n await clickLikelySubmit(page);\n }\n\n // Wait for post-submit navigation to complete \u2014 waitForLoadState resolves immediately\n // if page is already idle, so use waitForURL to detect actual navigation away from login.\n const LOGIN_PATH_RE_NAV = /\\/(login|signin|sign-in|auth|log-in|sso|oauth)(\\.[a-z]+)?(\\/|$|\\?)/i;\n if (LOGIN_PATH_RE_NAV.test(new URL(page.url()).pathname)) {\n await page.waitForURL(\n (url) => { try { return !LOGIN_PATH_RE_NAV.test(new URL(url).pathname); } catch { return true; } },\n { timeout: 12_000 }\n ).catch(() => {});\n }\n\n // Check for SSO redirect after password submit\n const ssoAfterLogin = isOAuthProviderUrl(page.url());\n if (ssoAfterLogin) {\n throw Object.assign(\n new Error(\n `This app uses ${ssoAfterLogin} SSO \u2014 ZeTa cannot automate OAuth login. ` +\n `Use the ZeTa Capture Chrome Extension instead.`\n ),\n { isLoginError: true, isSSOError: true, ssoProvider: ssoAfterLogin }\n );\n }\n\n // Handle OTP / 2FA\n const otpField = await findOtpField(page);\n if (otpField) {\n const code = creds.oneTimeCode ?? await creds.getOneTimeCode?.();\n if (!code) return false;\n await reactFill(otpField, code);\n await clickLikelySubmit(page);\n }\n\n // Success check: navigated away from login page, and/or no password field remains.\n // Use URL as primary signal \u2014 dashboards can have hidden password fields (profile forms, etc.)\n // that would cause false negatives if we only check input[type=\"password\"].\n const postLoginUrl = page.url();\n const LOGIN_PATH_RE = /\\/(login|signin|sign-in|auth|log-in|sso|oauth)(\\.[a-z]+)?(\\/|$|\\?)/i;\n const stillOnLoginPath = LOGIN_PATH_RE.test(new URL(postLoginUrl).pathname);\n const stillOnLogin = await page.locator(PASS_SELECTOR).count();\n\n // Only treat as failure if BOTH: still on a login-path URL AND password field present\n if (stillOnLoginPath && stillOnLogin > 0) {\n const errorLoc = page.locator('[role=\"alert\"], .error, [class*=\"error\"], [class*=\"alert\"]');\n await errorLoc.first().waitFor({ state: 'visible', timeout: 2_000 }).catch(() => {});\n const pageError = await errorLoc.first().textContent().catch(() => null);\n if (pageError?.trim()) {\n throw Object.assign(new Error(pageError.trim()), { isLoginError: true });\n }\n return false;\n }\n return true;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser extension injection support.
|
|
3
|
+
*
|
|
4
|
+
* Chrome extensions require a persistent profile directory — incompatible
|
|
5
|
+
* with headless Docker by default. This module handles:
|
|
6
|
+
* 1. Persistent profile directory per extension set
|
|
7
|
+
* 2. Extension download from .crx URL or load from local path
|
|
8
|
+
* 3. Launch args for Playwright / Stagehand
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const profile = await prepareExtensionProfile(['/path/to/ext.crx']);
|
|
12
|
+
* // pass profile.userDataDir and profile.args to browser launch
|
|
13
|
+
*/
|
|
14
|
+
export interface ExtensionSource {
|
|
15
|
+
/** Local .crx file path or https:// URL to download from */
|
|
16
|
+
source: string;
|
|
17
|
+
/** Friendly name for logging */
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ExtensionProfile {
|
|
21
|
+
/** Chrome user-data-dir containing Preferences + extension unpacked dirs */
|
|
22
|
+
userDataDir: string;
|
|
23
|
+
/** Args to append to chromium launch — includes --load-extension and --disable-extensions-except */
|
|
24
|
+
args: string[];
|
|
25
|
+
/** Cleanup the profile directory when done */
|
|
26
|
+
cleanup: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Prepare a Chrome user-data-dir with the given extensions loaded.
|
|
30
|
+
* Returns profile info + cleanup function.
|
|
31
|
+
*/
|
|
32
|
+
export declare function prepareExtensionProfile(extensions: ExtensionSource[]): Promise<ExtensionProfile>;
|
|
33
|
+
/**
|
|
34
|
+
* Ad-blocker extension profile using the uBlock Origin CRX.
|
|
35
|
+
* Reduces noise from ad networks during crawls.
|
|
36
|
+
*/
|
|
37
|
+
export declare function adBlockerProfile(): Promise<ExtensionProfile>;
|