@taodigital/crawler 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +82 -0
- package/README.md +10 -2
- package/dist/accessibility-auditor.d.ts +29 -0
- package/dist/accessibility-auditor.js +57 -0
- package/dist/accessibility-auditor.js.map +7 -0
- package/dist/accessibility-tree-crawler.d.ts +44 -0
- package/dist/accessibility-tree-crawler.js +156 -0
- package/dist/accessibility-tree-crawler.js.map +7 -0
- package/dist/advanced-ui-probes.d.ts +47 -0
- package/dist/advanced-ui-probes.js +310 -0
- package/dist/advanced-ui-probes.js.map +7 -0
- package/dist/ai-login.d.ts +32 -0
- package/dist/ai-login.js +250 -0
- package/dist/ai-login.js.map +7 -0
- package/dist/auth-token-rotator.d.ts +47 -0
- package/dist/auth-token-rotator.js +58 -0
- package/dist/auth-token-rotator.js.map +7 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +348 -0
- package/dist/auth.js.map +7 -0
- package/dist/browser-extension.d.ts +37 -0
- package/dist/browser-extension.js +113 -0
- package/dist/browser-extension.js.map +7 -0
- package/dist/captcha-solver.d.ts +16 -0
- package/dist/captcha-solver.js +105 -0
- package/dist/captcha-solver.js.map +7 -0
- package/dist/color-contrast.d.ts +30 -0
- package/dist/color-contrast.js +78 -0
- package/dist/color-contrast.js.map +7 -0
- package/dist/content-chunker.d.ts +18 -0
- package/dist/content-chunker.js +152 -0
- package/dist/content-chunker.js.map +7 -0
- package/dist/content-dedup.d.ts +23 -0
- package/dist/content-dedup.js +60 -0
- package/dist/content-dedup.js.map +7 -0
- package/dist/cookie-consent.d.ts +25 -0
- package/dist/cookie-consent.js +78 -0
- package/dist/cookie-consent.js.map +7 -0
- package/dist/cookie-jar.d.ts +20 -0
- package/dist/cookie-jar.js +61 -0
- package/dist/cookie-jar.js.map +7 -0
- package/dist/crawl-session-manager.d.ts +44 -0
- package/dist/crawl-session-manager.js +145 -0
- package/dist/crawl-session-manager.js.map +7 -0
- package/dist/crawler.d.ts +233 -0
- package/dist/crawler.js +6239 -0
- package/dist/crawler.js.map +7 -0
- package/dist/crud-interaction.d.ts +31 -0
- package/dist/crud-interaction.js +264 -0
- package/dist/crud-interaction.js.map +7 -0
- package/dist/custom-js-injector.d.ts +42 -0
- package/dist/custom-js-injector.js +50 -0
- package/dist/custom-js-injector.js.map +7 -0
- package/dist/dark-mode-capture.d.ts +23 -0
- package/dist/dark-mode-capture.js +75 -0
- package/dist/dark-mode-capture.js.map +7 -0
- package/dist/dead-link-detector.d.ts +37 -0
- package/dist/dead-link-detector.js +50 -0
- package/dist/dead-link-detector.js.map +7 -0
- package/dist/dropdown-enumerator.d.ts +20 -0
- package/dist/dropdown-enumerator.js +230 -0
- package/dist/dropdown-enumerator.js.map +7 -0
- package/dist/element-hash.d.ts +9 -0
- package/dist/element-hash.js +9 -0
- package/dist/element-hash.js.map +7 -0
- package/dist/email-otp.d.ts +42 -0
- package/dist/email-otp.js +177 -0
- package/dist/email-otp.js.map +7 -0
- package/dist/engine-router.d.ts +47 -0
- package/dist/engine-router.js +94 -0
- package/dist/engine-router.js.map +7 -0
- package/dist/export/bulk-export.d.ts +26 -0
- package/dist/export/bulk-export.js +76 -0
- package/dist/export/bulk-export.js.map +7 -0
- package/dist/export/vector-export.d.ts +76 -0
- package/dist/export/vector-export.js +85 -0
- package/dist/export/vector-export.js.map +7 -0
- package/dist/extractors/advanced.d.ts +64 -0
- package/dist/extractors/advanced.js +265 -0
- package/dist/extractors/advanced.js.map +7 -0
- package/dist/extractors/artifact-envelope.d.ts +63 -0
- package/dist/extractors/artifact-envelope.js +63 -0
- package/dist/extractors/artifact-envelope.js.map +7 -0
- package/dist/extractors/branding-extractor.d.ts +46 -0
- package/dist/extractors/branding-extractor.js +104 -0
- package/dist/extractors/branding-extractor.js.map +7 -0
- package/dist/extractors/summary-extractor.d.ts +10 -0
- package/dist/extractors/summary-extractor.js +36 -0
- package/dist/extractors/summary-extractor.js.map +7 -0
- package/dist/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +42 -0
- package/dist/failure-analysis-agent.js.map +7 -0
- package/dist/feature-flag-detector.d.ts +30 -0
- package/dist/feature-flag-detector.js +55 -0
- package/dist/feature-flag-detector.js.map +7 -0
- package/dist/filter-pagination-crawler.d.ts +52 -0
- package/dist/filter-pagination-crawler.js +299 -0
- package/dist/filter-pagination-crawler.js.map +7 -0
- package/dist/form-detector.d.ts +34 -0
- package/dist/form-detector.js +58 -0
- package/dist/form-detector.js.map +7 -0
- package/dist/frontier.d.ts +40 -0
- package/dist/frontier.js +103 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +36 -0
- package/dist/gap-pass.js +115 -0
- package/dist/gap-pass.js.map +7 -0
- package/dist/graphql-detector.d.ts +39 -0
- package/dist/graphql-detector.js +86 -0
- package/dist/graphql-detector.js.map +7 -0
- package/dist/http-engine.d.ts +80 -0
- package/dist/http-engine.js +318 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +116 -0
- package/dist/incremental-diff.js +137 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +7 -0
- package/dist/input-fuzzer.d.ts +30 -0
- package/dist/input-fuzzer.js +105 -0
- package/dist/input-fuzzer.js.map +7 -0
- package/dist/interactive-probe.d.ts +144 -0
- package/dist/interactive-probe.js +865 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +297 -0
- package/dist/job-queue.js +3812 -0
- package/dist/job-queue.js.map +7 -0
- package/dist/js-coverage.d.ts +29 -0
- package/dist/js-coverage.js +56 -0
- package/dist/js-coverage.js.map +7 -0
- package/dist/js-router-detector.d.ts +17 -0
- package/dist/js-router-detector.js +90 -0
- package/dist/js-router-detector.js.map +7 -0
- package/dist/language-detector.d.ts +22 -0
- package/dist/language-detector.js +57 -0
- package/dist/language-detector.js.map +7 -0
- package/dist/link-classifier.d.ts +23 -0
- package/dist/link-classifier.js +40 -0
- package/dist/link-classifier.js.map +7 -0
- package/dist/live-view.d.ts +36 -0
- package/dist/live-view.js +105 -0
- package/dist/live-view.js.map +7 -0
- package/dist/locator-extractor.d.ts +46 -0
- package/dist/locator-extractor.js +220 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +10 -0
- package/dist/logger.js.map +7 -0
- package/dist/markdown-extract.d.ts +8 -0
- package/dist/markdown-extract.js +54 -0
- package/dist/markdown-extract.js.map +7 -0
- package/dist/multi-context-handler.d.ts +55 -0
- package/dist/multi-context-handler.js +182 -0
- package/dist/multi-context-handler.js.map +7 -0
- package/dist/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.js.map +7 -0
- package/dist/nested-window-crawler.d.ts +41 -0
- package/dist/nested-window-crawler.js +172 -0
- package/dist/nested-window-crawler.js.map +7 -0
- package/dist/network-action-correlator.d.ts +47 -0
- package/dist/network-action-correlator.js +209 -0
- package/dist/network-action-correlator.js.map +7 -0
- package/dist/network-observer.d.ts +49 -0
- package/dist/network-observer.js +149 -0
- package/dist/network-observer.js.map +7 -0
- package/dist/network-throttle.d.ts +25 -0
- package/dist/network-throttle.js +34 -0
- package/dist/network-throttle.js.map +7 -0
- package/dist/openapi-generator.d.ts +49 -0
- package/dist/openapi-generator.js +89 -0
- package/dist/openapi-generator.js.map +7 -0
- package/dist/pdf-extractor.d.ts +28 -0
- package/dist/pdf-extractor.js +81 -0
- package/dist/pdf-extractor.js.map +7 -0
- package/dist/performance-metrics-collector.d.ts +50 -0
- package/dist/performance-metrics-collector.js +155 -0
- package/dist/performance-metrics-collector.js.map +7 -0
- package/dist/performance-metrics.d.ts +52 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +50 -0
- package/dist/phash.js +136 -0
- package/dist/phash.js.map +7 -0
- package/dist/pii-scrubber.d.ts +21 -0
- package/dist/pii-scrubber.js +75 -0
- package/dist/pii-scrubber.js.map +7 -0
- package/dist/playwright-recorder.d.ts +39 -0
- package/dist/playwright-recorder.js +123 -0
- package/dist/playwright-recorder.js.map +7 -0
- package/dist/policies/crawl-safety.d.ts +9 -0
- package/dist/policies/crawl-safety.js +41 -0
- package/dist/policies/crawl-safety.js.map +7 -0
- package/dist/profiles/domain-profile.d.ts +19 -0
- package/dist/profiles/domain-profile.js +160 -0
- package/dist/profiles/domain-profile.js.map +7 -0
- package/dist/proxy.d.ts +50 -0
- package/dist/proxy.js +165 -0
- package/dist/proxy.js.map +7 -0
- package/dist/rate-limiter.d.ts +31 -0
- package/dist/rate-limiter.js +64 -0
- package/dist/rate-limiter.js.map +7 -0
- package/dist/redirect-tracker.d.ts +37 -0
- package/dist/redirect-tracker.js +65 -0
- package/dist/redirect-tracker.js.map +7 -0
- package/dist/redis-frontier.d.ts +48 -0
- package/dist/redis-frontier.js +126 -0
- package/dist/redis-frontier.js.map +7 -0
- package/dist/redis-queue.d.ts +48 -0
- package/dist/redis-queue.js +165 -0
- package/dist/redis-queue.js.map +7 -0
- package/dist/response-cache.d.ts +38 -0
- package/dist/response-cache.js +140 -0
- package/dist/response-cache.js.map +7 -0
- package/dist/responsive-detector.d.ts +29 -0
- package/dist/responsive-detector.js +46 -0
- package/dist/responsive-detector.js.map +7 -0
- package/dist/retry-policy.d.ts +20 -0
- package/dist/retry-policy.js +54 -0
- package/dist/retry-policy.js.map +7 -0
- package/dist/robots-txt.d.ts +9 -0
- package/dist/robots-txt.js +77 -0
- package/dist/robots-txt.js.map +7 -0
- package/dist/screen-reader.d.ts +15 -0
- package/dist/screen-reader.js +48 -0
- package/dist/screen-reader.js.map +7 -0
- package/dist/screenshot-masker.d.ts +28 -0
- package/dist/screenshot-masker.js +62 -0
- package/dist/screenshot-masker.js.map +7 -0
- package/dist/seo-checker.d.ts +33 -0
- package/dist/seo-checker.js +91 -0
- package/dist/seo-checker.js.map +7 -0
- package/dist/service-worker-interceptor.d.ts +36 -0
- package/dist/service-worker-interceptor.js +136 -0
- package/dist/service-worker-interceptor.js.map +7 -0
- package/dist/session-capture.d.ts +39 -0
- package/dist/session-capture.js +274 -0
- package/dist/session-capture.js.map +7 -0
- package/dist/session-export.d.ts +49 -0
- package/dist/session-export.js +117 -0
- package/dist/session-export.js.map +7 -0
- package/dist/shadow-dom-crawler.d.ts +35 -0
- package/dist/shadow-dom-crawler.js +132 -0
- package/dist/shadow-dom-crawler.js.map +7 -0
- package/dist/sitemap-writer.d.ts +21 -0
- package/dist/sitemap-writer.js +47 -0
- package/dist/sitemap-writer.js.map +7 -0
- package/dist/sitemap.d.ts +51 -0
- package/dist/sitemap.js +160 -0
- package/dist/sitemap.js.map +7 -0
- package/dist/skyvern.d.ts +56 -0
- package/dist/skyvern.js +129 -0
- package/dist/skyvern.js.map +7 -0
- package/dist/sqs-queue.d.ts +13 -0
- package/dist/sqs-queue.js +115 -0
- package/dist/sqs-queue.js.map +7 -0
- package/dist/sso-token.d.ts +29 -0
- package/dist/sso-token.js +76 -0
- package/dist/sso-token.js.map +7 -0
- package/dist/state-graph.d.ts +59 -0
- package/dist/state-graph.js +193 -0
- package/dist/state-graph.js.map +7 -0
- package/dist/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +202 -0
- package/dist/stealth/human-behavior.js.map +7 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +77 -0
- package/dist/telemetry.js.map +7 -0
- package/dist/test-executor.d.ts +81 -0
- package/dist/test-executor.js +443 -0
- package/dist/test-executor.js.map +7 -0
- package/dist/tls-fingerprint.d.ts +50 -0
- package/dist/tls-fingerprint.js +81 -0
- package/dist/tls-fingerprint.js.map +7 -0
- package/dist/url-pattern-completer.d.ts +28 -0
- package/dist/url-pattern-completer.js +122 -0
- package/dist/url-pattern-completer.js.map +7 -0
- package/dist/visual-diff-verdict.d.ts +40 -0
- package/dist/visual-diff-verdict.js +27 -0
- package/dist/visual-diff-verdict.js.map +7 -0
- package/dist/webhook-notifier.d.ts +30 -0
- package/dist/webhook-notifier.js +76 -0
- package/dist/webhook-notifier.js.map +7 -0
- package/dist/websocket-detector.d.ts +24 -0
- package/dist/websocket-detector.js +60 -0
- package/dist/websocket-detector.js.map +7 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +33 -0
- package/dist/worker.js.map +7 -0
- package/package.json +49 -4
- package/src/accessibility-auditor.ts +102 -0
- package/src/accessibility-tree-crawler.ts +246 -0
- package/src/advanced-ui-probes.ts +470 -0
- package/src/ai-login.ts +298 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +418 -0
- package/src/browser-extension.ts +186 -0
- package/src/captcha-solver.ts +143 -0
- package/src/color-contrast.ts +116 -0
- package/src/content-chunker.ts +170 -0
- package/src/content-dedup.ts +99 -0
- package/src/cookie-consent.ts +106 -0
- package/src/cookie-jar.ts +91 -0
- package/src/crawl-session-manager.ts +231 -0
- package/src/crawler.ts +7123 -0
- package/src/crawler.ts.rej +12 -0
- package/src/crud-interaction.ts +386 -0
- package/src/custom-js-injector.ts +93 -0
- package/src/dark-mode-capture.ts +110 -0
- package/src/dead-link-detector.ts +76 -0
- package/src/dropdown-enumerator.ts +312 -0
- package/src/element-hash.ts +16 -0
- package/src/email-otp.ts +252 -0
- package/src/engine-router.ts +141 -0
- package/src/export/bulk-export.ts +85 -0
- package/src/export/vector-export.ts +160 -0
- package/src/extractors/advanced.ts +309 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/extractors/branding-extractor.ts +159 -0
- package/src/extractors/summary-extractor.ts +49 -0
- package/src/failure-analysis-agent.ts +51 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/filter-pagination-crawler.ts +500 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +144 -0
- package/src/gap-pass.ts +165 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +453 -0
- package/src/incremental-diff.ts +259 -0
- package/src/index.ts +52 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +1136 -0
- package/src/job-queue.test.ts +187 -0
- package/src/job-queue.ts +4504 -0
- package/src/js-coverage.ts +87 -0
- package/src/js-router-detector.ts +112 -0
- package/src/language-detector.ts +82 -0
- package/src/link-classifier.ts +65 -0
- package/src/live-view.ts +146 -0
- package/src/locator-extractor.ts +309 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/multi-context-handler.ts +296 -0
- package/src/native-crawler.ts +1108 -0
- package/src/nested-window-crawler.ts +310 -0
- package/src/network-action-correlator.ts +320 -0
- package/src/network-observer.ts +204 -0
- package/src/network-throttle.ts +69 -0
- package/src/openapi-generator.ts +135 -0
- package/src/pdf-extractor.ts +123 -0
- package/src/performance-metrics-collector.ts +243 -0
- package/src/performance-metrics.ts +134 -0
- package/src/phash.ts +184 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +52 -0
- package/src/profiles/domain-profile.ts +190 -0
- package/src/proxy.ts +238 -0
- package/src/rate-limiter.ts +87 -0
- package/src/redirect-tracker.ts +97 -0
- package/src/redis-frontier.ts +143 -0
- package/src/redis-queue.ts +208 -0
- package/src/response-cache.ts +150 -0
- package/src/responsive-detector.ts +92 -0
- package/src/retry-policy.ts +87 -0
- package/src/robots-txt.ts +96 -0
- package/src/screen-reader.ts +98 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/service-worker-interceptor.ts +185 -0
- package/src/session-capture.ts +335 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/shadow-dom-crawler.ts +198 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +127 -0
- package/src/sitemap.ts +233 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +129 -0
- package/src/sso-token.ts +120 -0
- package/src/state-graph.ts +280 -0
- package/src/stealth/human-behavior.ts +227 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +578 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +161 -0
- package/src/vendor.d.ts +20 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +59 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +50 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -0
- package/types/accessibility-tree-crawler.d.ts +44 -0
- package/types/advanced-ui-probes.d.ts +47 -0
- package/types/ai-login.d.ts +32 -0
- package/types/auth-token-rotator.d.ts +47 -0
- package/types/auth.d.ts +20 -0
- package/types/browser-extension.d.ts +37 -0
- package/types/bull-worker.d.ts +6 -0
- package/types/captcha-solver.d.ts +16 -0
- package/types/color-contrast.d.ts +30 -0
- package/types/content-chunker.d.ts +18 -0
- package/types/content-dedup.d.ts +23 -0
- package/types/cookie-consent.d.ts +25 -0
- package/types/cookie-jar.d.ts +20 -0
- package/types/crawl-session-manager.d.ts +44 -0
- package/types/crawler.d.ts +233 -0
- package/types/crud-interaction.d.ts +31 -0
- package/types/custom-js-injector.d.ts +42 -0
- package/types/dark-mode-capture.d.ts +23 -0
- package/types/dead-link-detector.d.ts +37 -0
- package/types/dropdown-enumerator.d.ts +20 -0
- package/types/element-hash.d.ts +9 -0
- package/types/email-otp.d.ts +42 -0
- package/types/engine-router.d.ts +47 -0
- package/types/export/bulk-export.d.ts +26 -0
- package/types/export/vector-export.d.ts +76 -0
- package/types/extractors/advanced.d.ts +64 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/extractors/branding-extractor.d.ts +46 -0
- package/types/extractors/summary-extractor.d.ts +10 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/filter-pagination-crawler.d.ts +52 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +40 -0
- package/types/gap-pass.d.ts +36 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +116 -0
- package/types/index.d.ts +52 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +144 -0
- package/types/job-queue.d.ts +308 -0
- package/types/js-coverage.d.ts +29 -0
- package/types/js-router-detector.d.ts +17 -0
- package/types/language-detector.d.ts +22 -0
- package/types/link-classifier.d.ts +23 -0
- package/types/live-view.d.ts +36 -0
- package/types/locator-extractor.d.ts +46 -0
- package/types/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/multi-context-handler.d.ts +55 -0
- package/types/native-crawler.d.ts +69 -0
- package/types/nested-window-crawler.d.ts +41 -0
- package/types/network-action-correlator.d.ts +47 -0
- package/types/network-observer.d.ts +49 -0
- package/types/network-throttle.d.ts +25 -0
- package/types/openapi-generator.d.ts +49 -0
- package/types/pdf-extractor.d.ts +28 -0
- package/types/performance-metrics-collector.d.ts +50 -0
- package/types/performance-metrics.d.ts +52 -0
- package/types/phash.d.ts +50 -0
- package/types/pii-scrubber.d.ts +21 -0
- package/types/playwright-recorder.d.ts +39 -0
- package/types/policies/crawl-safety.d.ts +9 -0
- package/types/profiles/domain-profile.d.ts +19 -0
- package/types/proxy.d.ts +50 -0
- package/types/rate-limiter.d.ts +31 -0
- package/types/redirect-tracker.d.ts +37 -0
- package/types/redis-frontier.d.ts +48 -0
- package/types/redis-queue.d.ts +48 -0
- package/types/response-cache.d.ts +38 -0
- package/types/responsive-detector.d.ts +29 -0
- package/types/retry-policy.d.ts +20 -0
- package/types/robots-txt.d.ts +9 -0
- package/types/screen-reader.d.ts +15 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/service-worker-interceptor.d.ts +36 -0
- package/types/session-capture.d.ts +39 -0
- package/types/session-export.d.ts +49 -0
- package/types/shadow-dom-crawler.d.ts +35 -0
- package/types/sitemap-writer.d.ts +21 -0
- package/types/sitemap.d.ts +51 -0
- package/types/skyvern.d.ts +56 -0
- package/types/sqs-queue.d.ts +13 -0
- package/types/sso-token.d.ts +29 -0
- package/types/state-graph.d.ts +59 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +84 -0
- package/types/tls-fingerprint.d.ts +50 -0
- package/types/url-pattern-completer.d.ts +28 -0
- package/types/visual-diff-verdict.d.ts +40 -0
- package/types/webhook-notifier.d.ts +30 -0
- package/types/websocket-detector.d.ts +24 -0
- package/types/worker.d.ts +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance metrics collector — captures Core Web Vitals and navigation timing
|
|
3
|
+
* for pages visited during crawl runs.
|
|
4
|
+
* Collects LCP, CLS, INP, TTFB, long tasks, and slow resources via PerformanceObserver
|
|
5
|
+
* and the Navigation/Resource Timing APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Page } from 'playwright';
|
|
9
|
+
|
|
10
|
+
export type CwvScore = 'good' | 'needs-improvement' | 'poor';
|
|
11
|
+
|
|
12
|
+
export interface LcpMetric {
|
|
13
|
+
value: number;
|
|
14
|
+
score: CwvScore;
|
|
15
|
+
elementTag: string | null;
|
|
16
|
+
url: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ClsMetric {
|
|
20
|
+
value: number;
|
|
21
|
+
score: CwvScore;
|
|
22
|
+
shiftCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface InpMetric {
|
|
26
|
+
p75: number;
|
|
27
|
+
score: CwvScore;
|
|
28
|
+
sampleCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface NavTimingMetric {
|
|
32
|
+
ttfb: number;
|
|
33
|
+
ttfbScore: CwvScore;
|
|
34
|
+
domContentLoaded: number;
|
|
35
|
+
domComplete: number;
|
|
36
|
+
loadComplete: number;
|
|
37
|
+
transferSizeBytes: number;
|
|
38
|
+
protocol: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SlowResource {
|
|
42
|
+
path: string;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
transferSizeBytes: number;
|
|
45
|
+
initiatorType: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PerformanceMetricsResult {
|
|
49
|
+
lcp: LcpMetric | null;
|
|
50
|
+
cls: ClsMetric | null;
|
|
51
|
+
inp: InpMetric | null;
|
|
52
|
+
navTiming: NavTimingMetric | null;
|
|
53
|
+
slowResources: SlowResource[];
|
|
54
|
+
longTaskCount: number;
|
|
55
|
+
captureSuccess: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Observer injection — call before page navigation via addInitScript
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export async function injectPerformanceObservers(page: Page): Promise<void> {
|
|
63
|
+
await page.addInitScript(() => {
|
|
64
|
+
(window as any).__zetaPerf = { lcp: null, cls: 0, clsCount: 0, inp: [], longTaskCount: 0 };
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
new PerformanceObserver(list => {
|
|
68
|
+
const entries = list.getEntries();
|
|
69
|
+
const last = entries[entries.length - 1] as any;
|
|
70
|
+
if (last) {
|
|
71
|
+
(window as any).__zetaPerf.lcp = {
|
|
72
|
+
value: last.startTime,
|
|
73
|
+
elementTag: last.element?.tagName || null,
|
|
74
|
+
url: last.url || null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}).observe({ entryTypes: ['largest-contentful-paint'] });
|
|
78
|
+
} catch {}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
new PerformanceObserver(list => {
|
|
82
|
+
for (const entry of list.getEntries() as any[]) {
|
|
83
|
+
if (!entry.hadRecentInput) {
|
|
84
|
+
(window as any).__zetaPerf.cls += entry.value;
|
|
85
|
+
(window as any).__zetaPerf.clsCount++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}).observe({ entryTypes: ['layout-shift'] });
|
|
89
|
+
} catch {}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
new PerformanceObserver(list => {
|
|
93
|
+
for (const entry of list.getEntries() as any[]) {
|
|
94
|
+
(window as any).__zetaPerf.inp.push(entry.duration);
|
|
95
|
+
}
|
|
96
|
+
}).observe({ entryTypes: ['event'], durationThreshold: 40 } as any);
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
new PerformanceObserver(list => {
|
|
101
|
+
(window as any).__zetaPerf.longTaskCount += list.getEntries().length;
|
|
102
|
+
}).observe({ entryTypes: ['longtask'] });
|
|
103
|
+
} catch {}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Scoring helpers
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function scoreLcp(value: number): CwvScore {
|
|
112
|
+
if (value <= 2500) return 'good';
|
|
113
|
+
if (value <= 4000) return 'needs-improvement';
|
|
114
|
+
return 'poor';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scoreCls(value: number): CwvScore {
|
|
118
|
+
if (value <= 0.1) return 'good';
|
|
119
|
+
if (value <= 0.25) return 'needs-improvement';
|
|
120
|
+
return 'poor';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function scoreInp(p75: number): CwvScore {
|
|
124
|
+
if (p75 <= 200) return 'good';
|
|
125
|
+
if (p75 <= 500) return 'needs-improvement';
|
|
126
|
+
return 'poor';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scoreTtfb(value: number): CwvScore {
|
|
130
|
+
if (value <= 800) return 'good';
|
|
131
|
+
if (value <= 1800) return 'needs-improvement';
|
|
132
|
+
return 'poor';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Internal collector
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
async function collectMetrics(page: Page): Promise<PerformanceMetricsResult> {
|
|
140
|
+
const rawMetrics = await page.evaluate(() => {
|
|
141
|
+
const p = (window as any).__zetaPerf || {};
|
|
142
|
+
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined;
|
|
143
|
+
const resources = performance.getEntriesByType('resource')
|
|
144
|
+
.filter(r => r.duration > 500)
|
|
145
|
+
.sort((a, b) => b.duration - a.duration)
|
|
146
|
+
.slice(0, 10)
|
|
147
|
+
.map(r => ({
|
|
148
|
+
path: (() => { try { return new URL(r.name).pathname; } catch { return r.name.slice(0, 80); } })(),
|
|
149
|
+
durationMs: Math.round(r.duration),
|
|
150
|
+
transferSizeBytes: (r as any).transferSize || 0,
|
|
151
|
+
initiatorType: (r as any).initiatorType || '',
|
|
152
|
+
}));
|
|
153
|
+
const inpValues = p.inp || [];
|
|
154
|
+
const p75 = inpValues.length > 0
|
|
155
|
+
? inpValues.sort((a: number, b: number) => a - b)[Math.floor(inpValues.length * 0.75)]
|
|
156
|
+
: 0;
|
|
157
|
+
return {
|
|
158
|
+
lcp: p.lcp || null,
|
|
159
|
+
cls: p.cls ?? null,
|
|
160
|
+
clsCount: p.clsCount ?? 0,
|
|
161
|
+
inpP75: p75,
|
|
162
|
+
inpCount: inpValues.length,
|
|
163
|
+
longTaskCount: p.longTaskCount || 0,
|
|
164
|
+
nav: nav ? {
|
|
165
|
+
ttfb: Math.round(nav.responseStart - nav.requestStart),
|
|
166
|
+
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
|
|
167
|
+
domComplete: Math.round(nav.domComplete - nav.startTime),
|
|
168
|
+
loadComplete: Math.round(nav.loadEventEnd - nav.startTime),
|
|
169
|
+
transferSizeBytes: (nav as any).transferSize || 0,
|
|
170
|
+
protocol: (nav as any).nextHopProtocol || '',
|
|
171
|
+
} : null,
|
|
172
|
+
resources,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const lcp: LcpMetric | null = rawMetrics.lcp
|
|
177
|
+
? {
|
|
178
|
+
value: rawMetrics.lcp.value,
|
|
179
|
+
score: scoreLcp(rawMetrics.lcp.value),
|
|
180
|
+
elementTag: rawMetrics.lcp.elementTag,
|
|
181
|
+
url: rawMetrics.lcp.url,
|
|
182
|
+
}
|
|
183
|
+
: null;
|
|
184
|
+
|
|
185
|
+
const cls: ClsMetric | null = rawMetrics.cls !== null
|
|
186
|
+
? {
|
|
187
|
+
value: rawMetrics.cls,
|
|
188
|
+
score: scoreCls(rawMetrics.cls),
|
|
189
|
+
shiftCount: rawMetrics.clsCount,
|
|
190
|
+
}
|
|
191
|
+
: null;
|
|
192
|
+
|
|
193
|
+
const inp: InpMetric | null = rawMetrics.inpCount > 0
|
|
194
|
+
? {
|
|
195
|
+
p75: rawMetrics.inpP75,
|
|
196
|
+
score: scoreInp(rawMetrics.inpP75),
|
|
197
|
+
sampleCount: rawMetrics.inpCount,
|
|
198
|
+
}
|
|
199
|
+
: null;
|
|
200
|
+
|
|
201
|
+
const navTiming: NavTimingMetric | null = rawMetrics.nav
|
|
202
|
+
? {
|
|
203
|
+
ttfb: rawMetrics.nav.ttfb,
|
|
204
|
+
ttfbScore: scoreTtfb(rawMetrics.nav.ttfb),
|
|
205
|
+
domContentLoaded: rawMetrics.nav.domContentLoaded,
|
|
206
|
+
domComplete: rawMetrics.nav.domComplete,
|
|
207
|
+
loadComplete: rawMetrics.nav.loadComplete,
|
|
208
|
+
transferSizeBytes: rawMetrics.nav.transferSizeBytes,
|
|
209
|
+
protocol: rawMetrics.nav.protocol,
|
|
210
|
+
}
|
|
211
|
+
: null;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
lcp,
|
|
215
|
+
cls,
|
|
216
|
+
inp,
|
|
217
|
+
navTiming,
|
|
218
|
+
slowResources: rawMetrics.resources,
|
|
219
|
+
longTaskCount: rawMetrics.longTaskCount,
|
|
220
|
+
captureSuccess: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Public API
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const emptyResult: Omit<PerformanceMetricsResult, 'captureSuccess'> = {
|
|
229
|
+
lcp: null,
|
|
230
|
+
cls: null,
|
|
231
|
+
inp: null,
|
|
232
|
+
navTiming: null,
|
|
233
|
+
slowResources: [],
|
|
234
|
+
longTaskCount: 0,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export async function collectPagePerformance(page: Page): Promise<PerformanceMetricsResult> {
|
|
238
|
+
try {
|
|
239
|
+
return await collectMetrics(page);
|
|
240
|
+
} catch {
|
|
241
|
+
return { ...emptyResult, captureSuccess: false };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance metrics collection via CDP + Playwright page.evaluate().
|
|
3
|
+
*
|
|
4
|
+
* Collects Core Web Vitals and resource timing after page navigation.
|
|
5
|
+
* Stored alongside screen data for test generation context and regression detection.
|
|
6
|
+
*
|
|
7
|
+
* Metrics:
|
|
8
|
+
* - LCP (Largest Contentful Paint) — via PerformanceObserver in page
|
|
9
|
+
* - CLS (Cumulative Layout Shift) — via PerformanceObserver in page
|
|
10
|
+
* - TTFB (Time to First Byte) — from navigation timing
|
|
11
|
+
* - DOMContentLoaded — from navigation timing
|
|
12
|
+
* - Resource count and size by type
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Page } from 'playwright';
|
|
16
|
+
|
|
17
|
+
export interface CoreWebVitals {
|
|
18
|
+
lcp?: number; // ms
|
|
19
|
+
cls?: number; // unitless score
|
|
20
|
+
ttfb?: number; // ms
|
|
21
|
+
fcp?: number; // First Contentful Paint ms
|
|
22
|
+
domContentLoaded?: number; // ms
|
|
23
|
+
loadEventEnd?: number; // ms
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResourceSummary {
|
|
27
|
+
scripts: { count: number; totalBytes: number };
|
|
28
|
+
styles: { count: number; totalBytes: number };
|
|
29
|
+
images: { count: number; totalBytes: number };
|
|
30
|
+
fonts: { count: number; totalBytes: number };
|
|
31
|
+
other: { count: number; totalBytes: number };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PagePerfMetrics {
|
|
35
|
+
url: string;
|
|
36
|
+
vitals: CoreWebVitals;
|
|
37
|
+
resources: ResourceSummary;
|
|
38
|
+
totalTransferBytes: number;
|
|
39
|
+
requestCount: number;
|
|
40
|
+
measuredAt: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function collectPerfMetrics(page: any, url: string): Promise<PagePerfMetrics> {
|
|
44
|
+
const measuredAt = new Date().toISOString();
|
|
45
|
+
|
|
46
|
+
// Inject PerformanceObserver for LCP + CLS, wait up to 3s
|
|
47
|
+
let vitals: CoreWebVitals = {};
|
|
48
|
+
try {
|
|
49
|
+
vitals = await page.evaluate(() => {
|
|
50
|
+
return new Promise<any>((resolve) => {
|
|
51
|
+
const result: any = {};
|
|
52
|
+
|
|
53
|
+
// Navigation timing
|
|
54
|
+
const nav = performance.getEntriesByType('navigation')[0] as any;
|
|
55
|
+
if (nav) {
|
|
56
|
+
result.ttfb = Math.round(nav.responseStart - nav.requestStart);
|
|
57
|
+
result.domContentLoaded = Math.round(nav.domContentLoadedEventEnd);
|
|
58
|
+
result.loadEventEnd = Math.round(nav.loadEventEnd);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let lcpDone = false;
|
|
62
|
+
let clsDone = false;
|
|
63
|
+
const tryResolve = () => { if (lcpDone && clsDone) resolve(result); };
|
|
64
|
+
|
|
65
|
+
// LCP
|
|
66
|
+
try {
|
|
67
|
+
new PerformanceObserver((list) => {
|
|
68
|
+
const entries = list.getEntries();
|
|
69
|
+
if (entries.length > 0) result.lcp = Math.round((entries[entries.length - 1] as any).startTime);
|
|
70
|
+
lcpDone = true;
|
|
71
|
+
tryResolve();
|
|
72
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
73
|
+
} catch { lcpDone = true; }
|
|
74
|
+
|
|
75
|
+
// CLS
|
|
76
|
+
try {
|
|
77
|
+
let cls = 0;
|
|
78
|
+
new PerformanceObserver((list) => {
|
|
79
|
+
for (const e of list.getEntries() as any[]) {
|
|
80
|
+
if (!e.hadRecentInput) cls += e.value;
|
|
81
|
+
}
|
|
82
|
+
result.cls = Math.round(cls * 1000) / 1000;
|
|
83
|
+
clsDone = true;
|
|
84
|
+
tryResolve();
|
|
85
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
86
|
+
} catch { clsDone = true; }
|
|
87
|
+
|
|
88
|
+
// FCP
|
|
89
|
+
try {
|
|
90
|
+
const fcp = performance.getEntriesByName('first-contentful-paint')[0] as any;
|
|
91
|
+
if (fcp) result.fcp = Math.round(fcp.startTime);
|
|
92
|
+
} catch {}
|
|
93
|
+
|
|
94
|
+
// Fallback resolve after 3s
|
|
95
|
+
setTimeout(() => resolve(result), 3_000);
|
|
96
|
+
});
|
|
97
|
+
}).catch(() => ({}));
|
|
98
|
+
} catch { /* page closed or navigate already */ }
|
|
99
|
+
|
|
100
|
+
// Resource timing
|
|
101
|
+
let resources: ResourceSummary = {
|
|
102
|
+
scripts: { count: 0, totalBytes: 0 },
|
|
103
|
+
styles: { count: 0, totalBytes: 0 },
|
|
104
|
+
images: { count: 0, totalBytes: 0 },
|
|
105
|
+
fonts: { count: 0, totalBytes: 0 },
|
|
106
|
+
other: { count: 0, totalBytes: 0 },
|
|
107
|
+
};
|
|
108
|
+
let totalTransferBytes = 0;
|
|
109
|
+
let requestCount = 0;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const entries = await page.evaluate(() =>
|
|
113
|
+
performance.getEntriesByType('resource').map((e: any) => ({
|
|
114
|
+
name: e.name,
|
|
115
|
+
initiatorType: e.initiatorType,
|
|
116
|
+
transferSize: e.transferSize ?? 0,
|
|
117
|
+
}))
|
|
118
|
+
).catch(() => [] as any[]);
|
|
119
|
+
|
|
120
|
+
for (const e of entries) {
|
|
121
|
+
requestCount++;
|
|
122
|
+
totalTransferBytes += e.transferSize;
|
|
123
|
+
const cat = e.initiatorType === 'script' ? 'scripts'
|
|
124
|
+
: e.initiatorType === 'css' || e.name?.endsWith('.css') ? 'styles'
|
|
125
|
+
: e.initiatorType === 'img' ? 'images'
|
|
126
|
+
: e.name?.match(/\.(woff|woff2|ttf|otf|eot)/) ? 'fonts'
|
|
127
|
+
: 'other';
|
|
128
|
+
resources[cat as keyof ResourceSummary].count++;
|
|
129
|
+
resources[cat as keyof ResourceSummary].totalBytes += e.transferSize;
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
|
|
133
|
+
return { url, vitals, resources, totalTransferBytes, requestCount, measuredAt };
|
|
134
|
+
}
|
package/src/phash.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import type { OverlayOptions } from 'sharp';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composites neutral grey (128,128,128) over each ignoredRegion (fractional
|
|
6
|
+
* 0-1 coordinates) so masked content — timestamps, live counters, ads —
|
|
7
|
+
* contributes nothing to any downstream pixel comparison. Shared by every
|
|
8
|
+
* comparison path (whole-image hash, per-cell grid hash, AI vision) so a
|
|
9
|
+
* region masked once is actually ignored everywhere, not just in the
|
|
10
|
+
* cheapest check.
|
|
11
|
+
*/
|
|
12
|
+
export async function applyMask(
|
|
13
|
+
imagePath: string,
|
|
14
|
+
ignoredRegions: Array<{ x: number; y: number; width: number; height: number }>,
|
|
15
|
+
): Promise<Buffer> {
|
|
16
|
+
const meta = await sharp(imagePath).metadata();
|
|
17
|
+
const W = meta.width ?? 1200;
|
|
18
|
+
const H = meta.height ?? 800;
|
|
19
|
+
|
|
20
|
+
const overlays: OverlayOptions[] = await Promise.all(
|
|
21
|
+
ignoredRegions.map(async (r) => {
|
|
22
|
+
const rW = Math.max(1, Math.round(r.width * W));
|
|
23
|
+
const rH = Math.max(1, Math.round(r.height * H));
|
|
24
|
+
const buf = await sharp({
|
|
25
|
+
create: { width: rW, height: rH, channels: 3, background: { r: 128, g: 128, b: 128 } },
|
|
26
|
+
}).png().toBuffer();
|
|
27
|
+
return { input: buf, top: Math.round(r.y * H), left: Math.round(r.x * W) } as OverlayOptions;
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return sharp(imagePath).composite(overlays).png().toBuffer();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compute an 8x8 average hash (aHash) of an image file, with optional mask overlays.
|
|
36
|
+
* ignoredRegions are fractional (0-1 range), converted to pixels here.
|
|
37
|
+
* Masked regions are filled with neutral grey (128,128,128) before hashing.
|
|
38
|
+
*/
|
|
39
|
+
async function hashWithMasks(
|
|
40
|
+
imagePath: string,
|
|
41
|
+
ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],
|
|
42
|
+
): Promise<bigint> {
|
|
43
|
+
const pipeline = ignoredRegions.length > 0
|
|
44
|
+
? sharp(await applyMask(imagePath, ignoredRegions))
|
|
45
|
+
: sharp(imagePath);
|
|
46
|
+
|
|
47
|
+
const buf = await pipeline.resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer();
|
|
48
|
+
const pixels = Array.from(buf);
|
|
49
|
+
const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;
|
|
50
|
+
return pixels.reduce((h, p, i) => (p >= mean ? h | (1n << BigInt(63 - i)) : h), 0n);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hammingDistance(a: bigint, b: bigint): number {
|
|
54
|
+
let xor = a ^ b;
|
|
55
|
+
let count = 0;
|
|
56
|
+
while (xor !== 0n) { count += Number(xor & 1n); xor >>= 1n; }
|
|
57
|
+
return count;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates a pixelmatch diff image highlighting changed pixels in red.
|
|
62
|
+
* Applies the same ignoredRegions mask as screenshotDiffPercent so masked
|
|
63
|
+
* zones don't show as "changed" in the visual output.
|
|
64
|
+
* Returns a PNG Buffer suitable for upload to S3.
|
|
65
|
+
*/
|
|
66
|
+
export async function generatePixelmatchDiff(
|
|
67
|
+
pathA: string,
|
|
68
|
+
pathB: string,
|
|
69
|
+
ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],
|
|
70
|
+
): Promise<Buffer> {
|
|
71
|
+
const metaA = await sharp(pathA).metadata();
|
|
72
|
+
const W = metaA.width ?? 1200;
|
|
73
|
+
const H = metaA.height ?? 800;
|
|
74
|
+
|
|
75
|
+
const toRawRGBA = async (path: string): Promise<Buffer> => {
|
|
76
|
+
const base = ignoredRegions.length > 0 ? sharp(await applyMask(path, ignoredRegions)) : sharp(path);
|
|
77
|
+
const { data } = await base.resize(W, H, { fit: 'fill' }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
78
|
+
return data;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const [rawA, rawB] = await Promise.all([toRawRGBA(pathA), toRawRGBA(pathB)]);
|
|
82
|
+
const { default: pixelmatch } = await import('pixelmatch');
|
|
83
|
+
const diffData = Buffer.alloc(W * H * 4);
|
|
84
|
+
pixelmatch(rawA, rawB, diffData, W, H, {
|
|
85
|
+
threshold: 0.1,
|
|
86
|
+
alpha: 0.3,
|
|
87
|
+
diffColor: [255, 0, 0],
|
|
88
|
+
aaColor: [255, 255, 0],
|
|
89
|
+
});
|
|
90
|
+
return sharp(diffData, { raw: { width: W, height: H, channels: 4 } }).png().toBuffer();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function screenshotDiffPercent(
|
|
94
|
+
pathA: string,
|
|
95
|
+
pathB: string,
|
|
96
|
+
ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],
|
|
97
|
+
): Promise<{ pHashDistance: number; changeLevel: 'IDENTICAL' | 'MINOR' | 'MODERATE' | 'SIGNIFICANT' }> {
|
|
98
|
+
const [hashA, hashB] = await Promise.all([
|
|
99
|
+
hashWithMasks(pathA, ignoredRegions),
|
|
100
|
+
hashWithMasks(pathB, ignoredRegions),
|
|
101
|
+
]);
|
|
102
|
+
let xor = hashA ^ hashB;
|
|
103
|
+
let dist = 0;
|
|
104
|
+
while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }
|
|
105
|
+
const pct = parseFloat(((dist / 64) * 100).toFixed(1));
|
|
106
|
+
const changeLevel = pct === 0 ? 'IDENTICAL' : pct < 10 ? 'MINOR' : pct < 30 ? 'MODERATE' : 'SIGNIFICANT';
|
|
107
|
+
return { pHashDistance: pct, changeLevel };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Grid-based layout analysis: divides screenshots into a 3×3 grid and computes
|
|
112
|
+
* pHash per cell. Returns which regions changed and where, for the VisualDiff.changes field.
|
|
113
|
+
* Uses sharp's `extract` to crop each cell before hashing.
|
|
114
|
+
*/
|
|
115
|
+
export async function gridLayoutDiff(
|
|
116
|
+
pathA: string,
|
|
117
|
+
pathB: string,
|
|
118
|
+
ignoredRegions: Array<{ x: number; y: number; width: number; height: number }> = [],
|
|
119
|
+
): Promise<Array<{ location: string; severity: string; description: string }>> {
|
|
120
|
+
const GRID_LABELS = [
|
|
121
|
+
'top-left', 'top-center', 'top-right',
|
|
122
|
+
'mid-left', 'mid-center', 'mid-right',
|
|
123
|
+
'bottom-left', 'bottom-center', 'bottom-right',
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Mask before cropping into cells — a masked region (e.g. a timestamp)
|
|
128
|
+
// then contributes nothing to any cell's hash, instead of only being
|
|
129
|
+
// ignored by the separate whole-image hash in screenshotDiffPercent().
|
|
130
|
+
const [sourceA, sourceB] = ignoredRegions.length > 0
|
|
131
|
+
? await Promise.all([applyMask(pathA, ignoredRegions), applyMask(pathB, ignoredRegions)])
|
|
132
|
+
: [pathA, pathB];
|
|
133
|
+
|
|
134
|
+
const [metaA, metaB] = await Promise.all([
|
|
135
|
+
sharp(sourceA).metadata(),
|
|
136
|
+
sharp(sourceB).metadata(),
|
|
137
|
+
]);
|
|
138
|
+
const W = Math.min(metaA.width ?? 1200, metaB.width ?? 1200);
|
|
139
|
+
const H = Math.min(metaA.height ?? 800, metaB.height ?? 800);
|
|
140
|
+
const cW = Math.floor(W / 3);
|
|
141
|
+
const cH = Math.floor(H / 3);
|
|
142
|
+
|
|
143
|
+
const changes: Array<{ location: string; severity: string; description: string }> = [];
|
|
144
|
+
|
|
145
|
+
for (let row = 0; row < 3; row++) {
|
|
146
|
+
for (let col = 0; col < 3; col++) {
|
|
147
|
+
const region = { left: col * cW, top: row * cH, width: cW, height: cH };
|
|
148
|
+
const label = GRID_LABELS[row * 3 + col];
|
|
149
|
+
|
|
150
|
+
const [hashA, hashB] = await Promise.all([
|
|
151
|
+
sharp(sourceA).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()
|
|
152
|
+
.then((buf) => {
|
|
153
|
+
const pixels = Array.from(buf);
|
|
154
|
+
const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;
|
|
155
|
+
return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);
|
|
156
|
+
}),
|
|
157
|
+
sharp(sourceB).extract(region).resize(8, 8, { fit: 'fill' }).greyscale().raw().toBuffer()
|
|
158
|
+
.then((buf) => {
|
|
159
|
+
const pixels = Array.from(buf);
|
|
160
|
+
const mean = pixels.reduce((s, p) => s + p, 0) / pixels.length;
|
|
161
|
+
return pixels.reduce((h, p, i) => p >= mean ? h | (1n << BigInt(63 - i)) : h, 0n);
|
|
162
|
+
}),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
let xor = hashA ^ hashB;
|
|
166
|
+
let dist = 0;
|
|
167
|
+
while (xor !== 0n) { dist += Number(xor & 1n); xor >>= 1n; }
|
|
168
|
+
const pct = (dist / 64) * 100;
|
|
169
|
+
|
|
170
|
+
if (pct >= 5) {
|
|
171
|
+
const severity = pct >= 20 ? 'HIGH' : pct >= 10 ? 'MEDIUM' : 'LOW';
|
|
172
|
+
changes.push({
|
|
173
|
+
location: label,
|
|
174
|
+
severity,
|
|
175
|
+
description: `${Math.round(pct)}% visual change in ${label.replace('-', ' ')} region`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return changes;
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII scrubber — detects and redacts Personally Identifiable Information
|
|
3
|
+
* from crawled markdown, HTML, and extracted text before storage.
|
|
4
|
+
*
|
|
5
|
+
* GDPR Article 4(1): "any information relating to an identified or identifiable natural person"
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface PiiMatch {
|
|
9
|
+
type: 'email' | 'phone' | 'ssn' | 'credit-card' | 'ip-address' | 'passport' | 'iban' | 'dob';
|
|
10
|
+
value: string;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScrubResult {
|
|
16
|
+
text: string;
|
|
17
|
+
matches: PiiMatch[];
|
|
18
|
+
scrubbed: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Patterns — ordered from most-specific to least-specific
|
|
22
|
+
const PATTERNS: Array<{ type: PiiMatch['type']; re: RegExp }> = [
|
|
23
|
+
// Email — RFC 5321 simplified
|
|
24
|
+
{ type: 'email', re: /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/g },
|
|
25
|
+
// US SSN — 9 digits with dashes or spaces
|
|
26
|
+
{ type: 'ssn', re: /\b(?!000|666|9\d\d)\d{3}[-\s](?!00)\d{2}[-\s](?!0000)\d{4}\b/g },
|
|
27
|
+
// Credit card — Visa/MC/Amex/Discover with optional spaces/dashes
|
|
28
|
+
{ type: 'credit-card', re: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{3,4}\b/g },
|
|
29
|
+
// IBAN — international bank account (15-34 chars)
|
|
30
|
+
{ type: 'iban', re: /\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b/g },
|
|
31
|
+
// Phone — international or US formats
|
|
32
|
+
{ type: 'phone', re: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b|\+\d{1,3}[-.\s]\d{2,4}[-.\s]\d{3,4}[-.\s]\d{3,4}/g },
|
|
33
|
+
// Date of birth — common formats
|
|
34
|
+
{ type: 'dob', re: /\b(?:0?[1-9]|1[0-2])\/(?:0?[1-9]|[12]\d|3[01])\/(?:19|20)\d{2}\b/g },
|
|
35
|
+
// IPv4 private/internal addresses — only flag private-looking ones in content
|
|
36
|
+
{ type: 'ip-address', re: /\b(?:192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\b/g },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const REDACT_MAP: Record<PiiMatch['type'], string> = {
|
|
40
|
+
email: '[EMAIL]',
|
|
41
|
+
phone: '[PHONE]',
|
|
42
|
+
ssn: '[SSN]',
|
|
43
|
+
'credit-card': '[CARD]',
|
|
44
|
+
'ip-address': '[IP]',
|
|
45
|
+
passport: '[PASSPORT]',
|
|
46
|
+
iban: '[IBAN]',
|
|
47
|
+
dob: '[DOB]',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function detectPii(text: string): PiiMatch[] {
|
|
51
|
+
const matches: PiiMatch[] = [];
|
|
52
|
+
const seen = new Set<string>();
|
|
53
|
+
|
|
54
|
+
for (const { type, re } of PATTERNS) {
|
|
55
|
+
const r = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
|
|
56
|
+
let m: RegExpExecArray | null;
|
|
57
|
+
while ((m = r.exec(text)) !== null) {
|
|
58
|
+
const key = `${type}:${m.index}`;
|
|
59
|
+
if (!seen.has(key)) {
|
|
60
|
+
seen.add(key);
|
|
61
|
+
matches.push({ type, value: m[0], start: m.index, end: m.index + m[0].length });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return matches.sort((a, b) => a.start - b.start);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function scrubPii(text: string): ScrubResult {
|
|
70
|
+
const matches = detectPii(text);
|
|
71
|
+
if (matches.length === 0) return { text, matches: [], scrubbed: false };
|
|
72
|
+
|
|
73
|
+
// Replace from end to start so indices stay valid
|
|
74
|
+
let scrubbed = text;
|
|
75
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
76
|
+
const { type, start, end } = matches[i];
|
|
77
|
+
scrubbed = scrubbed.slice(0, start) + REDACT_MAP[type] + scrubbed.slice(end);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { text: scrubbed, matches, scrubbed: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function hasPii(text: string): boolean {
|
|
84
|
+
for (const { re } of PATTERNS) {
|
|
85
|
+
const r = new RegExp(re.source, re.flags);
|
|
86
|
+
if (r.test(text)) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function scrubObject<T extends Record<string, unknown>>(obj: T, fields: (keyof T)[]): T {
|
|
92
|
+
const result = { ...obj };
|
|
93
|
+
for (const field of fields) {
|
|
94
|
+
if (typeof result[field] === 'string') {
|
|
95
|
+
(result as any)[field] = scrubPii(result[field] as string).text;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|