@detiq/crawler 1.1.9 → 1.1.10
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/dist/accessibility-auditor.js +54 -74
- package/dist/ai-login.js +227 -249
- package/dist/auth-token-rotator.js +57 -67
- package/dist/auth.js +299 -336
- package/dist/browser-extension.js +104 -142
- package/dist/captcha-solver.js +98 -125
- package/dist/color-contrast.js +77 -89
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +54 -71
- package/dist/cookie-consent.js +68 -83
- package/dist/cookie-jar.js +53 -60
- package/dist/crawler.js +4494 -5353
- package/dist/custom-js-injector.js +45 -61
- package/dist/dark-mode-capture.js +67 -81
- package/dist/dead-link-detector.js +47 -55
- package/dist/element-hash.js +8 -12
- package/dist/email-otp.js +154 -195
- package/dist/engine-router.js +87 -98
- package/dist/export/bulk-export.js +70 -55
- package/dist/export/vector-export.js +77 -105
- package/dist/extractors/advanced.js +249 -266
- package/dist/extractors/artifact-envelope.js +59 -58
- package/dist/failure-analysis-agent.js +37 -33
- package/dist/feature-flag-detector.js +51 -66
- package/dist/form-detector.js +55 -75
- package/dist/frontier.js +76 -101
- package/dist/gap-pass.js +68 -89
- package/dist/graphql-detector.js +62 -72
- package/dist/http-engine.js +282 -330
- package/dist/incremental-diff.js +54 -87
- package/dist/index.js +67 -28
- package/dist/input-fuzzer.js +93 -97
- package/dist/interactive-probe.js +715 -827
- package/dist/job-queue.js +2151 -2169
- package/dist/js-coverage.js +52 -56
- package/dist/js-router-detector.js +86 -117
- package/dist/language-detector.js +51 -63
- package/dist/link-classifier.js +32 -50
- package/dist/live-view.js +91 -108
- package/dist/locator-extractor.js +151 -187
- package/dist/logger.js +9 -7
- package/dist/markdown-extract.js +48 -61
- package/dist/native-crawler.js +780 -946
- package/dist/network-observer.js +135 -156
- package/dist/network-throttle.js +29 -40
- package/dist/openapi-generator.js +77 -92
- package/dist/pdf-extractor.js +72 -90
- package/dist/performance-metrics.js +80 -102
- package/dist/phash.js +125 -144
- package/dist/pii-scrubber.js +65 -68
- package/dist/playwright-recorder.js +70 -58
- package/dist/policies/crawl-safety.js +31 -34
- package/dist/profiles/domain-profile.js +130 -125
- package/dist/proxy.js +146 -172
- package/dist/rate-limiter.js +58 -65
- package/dist/redirect-tracker.js +60 -64
- package/dist/redis-frontier.js +116 -136
- package/dist/redis-queue.js +157 -144
- package/dist/response-cache.js +127 -140
- package/dist/responsive-detector.js +42 -46
- package/dist/retry-policy.js +46 -50
- package/dist/robots-txt.js +69 -83
- package/dist/screen-reader.js +45 -70
- package/dist/screenshot-masker.js +59 -77
- package/dist/seo-checker.js +88 -107
- package/dist/session-capture.js +192 -212
- package/dist/session-export.js +107 -133
- package/dist/sitemap-writer.js +38 -41
- package/dist/sitemap.js +141 -202
- package/dist/skyvern.js +123 -148
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +74 -90
- package/dist/stealth/human-behavior.js +156 -173
- package/dist/telemetry.js +68 -82
- package/dist/test-executor.js +309 -335
- package/dist/tls-fingerprint.js +77 -88
- package/dist/url-pattern-completer.js +71 -90
- package/dist/visual-diff-verdict.js +23 -38
- package/dist/webhook-notifier.js +71 -69
- package/dist/websocket-detector.js +56 -68
- package/dist/worker.js +29 -38
- package/package.json +1 -1
package/dist/sitemap.js
CHANGED
|
@@ -1,215 +1,154 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { ProxyAgent } from 'undici';
|
|
10
|
-
import { assertCrawlUrlSafe } from '@detiq/core';
|
|
11
|
-
import { logger } from './logger.js';
|
|
12
|
-
const FETCH_TIMEOUT_MS = 10_000;
|
|
13
|
-
const MAX_SITEMAP_INDEX_DEPTH = 3; // sitemap index -> sub-sitemaps -> (no deeper nesting expected)
|
|
14
|
-
const MAX_SUB_SITEMAPS = 50; // bound fan-out from a single sitemap index
|
|
15
|
-
const MAX_URLS = 5000; // sane upper bound so a huge/malicious sitemap can't blow up memory
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { ProxyAgent } from "undici";
|
|
3
|
+
import { assertCrawlUrlSafe } from "@detiq/core";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
6
|
+
const MAX_SITEMAP_INDEX_DEPTH = 3;
|
|
7
|
+
const MAX_SUB_SITEMAPS = 50;
|
|
8
|
+
const MAX_URLS = 5e3;
|
|
16
9
|
function toArray(x) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return Array.isArray(x) ? x : [x];
|
|
10
|
+
if (x == null) return [];
|
|
11
|
+
return Array.isArray(x) ? x : [x];
|
|
20
12
|
}
|
|
21
|
-
/** Registrable-host key: lowercased hostname with a leading `www.` stripped. */
|
|
22
13
|
function hostKey(hostname) {
|
|
23
|
-
|
|
14
|
+
return hostname.replace(/^www\./i, "").toLowerCase();
|
|
24
15
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return hostKey(new URL(candidateUrl).hostname) === hostKey(originHost);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
16
|
+
function sameSite(candidateUrl, siteOrigin) {
|
|
17
|
+
try {
|
|
18
|
+
const originHost = /^https?:\/\//i.test(siteOrigin) ? new URL(siteOrigin).hostname : siteOrigin;
|
|
19
|
+
return hostKey(new URL(candidateUrl).hostname) === hostKey(originHost);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
37
23
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.slice(0, MAX_SUB_SITEMAPS);
|
|
52
|
-
}
|
|
53
|
-
if (parsed?.urlset) {
|
|
54
|
-
rawPageUrls.push(...toArray(parsed.urlset.url)
|
|
55
|
-
.map((u) => u?.loc)
|
|
56
|
-
.filter((loc) => typeof loc === 'string'));
|
|
57
|
-
}
|
|
58
|
-
// Same-site only (www/protocol-tolerant) — a sitemap listing external URLs shouldn't seed this project's queue.
|
|
59
|
-
const sameOrigin = rawPageUrls.filter((u) => sameSite(u, origin));
|
|
60
|
-
return { pageUrls: [...new Set(sameOrigin)].slice(0, MAX_URLS), subSitemapUrls };
|
|
24
|
+
function extractUrlsFromParsedSitemap(parsed, origin) {
|
|
25
|
+
const rawPageUrls = [];
|
|
26
|
+
let subSitemapUrls = [];
|
|
27
|
+
if (parsed?.sitemapindex) {
|
|
28
|
+
subSitemapUrls = toArray(parsed.sitemapindex.sitemap).map((s) => s?.loc).filter((loc) => typeof loc === "string").slice(0, MAX_SUB_SITEMAPS);
|
|
29
|
+
}
|
|
30
|
+
if (parsed?.urlset) {
|
|
31
|
+
rawPageUrls.push(
|
|
32
|
+
...toArray(parsed.urlset.url).map((u) => u?.loc).filter((loc) => typeof loc === "string")
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const sameOrigin = rawPageUrls.filter((u) => sameSite(u, origin));
|
|
36
|
+
return { pageUrls: [...new Set(sameOrigin)].slice(0, MAX_URLS), subSitemapUrls };
|
|
61
37
|
}
|
|
62
38
|
async function fetchAndParseXml(url, dispatcher) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const res = await fetch(url, {
|
|
81
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
82
|
-
headers: { 'User-Agent': 'ZeTa-AI-Crawler/1.0 (+sitemap-discovery)' },
|
|
83
|
-
...(dispatcher ? { dispatcher } : {}),
|
|
84
|
-
});
|
|
85
|
-
if (!res.ok)
|
|
86
|
-
return null;
|
|
87
|
-
const text = await res.text();
|
|
88
|
-
return new XMLParser().parse(text);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
39
|
+
try {
|
|
40
|
+
assertCrawlUrlSafe(url);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(url, {
|
|
46
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
47
|
+
headers: { "User-Agent": "ZeTa-AI-Crawler/1.0 (+sitemap-discovery)" },
|
|
48
|
+
...dispatcher ? { dispatcher } : {}
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) return null;
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
return new XMLParser().parse(text);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
93
56
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const subResults = await Promise.all(subSitemapUrls.slice(0, 10).map(subUrl => fetchSitemapUrls(subUrl, origin, depth + 1, dispatcher)));
|
|
108
|
-
for (const sub of subResults)
|
|
109
|
-
collected.push(...sub);
|
|
110
|
-
}
|
|
111
|
-
return [...new Set(collected)].slice(0, MAX_URLS);
|
|
57
|
+
async function fetchSitemapUrls(sitemapUrl, origin, depth = 0, dispatcher) {
|
|
58
|
+
if (depth > MAX_SITEMAP_INDEX_DEPTH) return [];
|
|
59
|
+
const parsed = await fetchAndParseXml(sitemapUrl, dispatcher);
|
|
60
|
+
if (!parsed) return [];
|
|
61
|
+
const { pageUrls, subSitemapUrls } = extractUrlsFromParsedSitemap(parsed, origin);
|
|
62
|
+
const collected = [...pageUrls];
|
|
63
|
+
if (subSitemapUrls.length > 0 && collected.length < MAX_URLS) {
|
|
64
|
+
const subResults = await Promise.all(
|
|
65
|
+
subSitemapUrls.slice(0, 10).map((subUrl) => fetchSitemapUrls(subUrl, origin, depth + 1, dispatcher))
|
|
66
|
+
);
|
|
67
|
+
for (const sub of subResults) collected.push(...sub);
|
|
68
|
+
}
|
|
69
|
+
return [...new Set(collected)].slice(0, MAX_URLS);
|
|
112
70
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return await fetchSitemapUrls(target, origin);
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
logger.warn({ err: String(err), target }, '[sitemap] discovery failed, continuing without it');
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
71
|
+
async function discoverSitemapUrls(appUrl, sitemapUrl) {
|
|
72
|
+
let origin;
|
|
73
|
+
try {
|
|
74
|
+
origin = new URL(appUrl).origin;
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const target = sitemapUrl || `${origin}/sitemap.xml`;
|
|
79
|
+
try {
|
|
80
|
+
return await fetchSitemapUrls(target, origin);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.warn({ err: String(err), target }, "[sitemap] discovery failed, continuing without it");
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
133
85
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const token = 'Basic ' + Buffer.from(`${decodeURIComponent(user)}:${decodeURIComponent(pass ?? '')}`).toString('base64');
|
|
150
|
-
return new ProxyAgent({ uri, token });
|
|
151
|
-
}
|
|
152
|
-
return new ProxyAgent({ uri });
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
86
|
+
function buildProxyDispatcher(proxyUrl) {
|
|
87
|
+
if (!proxyUrl || proxyUrl === "auto") return void 0;
|
|
88
|
+
try {
|
|
89
|
+
const m = proxyUrl.match(/^(https?):\/\/(?:([^:@]*)(?::([^@]*))?@)?(.+)$/);
|
|
90
|
+
if (!m) return new ProxyAgent(proxyUrl);
|
|
91
|
+
const [, scheme, user, pass, hostPort] = m;
|
|
92
|
+
const uri = `${scheme}://${hostPort}`;
|
|
93
|
+
if (user) {
|
|
94
|
+
const token = "Basic " + Buffer.from(`${decodeURIComponent(user)}:${decodeURIComponent(pass ?? "")}`).toString("base64");
|
|
95
|
+
return new ProxyAgent({ uri, token });
|
|
96
|
+
}
|
|
97
|
+
return new ProxyAgent({ uri });
|
|
98
|
+
} catch {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
157
101
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
return [...new Set(out)];
|
|
102
|
+
function parseRobotsSitemaps(robotsTxt, origin) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const line of robotsTxt.split(/\r?\n/)) {
|
|
105
|
+
const m = line.match(/^\s*sitemap:\s*(\S+)/i);
|
|
106
|
+
if (m && sameSite(m[1], origin)) out.push(m[1]);
|
|
107
|
+
}
|
|
108
|
+
return [...new Set(out)];
|
|
167
109
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
])];
|
|
203
|
-
const seen = new Map();
|
|
204
|
-
let sitemapFound = false;
|
|
205
|
-
for (const sm of sitemapTargets) {
|
|
206
|
-
const urls = await fetchSitemapUrls(sm, origin, 0, dispatcher);
|
|
207
|
-
if (urls.length)
|
|
208
|
-
sitemapFound = true;
|
|
209
|
-
const source = robotsSet.has(sm) ? 'robots' : 'sitemap';
|
|
210
|
-
for (const u of urls)
|
|
211
|
-
if (!seen.has(u))
|
|
212
|
-
seen.set(u, { url: u, source });
|
|
213
|
-
}
|
|
214
|
-
return { candidates: [...seen.values()].slice(0, MAX_URLS), sitemapFound };
|
|
110
|
+
async function discoverProjectUrls(appUrl, opts) {
|
|
111
|
+
let origin;
|
|
112
|
+
try {
|
|
113
|
+
origin = new URL(appUrl).origin;
|
|
114
|
+
} catch {
|
|
115
|
+
return { candidates: [], sitemapFound: false };
|
|
116
|
+
}
|
|
117
|
+
const dispatcher = buildProxyDispatcher(opts?.proxyUrl);
|
|
118
|
+
let robotsSitemaps = [];
|
|
119
|
+
try {
|
|
120
|
+
assertCrawlUrlSafe(`${origin}/robots.txt`);
|
|
121
|
+
const res = await fetch(`${origin}/robots.txt`, {
|
|
122
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
123
|
+
headers: { "User-Agent": "ZeTa-AI-Crawler/1.0 (+url-discovery)" },
|
|
124
|
+
...dispatcher ? { dispatcher } : {}
|
|
125
|
+
});
|
|
126
|
+
if (res.ok) robotsSitemaps = parseRobotsSitemaps(await res.text(), origin);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
const robotsSet = new Set(robotsSitemaps);
|
|
130
|
+
const sitemapTargets = [.../* @__PURE__ */ new Set([
|
|
131
|
+
...opts?.sitemapUrl ? [opts.sitemapUrl] : [],
|
|
132
|
+
...robotsSitemaps,
|
|
133
|
+
`${origin}/sitemap.xml`
|
|
134
|
+
])];
|
|
135
|
+
const seen = /* @__PURE__ */ new Map();
|
|
136
|
+
let sitemapFound = false;
|
|
137
|
+
for (const sm of sitemapTargets) {
|
|
138
|
+
const urls = await fetchSitemapUrls(sm, origin, 0, dispatcher);
|
|
139
|
+
if (urls.length) sitemapFound = true;
|
|
140
|
+
const source = robotsSet.has(sm) ? "robots" : "sitemap";
|
|
141
|
+
for (const u of urls) if (!seen.has(u)) seen.set(u, { url: u, source });
|
|
142
|
+
}
|
|
143
|
+
return { candidates: [...seen.values()].slice(0, MAX_URLS), sitemapFound };
|
|
215
144
|
}
|
|
145
|
+
export {
|
|
146
|
+
buildProxyDispatcher,
|
|
147
|
+
discoverProjectUrls,
|
|
148
|
+
discoverSitemapUrls,
|
|
149
|
+
extractUrlsFromParsedSitemap,
|
|
150
|
+
fetchSitemapUrls,
|
|
151
|
+
parseRobotsSitemaps,
|
|
152
|
+
sameSite
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=sitemap.js.map
|