@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/dist/sitemap.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { ProxyAgent } from "undici";
|
|
3
|
+
import { assertCrawlUrlSafe } from "@qip/core";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
6
|
+
const MAX_SITEMAP_INDEX_DEPTH = 3;
|
|
7
|
+
const MAX_SUB_SITEMAPS = 50;
|
|
8
|
+
const MAX_URLS = 5e3;
|
|
9
|
+
function toArray(x) {
|
|
10
|
+
if (x == null) return [];
|
|
11
|
+
return Array.isArray(x) ? x : [x];
|
|
12
|
+
}
|
|
13
|
+
function hostKey(hostname) {
|
|
14
|
+
return hostname.replace(/^www\./i, "").toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function sameSite(candidateUrl, siteOrigin) {
|
|
17
|
+
try {
|
|
18
|
+
const originHost = /^https?:\/\//i.test(siteOrigin) ? new URL(siteOrigin).hostname : siteOrigin;
|
|
19
|
+
return hostKey(new URL(candidateUrl).hostname) === hostKey(originHost);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function extractUrlsFromParsedSitemap(parsed, origin) {
|
|
25
|
+
const rawPageUrls = [];
|
|
26
|
+
let subSitemapUrls = [];
|
|
27
|
+
if (parsed?.sitemapindex) {
|
|
28
|
+
subSitemapUrls = toArray(parsed.sitemapindex.sitemap).map((s) => s?.loc).filter((loc) => typeof loc === "string").slice(0, MAX_SUB_SITEMAPS);
|
|
29
|
+
}
|
|
30
|
+
if (parsed?.urlset) {
|
|
31
|
+
rawPageUrls.push(
|
|
32
|
+
...toArray(parsed.urlset.url).map((u) => u?.loc).filter((loc) => typeof loc === "string")
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const sameOrigin = rawPageUrls.filter((u) => sameSite(u, origin));
|
|
36
|
+
return { pageUrls: [...new Set(sameOrigin)].slice(0, MAX_URLS), subSitemapUrls };
|
|
37
|
+
}
|
|
38
|
+
async function fetchAndParseXml(url, dispatcher) {
|
|
39
|
+
try {
|
|
40
|
+
assertCrawlUrlSafe(url);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
47
|
+
headers: { "User-Agent": "ZeTa-AI-Crawler/1.0 (+sitemap-discovery)" },
|
|
48
|
+
...dispatcher ? { dispatcher } : {}
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) return null;
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
return new XMLParser().parse(text);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function fetchSitemapUrls(sitemapUrl, origin, depth = 0, dispatcher) {
|
|
58
|
+
if (depth > MAX_SITEMAP_INDEX_DEPTH) return [];
|
|
59
|
+
const parsed = await fetchAndParseXml(sitemapUrl, dispatcher);
|
|
60
|
+
if (!parsed) return [];
|
|
61
|
+
const { pageUrls, subSitemapUrls } = extractUrlsFromParsedSitemap(parsed, origin);
|
|
62
|
+
const collected = [...pageUrls];
|
|
63
|
+
for (const subUrl of subSitemapUrls) {
|
|
64
|
+
if (collected.length >= MAX_URLS) break;
|
|
65
|
+
collected.push(...await fetchSitemapUrls(subUrl, origin, depth + 1, dispatcher));
|
|
66
|
+
}
|
|
67
|
+
return [...new Set(collected)].slice(0, MAX_URLS);
|
|
68
|
+
}
|
|
69
|
+
async function discoverSitemapUrls(appUrl, sitemapUrl) {
|
|
70
|
+
let origin;
|
|
71
|
+
try {
|
|
72
|
+
origin = new URL(appUrl).origin;
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const target = sitemapUrl || `${origin}/sitemap.xml`;
|
|
77
|
+
try {
|
|
78
|
+
return await fetchSitemapUrls(target, origin);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.warn({ err: String(err), target }, "[sitemap] discovery failed, continuing without it");
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function buildProxyDispatcher(proxyUrl) {
|
|
85
|
+
if (!proxyUrl || proxyUrl === "auto") return void 0;
|
|
86
|
+
try {
|
|
87
|
+
const m = proxyUrl.match(/^(https?):\/\/(?:([^:@]*)(?::([^@]*))?@)?(.+)$/);
|
|
88
|
+
if (!m) return new ProxyAgent(proxyUrl);
|
|
89
|
+
const [, scheme, user, pass, hostPort] = m;
|
|
90
|
+
const uri = `${scheme}://${hostPort}`;
|
|
91
|
+
if (user) {
|
|
92
|
+
const token = "Basic " + Buffer.from(`${decodeURIComponent(user)}:${decodeURIComponent(pass ?? "")}`).toString("base64");
|
|
93
|
+
return new ProxyAgent({ uri, token });
|
|
94
|
+
}
|
|
95
|
+
return new ProxyAgent({ uri });
|
|
96
|
+
} catch {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function parseRobotsSitemaps(robotsTxt, origin) {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const line of robotsTxt.split(/\r?\n/)) {
|
|
103
|
+
const m = line.match(/^\s*sitemap:\s*(\S+)/i);
|
|
104
|
+
if (m && sameSite(m[1], origin)) out.push(m[1]);
|
|
105
|
+
}
|
|
106
|
+
return [...new Set(out)];
|
|
107
|
+
}
|
|
108
|
+
async function discoverProjectUrls(appUrl, opts) {
|
|
109
|
+
let origin;
|
|
110
|
+
try {
|
|
111
|
+
origin = new URL(appUrl).origin;
|
|
112
|
+
} catch {
|
|
113
|
+
return { candidates: [], sitemapFound: false };
|
|
114
|
+
}
|
|
115
|
+
const dispatcher = buildProxyDispatcher(opts?.proxyUrl);
|
|
116
|
+
let robotsSitemaps = [];
|
|
117
|
+
try {
|
|
118
|
+
assertCrawlUrlSafe(`${origin}/robots.txt`);
|
|
119
|
+
const res = await fetch(`${origin}/robots.txt`, {
|
|
120
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
121
|
+
headers: { "User-Agent": "ZeTa-AI-Crawler/1.0 (+url-discovery)" },
|
|
122
|
+
...dispatcher ? { dispatcher } : {}
|
|
123
|
+
});
|
|
124
|
+
if (res.ok) robotsSitemaps = parseRobotsSitemaps(await res.text(), origin);
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
const robotsSet = new Set(robotsSitemaps);
|
|
128
|
+
const sitemapTargets = [.../* @__PURE__ */ new Set([
|
|
129
|
+
...opts?.sitemapUrl ? [opts.sitemapUrl] : [],
|
|
130
|
+
...robotsSitemaps,
|
|
131
|
+
`${origin}/sitemap.xml`
|
|
132
|
+
])];
|
|
133
|
+
const seen = /* @__PURE__ */ new Map();
|
|
134
|
+
let sitemapFound = false;
|
|
135
|
+
for (const sm of sitemapTargets) {
|
|
136
|
+
const urls = await fetchSitemapUrls(sm, origin, 0, dispatcher);
|
|
137
|
+
if (urls.length) sitemapFound = true;
|
|
138
|
+
const source = robotsSet.has(sm) ? "robots" : "sitemap";
|
|
139
|
+
for (const u of urls) if (!seen.has(u)) seen.set(u, { url: u, source });
|
|
140
|
+
}
|
|
141
|
+
return { candidates: [...seen.values()].slice(0, MAX_URLS), sitemapFound };
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
buildProxyDispatcher,
|
|
145
|
+
discoverProjectUrls,
|
|
146
|
+
discoverSitemapUrls,
|
|
147
|
+
extractUrlsFromParsedSitemap,
|
|
148
|
+
fetchSitemapUrls,
|
|
149
|
+
parseRobotsSitemaps,
|
|
150
|
+
sameSite
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sitemap.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Sitemap.xml discovery \u2014 seeds the crawl queue from a project's sitemap\n * instead of (or alongside) plain link-following, matching crawl4AI's\n * sitemap-seeding capability. Best-effort throughout: most sites don't have\n * a sitemap, or it 404s, or it's malformed \u2014 none of that should ever fail\n * a crawl, it just means sitemap seeding contributes nothing this run.\n */\nimport { XMLParser } from 'fast-xml-parser';\nimport { ProxyAgent } from 'undici';\nimport { assertCrawlUrlSafe } from '@qip/core';\nimport { logger } from './logger.js';\n\nconst FETCH_TIMEOUT_MS = 10_000;\nconst MAX_SITEMAP_INDEX_DEPTH = 3; // sitemap index -> sub-sitemaps -> (no deeper nesting expected)\nconst MAX_SUB_SITEMAPS = 50; // bound fan-out from a single sitemap index\nconst MAX_URLS = 5000; // sane upper bound so a huge/malicious sitemap can't blow up memory\n\nfunction toArray<T>(x: T | T[] | undefined | null): T[] {\n if (x == null) return [];\n return Array.isArray(x) ? x : [x];\n}\n\n/** Registrable-host key: lowercased hostname with a leading `www.` stripped. */\nfunction hostKey(hostname: string): string {\n return hostname.replace(/^www\\./i, '').toLowerCase();\n}\n\n/**\n * Same-site check that tolerates www/non-www and http/https differences \u2014 a sitemap on\n * `vercel.com` is valid for a project pointed at `www.vercel.com` (and vice versa).\n */\nexport function sameSite(candidateUrl: string, siteOrigin: string): boolean {\n try {\n const originHost = /^https?:\\/\\//i.test(siteOrigin) ? new URL(siteOrigin).hostname : siteOrigin;\n return hostKey(new URL(candidateUrl).hostname) === hostKey(originHost);\n } catch {\n return false;\n }\n}\n\n/**\n * Pulls page URLs and sub-sitemap URLs out of an already-XML-parsed sitemap\n * document (either a <urlset> or a <sitemapindex>), filtered to same-origin\n * and deduped. Pure function \u2014 no I/O \u2014 for direct unit testing without a\n * real HTTP fetch.\n */\nexport function extractUrlsFromParsedSitemap(parsed: any, origin: string): { pageUrls: string[]; subSitemapUrls: string[] } {\n const rawPageUrls: string[] = [];\n let subSitemapUrls: string[] = [];\n\n if (parsed?.sitemapindex) {\n subSitemapUrls = toArray(parsed.sitemapindex.sitemap)\n .map((s: any) => s?.loc)\n .filter((loc: any): loc is string => typeof loc === 'string')\n .slice(0, MAX_SUB_SITEMAPS);\n }\n\n if (parsed?.urlset) {\n rawPageUrls.push(\n ...toArray(parsed.urlset.url)\n .map((u: any) => u?.loc)\n .filter((loc: any): loc is string => typeof loc === 'string'),\n );\n }\n\n // Same-site only (www/protocol-tolerant) \u2014 a sitemap listing external URLs shouldn't seed this project's queue.\n const sameOrigin = rawPageUrls.filter((u) => sameSite(u, origin));\n\n return { pageUrls: [...new Set(sameOrigin)].slice(0, MAX_URLS), subSitemapUrls };\n}\n\nasync function fetchAndParseXml(url: string, dispatcher?: any): Promise<any | null> {\n // Same SSRF posture as the crawl itself (assertCrawlUrlSafe, not the\n // stricter webhook-oriented assertResolvesToPublicAddress) \u2014 a sitemap\n // living on a tenant's internal/VPN-only staging server is a legitimate\n // crawl target, not an attack; only loopback/cloud-metadata are blocked.\n try {\n assertCrawlUrlSafe(url);\n } catch {\n return null;\n }\n try {\n // Redirects followed normally here (unlike the webhook-dispatch.ts\n // 'manual' pattern) \u2014 an http\u2192https or bare\u2192www redirect on the\n // sitemap is completely ordinary, and Playwright's actual page\n // navigation later in the crawl already follows redirects to whatever\n // this same site sends it to; a sitemap fetch isn't a more sensitive\n // operation than that.\n const res = await fetch(url, {\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n headers: { 'User-Agent': 'ZeTa-AI-Crawler/1.0 (+sitemap-discovery)' },\n ...(dispatcher ? { dispatcher } : {}),\n });\n if (!res.ok) return null;\n const text = await res.text();\n return new XMLParser().parse(text);\n } catch {\n return null;\n }\n}\n\n/**\n * Fetches and parses a sitemap (or sitemap index, recursed up to a small\n * depth) into a flat, deduped list of same-origin URLs.\n */\nexport async function fetchSitemapUrls(sitemapUrl: string, origin: string, depth = 0, dispatcher?: any): Promise<string[]> {\n if (depth > MAX_SITEMAP_INDEX_DEPTH) return [];\n const parsed = await fetchAndParseXml(sitemapUrl, dispatcher);\n if (!parsed) return [];\n\n const { pageUrls, subSitemapUrls } = extractUrlsFromParsedSitemap(parsed, origin);\n const collected = [...pageUrls];\n\n for (const subUrl of subSitemapUrls) {\n if (collected.length >= MAX_URLS) break;\n collected.push(...(await fetchSitemapUrls(subUrl, origin, depth + 1, dispatcher)));\n }\n\n return [...new Set(collected)].slice(0, MAX_URLS);\n}\n\n/**\n * Discovers a project's sitemap (default `${origin}/sitemap.xml`, or an\n * explicit override) and returns the URLs it lists. Never throws.\n */\nexport async function discoverSitemapUrls(appUrl: string, sitemapUrl?: string): Promise<string[]> {\n let origin: string;\n try {\n origin = new URL(appUrl).origin;\n } catch {\n return [];\n }\n const target = sitemapUrl || `${origin}/sitemap.xml`;\n try {\n return await fetchSitemapUrls(target, origin);\n } catch (err) {\n logger.warn({ err: String(err), target }, '[sitemap] discovery failed, continuing without it');\n return [];\n }\n}\n\n/**\n * Build an undici ProxyAgent from a proxy URL. Parses credentials manually because\n * DataImpulse-style usernames contain commas (country codes) that break new URL().\n * Returns undefined for no/auto proxy \u2014 the caller then fetches directly.\n */\nexport function buildProxyDispatcher(proxyUrl?: string): ProxyAgent | undefined {\n if (!proxyUrl || proxyUrl === 'auto') return undefined;\n try {\n const m = proxyUrl.match(/^(https?):\\/\\/(?:([^:@]*)(?::([^@]*))?@)?(.+)$/);\n if (!m) return new ProxyAgent(proxyUrl);\n const [, scheme, user, pass, hostPort] = m;\n const uri = `${scheme}://${hostPort}`;\n if (user) {\n const token = 'Basic ' + Buffer.from(`${decodeURIComponent(user)}:${decodeURIComponent(pass ?? '')}`).toString('base64');\n return new ProxyAgent({ uri, token });\n }\n return new ProxyAgent({ uri });\n } catch {\n return undefined;\n }\n}\n\n/** Extract same-origin `Sitemap:` directives from a robots.txt body. */\nexport function parseRobotsSitemaps(robotsTxt: string, origin: string): string[] {\n const out: string[] = [];\n for (const line of robotsTxt.split(/\\r?\\n/)) {\n const m = line.match(/^\\s*sitemap:\\s*(\\S+)/i);\n if (m && sameSite(m[1], origin)) out.push(m[1]);\n }\n return [...new Set(out)];\n}\n\nexport interface UrlCandidate {\n url: string;\n source: 'sitemap' | 'robots' | 'link';\n}\n\n/**\n * Pre-crawl URL discovery for the review-before-crawl flow: reads robots.txt Sitemap\n * directives and sitemap.xml (recursed, same-origin), proxy-aware. Returns a deduped\n * candidate list the user can review and seed before launching the Playwright crawl.\n * Never throws \u2014 bot-protected sites (Akamai/Cloudflare) simply yield an empty list.\n */\nexport async function discoverProjectUrls(\n appUrl: string,\n opts?: { sitemapUrl?: string; proxyUrl?: string },\n): Promise<{ candidates: UrlCandidate[]; sitemapFound: boolean }> {\n let origin: string;\n try { origin = new URL(appUrl).origin; } catch { return { candidates: [], sitemapFound: false }; }\n\n const dispatcher = buildProxyDispatcher(opts?.proxyUrl);\n\n // 1. robots.txt \u2192 declared sitemaps\n let robotsSitemaps: string[] = [];\n try {\n assertCrawlUrlSafe(`${origin}/robots.txt`);\n const res = await fetch(`${origin}/robots.txt`, {\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n headers: { 'User-Agent': 'ZeTa-AI-Crawler/1.0 (+url-discovery)' },\n ...(dispatcher ? { dispatcher } : {}),\n });\n if (res.ok) robotsSitemaps = parseRobotsSitemaps(await res.text(), origin);\n } catch { /* best-effort */ }\n\n // 2. sitemap targets: explicit override + robots-declared + conventional default\n const robotsSet = new Set(robotsSitemaps);\n const sitemapTargets = [...new Set([\n ...(opts?.sitemapUrl ? [opts.sitemapUrl] : []),\n ...robotsSitemaps,\n `${origin}/sitemap.xml`,\n ])];\n\n const seen = new Map<string, UrlCandidate>();\n let sitemapFound = false;\n for (const sm of sitemapTargets) {\n const urls = await fetchSitemapUrls(sm, origin, 0, dispatcher);\n if (urls.length) sitemapFound = true;\n const source: UrlCandidate['source'] = robotsSet.has(sm) ? 'robots' : 'sitemap';\n for (const u of urls) if (!seen.has(u)) seen.set(u, { url: u, source });\n }\n\n return { candidates: [...seen.values()].slice(0, MAX_URLS), sitemapFound };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skyvern Vision Fallback
|
|
3
|
+
*
|
|
4
|
+
* Skyvern combines computer vision + LLM reasoning to navigate sites that
|
|
5
|
+
* defeat DOM-based approaches: CAPTCHAs, legacy apps, visual-only login flows,
|
|
6
|
+
* government portals. It's the "break glass" option when Stagehand fails.
|
|
7
|
+
*
|
|
8
|
+
* Production setup (Phase 4+):
|
|
9
|
+
* 1. Self-host: https://github.com/Skyvern-AI/skyvern
|
|
10
|
+
* 2. Or use cloud: https://app.skyvern.com
|
|
11
|
+
* 3. Set SKYVERN_API_URL + SKYVERN_API_KEY in .env
|
|
12
|
+
*
|
|
13
|
+
* Pricing note: vision tasks cost more per step than DOM-based Stagehand.
|
|
14
|
+
* Use only when the fast path fails.
|
|
15
|
+
*/
|
|
16
|
+
export interface SkyvernTask {
|
|
17
|
+
taskDescription: string;
|
|
18
|
+
url: string;
|
|
19
|
+
credentials?: {
|
|
20
|
+
username: string;
|
|
21
|
+
password: string;
|
|
22
|
+
oneTimeCode?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface SkyvernConfig {
|
|
26
|
+
apiUrl: string;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
llmKey?: string;
|
|
29
|
+
llmApiKey?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SkyvernStep {
|
|
32
|
+
order: number;
|
|
33
|
+
action_type: string;
|
|
34
|
+
element_description?: string;
|
|
35
|
+
reasoning?: string;
|
|
36
|
+
success: boolean;
|
|
37
|
+
screenshot_url?: string;
|
|
38
|
+
duration_ms?: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface SkyvernNavigateResult {
|
|
42
|
+
screenshotUrls: string[];
|
|
43
|
+
visitedUrls: string[];
|
|
44
|
+
status: 'success' | 'failed' | 'partial';
|
|
45
|
+
steps?: SkyvernStep[];
|
|
46
|
+
taskId?: string;
|
|
47
|
+
durationMs?: number;
|
|
48
|
+
triggerReason?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Gap 5: Post-login navigation via Skyvern vision for apps where DOM analysis
|
|
52
|
+
* cannot discover screens (canvas, WebGL, legacy Flash-replacement stacks).
|
|
53
|
+
* Skyvern drives the browser via screenshot + LLM, returning visited page list.
|
|
54
|
+
*/
|
|
55
|
+
export declare function skyvernNavigate(startUrl: string, navigationGoals: string[], tenantConfig?: SkyvernConfig, triggerReason?: string): Promise<SkyvernNavigateResult>;
|
|
56
|
+
export declare function skyvernFallback(task: SkyvernTask, tenantConfig?: SkyvernConfig): Promise<'success' | 'failed'>;
|
package/dist/skyvern.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
async function skyvernNavigate(startUrl, navigationGoals, tenantConfig, triggerReason) {
|
|
2
|
+
const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;
|
|
3
|
+
const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;
|
|
4
|
+
if (!apiUrl || !apiKey) {
|
|
5
|
+
console.warn("[skyvern] skyvernNavigate: no API config \u2014 skipping post-login vision navigation");
|
|
6
|
+
return { screenshotUrls: [], visitedUrls: [], status: "failed" };
|
|
7
|
+
}
|
|
8
|
+
const visitedUrls = [];
|
|
9
|
+
const screenshotUrls = [];
|
|
10
|
+
const skyvernSteps = [];
|
|
11
|
+
let lastTaskId;
|
|
12
|
+
let totalDurationMs = 0;
|
|
13
|
+
for (const goal of navigationGoals) {
|
|
14
|
+
try {
|
|
15
|
+
const taskStart = Date.now();
|
|
16
|
+
const res = await fetch(`${apiUrl}/api/v1/tasks`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
url: startUrl,
|
|
21
|
+
navigation_goal: goal,
|
|
22
|
+
...tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {},
|
|
23
|
+
...tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) continue;
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
const taskId = data.task_id;
|
|
29
|
+
if (!taskId) continue;
|
|
30
|
+
lastTaskId = taskId;
|
|
31
|
+
let done = false;
|
|
32
|
+
for (let p = 0; p < 12 && !done; p++) {
|
|
33
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
34
|
+
const poll = await fetch(`${apiUrl}/api/v1/tasks/${taskId}`, {
|
|
35
|
+
headers: { "x-api-key": apiKey }
|
|
36
|
+
}).catch(() => null);
|
|
37
|
+
if (!poll?.ok) continue;
|
|
38
|
+
const pollData = await poll.json();
|
|
39
|
+
if (pollData.status === "completed" || pollData.status === "failed") {
|
|
40
|
+
done = true;
|
|
41
|
+
totalDurationMs += Date.now() - taskStart;
|
|
42
|
+
if (pollData.status === "completed") {
|
|
43
|
+
if (pollData.output?.url) visitedUrls.push(pollData.output.url);
|
|
44
|
+
if (pollData.output?.screenshot_url) screenshotUrls.push(pollData.output.screenshot_url);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const stepsRes = await fetch(`${apiUrl}/api/v1/tasks/${taskId}/steps`, {
|
|
48
|
+
headers: { "x-api-key": apiKey }
|
|
49
|
+
}).catch(() => null);
|
|
50
|
+
if (stepsRes?.ok) {
|
|
51
|
+
const stepsData = await stepsRes.json();
|
|
52
|
+
const stepsList = Array.isArray(stepsData) ? stepsData : stepsData?.steps ?? [];
|
|
53
|
+
for (const s of stepsList) {
|
|
54
|
+
if (s.output?.screenshot_url) screenshotUrls.push(s.output.screenshot_url);
|
|
55
|
+
skyvernSteps.push({
|
|
56
|
+
order: s.step_order ?? s.order ?? 0,
|
|
57
|
+
action_type: s.action_type ?? s.action ?? "unknown",
|
|
58
|
+
element_description: s.output?.element_description,
|
|
59
|
+
reasoning: s.output?.llm_response?.action_description ?? s.output?.reasoning,
|
|
60
|
+
success: s.status === "completed",
|
|
61
|
+
screenshot_url: s.output?.screenshot_url,
|
|
62
|
+
error: s.status === "failed" ? s.output?.errors?.[0] ?? "Step failed" : void 0
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.warn(`[skyvern] Navigation goal failed: ${goal}`, err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
screenshotUrls,
|
|
76
|
+
visitedUrls,
|
|
77
|
+
status: visitedUrls.length > 0 ? "success" : screenshotUrls.length > 0 ? "partial" : "failed",
|
|
78
|
+
steps: skyvernSteps,
|
|
79
|
+
taskId: lastTaskId,
|
|
80
|
+
durationMs: totalDurationMs || void 0,
|
|
81
|
+
triggerReason
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function skyvernFallback(task, tenantConfig) {
|
|
85
|
+
const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;
|
|
86
|
+
const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;
|
|
87
|
+
if (!apiUrl || !apiKey) {
|
|
88
|
+
console.warn(
|
|
89
|
+
"[skyvern] Vision fallback disabled \u2014 set SKYVERN_API_URL + SKYVERN_API_KEY in .env to enable.",
|
|
90
|
+
"\n[skyvern] Self-host: https://github.com/Skyvern-AI/skyvern"
|
|
91
|
+
);
|
|
92
|
+
return "failed";
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${apiUrl}/api/v1/tasks`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
url: task.url,
|
|
100
|
+
navigation_goal: task.taskDescription,
|
|
101
|
+
...task.credentials && {
|
|
102
|
+
navigation_payload: {
|
|
103
|
+
username: task.credentials.username,
|
|
104
|
+
password: task.credentials.password,
|
|
105
|
+
...task.credentials.oneTimeCode && { otp: task.credentials.oneTimeCode }
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
...tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {},
|
|
109
|
+
...tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const body = await res.text().catch(() => "");
|
|
114
|
+
console.error(`[skyvern] Task creation failed: ${res.status}`, body);
|
|
115
|
+
return "failed";
|
|
116
|
+
}
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
console.log(`[skyvern] Task queued: ${data.task_id}. Poll GET /api/v1/tasks/${data.task_id} for completion.`);
|
|
119
|
+
return "success";
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error("[skyvern] Error:", err);
|
|
122
|
+
return "failed";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
skyvernFallback,
|
|
127
|
+
skyvernNavigate
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=skyvern.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/skyvern.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Skyvern Vision Fallback\n *\n * Skyvern combines computer vision + LLM reasoning to navigate sites that\n * defeat DOM-based approaches: CAPTCHAs, legacy apps, visual-only login flows,\n * government portals. It's the \"break glass\" option when Stagehand fails.\n *\n * Production setup (Phase 4+):\n * 1. Self-host: https://github.com/Skyvern-AI/skyvern\n * 2. Or use cloud: https://app.skyvern.com\n * 3. Set SKYVERN_API_URL + SKYVERN_API_KEY in .env\n *\n * Pricing note: vision tasks cost more per step than DOM-based Stagehand.\n * Use only when the fast path fails.\n */\n\nexport interface SkyvernTask {\n taskDescription: string;\n url: string;\n credentials?: { username: string; password: string; oneTimeCode?: string };\n}\n\nexport interface SkyvernConfig {\n apiUrl: string;\n apiKey: string;\n llmKey?: string;\n llmApiKey?: string;\n}\n\nexport interface SkyvernStep {\n order: number;\n action_type: string;\n element_description?: string;\n reasoning?: string;\n success: boolean;\n screenshot_url?: string;\n duration_ms?: number;\n error?: string;\n}\n\nexport interface SkyvernNavigateResult {\n screenshotUrls: string[];\n visitedUrls: string[];\n status: 'success' | 'failed' | 'partial';\n steps?: SkyvernStep[];\n taskId?: string;\n durationMs?: number;\n triggerReason?: string;\n}\n\n/**\n * Gap 5: Post-login navigation via Skyvern vision for apps where DOM analysis\n * cannot discover screens (canvas, WebGL, legacy Flash-replacement stacks).\n * Skyvern drives the browser via screenshot + LLM, returning visited page list.\n */\nexport async function skyvernNavigate(\n startUrl: string,\n navigationGoals: string[],\n tenantConfig?: SkyvernConfig,\n triggerReason?: string,\n): Promise<SkyvernNavigateResult> {\n const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;\n const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;\n\n if (!apiUrl || !apiKey) {\n console.warn('[skyvern] skyvernNavigate: no API config \u2014 skipping post-login vision navigation');\n return { screenshotUrls: [], visitedUrls: [], status: 'failed' };\n }\n\n const visitedUrls: string[] = [];\n const screenshotUrls: string[] = [];\n const skyvernSteps: SkyvernStep[] = [];\n let lastTaskId: string | undefined;\n let totalDurationMs = 0;\n\n for (const goal of navigationGoals) {\n try {\n const taskStart = Date.now();\n const res = await fetch(`${apiUrl}/api/v1/tasks`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify({\n url: startUrl,\n navigation_goal: goal,\n ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),\n ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),\n }),\n });\n if (!res.ok) continue;\n const data: any = await res.json();\n const taskId: string = data.task_id;\n if (!taskId) continue;\n lastTaskId = taskId;\n\n // Poll for task completion (max 60s per goal)\n let done = false;\n for (let p = 0; p < 12 && !done; p++) {\n await new Promise((r) => setTimeout(r, 5000));\n const poll = await fetch(`${apiUrl}/api/v1/tasks/${taskId}`, {\n headers: { 'x-api-key': apiKey },\n }).catch(() => null);\n if (!poll?.ok) continue;\n const pollData: any = await poll.json();\n if (pollData.status === 'completed' || pollData.status === 'failed') {\n done = true;\n totalDurationMs += Date.now() - taskStart;\n if (pollData.status === 'completed') {\n if (pollData.output?.url) visitedUrls.push(pollData.output.url);\n if (pollData.output?.screenshot_url) screenshotUrls.push(pollData.output.screenshot_url);\n }\n\n // Fetch step-level detail from Skyvern\n try {\n const stepsRes = await fetch(`${apiUrl}/api/v1/tasks/${taskId}/steps`, {\n headers: { 'x-api-key': apiKey },\n }).catch(() => null);\n if (stepsRes?.ok) {\n const stepsData: any = await stepsRes.json();\n const stepsList = Array.isArray(stepsData) ? stepsData : (stepsData?.steps ?? []);\n for (const s of stepsList) {\n if (s.output?.screenshot_url) screenshotUrls.push(s.output.screenshot_url);\n skyvernSteps.push({\n order: s.step_order ?? s.order ?? 0,\n action_type: s.action_type ?? s.action ?? 'unknown',\n element_description: s.output?.element_description,\n reasoning: s.output?.llm_response?.action_description ?? s.output?.reasoning,\n success: s.status === 'completed',\n screenshot_url: s.output?.screenshot_url,\n error: s.status === 'failed' ? (s.output?.errors?.[0] ?? 'Step failed') : undefined,\n });\n }\n }\n } catch { /* non-fatal */ }\n }\n }\n } catch (err) {\n console.warn(`[skyvern] Navigation goal failed: ${goal}`, err);\n }\n }\n\n return {\n screenshotUrls,\n visitedUrls,\n status: visitedUrls.length > 0 ? 'success' : screenshotUrls.length > 0 ? 'partial' : 'failed',\n steps: skyvernSteps,\n taskId: lastTaskId,\n durationMs: totalDurationMs || undefined,\n triggerReason,\n };\n}\n\nexport async function skyvernFallback(task: SkyvernTask, tenantConfig?: SkyvernConfig): Promise<'success' | 'failed'> {\n const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;\n const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;\n\n if (!apiUrl || !apiKey) {\n console.warn(\n '[skyvern] Vision fallback disabled \u2014 set SKYVERN_API_URL + SKYVERN_API_KEY in .env to enable.',\n '\\n[skyvern] Self-host: https://github.com/Skyvern-AI/skyvern'\n );\n return 'failed';\n }\n\n try {\n const res = await fetch(`${apiUrl}/api/v1/tasks`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify({\n url: task.url,\n navigation_goal: task.taskDescription,\n ...(task.credentials && {\n navigation_payload: {\n username: task.credentials.username,\n password: task.credentials.password,\n ...(task.credentials.oneTimeCode && { otp: task.credentials.oneTimeCode }),\n },\n }),\n ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),\n ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),\n }),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n console.error(`[skyvern] Task creation failed: ${res.status}`, body);\n return 'failed';\n }\n\n const data: any = await res.json();\n console.log(`[skyvern] Task queued: ${data.task_id}. Poll GET /api/v1/tasks/${data.task_id} for completion.`);\n // Full implementation: poll until status === \"completed\" | \"failed\".\n // For Phase 1 the login already happened in the browser context Skyvern manages,\n // so returning 'success' here means we can proceed in that browser session.\n return 'success';\n } catch (err) {\n console.error('[skyvern] Error:', err);\n return 'failed';\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs";
|
|
2
|
+
const sqs = new SQSClient({ region: process.env.AWS_REGION ?? "us-east-1" });
|
|
3
|
+
async function sendJobToSqs(jobId, tenantId, config) {
|
|
4
|
+
const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;
|
|
5
|
+
if (!queueUrl) throw new Error("SQS_CRAWL_QUEUE_URL not configured");
|
|
6
|
+
await sqs.send(new SendMessageCommand({
|
|
7
|
+
QueueUrl: queueUrl,
|
|
8
|
+
MessageBody: JSON.stringify({ jobId, tenantId, config })
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
async function startSqsWorker() {
|
|
12
|
+
const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;
|
|
13
|
+
if (!queueUrl) throw new Error("SQS_CRAWL_QUEUE_URL not configured");
|
|
14
|
+
const { processJob } = await import("./job-queue.js");
|
|
15
|
+
console.log("[sqs-worker] starting, queue:", queueUrl);
|
|
16
|
+
while (true) {
|
|
17
|
+
try {
|
|
18
|
+
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
|
|
19
|
+
QueueUrl: queueUrl,
|
|
20
|
+
WaitTimeSeconds: 20,
|
|
21
|
+
MaxNumberOfMessages: 2
|
|
22
|
+
}));
|
|
23
|
+
await Promise.all(Messages.map(async (msg) => {
|
|
24
|
+
let jobId = "?";
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(msg.Body);
|
|
27
|
+
jobId = data.jobId;
|
|
28
|
+
await processJob(data.jobId, data.config);
|
|
29
|
+
await sqs.send(new DeleteMessageCommand({
|
|
30
|
+
QueueUrl: queueUrl,
|
|
31
|
+
ReceiptHandle: msg.ReceiptHandle
|
|
32
|
+
}));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`[sqs-worker] job ${jobId} failed \u2014 leaving for SQS redrive:`, err.message);
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error("[sqs-worker] receive error:", err.message);
|
|
39
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
sendJobToSqs,
|
|
45
|
+
startSqsWorker
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=sqs-queue.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sqs-queue.ts"],
|
|
4
|
+
"sourcesContent": ["import { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';\n\nconst sqs = new SQSClient({ region: process.env.AWS_REGION ?? 'us-east-1' });\n\nexport async function sendJobToSqs(jobId: string, tenantId: string, config: any): Promise<void> {\n const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;\n if (!queueUrl) throw new Error('SQS_CRAWL_QUEUE_URL not configured');\n await sqs.send(new SendMessageCommand({\n QueueUrl: queueUrl,\n MessageBody: JSON.stringify({ jobId, tenantId, config }),\n }));\n}\n\nexport async function startSqsWorker(): Promise<void> {\n const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;\n if (!queueUrl) throw new Error('SQS_CRAWL_QUEUE_URL not configured');\n\n const { processJob } = await import('./job-queue.js');\n console.log('[sqs-worker] starting, queue:', queueUrl);\n\n while (true) {\n try {\n const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({\n QueueUrl: queueUrl,\n WaitTimeSeconds: 20,\n MaxNumberOfMessages: 2,\n }));\n\n await Promise.all(Messages.map(async (msg) => {\n let jobId = '?';\n try {\n const data = JSON.parse(msg.Body!);\n jobId = data.jobId;\n await processJob(data.jobId, data.config);\n await sqs.send(new DeleteMessageCommand({\n QueueUrl: queueUrl,\n ReceiptHandle: msg.ReceiptHandle!,\n }));\n } catch (err: any) {\n console.error(`[sqs-worker] job ${jobId} failed \u2014 leaving for SQS redrive:`, err.message);\n // Do NOT delete \u2014 SQS redrive policy + DLQ handles retry\n }\n }));\n } catch (err: any) {\n console.error('[sqs-worker] receive error:', err.message);\n await new Promise((r) => setTimeout(r, 2000));\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,WAAW,oBAAoB,uBAAuB,4BAA4B;AAE3F,MAAM,MAAM,IAAI,UAAU,EAAE,QAAQ,QAAQ,IAAI,cAAc,YAAY,CAAC;AAE3E,eAAsB,aAAa,OAAe,UAAkB,QAA4B;AAC9F,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,SAAU,OAAM,IAAI,MAAM,oCAAoC;AACnE,QAAM,IAAI,KAAK,IAAI,mBAAmB;AAAA,IACpC,UAAU;AAAA,IACV,aAAa,KAAK,UAAU,EAAE,OAAO,UAAU,OAAO,CAAC;AAAA,EACzD,CAAC,CAAC;AACJ;AAEA,eAAsB,iBAAgC;AACpD,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,CAAC,SAAU,OAAM,IAAI,MAAM,oCAAoC;AAEnE,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,gBAAgB;AACpD,UAAQ,IAAI,iCAAiC,QAAQ;AAErD,SAAO,MAAM;AACX,QAAI;AACF,YAAM,EAAE,WAAW,CAAC,EAAE,IAAI,MAAM,IAAI,KAAK,IAAI,sBAAsB;AAAA,QACjE,UAAU;AAAA,QACV,iBAAiB;AAAA,QACjB,qBAAqB;AAAA,MACvB,CAAC,CAAC;AAEF,YAAM,QAAQ,IAAI,SAAS,IAAI,OAAO,QAAQ;AAC5C,YAAI,QAAQ;AACZ,YAAI;AACF,gBAAM,OAAO,KAAK,MAAM,IAAI,IAAK;AACjC,kBAAQ,KAAK;AACb,gBAAM,WAAW,KAAK,OAAO,KAAK,MAAM;AACxC,gBAAM,IAAI,KAAK,IAAI,qBAAqB;AAAA,YACtC,UAAU;AAAA,YACV,eAAe,IAAI;AAAA,UACrB,CAAC,CAAC;AAAA,QACJ,SAAS,KAAU;AACjB,kBAAQ,MAAM,oBAAoB,KAAK,2CAAsC,IAAI,OAAO;AAAA,QAE1F;AAAA,MACF,CAAC,CAAC;AAAA,IACJ,SAAS,KAAU;AACjB,cAAQ,MAAM,+BAA+B,IAAI,OAAO;AACxD,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAI,CAAC;AAAA,IAC9C;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSO token retrieval for Okta and Azure AD.
|
|
3
|
+
* Uses OAuth2 ROPC (Resource Owner Password Credentials) flow to get access tokens
|
|
4
|
+
* without browser-based login.
|
|
5
|
+
*
|
|
6
|
+
* Note: ROPC is deprecated in OAuth 2.1 but still supported by Okta and Azure AD
|
|
7
|
+
* for service accounts and automated testing scenarios.
|
|
8
|
+
*/
|
|
9
|
+
export type SsoProvider = 'okta' | 'azure_ad' | 'generic_oidc';
|
|
10
|
+
export interface SsoConfig {
|
|
11
|
+
provider: SsoProvider;
|
|
12
|
+
/** Okta domain (e.g. 'mycompany.okta.com') or Azure tenant ID */
|
|
13
|
+
domain: string;
|
|
14
|
+
/** OAuth2 client ID */
|
|
15
|
+
clientId: string;
|
|
16
|
+
/** OAuth2 client secret (for client_credentials or confidential ROPC) */
|
|
17
|
+
clientSecret?: string;
|
|
18
|
+
/** Scopes to request (space-separated). Default: 'openid profile' */
|
|
19
|
+
scope?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get an access token using ROPC flow (username + password → token).
|
|
23
|
+
* Returns the access token string.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getSsoToken(config: SsoConfig, username: string, password: string): Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Get a client credentials token (no user — for service accounts).
|
|
28
|
+
*/
|
|
29
|
+
export declare function getSsoClientCredentialsToken(config: SsoConfig): Promise<string>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
async function getSsoToken(config, username, password) {
|
|
2
|
+
let tokenUrl;
|
|
3
|
+
const params = {
|
|
4
|
+
grant_type: "password",
|
|
5
|
+
username,
|
|
6
|
+
password,
|
|
7
|
+
scope: config.scope ?? "openid profile",
|
|
8
|
+
client_id: config.clientId
|
|
9
|
+
};
|
|
10
|
+
if (config.clientSecret) {
|
|
11
|
+
params.client_secret = config.clientSecret;
|
|
12
|
+
}
|
|
13
|
+
switch (config.provider) {
|
|
14
|
+
case "okta":
|
|
15
|
+
tokenUrl = `https://${config.domain}/oauth2/v1/token`;
|
|
16
|
+
break;
|
|
17
|
+
case "azure_ad":
|
|
18
|
+
tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;
|
|
19
|
+
break;
|
|
20
|
+
case "generic_oidc":
|
|
21
|
+
tokenUrl = config.domain.startsWith("http") ? config.domain : `https://${config.domain}/oauth2/token`;
|
|
22
|
+
break;
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(`Unknown SSO provider: ${config.provider}`);
|
|
25
|
+
}
|
|
26
|
+
const res = await fetch(tokenUrl, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
29
|
+
body: new URLSearchParams(params).toString()
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const body = await res.text().catch(() => res.statusText);
|
|
33
|
+
throw new Error(`SSO token request failed (${res.status}): ${body}`);
|
|
34
|
+
}
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
if (data.error) throw new Error(`SSO error: ${data.error} \u2014 ${data.error_description}`);
|
|
37
|
+
if (!data.access_token) throw new Error("SSO response missing access_token");
|
|
38
|
+
return data.access_token;
|
|
39
|
+
}
|
|
40
|
+
async function getSsoClientCredentialsToken(config) {
|
|
41
|
+
if (!config.clientSecret) throw new Error("clientSecret required for client credentials flow");
|
|
42
|
+
let tokenUrl;
|
|
43
|
+
switch (config.provider) {
|
|
44
|
+
case "okta":
|
|
45
|
+
tokenUrl = `https://${config.domain}/oauth2/v1/token`;
|
|
46
|
+
break;
|
|
47
|
+
case "azure_ad":
|
|
48
|
+
tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
tokenUrl = `https://${config.domain}/oauth2/token`;
|
|
52
|
+
}
|
|
53
|
+
const res = await fetch(tokenUrl, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
56
|
+
body: new URLSearchParams({
|
|
57
|
+
grant_type: "client_credentials",
|
|
58
|
+
client_id: config.clientId,
|
|
59
|
+
client_secret: config.clientSecret,
|
|
60
|
+
scope: config.scope ?? "openid profile"
|
|
61
|
+
}).toString()
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const body = await res.text().catch(() => res.statusText);
|
|
65
|
+
throw new Error(`SSO client credentials failed (${res.status}): ${body}`);
|
|
66
|
+
}
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
if (data.error) throw new Error(`SSO error: ${data.error} \u2014 ${data.error_description}`);
|
|
69
|
+
if (!data.access_token) throw new Error("SSO response missing access_token");
|
|
70
|
+
return data.access_token;
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
getSsoClientCredentialsToken,
|
|
74
|
+
getSsoToken
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=sso-token.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sso-token.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * SSO token retrieval for Okta and Azure AD.\n * Uses OAuth2 ROPC (Resource Owner Password Credentials) flow to get access tokens\n * without browser-based login.\n *\n * Note: ROPC is deprecated in OAuth 2.1 but still supported by Okta and Azure AD\n * for service accounts and automated testing scenarios.\n */\n\nexport type SsoProvider = 'okta' | 'azure_ad' | 'generic_oidc';\n\nexport interface SsoConfig {\n provider: SsoProvider;\n /** Okta domain (e.g. 'mycompany.okta.com') or Azure tenant ID */\n domain: string;\n /** OAuth2 client ID */\n clientId: string;\n /** OAuth2 client secret (for client_credentials or confidential ROPC) */\n clientSecret?: string;\n /** Scopes to request (space-separated). Default: 'openid profile' */\n scope?: string;\n}\n\n/**\n * Get an access token using ROPC flow (username + password \u2192 token).\n * Returns the access token string.\n */\nexport async function getSsoToken(\n config: SsoConfig,\n username: string,\n password: string\n): Promise<string> {\n let tokenUrl: string;\n const params: Record<string, string> = {\n grant_type: 'password',\n username,\n password,\n scope: config.scope ?? 'openid profile',\n client_id: config.clientId,\n };\n\n if (config.clientSecret) {\n params.client_secret = config.clientSecret;\n }\n\n switch (config.provider) {\n case 'okta':\n tokenUrl = `https://${config.domain}/oauth2/v1/token`;\n break;\n case 'azure_ad':\n // domain here is the tenant ID (e.g. XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX or 'mycompany.onmicrosoft.com')\n tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;\n break;\n case 'generic_oidc':\n tokenUrl = config.domain.startsWith('http') ? config.domain : `https://${config.domain}/oauth2/token`;\n break;\n default:\n throw new Error(`Unknown SSO provider: ${config.provider}`);\n }\n\n const res = await fetch(tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams(params).toString(),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => res.statusText);\n throw new Error(`SSO token request failed (${res.status}): ${body}`);\n }\n\n const data = await res.json() as { access_token?: string; error?: string; error_description?: string };\n if (data.error) throw new Error(`SSO error: ${data.error} \u2014 ${data.error_description}`);\n if (!data.access_token) throw new Error('SSO response missing access_token');\n\n return data.access_token;\n}\n\n/**\n * Get a client credentials token (no user \u2014 for service accounts).\n */\nexport async function getSsoClientCredentialsToken(\n config: SsoConfig\n): Promise<string> {\n if (!config.clientSecret) throw new Error('clientSecret required for client credentials flow');\n\n let tokenUrl: string;\n switch (config.provider) {\n case 'okta':\n tokenUrl = `https://${config.domain}/oauth2/v1/token`;\n break;\n case 'azure_ad':\n tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;\n break;\n default:\n tokenUrl = `https://${config.domain}/oauth2/token`;\n }\n\n const res = await fetch(tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: config.clientId,\n client_secret: config.clientSecret,\n scope: config.scope ?? 'openid profile',\n }).toString(),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => res.statusText);\n throw new Error(`SSO client credentials failed (${res.status}): ${body}`);\n }\n\n const data = await res.json() as { access_token?: string; error?: string; error_description?: string };\n if (data.error) throw new Error(`SSO error: ${data.error} \u2014 ${data.error_description}`);\n if (!data.access_token) throw new Error('SSO response missing access_token');\n\n return data.access_token;\n}\n"],
|
|
5
|
+
"mappings": "AA2BA,eAAsB,YACpB,QACA,UACA,UACiB;AACjB,MAAI;AACJ,QAAM,SAAiC;AAAA,IACrC,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA,OAAO,OAAO,SAAS;AAAA,IACvB,WAAW,OAAO;AAAA,EACpB;AAEA,MAAI,OAAO,cAAc;AACvB,WAAO,gBAAgB,OAAO;AAAA,EAChC;AAEA,UAAQ,OAAO,UAAU;AAAA,IACvB,KAAK;AACH,iBAAW,WAAW,OAAO,MAAM;AACnC;AAAA,IACF,KAAK;AAEH,iBAAW,qCAAqC,OAAO,MAAM;AAC7D;AAAA,IACF,KAAK;AACH,iBAAW,OAAO,OAAO,WAAW,MAAM,IAAI,OAAO,SAAS,WAAW,OAAO,MAAM;AACtF;AAAA,IACF;AACE,YAAM,IAAI,MAAM,yBAAyB,OAAO,QAAQ,EAAE;AAAA,EAC9D;AAEA,QAAM,MAAM,MAAM,MAAM,UAAU;AAAA,IAChC,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,IAC/D,MAAM,IAAI,gBAAgB,MAAM,EAAE,SAAS;AAAA,EAC7C,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,UAAU;AACxD,UAAM,IAAI,MAAM,6BAA6B,IAAI,MAAM,MAAM,IAAI,EAAE;AAAA,EACrE;AAEA,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,KAAK,MAAO,OAAM,IAAI,MAAM,cAAc,KAAK,KAAK,WAAM,KAAK,iBAAiB,EAAE;AACtF,MAAI,CAAC,KAAK,aAAc,OAAM,IAAI,MAAM,mCAAmC;AAE3E,SAAO,KAAK;AACd;AAKA,eAAsB,6BACpB,QACiB;AACjB,MAAI,CAAC,OAAO,aAAc,OAAM,IAAI,MAAM,mDAAmD;AAE7F,MAAI;AACJ,UAAQ,OAAO,UAAU;AAAA,IACvB,KAAK;AACH,iBAAW,WAAW,OAAO,MAAM;AACnC;AAAA,IACF,KAAK;AACH,iBAAW,qCAAqC,OAAO,MAAM;AAC7D;AAAA,IACF;AACE,iBAAW,WAAW,OAAO,MAAM;AAAA,EACvC;AAEA,QAAM,MAAM,MAAM,MAAM,UAAU;AAAA,IAChC,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,IAC/D,MAAM,IAAI,gBAAgB;AAAA,MACxB,YAAY;AAAA,MACZ,WAAW,OAAO;AAAA,MAClB,eAAe,OAAO;AAAA,MACtB,OAAO,OAAO,SAAS;AAAA,IACzB,CAAC,EAAE,SAAS;AAAA,EACd,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,UAAU;AACxD,UAAM,IAAI,MAAM,kCAAkC,IAAI,MAAM,MAAM,IAAI,EAAE;AAAA,EAC1E;AAEA,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,MAAI,KAAK,MAAO,OAAM,IAAI,MAAM,cAAc,KAAK,KAAK,WAAM,KAAK,iBAAiB,EAAE;AACtF,MAAI,CAAC,KAAK,aAAc,OAAM,IAAI,MAAM,mCAAmC;AAE3E,SAAO,KAAK;AACd;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export declare function humanDelay(min?: number, max?: number): Promise<void>;
|
|
3
|
+
export declare function simulateMouseMovement(page: Page, targetX: number, targetY: number): Promise<void>;
|
|
4
|
+
export declare function simulateMouseHover(page: Page, selector: string): Promise<void>;
|
|
5
|
+
export declare function simulateClick(page: Page, selector: string): Promise<void>;
|
|
6
|
+
export declare function injectFingerprintOverrides(page: Page): Promise<void>;
|
|
7
|
+
export declare function infiniteScrollToBottom(page: Page, opts?: {
|
|
8
|
+
maxScrolls?: number;
|
|
9
|
+
scrollDelay?: number;
|
|
10
|
+
stabilizeMs?: number;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
scrollCount: number;
|
|
13
|
+
heightReached: number;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function extractIframeContent(page: Page): Promise<Array<{
|
|
16
|
+
src: string;
|
|
17
|
+
content: string;
|
|
18
|
+
markdown: string;
|
|
19
|
+
}>>;
|
|
20
|
+
export declare function pierceShadowDom(page: Page): Promise<string>;
|