webcrawl 1.2.9 → 1.3.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.
@@ -17,7 +17,7 @@ export interface ScrapeResult {
17
17
  links?: string[];
18
18
  statusCode: number;
19
19
  contentHash: string;
20
- tier: "cache" | "static" | "sandbox" | "cloudflare" | "browser";
20
+ tier: "cache" | "static" | "sandbox" | "cloudflare" | "browser" | "impersonate";
21
21
  latencyMs: number;
22
22
  error?: string;
23
23
  }
@@ -37,8 +37,10 @@ export declare function setDomainUserAgent(hostname: string, userAgent: string):
37
37
  */
38
38
  export declare function isAwsWafChallenge(html: string, status?: number): boolean;
39
39
  export declare function isAntiBotChallenge(html: string, status: number): boolean;
40
+ /** Transport tier that won the last fetch ladder run (for result evidence). */
41
+ export declare function lastFetchTier(): "impit-tls" | "impersonate" | "fetch";
40
42
  /**
41
- * Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)
43
+ * Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)sonation)
42
44
  * so Cloudflare Bot Management and Vercel Security Checkpoint treat requests as real Chrome.
43
45
  * Falls back to standard `fetch` if `impit` is unavailable on the current platform.
44
46
  * Every redirect hop is checked against the SSRF guard.
@@ -109,6 +109,12 @@ export function isAntiBotChallenge(html, status) {
109
109
  return isAwsWafChallenge(html, status);
110
110
  }
111
111
  let sharedImpit = null;
112
+ /** Tier that won the most recent rawGet ladder ("impit-tls" → "impersonate" → "fetch"). */
113
+ let lastTier = "impit-tls";
114
+ /** Transport tier that won the last fetch ladder run (for result evidence). */
115
+ export function lastFetchTier() {
116
+ return lastTier;
117
+ }
112
118
  /** One request, redirects NOT followed (callers validate each hop via followSafely). */
113
119
  async function rawGet(url, reqHeaders, timeoutMs, maxBytes) {
114
120
  const read = async (status, headers, getBuf) => {
@@ -132,21 +138,92 @@ async function rawGet(url, reqHeaders, timeoutMs, maxBytes) {
132
138
  timeout: timeoutMs,
133
139
  redirect: "manual",
134
140
  });
135
- return await read(res.status, headersToObject(res.headers), () => res.arrayBuffer());
141
+ const hop = await read(res.status, headersToObject(res.headers), () => res.arrayBuffer());
142
+ // AWS-WAF-style blocks (403/429/challenge shells) on the impit tier
143
+ // (Chrome 136) retry once on the curl-impersonate tier (Chrome 150),
144
+ // which passes WAFs that block the older fingerprint.
145
+ if (isAntiBotChallenge(new TextDecoder().decode(hop.body).slice(0, 8000), hop.status)) {
146
+ const impersonateHop = await rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes);
147
+ if (impersonateHop) {
148
+ lastTier = "impersonate";
149
+ return impersonateHop;
150
+ }
151
+ }
152
+ lastTier = "impit-tls";
153
+ return hop;
136
154
  }
137
155
  catch (err) {
138
156
  if (err instanceof ResourceTooLargeError || err instanceof UnsafeUrlError)
139
157
  throw err;
158
+ // impit unavailable/failed: curl-impersonate tier, then plain fetch.
159
+ const impersonateHop = await rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes);
160
+ if (impersonateHop) {
161
+ lastTier = "impersonate";
162
+ return impersonateHop;
163
+ }
140
164
  const res = await fetch(url, {
141
165
  headers: reqHeaders,
142
166
  redirect: "manual",
143
167
  signal: AbortSignal.timeout(timeoutMs),
144
168
  });
169
+ lastTier = "fetch";
145
170
  return await read(res.status, headersToObject(res.headers), () => res.arrayBuffer());
146
171
  }
147
172
  }
