@detiq/crawler 1.1.7 → 1.1.9
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 +74 -54
- package/dist/ai-login.js +249 -227
- package/dist/auth-token-rotator.js +67 -57
- package/dist/auth.js +336 -299
- package/dist/browser-extension.js +142 -104
- package/dist/captcha-solver.js +125 -98
- package/dist/color-contrast.js +89 -77
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +71 -54
- package/dist/cookie-consent.js +83 -68
- package/dist/cookie-jar.js +60 -53
- package/dist/crawler.js +5345 -4486
- package/dist/custom-js-injector.js +61 -45
- package/dist/dark-mode-capture.js +81 -67
- package/dist/dead-link-detector.js +55 -47
- package/dist/element-hash.js +12 -8
- package/dist/email-otp.js +195 -154
- package/dist/engine-router.js +98 -87
- package/dist/export/bulk-export.js +55 -70
- package/dist/export/vector-export.js +105 -77
- package/dist/extractors/advanced.d.ts +10 -0
- package/dist/extractors/advanced.js +266 -249
- package/dist/extractors/artifact-envelope.js +58 -59
- package/dist/failure-analysis-agent.js +33 -37
- package/dist/feature-flag-detector.js +66 -51
- package/dist/form-detector.js +75 -55
- package/dist/frontier.js +101 -76
- package/dist/gap-pass.js +89 -68
- package/dist/graphql-detector.js +72 -62
- package/dist/http-engine.js +330 -282
- package/dist/incremental-diff.js +87 -54
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -63
- package/dist/index.js.map +2 -2
- package/dist/input-fuzzer.js +97 -93
- package/dist/interactive-probe.d.ts +36 -0
- package/dist/interactive-probe.js +827 -715
- package/dist/job-queue.js +2169 -2151
- package/dist/js-coverage.js +56 -52
- package/dist/js-router-detector.js +117 -86
- package/dist/language-detector.js +63 -51
- package/dist/link-classifier.js +50 -32
- package/dist/live-view.js +108 -91
- package/dist/locator-extractor.d.ts +37 -0
- package/dist/locator-extractor.js +199 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.js +7 -9
- package/dist/markdown-extract.js +61 -48
- package/dist/native-crawler.js +946 -780
- package/dist/network-observer.js +156 -135
- package/dist/network-throttle.js +40 -29
- package/dist/openapi-generator.js +92 -77
- package/dist/pdf-extractor.js +90 -72
- package/dist/performance-metrics.js +102 -80
- package/dist/phash.js +144 -125
- package/dist/pii-scrubber.js +68 -65
- package/dist/playwright-recorder.js +58 -70
- package/dist/policies/crawl-safety.js +34 -31
- package/dist/profiles/domain-profile.js +125 -130
- package/dist/proxy.js +172 -146
- package/dist/rate-limiter.js +65 -58
- package/dist/redirect-tracker.js +64 -60
- package/dist/redis-frontier.js +136 -116
- package/dist/redis-queue.js +144 -157
- package/dist/response-cache.js +140 -127
- package/dist/responsive-detector.js +46 -42
- package/dist/retry-policy.js +50 -46
- package/dist/robots-txt.js +83 -69
- package/dist/screen-reader.js +70 -45
- package/dist/screenshot-masker.js +77 -59
- package/dist/seo-checker.js +107 -88
- package/dist/session-capture.js +212 -192
- package/dist/session-export.js +133 -107
- package/dist/sitemap-writer.js +41 -38
- package/dist/sitemap.js +202 -141
- package/dist/skyvern.js +148 -123
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +90 -74
- package/dist/stealth/human-behavior.js +173 -156
- package/dist/telemetry.js +82 -68
- package/dist/test-executor.js +335 -309
- package/dist/tls-fingerprint.js +88 -77
- package/dist/url-pattern-completer.js +90 -71
- package/dist/visual-diff-verdict.js +38 -23
- package/dist/webhook-notifier.js +69 -71
- package/dist/websocket-detector.js +68 -56
- package/dist/worker.js +38 -29
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/locator-extractor.ts +226 -0
- package/types/index.d.ts +2 -0
- package/types/locator-extractor.d.ts +37 -0
package/dist/sitemap.js
CHANGED
|
@@ -1,154 +1,215 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap.xml discovery — seeds the crawl queue from a project's sitemap
|
|
3
|
+
* instead of (or alongside) plain link-following, matching crawl4AI's
|
|
4
|
+
* sitemap-seeding capability. Best-effort throughout: most sites don't have
|
|
5
|
+
* a sitemap, or it 404s, or it's malformed — none of that should ever fail
|
|
6
|
+
* a crawl, it just means sitemap seeding contributes nothing this run.
|
|
7
|
+
*/
|
|
8
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
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
|
|
9
16
|
function toArray(x) {
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
if (x == null)
|
|
18
|
+
return [];
|
|
19
|
+
return Array.isArray(x) ? x : [x];
|
|
12
20
|
}
|
|
21
|
+
/** Registrable-host key: lowercased hostname with a leading `www.` stripped. */
|
|
13
22
|
function hostKey(hostname) {
|
|
14
|
-
|
|
23
|
+
return hostname.replace(/^www\./i, '').toLowerCase();
|
|
15
24
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Same-site check that tolerates www/non-www and http/https differences — a sitemap on
|
|
27
|
+
* `vercel.com` is valid for a project pointed at `www.vercel.com` (and vice versa).
|
|
28
|
+
*/
|
|
29
|
+
export function sameSite(candidateUrl, siteOrigin) {
|
|
30
|
+
try {
|
|
31
|
+
const originHost = /^https?:\/\//i.test(siteOrigin) ? new URL(siteOrigin).hostname : siteOrigin;
|
|
32
|
+
return hostKey(new URL(candidateUrl).hostname) === hostKey(originHost);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
23
37
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
rawPageUrls
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Pulls page URLs and sub-sitemap URLs out of an already-XML-parsed sitemap
|
|
40
|
+
* document (either a <urlset> or a <sitemapindex>), filtered to same-origin
|
|
41
|
+
* and deduped. Pure function — no I/O — for direct unit testing without a
|
|
42
|
+
* real HTTP fetch.
|
|
43
|
+
*/
|
|
44
|
+
export function extractUrlsFromParsedSitemap(parsed, origin) {
|
|
45
|
+
const rawPageUrls = [];
|
|
46
|
+
let subSitemapUrls = [];
|
|
47
|
+
if (parsed?.sitemapindex) {
|
|
48
|
+
subSitemapUrls = toArray(parsed.sitemapindex.sitemap)
|
|
49
|
+
.map((s) => s?.loc)
|
|
50
|
+
.filter((loc) => typeof loc === 'string')
|
|
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 };
|
|
37
61
|
}
|
|
38
62
|
async function fetchAndParseXml(url, dispatcher) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
// Same SSRF posture as the crawl itself (assertCrawlUrlSafe, not the
|
|
64
|
+
// stricter webhook-oriented assertResolvesToPublicAddress) — a sitemap
|
|
65
|
+
// living on a tenant's internal/VPN-only staging server is a legitimate
|
|
66
|
+
// crawl target, not an attack; only loopback/cloud-metadata are blocked.
|
|
67
|
+
try {
|
|
68
|
+
assertCrawlUrlSafe(url);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
// Redirects followed normally here (unlike the webhook-dispatch.ts
|
|
75
|
+
// 'manual' pattern) — an http→https or bare→www redirect on the
|
|
76
|
+
// sitemap is completely ordinary, and Playwright's actual page
|
|
77
|
+
// navigation later in the crawl already follows redirects to whatever
|
|
78
|
+
// this same site sends it to; a sitemap fetch isn't a more sensitive
|
|
79
|
+
// operation than that.
|
|
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
|
+
}
|
|
56
93
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Fetches and parses a sitemap (or sitemap index, recursed up to a small
|
|
96
|
+
* depth) into a flat, deduped list of same-origin URLs.
|
|
97
|
+
*/
|
|
98
|
+
export async function fetchSitemapUrls(sitemapUrl, origin, depth = 0, dispatcher) {
|
|
99
|
+
if (depth > MAX_SITEMAP_INDEX_DEPTH)
|
|
100
|
+
return [];
|
|
101
|
+
const parsed = await fetchAndParseXml(sitemapUrl, dispatcher);
|
|
102
|
+
if (!parsed)
|
|
103
|
+
return [];
|
|
104
|
+
const { pageUrls, subSitemapUrls } = extractUrlsFromParsedSitemap(parsed, origin);
|
|
105
|
+
const collected = [...pageUrls];
|
|
106
|
+
if (subSitemapUrls.length > 0 && collected.length < MAX_URLS) {
|
|
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);
|
|
70
112
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Discovers a project's sitemap (default `${origin}/sitemap.xml`, or an
|
|
115
|
+
* explicit override) and returns the URLs it lists. Never throws.
|
|
116
|
+
*/
|
|
117
|
+
export async function discoverSitemapUrls(appUrl, sitemapUrl) {
|
|
118
|
+
let origin;
|
|
119
|
+
try {
|
|
120
|
+
origin = new URL(appUrl).origin;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
const target = sitemapUrl || `${origin}/sitemap.xml`;
|
|
126
|
+
try {
|
|
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
|
+
}
|
|
85
133
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Build an undici ProxyAgent from a proxy URL. Parses credentials manually because
|
|
136
|
+
* DataImpulse-style usernames contain commas (country codes) that break new URL().
|
|
137
|
+
* Returns undefined for no/auto proxy — the caller then fetches directly.
|
|
138
|
+
*/
|
|
139
|
+
export function buildProxyDispatcher(proxyUrl) {
|
|
140
|
+
if (!proxyUrl || proxyUrl === 'auto')
|
|
141
|
+
return undefined;
|
|
142
|
+
try {
|
|
143
|
+
const m = proxyUrl.match(/^(https?):\/\/(?:([^:@]*)(?::([^@]*))?@)?(.+)$/);
|
|
144
|
+
if (!m)
|
|
145
|
+
return new ProxyAgent(proxyUrl);
|
|
146
|
+
const [, scheme, user, pass, hostPort] = m;
|
|
147
|
+
const uri = `${scheme}://${hostPort}`;
|
|
148
|
+
if (user) {
|
|
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
|
+
}
|
|
101
157
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
/** Extract same-origin `Sitemap:` directives from a robots.txt body. */
|
|
159
|
+
export function parseRobotsSitemaps(robotsTxt, origin) {
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const line of robotsTxt.split(/\r?\n/)) {
|
|
162
|
+
const m = line.match(/^\s*sitemap:\s*(\S+)/i);
|
|
163
|
+
if (m && sameSite(m[1], origin))
|
|
164
|
+
out.push(m[1]);
|
|
165
|
+
}
|
|
166
|
+
return [...new Set(out)];
|
|
109
167
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Pre-crawl URL discovery for the review-before-crawl flow: reads robots.txt Sitemap
|
|
170
|
+
* directives and sitemap.xml (recursed, same-origin), proxy-aware. Returns a deduped
|
|
171
|
+
* candidate list the user can review and seed before launching the Playwright crawl.
|
|
172
|
+
* Never throws — bot-protected sites (Akamai/Cloudflare) simply yield an empty list.
|
|
173
|
+
*/
|
|
174
|
+
export async function discoverProjectUrls(appUrl, opts) {
|
|
175
|
+
let origin;
|
|
176
|
+
try {
|
|
177
|
+
origin = new URL(appUrl).origin;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return { candidates: [], sitemapFound: false };
|
|
181
|
+
}
|
|
182
|
+
const dispatcher = buildProxyDispatcher(opts?.proxyUrl);
|
|
183
|
+
// 1. robots.txt → declared sitemaps
|
|
184
|
+
let robotsSitemaps = [];
|
|
185
|
+
try {
|
|
186
|
+
assertCrawlUrlSafe(`${origin}/robots.txt`);
|
|
187
|
+
const res = await fetch(`${origin}/robots.txt`, {
|
|
188
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
189
|
+
headers: { 'User-Agent': 'ZeTa-AI-Crawler/1.0 (+url-discovery)' },
|
|
190
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
191
|
+
});
|
|
192
|
+
if (res.ok)
|
|
193
|
+
robotsSitemaps = parseRobotsSitemaps(await res.text(), origin);
|
|
194
|
+
}
|
|
195
|
+
catch { /* best-effort */ }
|
|
196
|
+
// 2. sitemap targets: explicit override + robots-declared + conventional default
|
|
197
|
+
const robotsSet = new Set(robotsSitemaps);
|
|
198
|
+
const sitemapTargets = [...new Set([
|
|
199
|
+
...(opts?.sitemapUrl ? [opts.sitemapUrl] : []),
|
|
200
|
+
...robotsSitemaps,
|
|
201
|
+
`${origin}/sitemap.xml`,
|
|
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 };
|
|
144
215
|
}
|
|
145
|
-
export {
|
|
146
|
-
buildProxyDispatcher,
|
|
147
|
-
discoverProjectUrls,
|
|
148
|
-
discoverSitemapUrls,
|
|
149
|
-
extractUrlsFromParsedSitemap,
|
|
150
|
-
fetchSitemapUrls,
|
|
151
|
-
parseRobotsSitemaps,
|
|
152
|
-
sameSite
|
|
153
|
-
};
|
|
154
|
-
//# sourceMappingURL=sitemap.js.map
|