@detiq/crawler 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +49 -0
- package/README.md +11 -0
- 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/ai-login.d.ts +32 -0
- package/dist/ai-login.js +177 -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 +329 -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/crawler.d.ts +179 -0
- package/dist/crawler.js +3672 -0
- package/dist/crawler.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/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 +54 -0
- package/dist/extractors/advanced.js +245 -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/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +40 -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/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 +32 -0
- package/dist/frontier.js +83 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +35 -0
- package/dist/gap-pass.js +81 -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 +304 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +59 -0
- package/dist/incremental-diff.js +61 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +61 -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 +104 -0
- package/dist/interactive-probe.js +442 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +171 -0
- package/dist/job-queue.js +2256 -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 +100 -0
- package/dist/live-view.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/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.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.d.ts +53 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +38 -0
- package/dist/phash.js +115 -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 +37 -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 +140 -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 +16 -0
- package/dist/screen-reader.js +46 -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/session-capture.d.ts +35 -0
- package/dist/session-capture.js +267 -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/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 +152 -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 +2 -0
- package/dist/sqs-queue.js +47 -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/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +168 -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 +67 -0
- package/dist/test-executor.js +311 -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 +87 -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 +25 -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 +45 -0
- package/src/accessibility-auditor.ts +102 -0
- package/src/ai-login.ts +218 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +396 -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/crawler.ts +4305 -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/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 +283 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/failure-analysis-agent.ts +49 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +112 -0
- package/src/gap-pass.ts +122 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +433 -0
- package/src/incremental-diff.ts +121 -0
- package/src/index.ts +35 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +623 -0
- package/src/job-queue.test.ts +120 -0
- package/src/job-queue.ts +2701 -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 +141 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/native-crawler.ts +1106 -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.ts +134 -0
- package/src/phash.ts +151 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +46 -0
- package/src/profiles/domain-profile.ts +167 -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 +89 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/session-capture.ts +325 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +131 -0
- package/src/sitemap.ts +224 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +49 -0
- package/src/sso-token.ts +120 -0
- package/src/stealth/human-behavior.ts +191 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +424 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +114 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +57 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +48 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -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/crawler.d.ts +179 -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/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 +54 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +32 -0
- package/types/gap-pass.d.ts +35 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +59 -0
- package/types/index.d.ts +35 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +104 -0
- package/types/job-queue.d.ts +171 -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/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/native-crawler.d.ts +69 -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.d.ts +53 -0
- package/types/phash.d.ts +38 -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 +16 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/session-capture.d.ts +38 -0
- package/types/session-export.d.ts +49 -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 +2 -0
- package/types/sso-token.d.ts +29 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +67 -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
package/Dockerfile
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ZeTa-AI Crawler — includes Playwright + Chromium
|
|
2
|
+
# Playwright needs a real Linux environment with system deps.
|
|
3
|
+
# This service CANNOT run on Vercel serverless — use Railway/Render.
|
|
4
|
+
# IMPORTANT: ECS Fargate task definition must set sharedMemorySize: 2048 (MB) for Chromium stability
|
|
5
|
+
FROM mcr.microsoft.com/playwright:v1.55.0-jammy AS runner
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
ENV NODE_ENV=production
|
|
8
|
+
|
|
9
|
+
# Install Node.js 20 (Playwright base image ships with 18)
|
|
10
|
+
RUN apt-get update && apt-get install -y curl && \
|
|
11
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
|
12
|
+
apt-get install -y nodejs && \
|
|
13
|
+
apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
14
|
+
|
|
15
|
+
# Workspace manifests (layer cache)
|
|
16
|
+
COPY package.json package-lock.json ./
|
|
17
|
+
COPY packages/core/package.json packages/core/
|
|
18
|
+
COPY packages/database/package.json packages/database/
|
|
19
|
+
COPY packages/app-brain/package.json packages/app-brain/
|
|
20
|
+
COPY packages/agents/package.json packages/agents/
|
|
21
|
+
COPY packages/crawler/package.json packages/crawler/
|
|
22
|
+
RUN npm ci
|
|
23
|
+
|
|
24
|
+
# Source
|
|
25
|
+
COPY tsconfig.base.json ./
|
|
26
|
+
COPY packages/core packages/core
|
|
27
|
+
COPY packages/database packages/database
|
|
28
|
+
COPY packages/app-brain packages/app-brain
|
|
29
|
+
COPY packages/agents packages/agents
|
|
30
|
+
COPY packages/crawler packages/crawler
|
|
31
|
+
|
|
32
|
+
# Build
|
|
33
|
+
RUN npx prisma generate --schema packages/database/prisma/schema.prisma && \
|
|
34
|
+
npm run -w @detiq/core build && \
|
|
35
|
+
npm run -w @detiq/database build && \
|
|
36
|
+
npm run -w @detiq/app-brain build && \
|
|
37
|
+
npm run -w @detiq/agents build && \
|
|
38
|
+
npm run -w @detiq/crawler build
|
|
39
|
+
|
|
40
|
+
# Screenshot cache dir
|
|
41
|
+
RUN mkdir -p /app/.screenshots-cache
|
|
42
|
+
|
|
43
|
+
EXPOSE 4001
|
|
44
|
+
# index.js is a side-effect-free library barrel (also imported by @detiq/api) — it holds
|
|
45
|
+
# no listener/timer, so running it directly starts the process, finishes its imports,
|
|
46
|
+
# and exits immediately with nothing ever consuming the job queue. worker.js is the
|
|
47
|
+
# actual long-running entrypoint: it starts the durable job-poll loop (and the SQS
|
|
48
|
+
# consumer when JOB_QUEUE_BACKEND=sqs) and keeps the process alive.
|
|
49
|
+
CMD ["node", "packages/crawler/dist/worker.js"]
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @detiq/crawler — The Explorer
|
|
2
|
+
|
|
3
|
+
Crawls an app, **gets past the login** (the star of Phase 1), discovers
|
|
4
|
+
screens, captures screenshots, and reads each page's elements into the App
|
|
5
|
+
Brain.
|
|
6
|
+
|
|
7
|
+
- `auth.ts` — login handling. **Invest here**; it's the competitive edge.
|
|
8
|
+
- `screen-reader.ts` — reads pages as a cheap structured outline (accessibility
|
|
9
|
+
tree), not pixels. Vision is a later fallback.
|
|
10
|
+
- `crawler.ts` — the discovery loop. Swap in an AI navigation library
|
|
11
|
+
(Stagehand / Browser Use) to explore more intelligently.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility auditor — lightweight a11y checks using Playwright's aria snapshot
|
|
3
|
+
* and manual DOM queries. No axe-core dependency needed.
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* 1. Images without alt text
|
|
7
|
+
* 2. Form inputs without associated labels
|
|
8
|
+
* 3. Buttons/links with no accessible name
|
|
9
|
+
* 4. Missing lang attribute on html element
|
|
10
|
+
* 5. Missing page title
|
|
11
|
+
* 6. Empty headings
|
|
12
|
+
* 7. Color contrast (limited — detects text with inline style color only)
|
|
13
|
+
* 8. Keyboard-unfocusable interactive elements (tabIndex=-1 on buttons)
|
|
14
|
+
*/
|
|
15
|
+
import type { Page } from 'playwright';
|
|
16
|
+
export interface A11yViolation {
|
|
17
|
+
rule: string;
|
|
18
|
+
severity: 'critical' | 'serious' | 'moderate' | 'minor';
|
|
19
|
+
count: number;
|
|
20
|
+
examples: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface A11yAuditResult {
|
|
23
|
+
url: string;
|
|
24
|
+
violations: A11yViolation[];
|
|
25
|
+
passCount: number;
|
|
26
|
+
score: number;
|
|
27
|
+
auditedAt: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function auditAccessibility(page: Page, url: string): Promise<A11yAuditResult>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
async function queryAll(page, selector) {
|
|
2
|
+
try {
|
|
3
|
+
return await page.$$eval(selector, (els) => els.map((el) => ({
|
|
4
|
+
outerHTML: el.outerHTML?.slice(0, 200) ?? "",
|
|
5
|
+
textContent: (el.textContent ?? "").trim().slice(0, 100),
|
|
6
|
+
tabIndex: el.tabIndex,
|
|
7
|
+
getAttribute: (a) => el.getAttribute(a)
|
|
8
|
+
})));
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function auditAccessibility(page, url) {
|
|
14
|
+
const violations = [];
|
|
15
|
+
let passCount = 0;
|
|
16
|
+
const auditedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17
|
+
const imgs = await page.$$eval("img:not([alt])", (els) => els.map((e) => e.outerHTML?.slice(0, 120) ?? ""));
|
|
18
|
+
if (imgs.length > 0) violations.push({ rule: "image-alt", severity: "critical", count: imgs.length, examples: imgs.slice(0, 3) });
|
|
19
|
+
else passCount++;
|
|
20
|
+
const unlabeledInputs = await page.$$eval(
|
|
21
|
+
'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([aria-label]):not([aria-labelledby]):not([id])',
|
|
22
|
+
(els) => els.map((e) => e.outerHTML?.slice(0, 120) ?? "")
|
|
23
|
+
);
|
|
24
|
+
if (unlabeledInputs.length > 0) violations.push({ rule: "label", severity: "serious", count: unlabeledInputs.length, examples: unlabeledInputs.slice(0, 3) });
|
|
25
|
+
else passCount++;
|
|
26
|
+
const emptyButtons = await page.$$eval(
|
|
27
|
+
"button:not([aria-label]):not([aria-labelledby])",
|
|
28
|
+
(els) => els.filter((e) => !(e.textContent ?? "").trim()).map((e) => e.outerHTML?.slice(0, 120) ?? "")
|
|
29
|
+
);
|
|
30
|
+
if (emptyButtons.length > 0) violations.push({ rule: "button-name", severity: "critical", count: emptyButtons.length, examples: emptyButtons.slice(0, 3) });
|
|
31
|
+
else passCount++;
|
|
32
|
+
const htmlLang = await page.$eval("html", (el) => el.getAttribute("lang") ?? "").catch(() => "");
|
|
33
|
+
if (!htmlLang) violations.push({ rule: "html-has-lang", severity: "serious", count: 1, examples: ["<html> missing lang attribute"] });
|
|
34
|
+
else passCount++;
|
|
35
|
+
const title = await page.title().catch(() => "");
|
|
36
|
+
if (!title) violations.push({ rule: "document-title", severity: "serious", count: 1, examples: ["Page has no <title>"] });
|
|
37
|
+
else passCount++;
|
|
38
|
+
const emptyHeadings = await page.$$eval(
|
|
39
|
+
"h1,h2,h3,h4,h5,h6",
|
|
40
|
+
(els) => els.filter((e) => !(e.textContent ?? "").trim()).map((e) => e.tagName.toLowerCase())
|
|
41
|
+
);
|
|
42
|
+
if (emptyHeadings.length > 0) violations.push({ rule: "empty-heading", severity: "moderate", count: emptyHeadings.length, examples: emptyHeadings.slice(0, 3) });
|
|
43
|
+
else passCount++;
|
|
44
|
+
const emptyLinks = await page.$$eval(
|
|
45
|
+
"a[href]:not([aria-label]):not([aria-labelledby])",
|
|
46
|
+
(els) => els.filter((e) => !(e.textContent ?? "").trim() && !e.querySelector("img[alt]")).map((e) => e.outerHTML?.slice(0, 120) ?? "")
|
|
47
|
+
);
|
|
48
|
+
if (emptyLinks.length > 0) violations.push({ rule: "link-name", severity: "serious", count: emptyLinks.length, examples: emptyLinks.slice(0, 3) });
|
|
49
|
+
else passCount++;
|
|
50
|
+
const totalChecks = passCount + violations.length;
|
|
51
|
+
const score = totalChecks > 0 ? Math.round(passCount / totalChecks * 100) : 100;
|
|
52
|
+
return { url, violations, passCount, score, auditedAt };
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
auditAccessibility
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=accessibility-auditor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/accessibility-auditor.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Accessibility auditor \u2014 lightweight a11y checks using Playwright's aria snapshot\n * and manual DOM queries. No axe-core dependency needed.\n *\n * Checks:\n * 1. Images without alt text\n * 2. Form inputs without associated labels\n * 3. Buttons/links with no accessible name\n * 4. Missing lang attribute on html element\n * 5. Missing page title\n * 6. Empty headings\n * 7. Color contrast (limited \u2014 detects text with inline style color only)\n * 8. Keyboard-unfocusable interactive elements (tabIndex=-1 on buttons)\n */\n\nimport type { Page } from 'playwright';\n\nexport interface A11yViolation {\n rule: string;\n severity: 'critical' | 'serious' | 'moderate' | 'minor';\n count: number;\n examples: string[];\n}\n\nexport interface A11yAuditResult {\n url: string;\n violations: A11yViolation[];\n passCount: number;\n score: number; // 0-100\n auditedAt: string;\n}\n\nasync function queryAll(page: Page, selector: string): Promise<any[]> {\n try {\n return await page.$$eval(selector, els => els.map((el: any) => ({\n outerHTML: el.outerHTML?.slice(0, 200) ?? '',\n textContent: (el.textContent ?? '').trim().slice(0, 100),\n tabIndex: el.tabIndex,\n getAttribute: (a: string) => el.getAttribute(a),\n })));\n } catch {\n return [];\n }\n}\n\nexport async function auditAccessibility(page: Page, url: string): Promise<A11yAuditResult> {\n const violations: A11yViolation[] = [];\n let passCount = 0;\n const auditedAt = new Date().toISOString();\n\n // 1. Images without alt\n const imgs = await page.$$eval('img:not([alt])', els => els.map((e: any) => e.outerHTML?.slice(0, 120) ?? ''));\n if (imgs.length > 0) violations.push({ rule: 'image-alt', severity: 'critical', count: imgs.length, examples: imgs.slice(0, 3) });\n else passCount++;\n\n // 2. Inputs without labels\n const unlabeledInputs = await page.$$eval(\n 'input:not([type=\"hidden\"]):not([type=\"submit\"]):not([type=\"button\"]):not([aria-label]):not([aria-labelledby]):not([id])',\n els => els.map((e: any) => e.outerHTML?.slice(0, 120) ?? ''),\n );\n if (unlabeledInputs.length > 0) violations.push({ rule: 'label', severity: 'serious', count: unlabeledInputs.length, examples: unlabeledInputs.slice(0, 3) });\n else passCount++;\n\n // 3. Buttons with no text\n const emptyButtons = await page.$$eval(\n 'button:not([aria-label]):not([aria-labelledby])',\n els => els.filter((e: any) => !(e.textContent ?? '').trim()).map((e: any) => e.outerHTML?.slice(0, 120) ?? ''),\n );\n if (emptyButtons.length > 0) violations.push({ rule: 'button-name', severity: 'critical', count: emptyButtons.length, examples: emptyButtons.slice(0, 3) });\n else passCount++;\n\n // 4. Missing html lang\n const htmlLang = await page.$eval('html', (el: any) => el.getAttribute('lang') ?? '').catch(() => '');\n if (!htmlLang) violations.push({ rule: 'html-has-lang', severity: 'serious', count: 1, examples: ['<html> missing lang attribute'] });\n else passCount++;\n\n // 5. Missing page title\n const title = await page.title().catch(() => '');\n if (!title) violations.push({ rule: 'document-title', severity: 'serious', count: 1, examples: ['Page has no <title>'] });\n else passCount++;\n\n // 6. Empty headings\n const emptyHeadings = await page.$$eval(\n 'h1,h2,h3,h4,h5,h6',\n els => els.filter((e: any) => !(e.textContent ?? '').trim()).map((e: any) => e.tagName.toLowerCase()),\n );\n if (emptyHeadings.length > 0) violations.push({ rule: 'empty-heading', severity: 'moderate', count: emptyHeadings.length, examples: emptyHeadings.slice(0, 3) });\n else passCount++;\n\n // 7. Links with no accessible name\n const emptyLinks = await page.$$eval(\n 'a[href]:not([aria-label]):not([aria-labelledby])',\n els => els.filter((e: any) => !(e.textContent ?? '').trim() && !e.querySelector('img[alt]')).map((e: any) => e.outerHTML?.slice(0, 120) ?? ''),\n );\n if (emptyLinks.length > 0) violations.push({ rule: 'link-name', severity: 'serious', count: emptyLinks.length, examples: emptyLinks.slice(0, 3) });\n else passCount++;\n\n const totalChecks = passCount + violations.length;\n const score = totalChecks > 0 ? Math.round((passCount / totalChecks) * 100) : 100;\n\n return { url, violations, passCount, score, auditedAt };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Stagehand } from '@browserbasehq/stagehand';
|
|
2
|
+
import type { Credentials } from './auth.js';
|
|
3
|
+
/**
|
|
4
|
+
* Recorded login step from the Chrome extension's step recorder.
|
|
5
|
+
* {username} and {password} in `value` are substituted at replay time.
|
|
6
|
+
*/
|
|
7
|
+
export interface LoginStep {
|
|
8
|
+
action: 'fill' | 'click' | 'wait';
|
|
9
|
+
selector: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
ms?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* AI-guided login using Stagehand act/extract.
|
|
15
|
+
*
|
|
16
|
+
* @param loginInstructions - Optional natural language description of the login flow.
|
|
17
|
+
* When provided, an initial stagehand.act() is called with these instructions to
|
|
18
|
+
* navigate the UI (e.g. "click Sign In in the nav, fill email, click Continue").
|
|
19
|
+
* The standard 3-step credential fill follows immediately after.
|
|
20
|
+
* @param opts - Optional tenant/project/job IDs for writing AUTH_STEP CrawlEvidence.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loginWithStagehand(stagehand: Stagehand, creds: Credentials, startUrl: string, captchaOpts?: {
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
provider?: '2captcha' | 'capsolver';
|
|
25
|
+
}, emailOtpOpts?: {
|
|
26
|
+
apiKey?: string;
|
|
27
|
+
inboxId?: string;
|
|
28
|
+
}, loginInstructions?: string, opts?: {
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
projectId?: string;
|
|
31
|
+
jobId?: string;
|
|
32
|
+
}): Promise<boolean>;
|
package/dist/ai-login.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AppBrain } from "@qip/app-brain";
|
|
3
|
+
import { solveCaptcha } from "./captcha-solver.js";
|
|
4
|
+
import { waitForEmailContent } from "./email-otp.js";
|
|
5
|
+
import { logger } from "./logger.js";
|
|
6
|
+
import { simulateMouseMovement } from "./stealth/human-behavior.js";
|
|
7
|
+
const CLICK_INTENT = /\b(click|tap|press|submit|select|choose|toggle)\b/i;
|
|
8
|
+
async function actWithMouseSim(stagehand, instruction) {
|
|
9
|
+
if (CLICK_INTENT.test(instruction)) {
|
|
10
|
+
try {
|
|
11
|
+
const pg = await stagehand.resolvePage();
|
|
12
|
+
const viewport = pg.viewportSize() ?? { width: 1280, height: 720 };
|
|
13
|
+
const targetX = viewport.width * 0.3 + Math.random() * viewport.width * 0.4;
|
|
14
|
+
const targetY = viewport.height * 0.2 + Math.random() * viewport.height * 0.5;
|
|
15
|
+
await simulateMouseMovement(pg, targetX, targetY);
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return stagehand.act(instruction);
|
|
20
|
+
}
|
|
21
|
+
const TwoFASchema = z.object({
|
|
22
|
+
has2FA: z.boolean(),
|
|
23
|
+
fieldLabel: z.string().optional()
|
|
24
|
+
});
|
|
25
|
+
const CAPTCHA_SELECTORS = [
|
|
26
|
+
'iframe[src*="recaptcha"]',
|
|
27
|
+
'iframe[src*="hcaptcha"]',
|
|
28
|
+
'iframe[src*="challenges.cloudflare.com"]',
|
|
29
|
+
".cf-turnstile",
|
|
30
|
+
"[data-sitekey]",
|
|
31
|
+
"#captcha",
|
|
32
|
+
".captcha",
|
|
33
|
+
'[id*="captcha"]',
|
|
34
|
+
'[class*="captcha"]'
|
|
35
|
+
];
|
|
36
|
+
async function loginWithStagehand(stagehand, creds, startUrl, captchaOpts, emailOtpOpts, loginInstructions, opts) {
|
|
37
|
+
async function logStep(step, fn) {
|
|
38
|
+
const t0 = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const r = await fn();
|
|
41
|
+
const result = { success: true, detail: r?.description ?? r?.message ?? step };
|
|
42
|
+
if (opts?.tenantId && opts?.projectId) {
|
|
43
|
+
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
44
|
+
jobId: opts?.jobId,
|
|
45
|
+
type: "AUTH_STEP",
|
|
46
|
+
payload: { step, ...result, ms: Date.now() - t0 }
|
|
47
|
+
}).catch(() => {
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
const result = { success: false, detail: e.message ?? "failed" };
|
|
53
|
+
if (opts?.tenantId && opts?.projectId) {
|
|
54
|
+
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
55
|
+
jobId: opts?.jobId,
|
|
56
|
+
type: "AUTH_STEP",
|
|
57
|
+
payload: { step, ...result, ms: Date.now() - t0 }
|
|
58
|
+
}).catch(() => {
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const pg = await stagehand.resolvePage();
|
|
66
|
+
for (const sel of CAPTCHA_SELECTORS) {
|
|
67
|
+
const count = await pg.locator(sel).count().catch(() => 0);
|
|
68
|
+
if (count > 0) {
|
|
69
|
+
logger.warn({ selector: sel }, "[crawler] CAPTCHA detected on login page");
|
|
70
|
+
if (captchaOpts?.apiKey) {
|
|
71
|
+
logger.info({ provider: captchaOpts.provider ?? "2captcha" }, "[crawler] Attempting auto-solve");
|
|
72
|
+
try {
|
|
73
|
+
const result = await solveCaptcha(pg, startUrl, captchaOpts.apiKey, captchaOpts.provider ?? "2captcha");
|
|
74
|
+
if (result) {
|
|
75
|
+
logger.info({ provider: result.provider }, "[crawler] CAPTCHA solved \u2014 proceeding with login");
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
} catch (solveErr) {
|
|
79
|
+
logger.warn({ err: solveErr }, "[crawler] CAPTCHA solve failed \u2014 falling back to manual error");
|
|
80
|
+
throw new Error(`CAPTCHA_DETECTED: Auto-solve failed (${String(solveErr)}). Use session capture instead.`);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'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.'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (loginInstructions) {
|
|
91
|
+
await logStep("custom_recipe", () => actWithMouseSim(stagehand, loginInstructions));
|
|
92
|
+
await pg.waitForLoadState("networkidle").catch(() => pg.waitForTimeout(1500));
|
|
93
|
+
}
|
|
94
|
+
await logStep("fill_username", () => actWithMouseSim(stagehand, `Type "${creds.username}" into the username or email input field`));
|
|
95
|
+
await logStep("fill_password", () => actWithMouseSim(stagehand, `Type "${creds.password}" into the password input field`));
|
|
96
|
+
await logStep("click_submit", () => actWithMouseSim(stagehand, "Click the sign in, log in, login, or continue button to submit credentials"));
|
|
97
|
+
await pg.waitForLoadState("networkidle").catch(() => pg.waitForTimeout(2e3));
|
|
98
|
+
const twoFA = await stagehand.extract(
|
|
99
|
+
"Is there a two-factor authentication, OTP, or verification code input visible right now?",
|
|
100
|
+
TwoFASchema
|
|
101
|
+
);
|
|
102
|
+
if (twoFA.has2FA) {
|
|
103
|
+
if (opts?.tenantId && opts?.projectId) {
|
|
104
|
+
AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
|
|
105
|
+
jobId: opts?.jobId,
|
|
106
|
+
type: "AUTH_STEP",
|
|
107
|
+
payload: { step: "2fa_detect", success: true, detail: twoFA.fieldLabel ?? "2FA detected", ms: 0 }
|
|
108
|
+
}).catch(() => {
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
let code = creds.oneTimeCode ?? await creds.getOneTimeCode?.();
|
|
112
|
+
if (!code && emailOtpOpts?.apiKey && emailOtpOpts?.inboxId) {
|
|
113
|
+
const since = (/* @__PURE__ */ new Date()).toISOString();
|
|
114
|
+
logger.info("[crawler] 2FA detected \u2014 waiting for email OTP or magic link via MailSlurp");
|
|
115
|
+
const emailContent = await waitForEmailContent(
|
|
116
|
+
{ provider: "mailslurp", apiKey: emailOtpOpts.apiKey, inboxId: emailOtpOpts.inboxId },
|
|
117
|
+
since
|
|
118
|
+
);
|
|
119
|
+
if (emailContent?.magicLink) {
|
|
120
|
+
logger.info({ url: emailContent.magicLink }, "[crawler] Magic link received \u2014 navigating");
|
|
121
|
+
await pg.goto(emailContent.magicLink, { waitUntil: "domcontentloaded" });
|
|
122
|
+
await pg.waitForLoadState("networkidle").catch(() => {
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
code = emailContent?.otp ?? void 0;
|
|
127
|
+
if (code) {
|
|
128
|
+
logger.info("[crawler] Email OTP received");
|
|
129
|
+
} else {
|
|
130
|
+
logger.warn("[crawler] Email OTP timed out after 60s");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!code) {
|
|
134
|
+
console.warn("[crawler] 2FA detected but no oneTimeCode provided");
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
await logStep("2fa_inject", () => actWithMouseSim(stagehand, `Enter "${code}" into the ${twoFA.fieldLabel ?? "verification code"} field`));
|
|
138
|
+
await logStep("2fa_submit", () => actWithMouseSim(stagehand, "Click verify or confirm to submit the verification code"));
|
|
139
|
+
await pg.waitForLoadState("networkidle").catch(() => pg.waitForTimeout(2e3));
|
|
140
|
+
}
|
|
141
|
+
const currentUrl = pg.url();
|
|
142
|
+
const loginPathPattern = /\/(login|signin|sign-in|auth|authenticate|log-in|account\/login)(\?|$|\/)/i;
|
|
143
|
+
try {
|
|
144
|
+
const { pathname } = new URL(currentUrl);
|
|
145
|
+
const wasRedirected = currentUrl !== startUrl;
|
|
146
|
+
const stillOnLoginPath = loginPathPattern.test(pathname);
|
|
147
|
+
if (wasRedirected && !stillOnLoginPath) {
|
|
148
|
+
logger.info({ url: currentUrl }, "Login confirmed via URL change");
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
const passwordCount = await pg.locator('input[type="password"]').count().catch(() => 1);
|
|
154
|
+
const bodyText = await pg.evaluate(() => document.body?.innerText?.trim()?.length ?? 0).catch(() => 0);
|
|
155
|
+
if (passwordCount === 0 && bodyText > 30) {
|
|
156
|
+
logger.info("Login confirmed: password field no longer visible");
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
logger.warn({ url: currentUrl }, "Login appears failed");
|
|
160
|
+
try {
|
|
161
|
+
const screenshotPath = `/tmp/zeta-auth-failure-${Date.now()}.jpg`;
|
|
162
|
+
await pg.screenshot({ path: screenshotPath, type: "jpeg", quality: 70, fullPage: false });
|
|
163
|
+
logger.warn({ screenshotPath }, "[crawler] Auth failure screenshot saved");
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const msg = String(err.message ?? err);
|
|
169
|
+
if (msg.includes("CAPTCHA_DETECTED")) throw err;
|
|
170
|
+
logger.error({ err }, "Login error");
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export {
|
|
175
|
+
loginWithStagehand
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=ai-login.js.map
|
|
@@ -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 '@qip/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/** 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 if (loginInstructions) {\n await logStep('custom_recipe', () => actWithMouseSim(stagehand, loginInstructions));\n await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(1500));\n }\n\n await logStep('fill_username', () => actWithMouseSim(stagehand, `Type \"${creds.username}\" into the username or email input field`));\n await logStep('fill_password', () => actWithMouseSim(stagehand, `Type \"${creds.password}\" into the password input field`));\n await logStep('click_submit', () => actWithMouseSim(stagehand, 'Click the sign in, log in, login, or continue button to submit credentials'));\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', () => actWithMouseSim(stagehand, `Enter \"${code}\" into the ${twoFA.fieldLabel ?? 'verification code'} field`));\n await logStep('2fa_submit', () => actWithMouseSim(stagehand, 'Click verify or confirm to submit the verification code'));\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": "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",
|
|
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>;
|