webcrawl 1.3.3 → 1.3.4

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.
@@ -465,5 +465,11 @@ export function needsBrowserEscalation(status, html, markdown) {
465
465
  lower.includes('id="__next"'))) {
466
466
  return true;
467
467
  }
468
+ // Big-HTML/no-content SPA shell: a very large page (JS bundles / inline
469
+ // state, e.g. StubHub category pages ~3MB) that yields almost no
470
+ // extractable text is serving its real content client-side.
471
+ if (html.length > 250_000 && markdown.length < 500) {
472
+ return true;
473
+ }
468
474
  return false;
469
475
  }
@@ -2,4 +2,5 @@ export * from "./cache.js";
2
2
  export * from "./markdown.js";
3
3
  export * from "./session.js";
4
4
  export * from "./fetcher.js";
5
+ export * from "./render.js";
5
6
  export * from "./crawler.js";
@@ -2,4 +2,5 @@ export * from "./cache.js";
2
2
  export * from "./markdown.js";
3
3
  export * from "./session.js";
4
4
  export * from "./fetcher.js";
5
+ export * from "./render.js";
5
6
  export * from "./crawler.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Generic render-provider tier (Tier R): a server-side JS-rendering provider
3
+ * for pages the native fetch ladder cannot render (JS SPAs, challenge shells).
4
+ *
5
+ * Same pattern as the residential-proxy tier: the provider URL comes from
6
+ * `WEBCRAWL_RENDER_URL` and is unset in the open-source repo — `null` disables
7
+ * the tier entirely, so the OSS build never requires a provider. The hosted
8
+ * webcrawl.app server sets the env (and the optional key), so cloud-mode calls
9
+ * render JS pages through the provider while local zero-key mode degrades to
10
+ * the `webcrawl login` prompt.
11
+ *
12
+ * The wire format parsed here is the de-facto standard "reader" text format
13
+ * (`Title:` / `URL Source:` / `Markdown Content:` headers), so any compatible
14
+ * provider works without code changes.
15
+ */
16
+ /** Render-provider endpoint base URL, or null when the tier is disabled. */
17
+ export declare function renderProviderUrl(): string | null;
18
+ /** Optional bearer key for the render provider (hosted tier). */
19
+ export declare function renderProviderKey(): string | null;
20
+ /** Whether the render-provider tier is configured and usable at all. */
21
+ export declare function isRenderProviderEnabled(): boolean;
22
+ export interface RenderedPage {
23
+ title: string;
24
+ markdown: string;
25
+ sourceUrl: string;
26
+ warnings: string[];
27
+ }
28
+ /** Splits a reader-format response body into its header fields and markdown. */
29
+ export declare function parseRenderedResponse(body: string): {
30
+ title: string;
31
+ sourceUrl: string;
32
+ markdown: string;
33
+ };
34
+ /** Test helper: clears the in-memory rate-limit window (external env untouched). */
35
+ export declare function resetRenderStateForTests(): void;
36
+ /**
37
+ * Renders `url` through the configured render provider. Returns null when the
38
+ * tier is disabled, rate-limited, times out, or the provider itself reports a
39
+ * failed render — the caller then keeps its native result.
40
+ */
41
+ export declare function renderViaProvider(url: string): Promise<RenderedPage | null>;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Generic render-provider tier (Tier R): a server-side JS-rendering provider
3
+ * for pages the native fetch ladder cannot render (JS SPAs, challenge shells).
4
+ *
5
+ * Same pattern as the residential-proxy tier: the provider URL comes from
6
+ * `WEBCRAWL_RENDER_URL` and is unset in the open-source repo — `null` disables
7
+ * the tier entirely, so the OSS build never requires a provider. The hosted
8
+ * webcrawl.app server sets the env (and the optional key), so cloud-mode calls
9
+ * render JS pages through the provider while local zero-key mode degrades to
10
+ * the `webcrawl login` prompt.
11
+ *
12
+ * The wire format parsed here is the de-facto standard "reader" text format
13
+ * (`Title:` / `URL Source:` / `Markdown Content:` headers), so any compatible
14
+ * provider works without code changes.
15
+ */
16
+ /** Render-provider endpoint base URL, or null when the tier is disabled. */
17
+ export function renderProviderUrl() {
18
+ const url = (process.env.WEBCRAWL_RENDER_URL ?? "").trim();
19
+ return url.length > 0 ? url : null;
20
+ }
21
+ /** Optional bearer key for the render provider (hosted tier). */
22
+ export function renderProviderKey() {
23
+ const key = (process.env.WEBCRAWL_RENDER_KEY ?? "").trim();
24
+ return key.length > 0 ? key : null;
25
+ }
26
+ /** Whether the render-provider tier is configured and usable at all. */
27
+ export function isRenderProviderEnabled() {
28
+ return renderProviderUrl() !== null;
29
+ }
30
+ /** Default provider rate limit (requests/min) when no key is configured. */
31
+ const KEYLESS_RATE_LIMIT_PER_MIN = 15;
32
+ /** Provider rate limit (requests/min) assumed with a key configured. */
33
+ const KEYED_RATE_LIMIT_PER_MIN = 60;
34
+ /** Hard timeout for a single render request. */
35
+ const RENDER_TIMEOUT_MS = 45_000;
36
+ /** Max bytes accepted from the provider (matches the fetch ladder cap). */
37
+ const RENDER_MAX_BYTES = 2 * 1024 * 1024;
38
+ const rateWindow = [];
39
+ function withinRateLimit() {
40
+ const now = Date.now();
41
+ while (rateWindow.length > 0 && rateWindow[0] <= now - 60_000)
42
+ rateWindow.shift();
43
+ const limit = renderProviderKey() ? KEYED_RATE_LIMIT_PER_MIN : KEYLESS_RATE_LIMIT_PER_MIN;
44
+ if (rateWindow.length >= limit)
45
+ return false;
46
+ rateWindow.push(now);
47
+ return true;
48
+ }
49
+ /**
50
+ * Failure markers providers prepend/emit when the target page could not be
51
+ * rendered (challenge, CAPTCHA, upstream error). A matching response is not
52
+ * rendered content and must not be returned as such.
53
+ */
54
+ const RENDER_FAILURE_MARKERS = [
55
+ "target url returned error",
56
+ "may be requiring captcha",
57
+ "maybe requiring captcha",
58
+ "please make sure you are authorized",
59
+ ];
60
+ /** Splits a reader-format response body into its header fields and markdown. */
61
+ export function parseRenderedResponse(body) {
62
+ const lines = body.split("\n");
63
+ let title = "";
64
+ let sourceUrl = "";
65
+ let markdownStart = lines.length;
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i];
68
+ if (line.startsWith("Title:")) {
69
+ title = line.slice("Title:".length).trim();
70
+ continue;
71
+ }
72
+ if (line.startsWith("URL Source:")) {
73
+ sourceUrl = line.slice("URL Source:".length).trim();
74
+ continue;
75
+ }
76
+ if (/^Markdown Content:?\s*$/.test(line)) {
77
+ markdownStart = i + 1;
78
+ break;
79
+ }
80
+ }
81
+ let markdown = lines.slice(markdownStart).join("\n").trim();
82
+ if (markdownStart === lines.length && title) {
83
+ // No "Markdown Content:" separator: treat the whole body after the header
84
+ // block as markdown (some providers omit the separator).
85
+ markdown = lines
86
+ .filter((l) => !l.startsWith("Title:") && !l.startsWith("URL Source:"))
87
+ .join("\n")
88
+ .trim();
89
+ }
90
+ return { title, sourceUrl, markdown };
91
+ }
92
+ /** Test helper: clears the in-memory rate-limit window (external env untouched). */
93
+ export function resetRenderStateForTests() {
94
+ rateWindow.length = 0;
95
+ }
96
+ /**
97
+ * Renders `url` through the configured render provider. Returns null when the
98
+ * tier is disabled, rate-limited, times out, or the provider itself reports a
99
+ * failed render — the caller then keeps its native result.
100
+ */
101
+ export async function renderViaProvider(url) {
102
+ const base = renderProviderUrl();
103
+ if (!base)
104
+ return null;
105
+ if (!withinRateLimit())
106
+ return null;
107
+ const key = renderProviderKey();
108
+ const headers = {
109
+ Accept: "text/plain",
110
+ ...(key ? { Authorization: `Bearer ${key}` } : {}),
111
+ };
112
+ let res;
113
+ try {
114
+ res = await fetch(`${base}${url}`, {
115
+ headers,
116
+ signal: AbortSignal.timeout(RENDER_TIMEOUT_MS),
117
+ redirect: "follow",
118
+ });
119
+ }
120
+ catch {
121
+ return null; // network error / timeout / DNS — keep native result
122
+ }
123
+ if (!res.ok)
124
+ return null;
125
+ const body = (await res.text()).slice(0, RENDER_MAX_BYTES);
126
+ const lower = body.slice(0, 2000).toLowerCase();
127
+ if (RENDER_FAILURE_MARKERS.some((m) => lower.includes(m)))
128
+ return null;
129
+ const { title, sourceUrl, markdown } = parseRenderedResponse(body);
130
+ if (markdown.trim().length < 40)
131
+ return null; // nothing real came back
132
+ return {
133
+ title,
134
+ markdown,
135
+ sourceUrl: sourceUrl || url,
136
+ warnings: [],
137
+ };
138
+ }
@@ -38,6 +38,9 @@ export interface ExternalSessionStore {
38
38
  /** Optional persistence for the residential-proxy memo (Tier P1). */
39
39
  markDomainProxyNeeded?(domain: string): Promise<void>;
40
40
  isDomainProxyNeeded?(domain: string): Promise<boolean>;
41
+ /** Optional persistence for the render-provider memo (Tier R). */
42
+ markDomainRenderNeeded?(domain: string): Promise<void>;
43
+ isDomainRenderNeeded?(domain: string): Promise<boolean>;
41
44
  }