148
173
  /**
149
- * Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)
174
+ * One GET hop with the curl-impersonate tier (Chrome 150 TLS/HTTP2 fingerprint).
175
+ * Used by `rawGet` when the impit hop is blocked (403/429/challenge) — newer
176
+ * fingerprints pass AWS WAF domains (stubhub, expedia) that block Chrome 136.
177
+ * Falls back to plain `fetch` when the binary is unavailable.
178
+ */
179
+ async function rawGetImpersonate(url, reqHeaders, timeoutMs, maxBytes) {
180
+ let impersonate;
181
+ try {
182
+ impersonate = await import("@webcrawl/curl-impersonate");
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ // The chrome150 profile's own identity headers MUST survive (profileHeaders
188
+ // merges extras last, so a stale UA would override the profile UA and the
189
+ // UA/TLS mismatch is exactly what DataDome/AWS WAF block). Keep cookies and
190
+ // referer, drop UA + sec-ch/client-hint identity headers.
191
+ const extra = {};
192
+ for (const [key, value] of Object.entries(reqHeaders)) {
193
+ const lower = key.toLowerCase();
194
+ if (lower === "user-agent" ||
195
+ lower.startsWith("sec-ch-ua") ||
196
+ lower.startsWith("sec-fetch-") ||
197
+ lower === "upgrade-insecure-requests" ||
198
+ lower === "priority") {
199
+ continue;
200
+ }
201
+ extra[key] = value;
202
+ }
203
+ try {
204
+ const res = await impersonate.impersonateFetch(url, {
205
+ headers: extra,
206
+ timeoutMs,
207
+ maxBytes,
208
+ });
209
+ const headers = res.headers;
210
+ const isRedirect = res.status >= 300 && res.status < 400 && !!headers["location"];
211
+ if (!isRedirect && maxBytes !== undefined)
212
+ checkDeclaredLength(headers, maxBytes);
213
+ return {
214
+ status: res.status,
215
+ headers,
216
+ body: isRedirect ? new ArrayBuffer(0) : res.body.buffer,
217
+ };
218
+ }
219
+ catch (err) {
220
+ if (err instanceof UnsafeUrlError)
221
+ throw err;
222
+ return null; // tier unavailable or fetch failed — caller falls back
223
+ }
224
+ }
225
+ /**
226
+ * Fetches a URL using `impit` (BoringSSL Chrome 136 TLS JA3/JA4 + HTTP/2 fingerprint impersonation)sonation)
150
227
  * so Cloudflare Bot Management and Vercel Security Checkpoint treat requests as real Chrome.
151
228
  * Falls back to standard `fetch` if `impit` is unavailable on the current platform.
152
229
  * Every redirect hop is checked against the SSRF guard.
@@ -10,7 +10,7 @@
10
10
  * powers both this local engine (`npx webcrawl`) and the hosted `webcrawl.app` API.
11
11
  */
12
12
  import { extractWebsiteData, fastHtmlToMarkdown, fetchWithTlsImpersonation, getCachedPageAsync, mapWebsiteUrls, setCachedPage, sha256, } from "./engine.js";
13
- import { fetchResource } from "./engine/fetcher.js";
13
+ import { fetchResource, lastFetchTier } from "./engine/fetcher.js";
14
14
  import { parseFile, publicParseMeta } from "./parse/index.js";
15
15
  import { isBinaryContentType, looksLikeBinaryUrl } from "./parse/fetch.js";
16
16
  import { maxDownloadBytes } from "./parse/limits.js";
@@ -174,7 +174,7 @@ export function createLocalEngineClient() {
174
174
  },
175
175
  evidence: {
176
176
  ...targetInspection.evidence,
177
- tier: "impit-tls",
177
+ tier: lastFetchTier() === "impersonate" ? "impersonate" : "impit-tls",
178
178
  contentHash,
179
179
  latencyMs: Date.now() - start,
180
180
  },
@@ -6,7 +6,7 @@
6
6
  * and returns structured results paired with verifiable Evidence & Provenance.
7
7
  */
8
8
  export type TargetCategory = "web" | "documents" | "media" | "commerce" | "travel" | "real-estate" | "local" | "security" | "developer" | "academic";
9
- export type TargetTier = "cache" | "static" | "impit-tls" | "cloudflare" | "stealth-browser" | "sandbox" | "native-parser" | "ocr" | "api";
9
+ export type TargetTier = "cache" | "static" | "impit-tls" | "impersonate" | "cloudflare" | "stealth-browser" | "sandbox" | "native-parser" | "ocr" | "api";
10
10
  export interface TargetEvidence {
11
11
  /** Which target plugin handled the inspection */
12
12
  targetId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcrawl",
3
- "version": "1.2.9",
3
+ "version": "1.3.0",
4
4
  "description": "Webcrawl Anything CLI & Core Engine — crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, or CVE",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -127,7 +127,8 @@
127
127
  "@ai-sdk/gateway": "^4.0.123",
128
128
  "@firecrawl/pdf-inspector": "^1.25.2",
129
129
  "@vercel/sandbox": "^1.0.0",
130
- "@webcrawl/client": "^1.2.9",
130
+ "@webcrawl/client": "^1.3.0",
131
+ "@webcrawl/curl-impersonate": "^1.3.0",
131
132
  "ai": "^7.0.150",
132
133
  "impit": "^0.14.7",
133
134
  "mammoth": "^1.13.0",