@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
2
|
+
const MAX_ATTEMPTS = 40;
|
|
3
|
+
async function solveCaptcha(page, pageUrl, apiKey, provider = "2captcha") {
|
|
4
|
+
const captchaInfo = await page.evaluate(() => {
|
|
5
|
+
const recaptchaEl = document.querySelector(".g-recaptcha, [data-sitekey]");
|
|
6
|
+
if (recaptchaEl) {
|
|
7
|
+
const sitekey = recaptchaEl.getAttribute("data-sitekey") || "";
|
|
8
|
+
return { type: "recaptcha", sitekey };
|
|
9
|
+
}
|
|
10
|
+
const hcaptchaEl = document.querySelector(".h-captcha, [data-hcaptcha-sitekey]");
|
|
11
|
+
if (hcaptchaEl) {
|
|
12
|
+
const sitekey = hcaptchaEl.getAttribute("data-sitekey") || hcaptchaEl.getAttribute("data-hcaptcha-sitekey") || "";
|
|
13
|
+
return { type: "hcaptcha", sitekey };
|
|
14
|
+
}
|
|
15
|
+
const turnstileEl = document.querySelector(".cf-turnstile");
|
|
16
|
+
if (turnstileEl) {
|
|
17
|
+
const sitekey = turnstileEl.getAttribute("data-sitekey") || "";
|
|
18
|
+
return { type: "turnstile", sitekey };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
});
|
|
22
|
+
if (!captchaInfo?.sitekey) return null;
|
|
23
|
+
const token = provider === "capsolver" ? await solveWithCapSolver(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey) : await solveWith2Captcha(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey);
|
|
24
|
+
await page.evaluate((t, type) => {
|
|
25
|
+
if (type === "recaptcha") {
|
|
26
|
+
const el = document.querySelector("textarea.g-recaptcha-response");
|
|
27
|
+
if (el) {
|
|
28
|
+
el.value = t;
|
|
29
|
+
}
|
|
30
|
+
if (window.grecaptcha?.getResponse && document.querySelector("[data-callback]")) {
|
|
31
|
+
const cb = document.querySelector("[data-callback]")?.getAttribute("data-callback");
|
|
32
|
+
if (cb && window[cb]) window[cb](t);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (type === "hcaptcha") {
|
|
36
|
+
const el = document.querySelector('textarea[name="h-captcha-response"]');
|
|
37
|
+
if (el) {
|
|
38
|
+
el.value = t;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (type === "turnstile") {
|
|
42
|
+
const el = document.querySelector('[name="cf-turnstile-response"]');
|
|
43
|
+
if (el) {
|
|
44
|
+
el.value = t;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, token, captchaInfo.type);
|
|
48
|
+
return { token, provider };
|
|
49
|
+
}
|
|
50
|
+
async function solveWith2Captcha(type, sitekey, pageUrl, apiKey) {
|
|
51
|
+
const method = type === "hcaptcha" ? "hcaptcha" : type === "turnstile" ? "turnstile" : "userrecaptcha";
|
|
52
|
+
const submitRes = await fetch("http://2captcha.com/in.php", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: new URLSearchParams({
|
|
55
|
+
key: apiKey,
|
|
56
|
+
method,
|
|
57
|
+
googlekey: sitekey,
|
|
58
|
+
pageurl: pageUrl,
|
|
59
|
+
json: "1"
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
const submitData = await submitRes.json();
|
|
63
|
+
if (submitData.status !== 1) throw new Error(`2captcha submit failed: ${submitData.request}`);
|
|
64
|
+
const taskId = submitData.request;
|
|
65
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
66
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
67
|
+
const pollRes = await fetch(`http://2captcha.com/res.php?key=${apiKey}&action=get&id=${taskId}&json=1`);
|
|
68
|
+
const pollData = await pollRes.json();
|
|
69
|
+
if (pollData.status === 1) return pollData.request;
|
|
70
|
+
if (pollData.request !== "CAPCHA_NOT_READY") throw new Error(`2captcha error: ${pollData.request}`);
|
|
71
|
+
}
|
|
72
|
+
throw new Error("2captcha timed out after 2 minutes");
|
|
73
|
+
}
|
|
74
|
+
async function solveWithCapSolver(type, sitekey, pageUrl, apiKey) {
|
|
75
|
+
const taskType = type === "hcaptcha" ? "HCaptchaTaskProxyless" : type === "turnstile" ? "AntiTurnstileTaskProxyless" : "ReCaptchaV2TaskProxyless";
|
|
76
|
+
const createRes = await fetch("https://api.capsolver.com/createTask", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
clientKey: apiKey,
|
|
81
|
+
task: { type: taskType, websiteURL: pageUrl, websiteKey: sitekey }
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
const createData = await createRes.json();
|
|
85
|
+
if (createData.errorId !== 0) throw new Error(`CapSolver create failed: ${createData.errorDescription}`);
|
|
86
|
+
const taskId = createData.taskId;
|
|
87
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
88
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
89
|
+
const pollRes = await fetch("https://api.capsolver.com/getTaskResult", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ clientKey: apiKey, taskId })
|
|
93
|
+
});
|
|
94
|
+
const pollData = await pollRes.json();
|
|
95
|
+
if (pollData.errorId !== 0) throw new Error(`CapSolver error: ${pollData.errorDescription}`);
|
|
96
|
+
if (pollData.status === "ready") {
|
|
97
|
+
return pollData.solution?.gRecaptchaResponse ?? pollData.solution?.token ?? "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error("CapSolver timed out after 2 minutes");
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
solveCaptcha
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=captcha-solver.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/captcha-solver.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * CAPTCHA auto-solving via 2captcha or CapSolver APIs.\n * Supports reCAPTCHA v2, hCaptcha, and Cloudflare Turnstile.\n */\n\nexport type CaptchaSolverProvider = '2captcha' | 'capsolver';\n\ninterface SolveResult {\n token: string;\n provider: CaptchaSolverProvider;\n}\n\n// Poll interval and max attempts\nconst POLL_INTERVAL_MS = 3_000;\nconst MAX_ATTEMPTS = 40; // 2 min max\n\n/**\n * Detect CAPTCHA type and sitekey on the page, then solve it.\n * Returns null if no CAPTCHA found or solver not configured.\n * Throws on solve failure.\n */\nexport async function solveCaptcha(\n page: any,\n pageUrl: string,\n apiKey: string,\n provider: CaptchaSolverProvider = '2captcha'\n): Promise<SolveResult | null> {\n // Detect CAPTCHA type\n const captchaInfo = await page.evaluate(() => {\n // reCAPTCHA v2\n const recaptchaEl = document.querySelector('.g-recaptcha, [data-sitekey]') as HTMLElement | null;\n if (recaptchaEl) {\n const sitekey = recaptchaEl.getAttribute('data-sitekey') || '';\n return { type: 'recaptcha', sitekey };\n }\n // hCaptcha\n const hcaptchaEl = document.querySelector('.h-captcha, [data-hcaptcha-sitekey]') as HTMLElement | null;\n if (hcaptchaEl) {\n const sitekey = hcaptchaEl.getAttribute('data-sitekey') || hcaptchaEl.getAttribute('data-hcaptcha-sitekey') || '';\n return { type: 'hcaptcha', sitekey };\n }\n // Cloudflare Turnstile\n const turnstileEl = document.querySelector('.cf-turnstile') as HTMLElement | null;\n if (turnstileEl) {\n const sitekey = turnstileEl.getAttribute('data-sitekey') || '';\n return { type: 'turnstile', sitekey };\n }\n return null;\n });\n\n if (!captchaInfo?.sitekey) return null;\n\n const token = provider === 'capsolver'\n ? await solveWithCapSolver(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey)\n : await solveWith2Captcha(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey);\n\n // Inject the token into the page\n await page.evaluate((t: string, type: string) => {\n // reCAPTCHA\n if (type === 'recaptcha') {\n const el = document.querySelector('textarea.g-recaptcha-response') as HTMLTextAreaElement | null;\n if (el) { el.value = t; }\n // Also trigger the callback if registered\n if ((window as any).grecaptcha?.getResponse && document.querySelector('[data-callback]')) {\n const cb = (document.querySelector('[data-callback]') as HTMLElement)?.getAttribute('data-callback');\n if (cb && (window as any)[cb]) (window as any)[cb](t);\n }\n }\n // hCaptcha\n if (type === 'hcaptcha') {\n const el = document.querySelector('textarea[name=\"h-captcha-response\"]') as HTMLTextAreaElement | null;\n if (el) { el.value = t; }\n }\n // Turnstile\n if (type === 'turnstile') {\n const el = document.querySelector('[name=\"cf-turnstile-response\"]') as HTMLInputElement | null;\n if (el) { el.value = t; }\n }\n }, token, captchaInfo.type);\n\n return { token, provider };\n}\n\nasync function solveWith2Captcha(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise<string> {\n const method = type === 'hcaptcha' ? 'hcaptcha' : type === 'turnstile' ? 'turnstile' : 'userrecaptcha';\n\n const submitRes = await fetch('http://2captcha.com/in.php', {\n method: 'POST',\n body: new URLSearchParams({\n key: apiKey,\n method,\n googlekey: sitekey,\n pageurl: pageUrl,\n json: '1',\n }),\n });\n const submitData = await submitRes.json() as { status: number; request: string };\n if (submitData.status !== 1) throw new Error(`2captcha submit failed: ${submitData.request}`);\n const taskId = submitData.request;\n\n // Poll for result\n for (let i = 0; i < MAX_ATTEMPTS; i++) {\n await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));\n const pollRes = await fetch(`http://2captcha.com/res.php?key=${apiKey}&action=get&id=${taskId}&json=1`);\n const pollData = await pollRes.json() as { status: number; request: string };\n if (pollData.status === 1) return pollData.request;\n if (pollData.request !== 'CAPCHA_NOT_READY') throw new Error(`2captcha error: ${pollData.request}`);\n }\n throw new Error('2captcha timed out after 2 minutes');\n}\n\nasync function solveWithCapSolver(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise<string> {\n const taskType = type === 'hcaptcha' ? 'HCaptchaTaskProxyless'\n : type === 'turnstile' ? 'AntiTurnstileTaskProxyless'\n : 'ReCaptchaV2TaskProxyless';\n\n const createRes = await fetch('https://api.capsolver.com/createTask', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n clientKey: apiKey,\n task: { type: taskType, websiteURL: pageUrl, websiteKey: sitekey },\n }),\n });\n const createData = await createRes.json() as { errorId: number; taskId?: string; errorDescription?: string };\n if (createData.errorId !== 0) throw new Error(`CapSolver create failed: ${createData.errorDescription}`);\n const taskId = createData.taskId!;\n\n for (let i = 0; i < MAX_ATTEMPTS; i++) {\n await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));\n const pollRes = await fetch('https://api.capsolver.com/getTaskResult', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ clientKey: apiKey, taskId }),\n });\n const pollData = await pollRes.json() as { errorId: number; status: string; solution?: { gRecaptchaResponse?: string; token?: string }; errorDescription?: string };\n if (pollData.errorId !== 0) throw new Error(`CapSolver error: ${pollData.errorDescription}`);\n if (pollData.status === 'ready') {\n return pollData.solution?.gRecaptchaResponse ?? pollData.solution?.token ?? '';\n }\n }\n throw new Error('CapSolver timed out after 2 minutes');\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG color contrast checker.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates text contrast ratios against backgrounds for all visible text elements.
|
|
5
|
+
* Reports elements that fail WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large text)
|
|
6
|
+
* or AAA (7:1 for normal, 4.5:1 for large text) requirements.
|
|
7
|
+
*
|
|
8
|
+
* Uses computedStyle to get actual rendered colors — accounts for CSS variables
|
|
9
|
+
* and inheritance.
|
|
10
|
+
*/
|
|
11
|
+
import type { Page } from 'playwright';
|
|
12
|
+
export interface ContrastViolation {
|
|
13
|
+
selector: string;
|
|
14
|
+
text: string;
|
|
15
|
+
foreground: string;
|
|
16
|
+
background: string;
|
|
17
|
+
ratio: number;
|
|
18
|
+
required: number;
|
|
19
|
+
level: 'AA' | 'AAA';
|
|
20
|
+
isLargeText: boolean;
|
|
21
|
+
severity: 'fail' | 'warn';
|
|
22
|
+
}
|
|
23
|
+
export interface ContrastAuditResult {
|
|
24
|
+
url: string;
|
|
25
|
+
violations: ContrastViolation[];
|
|
26
|
+
passCount: number;
|
|
27
|
+
score: number;
|
|
28
|
+
auditedAt: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function auditColorContrast(page: Page, url: string): Promise<ContrastAuditResult>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
async function auditColorContrast(page, url) {
|
|
2
|
+
const auditedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3
|
+
const violations = await page.evaluate(() => {
|
|
4
|
+
function parseRgb(color) {
|
|
5
|
+
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/);
|
|
6
|
+
return m ? [+m[1], +m[2], +m[3], m[4] !== void 0 ? +m[4] : 1] : null;
|
|
7
|
+
}
|
|
8
|
+
function luminance([r, g, b]) {
|
|
9
|
+
const toLinear = (c) => {
|
|
10
|
+
const n = c / 255;
|
|
11
|
+
return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
|
|
12
|
+
};
|
|
13
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
14
|
+
}
|
|
15
|
+
function contrastRatio(fg, bg) {
|
|
16
|
+
const l1 = luminance(fg);
|
|
17
|
+
const l2 = luminance(bg);
|
|
18
|
+
const lighter = Math.max(l1, l2);
|
|
19
|
+
const darker = Math.min(l1, l2);
|
|
20
|
+
return Math.round((lighter + 0.05) / (darker + 0.05) * 100) / 100;
|
|
21
|
+
}
|
|
22
|
+
const violations2 = [];
|
|
23
|
+
const elements = Array.from(document.querySelectorAll("p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button"));
|
|
24
|
+
let checked = 0;
|
|
25
|
+
for (const el of elements) {
|
|
26
|
+
if (checked >= 100) break;
|
|
27
|
+
const text = (el.textContent ?? "").trim().slice(0, 60);
|
|
28
|
+
if (!text) continue;
|
|
29
|
+
const style = window.getComputedStyle(el);
|
|
30
|
+
if (style.display === "none" || style.visibility === "hidden") continue;
|
|
31
|
+
const fg = parseRgb(style.color);
|
|
32
|
+
const bg = parseRgb(style.backgroundColor);
|
|
33
|
+
if (!fg || !bg || bg[3] === 0) continue;
|
|
34
|
+
const fontSize = parseFloat(style.fontSize);
|
|
35
|
+
const fontWeight = style.fontWeight;
|
|
36
|
+
const isLargeText = fontSize >= 18 || fontSize >= 14 && (fontWeight === "bold" || +fontWeight >= 700);
|
|
37
|
+
const ratio = contrastRatio(fg, bg);
|
|
38
|
+
const aaRequired = isLargeText ? 3 : 4.5;
|
|
39
|
+
const aaaRequired = isLargeText ? 4.5 : 7;
|
|
40
|
+
if (ratio < aaRequired) {
|
|
41
|
+
violations2.push({
|
|
42
|
+
selector: el.tagName.toLowerCase(),
|
|
43
|
+
text: text.slice(0, 50),
|
|
44
|
+
foreground: style.color,
|
|
45
|
+
background: style.backgroundColor,
|
|
46
|
+
ratio,
|
|
47
|
+
required: aaRequired,
|
|
48
|
+
level: "AA",
|
|
49
|
+
isLargeText,
|
|
50
|
+
severity: "fail"
|
|
51
|
+
});
|
|
52
|
+
} else if (ratio < aaaRequired) {
|
|
53
|
+
violations2.push({
|
|
54
|
+
selector: el.tagName.toLowerCase(),
|
|
55
|
+
text: text.slice(0, 50),
|
|
56
|
+
foreground: style.color,
|
|
57
|
+
background: style.backgroundColor,
|
|
58
|
+
ratio,
|
|
59
|
+
required: aaaRequired,
|
|
60
|
+
level: "AAA",
|
|
61
|
+
isLargeText,
|
|
62
|
+
severity: "warn"
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
checked++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return violations2;
|
|
69
|
+
}).catch(() => []);
|
|
70
|
+
const passCount = Math.max(0, 100 - violations.length);
|
|
71
|
+
const criticals = violations.filter((v) => v.severity === "fail").length;
|
|
72
|
+
const score = Math.max(0, 100 - criticals * 15 - violations.filter((v) => v.severity === "warn").length * 3);
|
|
73
|
+
return { url, violations, passCount, score, auditedAt };
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
auditColorContrast
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=color-contrast.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/color-contrast.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * WCAG color contrast checker.\n *\n * Evaluates text contrast ratios against backgrounds for all visible text elements.\n * Reports elements that fail WCAG 2.1 AA (4.5:1 for normal text, 3:1 for large text)\n * or AAA (7:1 for normal, 4.5:1 for large text) requirements.\n *\n * Uses computedStyle to get actual rendered colors \u2014 accounts for CSS variables\n * and inheritance.\n */\n\nimport type { Page } from 'playwright';\n\nexport interface ContrastViolation {\n selector: string;\n text: string;\n foreground: string; // rgb(r, g, b)\n background: string; // rgb(r, g, b)\n ratio: number; // actual contrast ratio\n required: number; // required ratio (4.5 or 3.0)\n level: 'AA' | 'AAA';\n isLargeText: boolean; // font-size >= 18px or bold >= 14px\n severity: 'fail' | 'warn'; // fail = below AA, warn = below AAA\n}\n\nexport interface ContrastAuditResult {\n url: string;\n violations: ContrastViolation[];\n passCount: number;\n score: number; // 0-100\n auditedAt: string;\n}\n\nexport async function auditColorContrast(page: Page, url: string): Promise<ContrastAuditResult> {\n const auditedAt = new Date().toISOString();\n\n const violations: ContrastViolation[] = await page.evaluate(() => {\n function parseRgb(color: string): [number, number, number, number] | null {\n const m = color.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?/);\n return m ? [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1] : null;\n }\n function luminance([r, g, b]: [number, number, number, number]): number {\n const toLinear = (c: number) => {\n const n = c / 255;\n return n <= 0.03928 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);\n };\n return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);\n }\n function contrastRatio(fg: [number,number,number,number], bg: [number,number,number,number]): number {\n const l1 = luminance(fg);\n const l2 = luminance(bg);\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;\n }\n\n const violations: any[] = [];\n const elements = Array.from(document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, a, span, li, td, th, label, button'));\n let checked = 0;\n\n for (const el of elements as HTMLElement[]) {\n if (checked >= 100) break; // limit to avoid perf hit\n const text = (el.textContent ?? '').trim().slice(0, 60);\n if (!text) continue;\n\n const style = window.getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden') continue;\n\n const fg = parseRgb(style.color);\n const bg = parseRgb(style.backgroundColor);\n if (!fg || !bg || bg[3] === 0) continue; // transparent bg \u2014 can't check\n\n const fontSize = parseFloat(style.fontSize);\n const fontWeight = style.fontWeight;\n const isLargeText = fontSize >= 18 || (fontSize >= 14 && (fontWeight === 'bold' || +fontWeight >= 700));\n const ratio = contrastRatio(fg, bg);\n const aaRequired = isLargeText ? 3.0 : 4.5;\n const aaaRequired = isLargeText ? 4.5 : 7.0;\n\n if (ratio < aaRequired) {\n violations.push({\n selector: el.tagName.toLowerCase(),\n text: text.slice(0, 50),\n foreground: style.color,\n background: style.backgroundColor,\n ratio,\n required: aaRequired,\n level: 'AA',\n isLargeText,\n severity: 'fail',\n });\n } else if (ratio < aaaRequired) {\n violations.push({\n selector: el.tagName.toLowerCase(),\n text: text.slice(0, 50),\n foreground: style.color,\n background: style.backgroundColor,\n ratio,\n required: aaaRequired,\n level: 'AAA',\n isLargeText,\n severity: 'warn',\n });\n } else {\n checked++;\n }\n }\n return violations;\n }).catch(() => [] as ContrastViolation[]);\n\n const passCount = Math.max(0, 100 - violations.length);\n const criticals = violations.filter(v => v.severity === 'fail').length;\n const score = Math.max(0, 100 - criticals * 15 - violations.filter(v => v.severity === 'warn').length * 3);\n\n return { url, violations, passCount, score, auditedAt };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["violations"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ChunkingStrategy = 'fixed' | 'overlap' | 'sentence' | 'paragraph' | 'markdown-heading' | 'semantic' | 'recursive';
|
|
2
|
+
export interface ChunkOpts {
|
|
3
|
+
strategy: ChunkingStrategy;
|
|
4
|
+
chunkSize?: number;
|
|
5
|
+
overlap?: number;
|
|
6
|
+
separators?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface Chunk {
|
|
9
|
+
index: number;
|
|
10
|
+
text: string;
|
|
11
|
+
tokenEstimate: number;
|
|
12
|
+
startChar: number;
|
|
13
|
+
endChar: number;
|
|
14
|
+
heading?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function estimateTokens(text: string): number;
|
|
17
|
+
export declare function chunkContent(text: string, opts: ChunkOpts): Chunk[];
|
|
18
|
+
export declare function mergeSmallChunks(chunks: Chunk[], minTokens: number): Chunk[];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
function estimateTokens(text) {
|
|
2
|
+
return Math.ceil(text.length / 4);
|
|
3
|
+
}
|
|
4
|
+
function chunkContent(text, opts) {
|
|
5
|
+
const size = opts.chunkSize ?? 2e3;
|
|
6
|
+
const charSize = size * 4;
|
|
7
|
+
const overlap = opts.overlap ?? 200;
|
|
8
|
+
const overlapChars = overlap * 4;
|
|
9
|
+
switch (opts.strategy) {
|
|
10
|
+
case "fixed":
|
|
11
|
+
return fixedChunk(text, charSize);
|
|
12
|
+
case "overlap":
|
|
13
|
+
return overlapChunk(text, charSize, overlapChars);
|
|
14
|
+
case "sentence":
|
|
15
|
+
return sentenceChunk(text, charSize);
|
|
16
|
+
case "paragraph":
|
|
17
|
+
return paragraphChunk(text, charSize);
|
|
18
|
+
case "markdown-heading":
|
|
19
|
+
return markdownHeadingChunk(text, charSize);
|
|
20
|
+
case "semantic":
|
|
21
|
+
return semanticChunk(text, charSize);
|
|
22
|
+
case "recursive":
|
|
23
|
+
return recursiveChunk(text, charSize, opts.separators ?? ["\n\n", "\n", ". ", " "]);
|
|
24
|
+
default:
|
|
25
|
+
return fixedChunk(text, charSize);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function makeChunk(text, index, startChar, heading) {
|
|
29
|
+
return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };
|
|
30
|
+
}
|
|
31
|
+
function fixedChunk(text, charSize) {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < text.length) {
|
|
35
|
+
const slice = text.slice(i, i + charSize);
|
|
36
|
+
chunks.push(makeChunk(slice, chunks.length, i));
|
|
37
|
+
i += charSize;
|
|
38
|
+
}
|
|
39
|
+
return chunks;
|
|
40
|
+
}
|
|
41
|
+
function overlapChunk(text, charSize, overlapChars) {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < text.length) {
|
|
45
|
+
const slice = text.slice(i, i + charSize);
|
|
46
|
+
chunks.push(makeChunk(slice, chunks.length, i));
|
|
47
|
+
i += charSize - overlapChars;
|
|
48
|
+
if (i <= 0) break;
|
|
49
|
+
}
|
|
50
|
+
return chunks;
|
|
51
|
+
}
|
|
52
|
+
function sentenceChunk(text, charSize) {
|
|
53
|
+
const sentences = text.match(/[^.!?]*[.!?]+["']?\s*/g) ?? [text];
|
|
54
|
+
return groupIntoChunks(sentences, charSize);
|
|
55
|
+
}
|
|
56
|
+
function paragraphChunk(text, charSize) {
|
|
57
|
+
const paras = text.split(/\n\n+/).filter((p) => p.trim());
|
|
58
|
+
return groupIntoChunks(paras, charSize);
|
|
59
|
+
}
|
|
60
|
+
function markdownHeadingChunk(text, charSize) {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
const headingRe = /^(#{1,3}\s.+)$/m;
|
|
63
|
+
const sections = text.split(/(?=^#{1,3}\s)/m).filter((s) => s.trim());
|
|
64
|
+
let pos = 0;
|
|
65
|
+
for (const section of sections) {
|
|
66
|
+
const headingMatch = section.match(headingRe);
|
|
67
|
+
const heading = headingMatch ? headingMatch[1].replace(/^#+\s*/, "").trim() : void 0;
|
|
68
|
+
if (section.length <= charSize) {
|
|
69
|
+
chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });
|
|
70
|
+
} else {
|
|
71
|
+
const sub = paragraphChunk(section, charSize);
|
|
72
|
+
for (const s of sub) {
|
|
73
|
+
chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
pos += section.length;
|
|
77
|
+
}
|
|
78
|
+
return chunks.length > 0 ? chunks : fixedChunk(text, charSize);
|
|
79
|
+
}
|
|
80
|
+
function semanticChunk(text, charSize) {
|
|
81
|
+
const paras = text.split(/\n\n+/).filter((p) => p.trim());
|
|
82
|
+
const merged = [];
|
|
83
|
+
let current = "";
|
|
84
|
+
for (const para of paras) {
|
|
85
|
+
if (current.length + para.length + 2 <= charSize) {
|
|
86
|
+
current = current ? current + "\n\n" + para : para;
|
|
87
|
+
} else {
|
|
88
|
+
if (current) merged.push(current);
|
|
89
|
+
current = para;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (current) merged.push(current);
|
|
93
|
+
return groupIntoChunks(merged, charSize);
|
|
94
|
+
}
|
|
95
|
+
function recursiveChunk(text, charSize, separators) {
|
|
96
|
+
if (text.length <= charSize) return [makeChunk(text, 0, 0)];
|
|
97
|
+
const [sep, ...rest] = separators;
|
|
98
|
+
if (!sep) return fixedChunk(text, charSize);
|
|
99
|
+
const parts = text.split(sep).filter((p) => p);
|
|
100
|
+
const chunks = [];
|
|
101
|
+
let pos = 0;
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
if (part.length <= charSize) {
|
|
104
|
+
chunks.push(makeChunk(part, chunks.length, pos));
|
|
105
|
+
} else {
|
|
106
|
+
const sub = recursiveChunk(part, charSize, rest);
|
|
107
|
+
for (const s of sub) chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });
|
|
108
|
+
}
|
|
109
|
+
pos += part.length + sep.length;
|
|
110
|
+
}
|
|
111
|
+
return chunks;
|
|
112
|
+
}
|
|
113
|
+
function groupIntoChunks(parts, charSize) {
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let current = "";
|
|
116
|
+
let startChar = 0;
|
|
117
|
+
let pos = 0;
|
|
118
|
+
for (const part of parts) {
|
|
119
|
+
if (current.length + part.length + 1 > charSize && current) {
|
|
120
|
+
chunks.push(makeChunk(current.trim(), chunks.length, startChar));
|
|
121
|
+
startChar = pos;
|
|
122
|
+
current = part;
|
|
123
|
+
} else {
|
|
124
|
+
current = current ? current + " " + part : part;
|
|
125
|
+
}
|
|
126
|
+
pos += part.length + 1;
|
|
127
|
+
}
|
|
128
|
+
if (current.trim()) chunks.push(makeChunk(current.trim(), chunks.length, startChar));
|
|
129
|
+
return chunks;
|
|
130
|
+
}
|
|
131
|
+
function mergeSmallChunks(chunks, minTokens) {
|
|
132
|
+
const minChars = minTokens * 4;
|
|
133
|
+
const result = [];
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < chunks.length) {
|
|
136
|
+
if (chunks[i].text.length < minChars && i + 1 < chunks.length) {
|
|
137
|
+
const merged = chunks[i].text + "\n\n" + chunks[i + 1].text;
|
|
138
|
+
result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));
|
|
139
|
+
i += 2;
|
|
140
|
+
} else {
|
|
141
|
+
result.push({ ...chunks[i], index: result.length });
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
chunkContent,
|
|
149
|
+
estimateTokens,
|
|
150
|
+
mergeSmallChunks
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=content-chunker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/content-chunker.ts"],
|
|
4
|
+
"sourcesContent": ["export type ChunkingStrategy = 'fixed' | 'overlap' | 'sentence' | 'paragraph' | 'markdown-heading' | 'semantic' | 'recursive';\n\nexport interface ChunkOpts {\n strategy: ChunkingStrategy;\n chunkSize?: number; // token estimate target (1 token \u2248 4 chars). Default 2000.\n overlap?: number; // overlap tokens for 'overlap' strategy. Default 200.\n separators?: string[]; // for 'recursive' strategy. Default ['\\\\n\\\\n', '\\\\n', '. ', ' ']\n}\n\nexport interface Chunk {\n index: number;\n text: string;\n tokenEstimate: number;\n startChar: number;\n endChar: number;\n heading?: string;\n}\n\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\nexport function chunkContent(text: string, opts: ChunkOpts): Chunk[] {\n const size = opts.chunkSize ?? 2000;\n const charSize = size * 4;\n const overlap = opts.overlap ?? 200;\n const overlapChars = overlap * 4;\n\n switch (opts.strategy) {\n case 'fixed': return fixedChunk(text, charSize);\n case 'overlap': return overlapChunk(text, charSize, overlapChars);\n case 'sentence': return sentenceChunk(text, charSize);\n case 'paragraph': return paragraphChunk(text, charSize);\n case 'markdown-heading': return markdownHeadingChunk(text, charSize);\n case 'semantic': return semanticChunk(text, charSize);\n case 'recursive': return recursiveChunk(text, charSize, opts.separators ?? ['\\n\\n', '\\n', '. ', ' ']);\n default: return fixedChunk(text, charSize);\n }\n}\n\nfunction makeChunk(text: string, index: number, startChar: number, heading?: string): Chunk {\n return { index, text, tokenEstimate: estimateTokens(text), startChar, endChar: startChar + text.length, heading };\n}\n\nfunction fixedChunk(text: string, charSize: number): Chunk[] {\n const chunks: Chunk[] = [];\n let i = 0;\n while (i < text.length) {\n const slice = text.slice(i, i + charSize);\n chunks.push(makeChunk(slice, chunks.length, i));\n i += charSize;\n }\n return chunks;\n}\n\nfunction overlapChunk(text: string, charSize: number, overlapChars: number): Chunk[] {\n const chunks: Chunk[] = [];\n let i = 0;\n while (i < text.length) {\n const slice = text.slice(i, i + charSize);\n chunks.push(makeChunk(slice, chunks.length, i));\n i += charSize - overlapChars;\n if (i <= 0) break;\n }\n return chunks;\n}\n\nfunction sentenceChunk(text: string, charSize: number): Chunk[] {\n const sentences = text.match(/[^.!?]*[.!?]+[\"']?\\s*/g) ?? [text];\n return groupIntoChunks(sentences, charSize);\n}\n\nfunction paragraphChunk(text: string, charSize: number): Chunk[] {\n const paras = text.split(/\\n\\n+/).filter(p => p.trim());\n return groupIntoChunks(paras, charSize);\n}\n\nfunction markdownHeadingChunk(text: string, charSize: number): Chunk[] {\n const chunks: Chunk[] = [];\n const headingRe = /^(#{1,3}\\s.+)$/m;\n const sections = text.split(/(?=^#{1,3}\\s)/m).filter(s => s.trim());\n let pos = 0;\n for (const section of sections) {\n const headingMatch = section.match(headingRe);\n const heading = headingMatch ? headingMatch[1].replace(/^#+\\s*/, '').trim() : undefined;\n if (section.length <= charSize) {\n chunks.push({ ...makeChunk(section.trim(), chunks.length, pos), heading });\n } else {\n // Section too large \u2014 sub-chunk by paragraph\n const sub = paragraphChunk(section, charSize);\n for (const s of sub) {\n chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar, heading });\n }\n }\n pos += section.length;\n }\n return chunks.length > 0 ? chunks : fixedChunk(text, charSize);\n}\n\nfunction semanticChunk(text: string, charSize: number): Chunk[] {\n // Like paragraph but merges short paragraphs with the next\n const paras = text.split(/\\n\\n+/).filter(p => p.trim());\n const merged: string[] = [];\n let current = '';\n for (const para of paras) {\n if (current.length + para.length + 2 <= charSize) {\n current = current ? current + '\\n\\n' + para : para;\n } else {\n if (current) merged.push(current);\n current = para;\n }\n }\n if (current) merged.push(current);\n return groupIntoChunks(merged, charSize);\n}\n\nfunction recursiveChunk(text: string, charSize: number, separators: string[]): Chunk[] {\n if (text.length <= charSize) return [makeChunk(text, 0, 0)];\n const [sep, ...rest] = separators;\n if (!sep) return fixedChunk(text, charSize);\n const parts = text.split(sep).filter(p => p);\n const chunks: Chunk[] = [];\n let pos = 0;\n for (const part of parts) {\n if (part.length <= charSize) {\n chunks.push(makeChunk(part, chunks.length, pos));\n } else {\n const sub = recursiveChunk(part, charSize, rest);\n for (const s of sub) chunks.push({ ...s, index: chunks.length, startChar: pos + s.startChar, endChar: pos + s.endChar });\n }\n pos += part.length + sep.length;\n }\n return chunks;\n}\n\nfunction groupIntoChunks(parts: string[], charSize: number): Chunk[] {\n const chunks: Chunk[] = [];\n let current = '';\n let startChar = 0;\n let pos = 0;\n for (const part of parts) {\n if (current.length + part.length + 1 > charSize && current) {\n chunks.push(makeChunk(current.trim(), chunks.length, startChar));\n startChar = pos;\n current = part;\n } else {\n current = current ? current + ' ' + part : part;\n }\n pos += part.length + 1;\n }\n if (current.trim()) chunks.push(makeChunk(current.trim(), chunks.length, startChar));\n return chunks;\n}\n\nexport function mergeSmallChunks(chunks: Chunk[], minTokens: number): Chunk[] {\n const minChars = minTokens * 4;\n const result: Chunk[] = [];\n let i = 0;\n while (i < chunks.length) {\n if (chunks[i].text.length < minChars && i + 1 < chunks.length) {\n const merged = chunks[i].text + '\\n\\n' + chunks[i + 1].text;\n result.push(makeChunk(merged, result.length, chunks[i].startChar, chunks[i].heading ?? chunks[i + 1].heading));\n i += 2;\n } else {\n result.push({ ...chunks[i], index: result.length });\n i++;\n }\n }\n return result;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content deduplication — detect near-duplicate pages during crawl.
|
|
3
|
+
*
|
|
4
|
+
* Uses MinHash-inspired approach: instead of full MinHash (needs many hash functions),
|
|
5
|
+
* we use 8 structural fingerprints extracted from the markdown to detect pages that
|
|
6
|
+
* share the same layout/template even if the data differs.
|
|
7
|
+
*
|
|
8
|
+
* Typical duplicates: /products?page=1 vs /products?page=2 (same template, different data).
|
|
9
|
+
* These waste crawl budget and generate redundant test cases.
|
|
10
|
+
*/
|
|
11
|
+
export interface DedupResult {
|
|
12
|
+
isDuplicate: boolean;
|
|
13
|
+
similarUrl?: string;
|
|
14
|
+
similarityScore: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class ContentDeduplicator {
|
|
17
|
+
private seen;
|
|
18
|
+
private readonly threshold;
|
|
19
|
+
constructor(threshold?: number);
|
|
20
|
+
check(url: string, markdown: string): DedupResult;
|
|
21
|
+
size(): number;
|
|
22
|
+
reset(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function extractFingerprints(markdown) {
|
|
2
|
+
const lines = markdown.split("\n").filter((l) => l.trim().length > 0);
|
|
3
|
+
const fps = [];
|
|
4
|
+
const headings = lines.filter((l) => /^#{1,6} /.test(l));
|
|
5
|
+
const hCounts = [0, 0, 0, 0, 0, 0];
|
|
6
|
+
for (const h of headings) {
|
|
7
|
+
const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;
|
|
8
|
+
hCounts[lvl - 1]++;
|
|
9
|
+
}
|
|
10
|
+
fps.push("h:" + hCounts.join(","));
|
|
11
|
+
const linkCount = (markdown.match(/\[.+?\]\(.+?\)/g) ?? []).length;
|
|
12
|
+
fps.push("lk:" + Math.floor(linkCount / 5));
|
|
13
|
+
const listCount = lines.filter((l) => /^[\-\*\+] |^\d+\. /.test(l)).length;
|
|
14
|
+
fps.push("li:" + Math.floor(listCount / 3));
|
|
15
|
+
fps.push("code:" + (markdown.includes("```") ? "1" : "0"));
|
|
16
|
+
fps.push("tbl:" + (markdown.includes("|") ? "1" : "0"));
|
|
17
|
+
const paraCount = markdown.split(/\n{2,}/).filter((p) => p.trim().length > 30).length;
|
|
18
|
+
fps.push("p:" + Math.floor(paraCount / 2));
|
|
19
|
+
const firstH = headings[0]?.replace(/^#{1,6} /, "").toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
|
|
20
|
+
fps.push("fh:" + firstH.slice(0, 20));
|
|
21
|
+
const wordCount = markdown.split(/\s+/).length;
|
|
22
|
+
fps.push("wc:" + Math.floor(wordCount / 50));
|
|
23
|
+
return fps;
|
|
24
|
+
}
|
|
25
|
+
function similarity(a, b) {
|
|
26
|
+
if (a.length !== b.length) return 0;
|
|
27
|
+
let matches = 0;
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
if (a[i] === b[i]) matches++;
|
|
30
|
+
}
|
|
31
|
+
return matches / a.length;
|
|
32
|
+
}
|
|
33
|
+
class ContentDeduplicator {
|
|
34
|
+
seen = /* @__PURE__ */ new Map();
|
|
35
|
+
threshold;
|
|
36
|
+
constructor(threshold = 0.85) {
|
|
37
|
+
this.threshold = threshold;
|
|
38
|
+
}
|
|
39
|
+
check(url, markdown) {
|
|
40
|
+
const fps = extractFingerprints(markdown);
|
|
41
|
+
for (const [key, entry] of this.seen) {
|
|
42
|
+
const score = similarity(fps, entry.fps);
|
|
43
|
+
if (score >= this.threshold) {
|
|
44
|
+
return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.seen.set(url, { fps, url });
|
|
48
|
+
return { isDuplicate: false, similarityScore: 1 };
|
|
49
|
+
}
|
|
50
|
+
size() {
|
|
51
|
+
return this.seen.size;
|
|
52
|
+
}
|
|
53
|
+
reset() {
|
|
54
|
+
this.seen.clear();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
ContentDeduplicator
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=content-dedup.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/content-dedup.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Content deduplication \u2014 detect near-duplicate pages during crawl.\n *\n * Uses MinHash-inspired approach: instead of full MinHash (needs many hash functions),\n * we use 8 structural fingerprints extracted from the markdown to detect pages that\n * share the same layout/template even if the data differs.\n *\n * Typical duplicates: /products?page=1 vs /products?page=2 (same template, different data).\n * These waste crawl budget and generate redundant test cases.\n */\n\n/** Extract structural fingerprints from markdown \u2014 layout-sensitive, data-insensitive */\nfunction extractFingerprints(markdown: string): string[] {\n const lines = markdown.split('\\n').filter(l => l.trim().length > 0);\n const fps: string[] = [];\n\n // FP1: heading structure (H1-H6 count distribution)\n const headings = lines.filter(l => /^#{1,6} /.test(l));\n const hCounts = [0, 0, 0, 0, 0, 0];\n for (const h of headings) {\n const lvl = h.match(/^(#{1,6}) /)?.[1].length ?? 1;\n hCounts[lvl - 1]++;\n }\n fps.push('h:' + hCounts.join(','));\n\n // FP2: link count (grouped into buckets)\n const linkCount = (markdown.match(/\\[.+?\\]\\(.+?\\)/g) ?? []).length;\n fps.push('lk:' + Math.floor(linkCount / 5));\n\n // FP3: list item count (grouped)\n const listCount = lines.filter(l => /^[\\-\\*\\+] |^\\d+\\. /.test(l)).length;\n fps.push('li:' + Math.floor(listCount / 3));\n\n // FP4: code block presence\n fps.push('code:' + (markdown.includes('```') ? '1' : '0'));\n\n // FP5: table presence\n fps.push('tbl:' + (markdown.includes('|') ? '1' : '0'));\n\n // FP6: approximate paragraph count\n const paraCount = markdown.split(/\\n{2,}/).filter(p => p.trim().length > 30).length;\n fps.push('p:' + Math.floor(paraCount / 2));\n\n // FP7: first heading text (normalized)\n const firstH = headings[0]?.replace(/^#{1,6} /, '').toLowerCase().replace(/[^a-z0-9]/g, '') ?? '';\n fps.push('fh:' + firstH.slice(0, 20));\n\n // FP8: word count bucket\n const wordCount = markdown.split(/\\s+/).length;\n fps.push('wc:' + Math.floor(wordCount / 50));\n\n return fps;\n}\n\nfunction similarity(a: string[], b: string[]): number {\n if (a.length !== b.length) return 0;\n let matches = 0;\n for (let i = 0; i < a.length; i++) {\n if (a[i] === b[i]) matches++;\n }\n return matches / a.length;\n}\n\nexport interface DedupResult {\n isDuplicate: boolean;\n similarUrl?: string;\n similarityScore: number;\n}\n\nexport class ContentDeduplicator {\n private seen = new Map<string, { fps: string[]; url: string }>();\n private readonly threshold: number;\n\n constructor(threshold = 0.85) {\n this.threshold = threshold;\n }\n\n check(url: string, markdown: string): DedupResult {\n const fps = extractFingerprints(markdown);\n\n for (const [key, entry] of this.seen) {\n const score = similarity(fps, entry.fps);\n if (score >= this.threshold) {\n return { isDuplicate: true, similarUrl: entry.url, similarityScore: score };\n }\n }\n\n this.seen.set(url, { fps, url });\n return { isDuplicate: false, similarityScore: 1.0 };\n }\n\n size(): number {\n return this.seen.size;\n }\n\n reset(): void {\n this.seen.clear();\n }\n}\n"],
|
|
5
|
+
"mappings": "AAYA,SAAS,oBAAoB,UAA4B;AACvD,QAAM,QAAQ,SAAS,MAAM,IAAI,EAAE,OAAO,OAAK,EAAE,KAAK,EAAE,SAAS,CAAC;AAClE,QAAM,MAAgB,CAAC;AAGvB,QAAM,WAAW,MAAM,OAAO,OAAK,WAAW,KAAK,CAAC,CAAC;AACrD,QAAM,UAAU,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AACjC,aAAW,KAAK,UAAU;AACxB,UAAM,MAAM,EAAE,MAAM,YAAY,IAAI,CAAC,EAAE,UAAU;AACjD,YAAQ,MAAM,CAAC;AAAA,EACjB;AACA,MAAI,KAAK,OAAO,QAAQ,KAAK,GAAG,CAAC;AAGjC,QAAM,aAAa,SAAS,MAAM,iBAAiB,KAAK,CAAC,GAAG;AAC5D,MAAI,KAAK,QAAQ,KAAK,MAAM,YAAY,CAAC,CAAC;AAG1C,QAAM,YAAY,MAAM,OAAO,OAAK,qBAAqB,KAAK,CAAC,CAAC,EAAE;AAClE,MAAI,KAAK,QAAQ,KAAK,MAAM,YAAY,CAAC,CAAC;AAG1C,MAAI,KAAK,WAAW,SAAS,SAAS,KAAK,IAAI,MAAM,IAAI;AAGzD,MAAI,KAAK,UAAU,SAAS,SAAS,GAAG,IAAI,MAAM,IAAI;AAGtD,QAAM,YAAY,SAAS,MAAM,QAAQ,EAAE,OAAO,OAAK,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;AAC7E,MAAI,KAAK,OAAO,KAAK,MAAM,YAAY,CAAC,CAAC;AAGzC,QAAM,SAAS,SAAS,CAAC,GAAG,QAAQ,YAAY,EAAE,EAAE,YAAY,EAAE,QAAQ,cAAc,EAAE,KAAK;AAC/F,MAAI,KAAK,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC;AAGpC,QAAM,YAAY,SAAS,MAAM,KAAK,EAAE;AACxC,MAAI,KAAK,QAAQ,KAAK,MAAM,YAAY,EAAE,CAAC;AAE3C,SAAO;AACT;AAEA,SAAS,WAAW,GAAa,GAAqB;AACpD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,UAAU;AACd,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG;AAAA,EACrB;AACA,SAAO,UAAU,EAAE;AACrB;AAQO,MAAM,oBAAoB;AAAA,EACvB,OAAO,oBAAI,IAA4C;AAAA,EAC9C;AAAA,EAEjB,YAAY,YAAY,MAAM;AAC5B,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,KAAa,UAA+B;AAChD,UAAM,MAAM,oBAAoB,QAAQ;AAExC,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,MAAM;AACpC,YAAM,QAAQ,WAAW,KAAK,MAAM,GAAG;AACvC,UAAI,SAAS,KAAK,WAAW;AAC3B,eAAO,EAAE,aAAa,MAAM,YAAY,MAAM,KAAK,iBAAiB,MAAM;AAAA,MAC5E;AAAA,IACF;AAEA,SAAK,KAAK,IAAI,KAAK,EAAE,KAAK,IAAI,CAAC;AAC/B,WAAO,EAAE,aAAa,OAAO,iBAAiB,EAAI;AAAA,EACpD;AAAA,EAEA,OAAe;AACb,WAAO,KAAK,KAAK;AAAA,EACnB;AAAA,EAEA,QAAc;AACZ,SAAK,KAAK,MAAM;AAAA,EAClB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie consent auto-acceptor.
|
|
3
|
+
*
|
|
4
|
+
* Detects and clicks "Accept" on common cookie consent banners before the
|
|
5
|
+
* crawler proceeds. Without this, many EU sites show a consent wall that
|
|
6
|
+
* blocks content or shows a different layout, making crawl results unreliable.
|
|
7
|
+
*
|
|
8
|
+
* Supports: Cookiebot, OneTrust, TrustArc, Usercentrics, GDPR Cookie Compliance,
|
|
9
|
+
* generic "Accept All" / "Accept Cookies" buttons.
|
|
10
|
+
*
|
|
11
|
+
* Strategy: try known selectors first, then fall back to text-based button search.
|
|
12
|
+
* Non-blocking — if no banner is found or click fails, crawl proceeds normally.
|
|
13
|
+
*/
|
|
14
|
+
import type { Page } from 'playwright';
|
|
15
|
+
export interface ConsentResult {
|
|
16
|
+
found: boolean;
|
|
17
|
+
clicked: boolean;
|
|
18
|
+
method?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function acceptCookieConsent(page: Page, opts?: {
|
|
22
|
+
waitMs?: number;
|
|
23
|
+
}): Promise<ConsentResult>;
|
|
24
|
+
/** Check if a consent banner is still visible after attempted dismissal */
|
|
25
|
+
export declare function isConsentBannerVisible(page: Page): Promise<boolean>;
|