42
45
  /** How long a solved session is offered for warm reconnect / cookie replay. */
43
46
  export declare const WARM_TTL_MS: number;
@@ -49,6 +52,8 @@ export declare const WARM_CAP = 4;
49
52
  export declare const CHALLENGE_MEMO_TTL_MS: number;
50
53
  /** How long a domain stays on the residential-proxy memo (Tier P1). */
51
54
  export declare const PROXY_MEMO_TTL_MS: number;
55
+ /** How long a domain stays on the render-provider memo (Tier R). */
56
+ export declare const RENDER_MEMO_TTL_MS: number;
52
57
  export declare function registerExternalSessionStore(store: ExternalSessionStore): void;
53
58
  export interface CookieLike {
54
59
  name: string;
@@ -77,5 +82,12 @@ export declare function isDomainChallenged(domain: string): Promise<boolean>;
77
82
  */
78
83
  export declare function markDomainProxyNeeded(domain: string): Promise<void>;
79
84
  export declare function isDomainProxyNeeded(domain: string): Promise<boolean>;
85
+ /**
86
+ * Marks a domain as needing the render-provider tier (Tier R): the native
87
+ * fetch ladder passes the bot check but the page's content only appears after
88
+ * JS renders, so future runs skip the ladder and go provider-first.
89
+ */
90
+ export declare function markDomainRenderNeeded(domain: string): Promise<void>;
91
+ export declare function isDomainRenderNeeded(domain: string): Promise<boolean>;
80
92
  /** Test helper: resets all in-memory state (external stores are untouched). */
81
93
  export declare function resetSessionStateForTests(): void;
@@ -29,6 +29,8 @@ export const WARM_CAP = 4;
29
29
  export const CHALLENGE_MEMO_TTL_MS = 30 * 60_000;
30
30
  /** How long a domain stays on the residential-proxy memo (Tier P1). */
31
31
  export const PROXY_MEMO_TTL_MS = 60 * 60_000;
32
+ /** How long a domain stays on the render-provider memo (Tier R). */
33
+ export const RENDER_MEMO_TTL_MS = 12 * 60 * 60_000;
32
34
  let external = null;
33
35
  export function registerExternalSessionStore(store) {
34
36
  external = store;
@@ -36,6 +38,7 @@ export function registerExternalSessionStore(store) {
36
38
  const warm = new Map();
37
39
  const challenged = new Map();
38
40
  const proxyNeeded = new Map();
41
+ const renderNeeded = new Map();
39
42
  function reap(now = Date.now()) {
40
43
  for (const [d, s] of warm)
41
44
  if (s.expiresAt <= now)
@@ -46,6 +49,9 @@ function reap(now = Date.now()) {
46
49
  for (const [d, t] of proxyNeeded)
47
50
  if (t <= now)
48
51
  proxyNeeded.delete(d);
52
+ for (const [d, t] of renderNeeded)
53
+ if (t <= now)
54
+ renderNeeded.delete(d);
49
55
  }
50
56
  /** Serializes captured browser cookies into a `Cookie:` header value (drops expired). */
51
57
  export function cookieHeaderFrom(cookies) {
@@ -195,9 +201,50 @@ export async function isDomainProxyNeeded(domain) {
195
201
  }
196
202
  return false;
197
203
  }
204
+ /**
205
+ * Marks a domain as needing the render-provider tier (Tier R): the native
206
+ * fetch ladder passes the bot check but the page's content only appears after
207
+ * JS renders, so future runs skip the ladder and go provider-first.
208
+ */
209
+ export async function markDomainRenderNeeded(domain) {
210
+ if (!domain)
211
+ return;
212
+ renderNeeded.set(domain, Date.now() + RENDER_MEMO_TTL_MS);
213
+ if (external?.markDomainRenderNeeded) {
214
+ try {
215
+ await external.markDomainRenderNeeded(domain);
216
+ }
217
+ catch {
218
+ /* store unavailable */
219
+ }
220
+ }
221
+ }
222
+ export async function isDomainRenderNeeded(domain) {
223
+ if (!domain)
224
+ return false;
225
+ reap();
226
+ const until = renderNeeded.get(domain);
227
+ if (until && until > Date.now())
228
+ return true;
229
+ if (until)
230
+ renderNeeded.delete(domain);
231
+ if (external?.isDomainRenderNeeded) {
232
+ try {
233
+ if (await external.isDomainRenderNeeded(domain)) {
234
+ renderNeeded.set(domain, Date.now() + RENDER_MEMO_TTL_MS);
235
+ return true;
236
+ }
237
+ }
238
+ catch {
239
+ /* store unavailable */
240
+ }
241
+ }
242
+ return false;
243
+ }
198
244
  /** Test helper: resets all in-memory state (external stores are untouched). */
199
245
  export function resetSessionStateForTests() {
200
246
  warm.clear();
201
247
  challenged.clear();
202
248
  proxyNeeded.clear();
249
+ renderNeeded.clear();
203
250
  }
@@ -10,7 +10,9 @@
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, lastFetchTier } from "./engine/fetcher.js";
13
+ import { isDomainRenderNeeded, markDomainRenderNeeded, } from "./engine/session.js";
14
+ import { isRenderProviderEnabled, renderViaProvider } from "./engine/render.js";
15
+ import { fetchResource, lastFetchTier, needsBrowserEscalation } from "./engine/fetcher.js";
14
16
  import { parseFile, publicParseMeta } from "./parse/index.js";
15
17
  import { isBinaryContentType, looksLikeBinaryUrl } from "./parse/fetch.js";
16
18
  import { maxDownloadBytes } from "./parse/limits.js";
@@ -18,6 +20,15 @@ import { parseYouTubeCached, youtubeVideoId } from "./parse/youtube.js";
18
20
  import { rankDocuments } from "./ranker.js";
19
21
  import { getSearchCategoriesCatalog, searchWeb } from "./search.js";
20
22
  import { inspectTarget, listTargets, resolveTarget } from "./targets.js";
23
+ /** Lowercased hostname of a URL (memo key for the Tier R render memo). */
24
+ function safeDomainOf(url) {
25
+ try {
26
+ return new URL(url).hostname.toLowerCase();
27
+ }
28
+ catch {
29
+ return "";
30
+ }
31
+ }
21
32
  export function createLocalEngineClient() {
22
33
  return {
23
34
  baseUrl: "local://engine",
@@ -142,7 +153,49 @@ export function createLocalEngineClient() {
142
153
  };
143
154
  }
144
155
  }
145
- // 4. Fetch with TLS impersonation (impit) + target-aware extraction
156
+ // 4. Fetch with TLS impersonation (impit) + target-aware extraction.
157
+ // Tier R: memoized render-needed domains skip the native ladder
158
+ // entirely when a render provider is configured — cache (step 3)
159
+ // already absorbed repeats, so the provider is the next best hop.
160
+ const renderDomain = safeDomainOf(targetUrl);
161
+ if (isRenderProviderEnabled() && (await isDomainRenderNeeded(renderDomain))) {
162
+ const rendered = await renderViaProvider(targetUrl);
163
+ if (rendered) {
164
+ const renderHash = sha256(rendered.markdown);
165
+ setCachedPage(targetUrl, "none", {
166
+ title: rendered.title,
167
+ markdown: rendered.markdown,
168
+ links: [],
169
+ statusCode: 200,
170
+ contentHash: renderHash,
171
+ });
172
+ return {
173
+ success: true,
174
+ mode: "local",
175
+ url: targetUrl,
176
+ title: rendered.title || targetUrl,
177
+ markdown: rendered.markdown,
178
+ links: [],
179
+ data: {
180
+ url: targetUrl,
181
+ title: rendered.title || targetUrl,
182
+ markdown: rendered.markdown,
183
+ links: [],
184
+ },
185
+ evidence: {
186
+ targetId: "render-provider",
187
+ tier: "render",
188
+ contentHash: renderHash,
189
+ latencyMs: Date.now() - start,
190
+ sourceUrl: rendered.sourceUrl,
191
+ matchedSignals: ["render:provider"],
192
+ limitations: rendered.warnings,
193
+ },
194
+ };
195
+ }
196
+ // Provider failed on a memo'd domain: fall through to the native
197
+ // ladder — the memo may be stale or the provider flaky.
198
+ }
146
199
  const fetched = await fetchWithTlsImpersonation(targetUrl);
147
200
  const { title, markdown, links } = fastHtmlToMarkdown(fetched.html, targetUrl);
148
201
  const contentHash = sha256(markdown || fetched.html);
@@ -158,6 +211,83 @@ export function createLocalEngineClient() {
158
211
  html: fetched.html,
159
212
  markdown,
160
213
  });
214
+ // Tier R: the native ladder passed the bot check but the page is a
215
+ // JS shell / challenge page the ladder cannot render. Memoize the
216
+ // domain and either render via the provider (when configured) or
217
+ // return the shell with an honest limitation note.
218
+ if (needsBrowserEscalation(fetched.status, fetched.html, markdown)) {
219
+ await markDomainRenderNeeded(renderDomain);
220
+ if (isRenderProviderEnabled()) {
221
+ const rendered = await renderViaProvider(targetUrl);
222
+ if (rendered) {
223
+ const renderHash = sha256(rendered.markdown);
224
+ setCachedPage(targetUrl, "none", {
225
+ title: rendered.title,
226
+ markdown: rendered.markdown,
227
+ links: [],
228
+ statusCode: 200,
229
+ contentHash: renderHash,
230
+ });
231
+ return {
232
+ success: true,
233
+ mode: "local",
234
+ url: targetUrl,
235
+ title: rendered.title || targetUrl,
236
+ markdown: rendered.markdown,
237
+ links: [],
238
+ data: {
239
+ url: targetUrl,
240
+ title: rendered.title || targetUrl,
241
+ markdown: rendered.markdown,
242
+ links: [],
243
+ },
244
+ evidence: {
245
+ targetId: "render-provider",
246
+ tier: "render",
247
+ contentHash: renderHash,
248
+ latencyMs: Date.now() - start,
249
+ sourceUrl: rendered.sourceUrl,
250
+ matchedSignals: ["render:provider"],
251
+ limitations: rendered.warnings,
252
+ },
253
+ };
254
+ }
255
+ }
256
+ const nativeTier = lastFetchTier() === "impersonate"
257
+ ? "impersonate"
258
+ : lastFetchTier() === "proxy-impersonate"
259
+ ? "proxy-impersonate"
260
+ : "impit-tls";
261
+ return {
262
+ success: true,
263
+ mode: "local",
264
+ url: targetUrl,
265
+ title: title || targetInspection.title || targetUrl,
266
+ markdown: targetInspection.markdown || markdown,
267
+ links,
268
+ data: {
269
+ url: targetUrl,
270
+ title: title || targetInspection.title || targetUrl,
271
+ markdown: targetInspection.markdown || markdown,
272
+ links,
273
+ structured: targetInspection.structured,
274
+ },
275
+ evidence: {
276
+ ...targetInspection.evidence,
277
+ tier: nativeTier,
278
+ contentHash,
279
+ latencyMs: Date.now() - start,
280
+ limitations: [
281
+ ...(Array.isArray(targetInspection.evidence?.limitations)
282
+ ? targetInspection.evidence.limitations
283
+ : []),
284
+ "Page needs JS rendering; the local native tiers cannot render it. " +
285
+ "Run `webcrawl login` and retry with `--mode cloud` (rendering runs in the hosted tier), " +
286
+ "or self-host a render provider via WEBCRAWL_RENDER_URL.",
287
+ ],
288
+ },
289
+ };
290
+ }
161
291
  return {
162
292
  success: true,
163
293
  mode: "local",
@@ -287,6 +417,49 @@ export function createLocalEngineClient() {
287
417
  async extract(p) {
288
418
  const targetUrl = /^https?:\/\//i.test(p.url) ? p.url : `https://${p.url}`;
289
419
  const fetched = await fetchWithTlsImpersonation(targetUrl);
420
+ const fastCheck = fastHtmlToMarkdown(fetched.html, targetUrl);
421
+ // Tier R: shell page the native ladder cannot render — retry
422
+ // through scrape() so the render path + cache apply, then extract
423
+ // from the rendered Markdown. Without a provider, surface the
424
+ // login/cloud limitation instead of silently returning {}.
425
+ if (needsBrowserEscalation(fetched.status, fetched.html, fastCheck.markdown)) {
426
+ if (isRenderProviderEnabled()) {
427
+ const scraped = (await this.scrape({ url: targetUrl, noCache: true }));
428
+ const renderedMarkdown = (scraped.data && scraped.data.markdown) || scraped.markdown || "";
429
+ if (renderedMarkdown.trim().length > 40) {
430
+ const targetInspection = await inspectTarget({
431
+ url: targetUrl,
432
+ markdown: renderedMarkdown,
433
+ });
434
+ const siteData = await extractWebsiteData(targetUrl);
435
+ return {
436
+ success: true,
437
+ mode: "local",
438
+ url: targetUrl,
439
+ data: {
440
+ ...siteData,
441
+ ...(targetInspection.structured ? { target: targetInspection.structured } : {}),
442
+ },
443
+ evidence: targetInspection.evidence,
444
+ };
445
+ }
446
+ }
447
+ return {
448
+ success: true,
449
+ mode: "local",
450
+ url: targetUrl,
451
+ data: {},
452
+ evidence: {
453
+ tier: lastFetchTier(),
454
+ sourceUrl: targetUrl,
455
+ limitations: [
456
+ "Page needs JS rendering; the local native tiers cannot render it. " +
457
+ "Run `webcrawl login` and retry with `--mode cloud` (rendering runs in the hosted tier), " +
458
+ "or self-host a render provider via WEBCRAWL_RENDER_URL.",
459
+ ],
460
+ },
461
+ };
462
+ }
290
463
  const targetInspection = await inspectTarget({
291
464
  url: targetUrl,
292
465
  html: fetched.html,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcrawl",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
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,8 +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.3.3",
131
- "@webcrawl/curl-impersonate": "^1.3.3",
130
+ "@webcrawl/client": "^1.3.4",
131
+ "@webcrawl/curl-impersonate": "^1.3.4",
132
132
  "ai": "^7.0.150",
133
133
  "impit": "^0.14.7",
134
134
  "mammoth": "^1.13.0",