@webcrawl/mcp 1.4.2 → 1.4.3
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/README.md +1 -1
- package/dist/bin.js +5 -7
- package/dist/core/engine/awswaf.d.ts +59 -0
- package/dist/core/engine/awswaf.js +164 -0
- package/dist/core/engine/cache.d.ts +70 -0
- package/dist/core/engine/cache.js +297 -0
- package/dist/core/engine/chrome.d.ts +14 -0
- package/dist/core/engine/chrome.js +140 -0
- package/dist/core/engine/crawl-policy.d.ts +36 -0
- package/dist/core/engine/crawl-policy.js +134 -0
- package/dist/core/engine/crawler.d.ts +152 -0
- package/dist/core/engine/crawler.js +918 -0
- package/dist/core/engine/fetcher.d.ts +92 -0
- package/dist/core/engine/fetcher.js +494 -0
- package/dist/core/engine/index.d.ts +7 -0
- package/dist/core/engine/index.js +7 -0
- package/dist/core/engine/markdown.d.ts +33 -0
- package/dist/core/engine/markdown.js +334 -0
- package/dist/core/engine/path-scope.d.ts +21 -0
- package/dist/core/engine/path-scope.js +88 -0
- package/dist/core/engine/render.d.ts +41 -0
- package/dist/core/engine/render.js +138 -0
- package/dist/core/engine/safe-url.d.ts +29 -0
- package/dist/core/engine/safe-url.js +109 -0
- package/dist/core/engine/session.d.ts +93 -0
- package/dist/core/engine/session.js +250 -0
- package/dist/core/engine/stealth.d.ts +83 -0
- package/dist/core/engine/stealth.js +222 -0
- package/dist/core/engine.d.ts +1 -0
- package/dist/core/engine.js +1 -0
- package/dist/core/extract/business.d.ts +28 -0
- package/dist/core/extract/business.js +344 -0
- package/dist/core/extract/currency.d.ts +15 -0
- package/dist/core/extract/currency.js +259 -0
- package/dist/core/extract/cve-cache.d.ts +62 -0
- package/dist/core/extract/cve-cache.js +277 -0
- package/dist/core/extract/cve.d.ts +57 -0
- package/dist/core/extract/cve.js +397 -0
- package/dist/core/extract/dedupe-key.d.ts +12 -0
- package/dist/core/extract/dedupe-key.js +36 -0
- package/dist/core/extract/flights.d.ts +37 -0
- package/dist/core/extract/flights.js +373 -0
- package/dist/core/extract/hotels.d.ts +35 -0
- package/dist/core/extract/hotels.js +473 -0
- package/dist/core/extract/index.d.ts +9 -0
- package/dist/core/extract/index.js +9 -0
- package/dist/core/extract/real-estate.d.ts +53 -0
- package/dist/core/extract/real-estate.js +835 -0
- package/dist/core/extract/scaledown.d.ts +14 -0
- package/dist/core/extract/scaledown.js +484 -0
- package/dist/core/extract/shopping.d.ts +44 -0
- package/dist/core/extract/shopping.js +393 -0
- package/dist/core/extract/structured.d.ts +14 -0
- package/dist/core/extract/structured.js +144 -0
- package/dist/core/image-url.d.ts +5 -0
- package/dist/core/image-url.js +13 -0
- package/dist/core/interact/driver.d.ts +72 -0
- package/dist/core/interact/driver.js +384 -0
- package/dist/core/interact/remote.d.ts +41 -0
- package/dist/core/interact/remote.js +133 -0
- package/dist/core/interact/spans.d.ts +46 -0
- package/dist/core/interact/spans.js +32 -0
- package/dist/core/interact/validate.d.ts +34 -0
- package/dist/core/interact/validate.js +135 -0
- package/dist/core/interact/vm.d.ts +43 -0
- package/dist/core/interact/vm.js +338 -0
- package/dist/core/local-engine.d.ts +56 -0
- package/dist/core/local-engine.js +667 -0
- package/dist/core/parse/detect.d.ts +29 -0
- package/dist/core/parse/detect.js +152 -0
- package/dist/core/parse/fetch.d.ts +20 -0
- package/dist/core/parse/fetch.js +39 -0
- package/dist/core/parse/image.d.ts +13 -0
- package/dist/core/parse/image.js +100 -0
- package/dist/core/parse/index.d.ts +53 -0
- package/dist/core/parse/index.js +153 -0
- package/dist/core/parse/limits.d.ts +12 -0
- package/dist/core/parse/limits.js +29 -0
- package/dist/core/parse/media.d.ts +42 -0
- package/dist/core/parse/media.js +238 -0
- package/dist/core/parse/merge.d.ts +19 -0
- package/dist/core/parse/merge.js +48 -0
- package/dist/core/parse/ocr.d.ts +25 -0
- package/dist/core/parse/ocr.js +121 -0
- package/dist/core/parse/office.d.ts +28 -0
- package/dist/core/parse/office.js +83 -0
- package/dist/core/parse/pdf.d.ts +27 -0
- package/dist/core/parse/pdf.js +156 -0
- package/dist/core/parse/types.d.ts +38 -0
- package/dist/core/parse/types.js +1 -0
- package/dist/core/parse/youtube.d.ts +30 -0
- package/dist/core/parse/youtube.js +264 -0
- package/dist/core/ranker/decision.d.ts +28 -0
- package/dist/core/ranker/decision.js +151 -0
- package/dist/core/ranker/index.d.ts +5 -0
- package/dist/core/ranker/index.js +361 -0
- package/dist/core/ranker/lexical.d.ts +82 -0
- package/dist/core/ranker/lexical.js +643 -0
- package/dist/core/ranker/types.d.ts +90 -0
- package/dist/core/ranker/types.js +1 -0
- package/dist/core/ranker.d.ts +1 -0
- package/dist/core/ranker.js +1 -0
- package/dist/core/sandbox.d.ts +29 -0
- package/dist/core/sandbox.js +222 -0
- package/dist/core/search/catalog.d.ts +176 -0
- package/dist/core/search/catalog.js +488 -0
- package/dist/core/search/index.d.ts +3 -0
- package/dist/core/search/index.js +3 -0
- package/dist/core/search/pipeline.d.ts +7 -0
- package/dist/core/search/pipeline.js +719 -0
- package/dist/core/search/providers.d.ts +72 -0
- package/dist/core/search/providers.js +1270 -0
- package/dist/core/search.d.ts +1 -0
- package/dist/core/search.js +1 -0
- package/dist/core/targets/developer.d.ts +44 -0
- package/dist/core/targets/developer.js +338 -0
- package/dist/core/targets/index.d.ts +3 -0
- package/dist/core/targets/index.js +3 -0
- package/dist/core/targets/registry.d.ts +32 -0
- package/dist/core/targets/registry.js +411 -0
- package/dist/core/targets/types.d.ts +72 -0
- package/dist/core/targets/types.js +8 -0
- package/dist/core/targets.d.ts +1 -0
- package/dist/core/targets.js +1 -0
- package/dist/format.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/load-local-engine.d.ts +7 -0
- package/dist/load-local-engine.js +22 -0
- package/dist/server.js +3 -0
- package/package.json +11 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type SearchEngineSource = "bing-shopping" | "google-shopping" | "brave-native" | "ddg-lite" | "bing-rss" | "google-news-rss" | "wikipedia" | "hn-algolia" | "arxiv" | "nvd" | "osv" | "serper" | "brave-api" | "jina";
|
|
2
|
+
export type DecisionModelId = "baseten/mammoth-1" | "baseten/elephant-1" | "convaiinnovations/laya" | "auto" | "none";
|
|
3
|
+
export interface RawCandidateDocument {
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
description: string;
|
|
7
|
+
markdown?: string;
|
|
8
|
+
links?: string[];
|
|
9
|
+
publishedDate?: string;
|
|
10
|
+
engine?: SearchEngineSource;
|
|
11
|
+
engineRank?: number;
|
|
12
|
+
sources?: SearchEngineSource[];
|
|
13
|
+
engineRanks?: Partial<Record<SearchEngineSource, number>>;
|
|
14
|
+
}
|
|
15
|
+
export interface UserLocationContext {
|
|
16
|
+
country?: string;
|
|
17
|
+
countryName?: string;
|
|
18
|
+
region?: string;
|
|
19
|
+
city?: string;
|
|
20
|
+
timezone?: string;
|
|
21
|
+
latitude?: number;
|
|
22
|
+
longitude?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SearchFilterContext {
|
|
25
|
+
category?: string;
|
|
26
|
+
subcategory?: string | string[];
|
|
27
|
+
preferLocalDomains?: boolean;
|
|
28
|
+
boostTerms?: string[];
|
|
29
|
+
mustIncludeTerms?: string[];
|
|
30
|
+
excludeTerms?: string[];
|
|
31
|
+
customInstruction?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface DocumentScoreBreakdown {
|
|
34
|
+
rrfScore: number;
|
|
35
|
+
bm25fScore: number;
|
|
36
|
+
passageMaxSimScore: number;
|
|
37
|
+
proximityScore: number;
|
|
38
|
+
freshnessAuthorityScore: number;
|
|
39
|
+
geoAffinityScore?: number;
|
|
40
|
+
lexicalHybridScore: number;
|
|
41
|
+
decisionScore?: number;
|
|
42
|
+
decisionRawScore?: number;
|
|
43
|
+
decisionConfidence?: number;
|
|
44
|
+
decisionModelUsed?: string;
|
|
45
|
+
multiEngineConsensusCount: number;
|
|
46
|
+
}
|
|
47
|
+
export interface RankedDocument {
|
|
48
|
+
title: string;
|
|
49
|
+
url: string;
|
|
50
|
+
canonicalUrl: string;
|
|
51
|
+
description: string;
|
|
52
|
+
markdown?: string;
|
|
53
|
+
links?: string[];
|
|
54
|
+
publishedDate?: string;
|
|
55
|
+
score: number;
|
|
56
|
+
rank: number;
|
|
57
|
+
sources: SearchEngineSource[];
|
|
58
|
+
highlights: string[];
|
|
59
|
+
scoreBreakdown: DocumentScoreBreakdown;
|
|
60
|
+
}
|
|
61
|
+
export interface RerankOptions {
|
|
62
|
+
topK?: number;
|
|
63
|
+
rrfK?: number;
|
|
64
|
+
useDecisionModel?: boolean;
|
|
65
|
+
decisionModel?: DecisionModelId;
|
|
66
|
+
decisionTimeoutMs?: number;
|
|
67
|
+
decisionTopN?: number;
|
|
68
|
+
extractHighlights?: boolean;
|
|
69
|
+
maxHighlights?: number;
|
|
70
|
+
/** Maximum results per publisher/domain in topK when diverse candidates exist (default: 1 for topK <= 10, 2 for topK > 10) */
|
|
71
|
+
maxPerDomain?: number;
|
|
72
|
+
/** Maximal Marginal Relevance lambda in [0, 1] balancing relevance vs diversity (default: 0.82) */
|
|
73
|
+
mmrLambda?: number;
|
|
74
|
+
/** Optional user geolocation context for boosting local/regional sources and ccTLDs */
|
|
75
|
+
userLocation?: UserLocationContext;
|
|
76
|
+
/** Optional dynamic category, subcategory, and agent filter context */
|
|
77
|
+
filterContext?: SearchFilterContext;
|
|
78
|
+
}
|
|
79
|
+
export interface RerankResult {
|
|
80
|
+
results: RankedDocument[];
|
|
81
|
+
rankingMetrics: {
|
|
82
|
+
totalCandidates: number;
|
|
83
|
+
uniqueCandidates: number;
|
|
84
|
+
lexicalLatencyMs: number;
|
|
85
|
+
decisionLatencyMs: number;
|
|
86
|
+
totalRankingLatencyMs: number;
|
|
87
|
+
decisionModelUsed: string | null;
|
|
88
|
+
enginesContributing: SearchEngineSource[];
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ranker/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ranker/index.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface BrowserEscalationResult {
|
|
2
|
+
html: string;
|
|
3
|
+
markdown: string;
|
|
4
|
+
title: string;
|
|
5
|
+
links: string[];
|
|
6
|
+
statusCode: number;
|
|
7
|
+
tier: "sandbox" | "cloudflare" | "browser";
|
|
8
|
+
}
|
|
9
|
+
/** True when an escalation result is still the bot check, not the page. */
|
|
10
|
+
export declare function isChallengeResult(html: string, markdown: string, status?: number): boolean;
|
|
11
|
+
/** True when a browser tier (Cloudflare Browser Rendering or Vercel Sandbox) is configured. */
|
|
12
|
+
export declare function browserEscalationAvailable(): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Tier 2/3 Anti-Bot Escalation:
|
|
15
|
+
* 1. Uses Cloudflare Browser Rendering `/markdown` if CF_ACCOUNT_ID + CF_API_TOKEN are set.
|
|
16
|
+
* 2. Uses `@vercel/sandbox` isolated Linux microVM (with curl-impersonate / Playwright / Node TLS fetch + proxy)
|
|
17
|
+
* to bypass JA3/JA4 TLS blocks, solve JS challenges, and cache `cf_clearance` cookies per domain.
|
|
18
|
+
*/
|
|
19
|
+
export declare function escalateToSandboxOrBrowser(url: string, options?: {
|
|
20
|
+
retainImages?: "all" | "none";
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Last resort: open the page in a real browser and wait out the bot check.
|
|
24
|
+
* Slow (cold start plus up to 20s solving), so only callers with a large
|
|
25
|
+
* time budget should opt in.
|
|
26
|
+
*/
|
|
27
|
+
browser?: boolean;
|
|
28
|
+
browserBudgetMs?: number;
|
|
29
|
+
}): Promise<BrowserEscalationResult | null>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { fastHtmlToMarkdown, setDomainCookie } from "./engine.js";
|
|
2
|
+
import { isAntiBotChallenge } from "./engine/fetcher.js";
|
|
3
|
+
import { getWarmSession, isDomainChallenged, markDomainChallenged } from "./engine/session.js";
|
|
4
|
+
/** Bot-check copy that survives HTML → markdown conversion (Cloudflare `/markdown`). */
|
|
5
|
+
const CHALLENGE_MARKDOWN = [
|
|
6
|
+
"just a moment",
|
|
7
|
+
"vercel security checkpoint",
|
|
8
|
+
"verify you are human",
|
|
9
|
+
"checking your browser",
|
|
10
|
+
"enable javascript and cookies to continue",
|
|
11
|
+
];
|
|
12
|
+
/** True when an escalation result is still the bot check, not the page. */
|
|
13
|
+
export function isChallengeResult(html, markdown, status = 200) {
|
|
14
|
+
if (html && isAntiBotChallenge(html, status))
|
|
15
|
+
return true;
|
|
16
|
+
const head = markdown.slice(0, 1500).toLowerCase();
|
|
17
|
+
// Short pages only: a long article that mentions "just a moment" is real content.
|
|
18
|
+
return markdown.length < 1500 && CHALLENGE_MARKDOWN.some((s) => head.includes(s));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* True when Vercel Sandbox can authenticate. On Vercel (VERCEL=1) the OIDC token is
|
|
22
|
+
* not in env at request time: it arrives in the `x-vercel-oidc-token` header, which
|
|
23
|
+
* @vercel/sandbox reads via @vercel/oidc. Locally it comes from `vercel env pull`.
|
|
24
|
+
*/
|
|
25
|
+
function vercelSandboxAvailable() {
|
|
26
|
+
return Boolean(process.env.VERCEL === "1" ||
|
|
27
|
+
process.env.VERCEL_OIDC_TOKEN ||
|
|
28
|
+
process.env.VERCEL_SANDBOX_ENABLED === "true");
|
|
29
|
+
}
|
|
30
|
+
/** True when a browser tier (Cloudflare Browser Rendering or Vercel Sandbox) is configured. */
|
|
31
|
+
export function browserEscalationAvailable() {
|
|
32
|
+
return Boolean((process.env.CF_ACCOUNT_ID && process.env.CF_API_TOKEN) || vercelSandboxAvailable());
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Tier 2/3 Anti-Bot Escalation:
|
|
36
|
+
* 1. Uses Cloudflare Browser Rendering `/markdown` if CF_ACCOUNT_ID + CF_API_TOKEN are set.
|
|
37
|
+
* 2. Uses `@vercel/sandbox` isolated Linux microVM (with curl-impersonate / Playwright / Node TLS fetch + proxy)
|
|
38
|
+
* to bypass JA3/JA4 TLS blocks, solve JS challenges, and cache `cf_clearance` cookies per domain.
|
|
39
|
+
*/
|
|
40
|
+
export async function escalateToSandboxOrBrowser(url, options = {}) {
|
|
41
|
+
// Option A: Cloudflare Browser Rendering Quick Action (/markdown) if configured
|
|
42
|
+
const cfAccountId = process.env.CF_ACCOUNT_ID;
|
|
43
|
+
const cfApiToken = process.env.CF_API_TOKEN;
|
|
44
|
+
if (cfAccountId && cfApiToken) {
|
|
45
|
+
try {
|
|
46
|
+
const cfRes = await fetch(`https://api.cloudflare.com/client/v4/accounts/${cfAccountId}/browser-rendering/markdown`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${cfApiToken}`,
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
url,
|
|
54
|
+
gotoOptions: { waitUntil: "domcontentloaded", timeout: options.timeoutMs ?? 15000 },
|
|
55
|
+
rejectRequestPattern: [
|
|
56
|
+
"^.*\\.(png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|mp4|mp3|css)(\\?.*)?$",
|
|
57
|
+
"^.*(googletagmanager|google-analytics|intercom|segment|hotjar|sentry).*$",
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 15000),
|
|
61
|
+
});
|
|
62
|
+
if (cfRes.ok) {
|
|
63
|
+
const payload = (await cfRes.json());
|
|
64
|
+
if (payload.success && payload.result && !isChallengeResult("", payload.result)) {
|
|
65
|
+
const titleMatch = payload.result.match(/^#\s+([^\n]+)/m);
|
|
66
|
+
return {
|
|
67
|
+
html: "",
|
|
68
|
+
markdown: payload.result,
|
|
69
|
+
title: titleMatch ? titleMatch[1].trim() : url,
|
|
70
|
+
links: [],
|
|
71
|
+
statusCode: 200,
|
|
72
|
+
tier: "cloudflare",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Fall through to Vercel Sandbox
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Option B: Vercel Sandbox microVM execution (`@vercel/sandbox`)
|
|
82
|
+
if (vercelSandboxAvailable()) {
|
|
83
|
+
// Challenge memo: curl can never solve a JS challenge, so for domains known
|
|
84
|
+
// to serve one (or with a warm solved session) skip this tier entirely and
|
|
85
|
+
// save its ~4-6 s sandbox boot.
|
|
86
|
+
let host = "";
|
|
87
|
+
try {
|
|
88
|
+
host = new URL(url).hostname;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* fall through to curl */
|
|
92
|
+
}
|
|
93
|
+
const skipCurlTier = host
|
|
94
|
+
? (await isDomainChallenged(host).catch(() => false)) || Boolean(await getWarmSession(host).catch(() => null))
|
|
95
|
+
: false;
|
|
96
|
+
if (!skipCurlTier) {
|
|
97
|
+
try {
|
|
98
|
+
const { Sandbox } = await import("@vercel/sandbox");
|
|
99
|
+
const sandbox = await Sandbox.create({
|
|
100
|
+
timeout: options.timeoutMs ?? 20_000,
|
|
101
|
+
});
|
|
102
|
+
try {
|
|
103
|
+
// Execute curl with browser cipher list + cookie capture inside the clean Linux microVM egress IP
|
|
104
|
+
const proxyArg = process.env.PROXY_URL ? `-x "${process.env.PROXY_URL}"` : "";
|
|
105
|
+
const cmd = await sandbox.runCommand({
|
|
106
|
+
cmd: "curl",
|
|
107
|
+
args: [
|
|
108
|
+
"-sSL",
|
|
109
|
+
"--compressed",
|
|
110
|
+
"--max-time",
|
|
111
|
+
"12",
|
|
112
|
+
"-D",
|
|
113
|
+
"/tmp/headers.txt",
|
|
114
|
+
...(proxyArg ? ["-x", process.env.PROXY_URL] : []),
|
|
115
|
+
"-H",
|
|
116
|
+
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
117
|
+
"-H",
|
|
118
|
+
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
119
|
+
"-H",
|
|
120
|
+
"Accept-Language: en-US,en;q=0.9",
|
|
121
|
+
url,
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
const html = await cmd.stdout();
|
|
125
|
+
try {
|
|
126
|
+
const headersCmd = await sandbox.runCommand({
|
|
127
|
+
cmd: "cat",
|
|
128
|
+
args: ["/tmp/headers.txt"],
|
|
129
|
+
});
|
|
130
|
+
const headersText = await headersCmd.stdout();
|
|
131
|
+
const setCookies = Array.from(headersText.matchAll(/^set-cookie:\s*([^;\r\n]+)/gim)).map((m) => m[1]);
|
|
132
|
+
if (setCookies.length > 0) {
|
|
133
|
+
setDomainCookie(new URL(url).hostname, setCookies.join("; "));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// ignore cookie extraction failure
|
|
138
|
+
}
|
|
139
|
+
// A challenge page is not content: never return it as a successful escalation.
|
|
140
|
+
// An empty shell (e.g. an AWS WAF 202 interstitial) is not content either:
|
|
141
|
+
// parse first and fall through to the browser tier instead.
|
|
142
|
+
if (html && html.length > 100 && !isAntiBotChallenge(html, 200)) {
|
|
143
|
+
const parsed = fastHtmlToMarkdown(html, url, {
|
|
144
|
+
retainImages: options.retainImages,
|
|
145
|
+
});
|
|
146
|
+
if (parsed.markdown.trim().length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
html,
|
|
149
|
+
markdown: parsed.markdown,
|
|
150
|
+
title: parsed.title,
|
|
151
|
+
links: parsed.links,
|
|
152
|
+
statusCode: 200,
|
|
153
|
+
tier: "sandbox",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// The clean-VM curl could not produce content: this domain serves a JS
|
|
158
|
+
// challenge (or similar), so memo it and skip this tier next time.
|
|
159
|
+
if (host)
|
|
160
|
+
await markDomainChallenged(host).catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await sandbox.stop().catch(() => { });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Sandbox unavailable or not configured in this environment
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Option C: real browser that waits out the bot check (opt-in, slow).
|
|
172
|
+
if (options.browser) {
|
|
173
|
+
const viaBrowser = await escalateViaStealthBrowser(url, options);
|
|
174
|
+
if (viaBrowser)
|
|
175
|
+
return viaBrowser;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
async function escalateViaStealthBrowser(url, options) {
|
|
180
|
+
try {
|
|
181
|
+
const { stealthAvailable, stealthFetch } = await import("./engine/stealth.js");
|
|
182
|
+
if (!(await stealthAvailable()))
|
|
183
|
+
return null;
|
|
184
|
+
// Tier P2: when the domain is memoized as needing a residential egress,
|
|
185
|
+
// the stealth browser itself egresses through the proxy (resource-blocked,
|
|
186
|
+
// byte-capped inside stealthFetch) instead of a doomed datacenter IP.
|
|
187
|
+
let proxy;
|
|
188
|
+
try {
|
|
189
|
+
const { residentialProxyUrl } = await import("./engine/fetcher.js");
|
|
190
|
+
const { isDomainProxyNeeded } = await import("./engine/session.js");
|
|
191
|
+
let host = "";
|
|
192
|
+
try {
|
|
193
|
+
host = new URL(url).hostname;
|
|
194
|
+
}
|
|
195
|
+
catch { /* bad url handled below */ }
|
|
196
|
+
if (host && (await isDomainProxyNeeded(host)))
|
|
197
|
+
proxy = residentialProxyUrl() ?? undefined;
|
|
198
|
+
}
|
|
199
|
+
catch { /* proxy memo unavailable — datacenter stealth as before */ }
|
|
200
|
+
const res = await stealthFetch(url, [], { budgetMs: options.browserBudgetMs ?? 30_000, ...(proxy ? { proxy } : {}) });
|
|
201
|
+
const entry = res.entry;
|
|
202
|
+
if (!entry?.body || entry.blocked || isAntiBotChallenge(entry.body, entry.status ?? 200)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const parsed = fastHtmlToMarkdown(entry.body, entry.finalUrl || entry.url || url, {
|
|
206
|
+
retainImages: options.retainImages,
|
|
207
|
+
});
|
|
208
|
+
if (!parsed.markdown.trim())
|
|
209
|
+
return null;
|
|
210
|
+
return {
|
|
211
|
+
html: entry.body,
|
|
212
|
+
markdown: parsed.markdown,
|
|
213
|
+
title: entry.title || parsed.title,
|
|
214
|
+
links: parsed.links,
|
|
215
|
+
statusCode: entry.status ?? 200,
|
|
216
|
+
tier: "browser",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { type AggregatedProduct, type BusinessListing, type CveRecord, type FlightOffer, type HotelListing, type ProductOffer, type PropertyListing } from "../extract/index.js";
|
|
2
|
+
import type { DecisionModelId, DocumentScoreBreakdown, SearchEngineSource, UserLocationContext } from "../ranker/index.js";
|
|
3
|
+
export type { AggregatedProduct, BusinessListing, CveRecord, FlightOffer, HotelListing, ProductOffer, PropertyListing, } from "../extract/index.js";
|
|
4
|
+
export interface SearchAgentFilters {
|
|
5
|
+
/** Restrict to these engines (overrides the category's default engine plan) */
|
|
6
|
+
engines?: SearchEngineSource[];
|
|
7
|
+
excludeEngines?: SearchEngineSource[];
|
|
8
|
+
/** Prefer domains on the user's country-code TLD and demote other country TLDs */
|
|
9
|
+
preferLocalDomains?: boolean;
|
|
10
|
+
/** How to localize the upstream query: add city, country, region, or nothing. Default "auto" = per category policy */
|
|
11
|
+
localizeBy?: "auto" | "city" | "country" | "region" | "none";
|
|
12
|
+
mustIncludeTerms?: string[];
|
|
13
|
+
excludeTerms?: string[];
|
|
14
|
+
boostTerms?: string[];
|
|
15
|
+
includeDomains?: string[];
|
|
16
|
+
excludeDomains?: string[];
|
|
17
|
+
/** Target specific retailer domains or URLs (e.g. ["jbhifi.com.au", "amazon.com.au"]) */
|
|
18
|
+
sites?: string[];
|
|
19
|
+
/** Enable structured product/offer extraction & grouping (default: true when category === "shopping") */
|
|
20
|
+
productMode?: boolean;
|
|
21
|
+
/** Allow headless browser / sandbox escalation on bot-gated candidate product pages (default: false) */
|
|
22
|
+
allowBrowserEscalation?: boolean;
|
|
23
|
+
/** Free-text ranking instruction passed to the decision model */
|
|
24
|
+
customInstruction?: string;
|
|
25
|
+
/** When true, mustIncludeTerms / excludeTerms hard-drop candidates instead of only lowering scores */
|
|
26
|
+
strict?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare const SEARCH_PROVIDER_CHOICES: readonly ["auto", "ddg", "jina", "serper", "brave"];
|
|
29
|
+
export type SearchProviderChoice = (typeof SEARCH_PROVIDER_CHOICES)[number];
|
|
30
|
+
/** Providers that bill an extra credit when they serve the search. */
|
|
31
|
+
export declare const PAID_SEARCH_PROVIDERS: ReadonlySet<string>;
|
|
32
|
+
export declare const SEARCH_PROVIDER_ENV_KEYS: Record<string, string>;
|
|
33
|
+
export interface SearchRequest {
|
|
34
|
+
query: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
tbs?: string;
|
|
37
|
+
lang?: string;
|
|
38
|
+
country?: string;
|
|
39
|
+
region?: string;
|
|
40
|
+
city?: string;
|
|
41
|
+
timezone?: string;
|
|
42
|
+
location?: string | UserLocationContext;
|
|
43
|
+
/** Any category string; see GET /v1/search/categories for the built-in catalog */
|
|
44
|
+
category?: string;
|
|
45
|
+
/** Any subcategory string(s) within the category */
|
|
46
|
+
subcategory?: string | string[];
|
|
47
|
+
/** Legacy multi-category list; first entry is used when `category` is unset */
|
|
48
|
+
categories?: string[];
|
|
49
|
+
filters?: SearchAgentFilters;
|
|
50
|
+
includeDomains?: string[];
|
|
51
|
+
excludeDomains?: string[];
|
|
52
|
+
bypassCache?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Pick one search provider and skip the default multi-engine pipeline.
|
|
55
|
+
* `auto` (default) uses the free engines and falls back to Jina when results are thin.
|
|
56
|
+
*/
|
|
57
|
+
provider?: SearchProviderChoice;
|
|
58
|
+
rankOptions?: {
|
|
59
|
+
useDecisionModel?: boolean;
|
|
60
|
+
decisionModel?: DecisionModelId;
|
|
61
|
+
decisionTimeoutMs?: number;
|
|
62
|
+
quorumTimeoutMs?: number;
|
|
63
|
+
maxPerDomain?: number;
|
|
64
|
+
};
|
|
65
|
+
scrapeOptions?: {
|
|
66
|
+
formats?: Array<"markdown" | "links" | "html">;
|
|
67
|
+
retainImages?: "all" | "none";
|
|
68
|
+
scrapeTimeoutMs?: number;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export interface SearchResultItem {
|
|
72
|
+
title: string;
|
|
73
|
+
url: string;
|
|
74
|
+
canonicalUrl?: string;
|
|
75
|
+
description: string;
|
|
76
|
+
markdown?: string;
|
|
77
|
+
links?: string[];
|
|
78
|
+
publishedDate?: string;
|
|
79
|
+
score?: number;
|
|
80
|
+
rank?: number;
|
|
81
|
+
sources?: SearchEngineSource[];
|
|
82
|
+
highlights?: string[];
|
|
83
|
+
scoreBreakdown?: DocumentScoreBreakdown;
|
|
84
|
+
offer?: ProductOffer;
|
|
85
|
+
listings?: PropertyListing[];
|
|
86
|
+
flights?: FlightOffer[];
|
|
87
|
+
hotels?: HotelListing[];
|
|
88
|
+
businesses?: BusinessListing[];
|
|
89
|
+
/** CVE records this page is the record for, or mentions (category === "security") */
|
|
90
|
+
cves?: CveRecord[];
|
|
91
|
+
}
|
|
92
|
+
export interface SearchResponse {
|
|
93
|
+
success: boolean;
|
|
94
|
+
data: SearchResultItem[];
|
|
95
|
+
/** Structured product clusters with normalized multi-vendor offers and lowest-price `bestOffer` (populated by default when category === "shopping" or filters.productMode === true) */
|
|
96
|
+
products?: AggregatedProduct[];
|
|
97
|
+
/** Flat list of all extracted product offers across engines and candidate pages */
|
|
98
|
+
offers?: ProductOffer[];
|
|
99
|
+
/** Structured real-estate property listings with address, price, beds/baths/parking, propertyType, portal, and agency (populated when category === "real-estate") */
|
|
100
|
+
listings?: PropertyListing[];
|
|
101
|
+
/** Structured flight offers with airline, origin, destination, duration, stops, cabinClass, and fare price (populated when category === "flights") */
|
|
102
|
+
flights?: FlightOffer[];
|
|
103
|
+
/** Structured hotel and accommodation listings with nightly rate, star/guest ratings, address, and amenities (populated when category === "hotels") */
|
|
104
|
+
hotels?: HotelListing[];
|
|
105
|
+
/** Structured business and company listings with category, address, phone, rating, hours, and website (populated when category === "business") */
|
|
106
|
+
businesses?: BusinessListing[];
|
|
107
|
+
/** Structured CVE records from NVD and OSV: CVSS score and severity, CWE, affected products and versions, fixed versions, CISA KEV flag, dates and references (populated when category === "security") */
|
|
108
|
+
cves?: CveRecord[];
|
|
109
|
+
provider: "native-federated" | "brave" | "serper" | "jina" | "ddg-edge";
|
|
110
|
+
/** Set when a keyless search returns no hits (engines blocked or empty). */
|
|
111
|
+
error?: string;
|
|
112
|
+
/** Set when the primary engines came back thin and a paid fallback engine filled in. */
|
|
113
|
+
fallback?: "jina";
|
|
114
|
+
cached?: boolean;
|
|
115
|
+
latencyMs: number;
|
|
116
|
+
userLocation?: UserLocationContext;
|
|
117
|
+
rankingMetrics?: {
|
|
118
|
+
totalCandidates: number;
|
|
119
|
+
uniqueCandidates: number;
|
|
120
|
+
lexicalLatencyMs: number;
|
|
121
|
+
decisionLatencyMs: number;
|
|
122
|
+
totalRankingLatencyMs: number;
|
|
123
|
+
decisionModelUsed: string | null;
|
|
124
|
+
enginesContributing: SearchEngineSource[];
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export interface CategoryDefinition {
|
|
128
|
+
description: string;
|
|
129
|
+
engines: SearchEngineSource[];
|
|
130
|
+
/** Where to localize the upstream query by default */
|
|
131
|
+
localizeBy: "city" | "country" | "none";
|
|
132
|
+
preferLocalDomains: boolean;
|
|
133
|
+
subcategories: Record<string, {
|
|
134
|
+
description: string;
|
|
135
|
+
}>;
|
|
136
|
+
}
|
|
137
|
+
/** Declarative category catalog. Categories/subcategories passed by agents are free-form; this is only the default engine plan. */
|
|
138
|
+
export declare const SEARCH_CATEGORY_CATALOG: Record<string, CategoryDefinition>;
|
|
139
|
+
export declare function resolveCategory(req: SearchRequest): string;
|
|
140
|
+
export declare function planEngines(category: string, filters?: SearchAgentFilters): SearchEngineSource[];
|
|
141
|
+
/** DuckDuckGo region param (`kl`) derived from ISO-3166 country + language. */
|
|
142
|
+
export declare function ddgRegion(country?: string, lang?: string): string | undefined;
|
|
143
|
+
export declare function getSearchCategoriesCatalog(): {
|
|
144
|
+
categories: {
|
|
145
|
+
subcategories: {
|
|
146
|
+
description: string;
|
|
147
|
+
id: string;
|
|
148
|
+
}[];
|
|
149
|
+
description: string;
|
|
150
|
+
engines: SearchEngineSource[];
|
|
151
|
+
/** Where to localize the upstream query by default */
|
|
152
|
+
localizeBy: "city" | "country" | "none";
|
|
153
|
+
preferLocalDomains: boolean;
|
|
154
|
+
id: string;
|
|
155
|
+
}[];
|
|
156
|
+
notes: string;
|
|
157
|
+
filters: {
|
|
158
|
+
engines: string;
|
|
159
|
+
excludeEngines: string;
|
|
160
|
+
preferLocalDomains: string;
|
|
161
|
+
localizeBy: string;
|
|
162
|
+
mustIncludeTerms: string;
|
|
163
|
+
excludeTerms: string;
|
|
164
|
+
boostTerms: string;
|
|
165
|
+
includeDomains: string;
|
|
166
|
+
excludeDomains: string;
|
|
167
|
+
sites: string;
|
|
168
|
+
productMode: string;
|
|
169
|
+
allowBrowserEscalation: string;
|
|
170
|
+
customInstruction: string;
|
|
171
|
+
strict: string;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
export declare function normalizeHostList(domainsOrUrls?: string[]): string[];
|
|
175
|
+
export declare function hostMatchesAny(urlOrHost: string, allowedHosts: string[]): boolean;
|
|
176
|
+
export declare function resolveUserLocation(req: SearchRequest): UserLocationContext;
|