@detiq/crawler 1.1.8 → 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.
Files changed (82) hide show
  1. package/dist/accessibility-auditor.js +74 -54
  2. package/dist/ai-login.js +249 -227
  3. package/dist/auth-token-rotator.js +67 -57
  4. package/dist/auth.js +336 -299
  5. package/dist/browser-extension.js +142 -104
  6. package/dist/captcha-solver.js +125 -98
  7. package/dist/color-contrast.js +89 -77
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +71 -54
  10. package/dist/cookie-consent.js +83 -68
  11. package/dist/cookie-jar.js +60 -53
  12. package/dist/crawler.js +5345 -4486
  13. package/dist/custom-js-injector.js +61 -45
  14. package/dist/dark-mode-capture.js +81 -67
  15. package/dist/dead-link-detector.js +55 -47
  16. package/dist/element-hash.js +12 -8
  17. package/dist/email-otp.js +195 -154
  18. package/dist/engine-router.js +98 -87
  19. package/dist/export/bulk-export.js +55 -70
  20. package/dist/export/vector-export.js +105 -77
  21. package/dist/extractors/advanced.js +266 -249
  22. package/dist/extractors/artifact-envelope.js +58 -59
  23. package/dist/failure-analysis-agent.js +33 -37
  24. package/dist/feature-flag-detector.js +66 -51
  25. package/dist/form-detector.js +75 -55
  26. package/dist/frontier.js +101 -76
  27. package/dist/gap-pass.js +89 -68
  28. package/dist/graphql-detector.js +72 -62
  29. package/dist/http-engine.js +330 -282
  30. package/dist/incremental-diff.js +87 -54
  31. package/dist/index.js +28 -67
  32. package/dist/input-fuzzer.js +97 -93
  33. package/dist/interactive-probe.js +827 -715
  34. package/dist/job-queue.js +2169 -2151
  35. package/dist/js-coverage.js +56 -52
  36. package/dist/js-router-detector.js +117 -86
  37. package/dist/language-detector.js +63 -51
  38. package/dist/link-classifier.js +50 -32
  39. package/dist/live-view.js +108 -91
  40. package/dist/locator-extractor.js +187 -151
  41. package/dist/logger.js +7 -9
  42. package/dist/markdown-extract.js +61 -48
  43. package/dist/native-crawler.js +946 -780
  44. package/dist/network-observer.js +156 -135
  45. package/dist/network-throttle.js +40 -29
  46. package/dist/openapi-generator.js +92 -77
  47. package/dist/pdf-extractor.js +90 -72
  48. package/dist/performance-metrics.js +102 -80
  49. package/dist/phash.js +144 -125
  50. package/dist/pii-scrubber.js +68 -65
  51. package/dist/playwright-recorder.js +58 -70
  52. package/dist/policies/crawl-safety.js +34 -31
  53. package/dist/profiles/domain-profile.js +125 -130
  54. package/dist/proxy.js +172 -146
  55. package/dist/rate-limiter.js +65 -58
  56. package/dist/redirect-tracker.js +64 -60
  57. package/dist/redis-frontier.js +136 -116
  58. package/dist/redis-queue.js +144 -157
  59. package/dist/response-cache.js +140 -127
  60. package/dist/responsive-detector.js +46 -42
  61. package/dist/retry-policy.js +50 -46
  62. package/dist/robots-txt.js +83 -69
  63. package/dist/screen-reader.js +70 -45
  64. package/dist/screenshot-masker.js +77 -59
  65. package/dist/seo-checker.js +107 -88
  66. package/dist/session-capture.js +212 -192
  67. package/dist/session-export.js +133 -107
  68. package/dist/sitemap-writer.js +41 -38
  69. package/dist/sitemap.js +202 -141
  70. package/dist/skyvern.js +148 -123
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +90 -74
  73. package/dist/stealth/human-behavior.js +173 -156
  74. package/dist/telemetry.js +82 -68
  75. package/dist/test-executor.js +335 -309
  76. package/dist/tls-fingerprint.js +88 -77
  77. package/dist/url-pattern-completer.js +90 -71
  78. package/dist/visual-diff-verdict.js +38 -23
  79. package/dist/webhook-notifier.js +69 -71
  80. package/dist/websocket-detector.js +68 -56
  81. package/dist/worker.js +38 -29
  82. package/package.json +1 -1
package/dist/proxy.js CHANGED
@@ -1,165 +1,191 @@
1
- import { createConnection } from "net";
2
- const PROXY_LIST_URL = "https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/all/data.json";
1
+ /**
2
+ * Free proxy auto-selection for bot-protection bypass.
3
+ *
4
+ * Source: proxifly/free-proxy-list (MIT) via jsDelivr CDN.
5
+ * These are datacenter proxies — they bypass simple IP blocks but NOT
6
+ * full Cloudflare Bot Manager / Akamai. For Cloudflare-protected sites,
7
+ * users need a residential proxy (Bright Data, Oxylabs, etc.).
8
+ */
9
+ import { createConnection } from 'net';
10
+ const PROXY_LIST_URL = 'https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/all/data.json';
11
+ /** Fetch and filter proxy list. Returns http proxies sorted by speed (fastest first). */
3
12
  async function fetchCandidates() {
4
- const res = await fetch(PROXY_LIST_URL, {
5
- signal: AbortSignal.timeout(1e4),
6
- headers: { "Accept": "application/json" }
7
- });
8
- if (!res.ok) throw new Error(`proxy list fetch failed: ${res.status}`);
9
- const raw = await res.json();
10
- return raw.filter((p) => p.protocols?.includes("http") && p.port > 0 && (p.upTime ?? 0) >= 30).sort((a, b) => (a.speed ?? 99999) - (b.speed ?? 99999)).slice(0, 50);
13
+ const res = await fetch(PROXY_LIST_URL, {
14
+ signal: AbortSignal.timeout(10_000),
15
+ headers: { 'Accept': 'application/json' },
16
+ });
17
+ if (!res.ok)
18
+ throw new Error(`proxy list fetch failed: ${res.status}`);
19
+ const raw = await res.json();
20
+ return raw
21
+ .filter((p) => p.protocols?.includes('http') && p.port > 0 && (p.upTime ?? 0) >= 30)
22
+ .sort((a, b) => (a.speed ?? 99999) - (b.speed ?? 99999))
23
+ .slice(0, 50); // test at most 50 candidates
11
24
  }
25
+ /** TCP-level proxy liveness check via HTTP CONNECT tunnel. No extra dependencies. */
12
26
  function testProxy(ip, port, timeoutMs) {
13
- return new Promise((resolve) => {
14
- let settled = false;
15
- const settle = (ok) => {
16
- if (settled) return;
17
- settled = true;
18
- resolve(ok);
19
- try {
20
- sock.destroy();
21
- } catch {
22
- }
23
- };
24
- const timer = setTimeout(() => settle(false), timeoutMs);
25
- const sock = createConnection({ host: ip, port, timeout: timeoutMs });
26
- sock.on("connect", () => {
27
- sock.write("CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n");
28
- sock.once("data", (data) => {
29
- clearTimeout(timer);
30
- const ok = /^HTTP\/1\.[01] 200/.test(data.toString());
31
- settle(ok);
32
- });
33
- });
34
- sock.on("error", () => {
35
- clearTimeout(timer);
36
- settle(false);
27
+ return new Promise((resolve) => {
28
+ let settled = false;
29
+ const settle = (ok) => {
30
+ if (settled)
31
+ return;
32
+ settled = true;
33
+ resolve(ok);
34
+ try {
35
+ sock.destroy();
36
+ }
37
+ catch { /* ignore */ }
38
+ };
39
+ const timer = setTimeout(() => settle(false), timeoutMs);
40
+ const sock = createConnection({ host: ip, port, timeout: timeoutMs });
41
+ sock.on('connect', () => {
42
+ sock.write('CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n');
43
+ sock.once('data', (data) => {
44
+ clearTimeout(timer);
45
+ const ok = /^HTTP\/1\.[01] 200/.test(data.toString());
46
+ settle(ok);
47
+ });
48
+ });
49
+ sock.on('error', () => { clearTimeout(timer); settle(false); });
50
+ sock.on('timeout', () => { clearTimeout(timer); settle(false); });
37
51
  });
38
- sock.on("timeout", () => {
39
- clearTimeout(timer);
40
- settle(false);
41
- });
42
- });
43
52
  }
53
+ /** Test `batchSize` proxies concurrently and return first working one. */
44
54
  async function testBatch(candidates, start, batchSize, timeoutMs) {
45
- const batch = candidates.slice(start, start + batchSize);
46
- if (batch.length === 0) return null;
47
- const results = await Promise.all(
48
- batch.map(async (p) => {
49
- const ok = await testProxy(p.ip, p.port, timeoutMs);
50
- return ok ? `http://${p.ip}:${p.port}` : null;
51
- })
52
- );
53
- return results.find((r) => r !== null) ?? null;
55
+ const batch = candidates.slice(start, start + batchSize);
56
+ if (batch.length === 0)
57
+ return null;
58
+ const results = await Promise.all(batch.map(async (p) => {
59
+ const ok = await testProxy(p.ip, p.port, timeoutMs);
60
+ return ok ? `http://${p.ip}:${p.port}` : null;
61
+ }));
62
+ return results.find((r) => r !== null) ?? null;
54
63
  }
55
- async function pickFreeProxy() {
56
- let candidates;
57
- try {
58
- candidates = await fetchCandidates();
59
- } catch (err) {
60
- console.warn("[proxy] failed to fetch proxy list:", err?.message ?? err);
61
- return null;
62
- }
63
- const BATCH = 10;
64
- const TIMEOUT = 4e3;
65
- for (let i = 0; i < candidates.length; i += BATCH) {
66
- const found = await testBatch(candidates, i, BATCH, TIMEOUT);
67
- if (found) {
68
- console.info(`[proxy] selected free proxy: ${found} (tested ${i + BATCH} candidates)`);
69
- return found;
64
+ /**
65
+ * Pick a working free proxy. Tests in parallel batches of 10 with a 4s timeout.
66
+ * Returns null if no working proxy found (not an error — crawl can proceed without proxy).
67
+ */
68
+ export async function pickFreeProxy() {
69
+ let candidates;
70
+ try {
71
+ candidates = await fetchCandidates();
72
+ }
73
+ catch (err) {
74
+ console.warn('[proxy] failed to fetch proxy list:', err?.message ?? err);
75
+ return null;
76
+ }
77
+ const BATCH = 10;
78
+ const TIMEOUT = 4_000;
79
+ for (let i = 0; i < candidates.length; i += BATCH) {
80
+ const found = await testBatch(candidates, i, BATCH, TIMEOUT);
81
+ if (found) {
82
+ console.info(`[proxy] selected free proxy: ${found} (tested ${i + BATCH} candidates)`);
83
+ return found;
84
+ }
70
85
  }
71
- }
72
- console.warn("[proxy] no working free proxy found in list");
73
- return null;
86
+ console.warn('[proxy] no working free proxy found in list');
87
+ return null;
74
88
  }
75
- const TIER_ORDER = ["free", "datacenter", "residential", "premium"];
89
+ const TIER_ORDER = ['free', 'datacenter', 'residential', 'premium'];
76
90
  function nextTier(current) {
77
- const idx = TIER_ORDER.indexOf(current);
78
- return TIER_ORDER[Math.min(idx + 1, TIER_ORDER.length - 1)];
91
+ const idx = TIER_ORDER.indexOf(current);
92
+ return TIER_ORDER[Math.min(idx + 1, TIER_ORDER.length - 1)];
79
93
  }
80
- class TieredProxyManager {
81
- tiers = /* @__PURE__ */ new Map();
82
- domainHistory = /* @__PURE__ */ new Map();
83
- ESCALATE_AFTER = 3;
84
- // consecutive failures before escalation
85
- DECAY_AFTER_MS = 30 * 60 * 1e3;
86
- // 30 min: probe tier downgrade
87
- constructor(configs) {
88
- for (const cfg of configs) {
89
- if (cfg.urls.length > 0) this.tiers.set(cfg.tier, cfg.urls);
94
+ /**
95
+ * Tiered proxy manager with per-domain error histograms.
96
+ *
97
+ * Tracks success/failure per domain and automatically escalates to higher proxy tiers
98
+ * on consecutive failures. Applies time-decay to probe returning to lower tiers.
99
+ *
100
+ * Usage:
101
+ * const mgr = new TieredProxyManager([{ tier: 'datacenter', urls: [...] }]);
102
+ * const proxy = await mgr.selectProxy('api.salesforce.com');
103
+ * // ... make request ...
104
+ * mgr.recordResult('api.salesforce.com', success);
105
+ */
106
+ export class TieredProxyManager {
107
+ tiers = new Map();
108
+ domainHistory = new Map();
109
+ ESCALATE_AFTER = 3; // consecutive failures before escalation
110
+ DECAY_AFTER_MS = 30 * 60 * 1000; // 30 min: probe tier downgrade
111
+ constructor(configs) {
112
+ for (const cfg of configs) {
113
+ if (cfg.urls.length > 0)
114
+ this.tiers.set(cfg.tier, cfg.urls);
115
+ }
90
116
  }
91
- }
92
- getHistory(domain) {
93
- if (!this.domainHistory.has(domain)) {
94
- this.domainHistory.set(domain, {
95
- tier: this.lowestAvailableTier(),
96
- consecutiveFailures: 0,
97
- lastEscalatedAt: 0,
98
- totalRequests: 0,
99
- successRate: 1
100
- });
117
+ getHistory(domain) {
118
+ if (!this.domainHistory.has(domain)) {
119
+ this.domainHistory.set(domain, {
120
+ tier: this.lowestAvailableTier(),
121
+ consecutiveFailures: 0,
122
+ lastEscalatedAt: 0,
123
+ totalRequests: 0,
124
+ successRate: 1,
125
+ });
126
+ }
127
+ return this.domainHistory.get(domain);
101
128
  }
102
- return this.domainHistory.get(domain);
103
- }
104
- lowestAvailableTier() {
105
- for (const t of TIER_ORDER) {
106
- if (this.tiers.has(t)) return t;
129
+ lowestAvailableTier() {
130
+ for (const t of TIER_ORDER) {
131
+ if (this.tiers.has(t))
132
+ return t;
133
+ }
134
+ return 'free';
107
135
  }
108
- return "free";
109
- }
110
- /** Pick a proxy URL for this domain, based on its tier history. */
111
- selectProxy(domain) {
112
- const history = this.getHistory(domain);
113
- if (history.consecutiveFailures === 0 && history.lastEscalatedAt > 0) {
114
- const elapsed = Date.now() - history.lastEscalatedAt;
115
- if (elapsed > this.DECAY_AFTER_MS) {
116
- const currentIdx = TIER_ORDER.indexOf(history.tier);
117
- if (currentIdx > 0) {
118
- const lowerTier = TIER_ORDER[currentIdx - 1];
119
- if (this.tiers.has(lowerTier)) {
120
- history.tier = lowerTier;
121
- history.lastEscalatedAt = 0;
122
- }
136
+ /** Pick a proxy URL for this domain, based on its tier history. */
137
+ selectProxy(domain) {
138
+ const history = this.getHistory(domain);
139
+ // Time-decay: if we've been on elevated tier for DECAY_AFTER_MS, try stepping down
140
+ if (history.consecutiveFailures === 0 && history.lastEscalatedAt > 0) {
141
+ const elapsed = Date.now() - history.lastEscalatedAt;
142
+ if (elapsed > this.DECAY_AFTER_MS) {
143
+ const currentIdx = TIER_ORDER.indexOf(history.tier);
144
+ if (currentIdx > 0) {
145
+ const lowerTier = TIER_ORDER[currentIdx - 1];
146
+ if (this.tiers.has(lowerTier)) {
147
+ history.tier = lowerTier;
148
+ history.lastEscalatedAt = 0;
149
+ }
150
+ }
151
+ }
123
152
  }
124
- }
153
+ const urls = this.tiers.get(history.tier);
154
+ if (!urls || urls.length === 0)
155
+ return null;
156
+ // Round-robin within tier
157
+ const idx = history.totalRequests % urls.length;
158
+ return urls[idx];
125
159
  }
126
- const urls = this.tiers.get(history.tier);
127
- if (!urls || urls.length === 0) return null;
128
- const idx = history.totalRequests % urls.length;
129
- return urls[idx];
130
- }
131
- /** Record a request result for a domain. Triggers tier escalation on repeated failures. */
132
- recordResult(domain, success) {
133
- const history = this.getHistory(domain);
134
- history.totalRequests++;
135
- if (success) {
136
- history.consecutiveFailures = 0;
137
- history.successRate = history.successRate * 0.9 + 0.1;
138
- } else {
139
- history.consecutiveFailures++;
140
- history.successRate = history.successRate * 0.9;
141
- if (history.consecutiveFailures >= this.ESCALATE_AFTER) {
142
- const elevated = nextTier(history.tier);
143
- if (elevated !== history.tier && this.tiers.has(elevated)) {
144
- console.warn(`[proxy-tier] domain=${domain} escalating ${history.tier}\u2192${elevated} after ${history.consecutiveFailures} failures`);
145
- history.tier = elevated;
146
- history.lastEscalatedAt = Date.now();
147
- history.consecutiveFailures = 0;
160
+ /** Record a request result for a domain. Triggers tier escalation on repeated failures. */
161
+ recordResult(domain, success) {
162
+ const history = this.getHistory(domain);
163
+ history.totalRequests++;
164
+ if (success) {
165
+ history.consecutiveFailures = 0;
166
+ // Update EMA success rate
167
+ history.successRate = history.successRate * 0.9 + 0.1;
168
+ }
169
+ else {
170
+ history.consecutiveFailures++;
171
+ history.successRate = history.successRate * 0.9;
172
+ if (history.consecutiveFailures >= this.ESCALATE_AFTER) {
173
+ const elevated = nextTier(history.tier);
174
+ if (elevated !== history.tier && this.tiers.has(elevated)) {
175
+ console.warn(`[proxy-tier] domain=${domain} escalating ${history.tier}→${elevated} after ${history.consecutiveFailures} failures`);
176
+ history.tier = elevated;
177
+ history.lastEscalatedAt = Date.now();
178
+ history.consecutiveFailures = 0;
179
+ }
180
+ }
148
181
  }
149
- }
150
182
  }
151
- }
152
- stats() {
153
- return Array.from(this.domainHistory.entries()).map(([domain, h]) => ({
154
- domain,
155
- tier: h.tier,
156
- successRate: Math.round(h.successRate * 100) / 100,
157
- totalRequests: h.totalRequests
158
- }));
159
- }
183
+ stats() {
184
+ return Array.from(this.domainHistory.entries()).map(([domain, h]) => ({
185
+ domain,
186
+ tier: h.tier,
187
+ successRate: Math.round(h.successRate * 100) / 100,
188
+ totalRequests: h.totalRequests,
189
+ }));
190
+ }
160
191
  }
161
- export {
162
- TieredProxyManager,
163
- pickFreeProxy
164
- };
165
- //# sourceMappingURL=proxy.js.map
@@ -1,64 +1,71 @@
1
+ /**
2
+ * Domain-aware rate limiter — ensures the crawler respects per-domain rate limits.
3
+ *
4
+ * Uses a token bucket algorithm per domain. When the bucket is empty, requests
5
+ * wait until a token is available. Tokens refill at a configurable rate.
6
+ *
7
+ * Default: 2 requests/second per domain, burst of 5.
8
+ * Can be configured per-domain via the rateMap (domain → requests/second).
9
+ */
1
10
  function extractDomain(url) {
2
- try {
3
- return new URL(url).hostname;
4
- } catch {
5
- return url;
6
- }
11
+ try {
12
+ return new URL(url).hostname;
13
+ }
14
+ catch {
15
+ return url;
16
+ }
7
17
  }
8
- class DomainRateLimiter {
9
- buckets = /* @__PURE__ */ new Map();
10
- defaultRps;
11
- burstSize;
12
- rateMap;
13
- constructor(opts = {}) {
14
- this.defaultRps = opts.defaultRps ?? 2;
15
- this.burstSize = opts.burstSize ?? 5;
16
- this.rateMap = opts.rateMap ?? {};
17
- }
18
- getBucket(domain) {
19
- if (!this.buckets.has(domain)) {
20
- this.buckets.set(domain, { tokens: this.burstSize, lastRefill: Date.now() });
18
+ export class DomainRateLimiter {
19
+ buckets = new Map();
20
+ defaultRps;
21
+ burstSize;
22
+ rateMap;
23
+ constructor(opts = {}) {
24
+ this.defaultRps = opts.defaultRps ?? 2;
25
+ this.burstSize = opts.burstSize ?? 5;
26
+ this.rateMap = opts.rateMap ?? {};
27
+ }
28
+ getBucket(domain) {
29
+ if (!this.buckets.has(domain)) {
30
+ this.buckets.set(domain, { tokens: this.burstSize, lastRefill: Date.now() });
31
+ }
32
+ return this.buckets.get(domain);
33
+ }
34
+ refill(domain, bucket) {
35
+ const rps = this.rateMap[domain] ?? this.defaultRps;
36
+ const now = Date.now();
37
+ const elapsed = (now - bucket.lastRefill) / 1000;
38
+ bucket.tokens = Math.min(this.burstSize, bucket.tokens + elapsed * rps);
39
+ bucket.lastRefill = now;
40
+ }
41
+ /** Wait until a token is available for the given URL's domain. */
42
+ async throttle(url) {
43
+ const domain = extractDomain(url);
44
+ const bucket = this.getBucket(domain);
45
+ this.refill(domain, bucket);
46
+ if (bucket.tokens >= 1) {
47
+ bucket.tokens -= 1;
48
+ return;
49
+ }
50
+ // Calculate wait time until next token
51
+ const rps = this.rateMap[domain] ?? this.defaultRps;
52
+ const waitMs = Math.ceil((1 - bucket.tokens) / rps * 1000);
53
+ await new Promise(r => setTimeout(r, waitMs));
54
+ bucket.tokens = 0;
55
+ bucket.lastRefill = Date.now();
21
56
  }
22
- return this.buckets.get(domain);
23
- }
24
- refill(domain, bucket) {
25
- const rps = this.rateMap[domain] ?? this.defaultRps;
26
- const now = Date.now();
27
- const elapsed = (now - bucket.lastRefill) / 1e3;
28
- bucket.tokens = Math.min(this.burstSize, bucket.tokens + elapsed * rps);
29
- bucket.lastRefill = now;
30
- }
31
- /** Wait until a token is available for the given URL's domain. */
32
- async throttle(url) {
33
- const domain = extractDomain(url);
34
- const bucket = this.getBucket(domain);
35
- this.refill(domain, bucket);
36
- if (bucket.tokens >= 1) {
37
- bucket.tokens -= 1;
38
- return;
57
+ /** Override rate for a specific domain at runtime (e.g. after seeing 429). */
58
+ setRate(domain, rps) {
59
+ this.rateMap[domain] = rps;
60
+ const bucket = this.getBucket(domain);
61
+ bucket.tokens = 0; // drain immediately on rate change
39
62
  }
40
- const rps = this.rateMap[domain] ?? this.defaultRps;
41
- const waitMs = Math.ceil((1 - bucket.tokens) / rps * 1e3);
42
- await new Promise((r) => setTimeout(r, waitMs));
43
- bucket.tokens = 0;
44
- bucket.lastRefill = Date.now();
45
- }
46
- /** Override rate for a specific domain at runtime (e.g. after seeing 429). */
47
- setRate(domain, rps) {
48
- this.rateMap[domain] = rps;
49
- const bucket = this.getBucket(domain);
50
- bucket.tokens = 0;
51
- }
52
- stats() {
53
- const result = {};
54
- for (const [domain, bucket] of this.buckets) {
55
- this.refill(domain, bucket);
56
- result[domain] = { tokens: Math.round(bucket.tokens * 100) / 100, rps: this.rateMap[domain] ?? this.defaultRps };
63
+ stats() {
64
+ const result = {};
65
+ for (const [domain, bucket] of this.buckets) {
66
+ this.refill(domain, bucket);
67
+ result[domain] = { tokens: Math.round(bucket.tokens * 100) / 100, rps: this.rateMap[domain] ?? this.defaultRps };
68
+ }
69
+ return result;
57
70
  }
58
- return result;
59
- }
60
71
  }
61
- export {
62
- DomainRateLimiter
63
- };
64
- //# sourceMappingURL=rate-limiter.js.map
@@ -1,65 +1,69 @@
1
- async function trackRedirects(page, url) {
2
- const hops = [];
3
- const seen = /* @__PURE__ */ new Set();
4
- let lastResponseTime = Date.now();
5
- const onResponse = (response) => {
6
- const status = response.status();
7
- if (status >= 300 && status < 400) {
8
- const from = response.url();
9
- const to = response.headers()["location"] ?? "";
10
- const now = Date.now();
11
- hops.push({ from, to, statusCode: status, durationMs: now - lastResponseTime });
12
- lastResponseTime = now;
1
+ /**
2
+ * Redirect chain tracker — records the full redirect chain for crawled URLs.
3
+ *
4
+ * Why: redirect chains (A→B→C) slow down pages; circular redirects crash crawlers;
5
+ * redirect loops indicate config errors. ZeTa records chains for SEO/perf audit.
6
+ *
7
+ * Integration: hook into Playwright's page.on('response') to see each redirect hop.
8
+ */
9
+ export async function trackRedirects(page, url) {
10
+ const hops = [];
11
+ const seen = new Set();
12
+ let lastResponseTime = Date.now();
13
+ const onResponse = (response) => {
14
+ const status = response.status();
15
+ if (status >= 300 && status < 400) {
16
+ const from = response.url();
17
+ const to = response.headers()['location'] ?? '';
18
+ const now = Date.now();
19
+ hops.push({ from, to, statusCode: status, durationMs: now - lastResponseTime });
20
+ lastResponseTime = now;
21
+ }
22
+ };
23
+ page.on('response', onResponse);
24
+ try {
25
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => { });
26
+ }
27
+ finally {
28
+ page.off('response', onResponse);
13
29
  }
14
- };
15
- page.on("response", onResponse);
16
- try {
17
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
30
+ const finalUrl = page.url();
31
+ const isCircular = hops.some(h => {
32
+ if (seen.has(h.to))
33
+ return true;
34
+ seen.add(h.from);
35
+ return false;
18
36
  });
19
- } finally {
20
- page.off("response", onResponse);
21
- }
22
- const finalUrl = page.url();
23
- const isCircular = hops.some((h) => {
24
- if (seen.has(h.to)) return true;
25
- seen.add(h.from);
26
- return false;
27
- });
28
- return {
29
- originalUrl: url,
30
- finalUrl,
31
- hops,
32
- isCircular,
33
- hopCount: hops.length,
34
- totalDurationMs: hops.reduce((sum, h) => sum + h.durationMs, 0)
35
- };
36
- }
37
- class RedirectRegistry {
38
- chains = /* @__PURE__ */ new Map();
39
- register(chain) {
40
- this.chains.set(chain.originalUrl, chain);
41
- }
42
- get(url) {
43
- return this.chains.get(url);
44
- }
45
- getAll() {
46
- return [...this.chains.values()];
47
- }
48
- getProblematic() {
49
- return [...this.chains.values()].filter((c) => c.isCircular || c.hopCount > 3);
50
- }
51
- stats() {
52
- const all = [...this.chains.values()];
53
37
  return {
54
- total: all.length,
55
- withRedirects: all.filter((c) => c.hopCount > 0).length,
56
- circular: all.filter((c) => c.isCircular).length,
57
- deepChains: all.filter((c) => c.hopCount > 3).length
38
+ originalUrl: url,
39
+ finalUrl,
40
+ hops,
41
+ isCircular,
42
+ hopCount: hops.length,
43
+ totalDurationMs: hops.reduce((sum, h) => sum + h.durationMs, 0),
58
44
  };
59
- }
60
45
  }
61
- export {
62
- RedirectRegistry,
63
- trackRedirects
64
- };
65
- //# sourceMappingURL=redirect-tracker.js.map
46
+ export class RedirectRegistry {
47
+ chains = new Map();
48
+ register(chain) {
49
+ this.chains.set(chain.originalUrl, chain);
50
+ }
51
+ get(url) {
52
+ return this.chains.get(url);
53
+ }
54
+ getAll() {
55
+ return [...this.chains.values()];
56
+ }
57
+ getProblematic() {
58
+ return [...this.chains.values()].filter(c => c.isCircular || c.hopCount > 3);
59
+ }
60
+ stats() {
61
+ const all = [...this.chains.values()];
62
+ return {
63
+ total: all.length,
64
+ withRedirects: all.filter(c => c.hopCount > 0).length,
65
+ circular: all.filter(c => c.isCircular).length,
66
+ deepChains: all.filter(c => c.hopCount > 3).length,
67
+ };
68
+ }
69
+ }