@octocrawl/cli 0.3.0 → 0.3.1
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/cli.js +2138 -343
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -385,6 +385,13 @@ function withEnvironmentProxy(policy, env) {
|
|
|
385
385
|
const egressProxy = environmentProxy(env);
|
|
386
386
|
return egressProxy === null ? policy : { ...policy, egressProxy };
|
|
387
387
|
}
|
|
388
|
+
function egressProxies(raw) {
|
|
389
|
+
const servers = (raw ?? "").split(/[\s,]+/).filter((entry3) => entry3 !== "").map((entry3, i) => proxyServer(`W2L_EGRESS_PROXIES entry ${i + 1}`, entry3));
|
|
390
|
+
return servers.filter((server, i) => servers.findIndex((other) => other.endpoint === server.endpoint) === i);
|
|
391
|
+
}
|
|
392
|
+
function withPoolProxy(policy, server) {
|
|
393
|
+
return { ...policy, origin: "operator", egressProxy: { source: "pool", https: server, http: server, noProxy: policy.egressProxy?.noProxy ?? [] } };
|
|
394
|
+
}
|
|
388
395
|
function proxyFor(url, policy) {
|
|
389
396
|
const proxy = policy.origin === "operator" ? policy.egressProxy ?? null : null;
|
|
390
397
|
if (proxy === null)
|
|
@@ -451,7 +458,7 @@ function proxyServer(name, raw) {
|
|
|
451
458
|
throw new ProxyConfigError(`${name} is not a valid proxy URL.`);
|
|
452
459
|
}
|
|
453
460
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
454
|
-
throw new ProxyConfigError(`${name} names a ${url.protocol.slice(0, -1)} proxy;
|
|
461
|
+
throw new ProxyConfigError(`${name} names a ${url.protocol.slice(0, -1)} proxy; Octocrawl supports http:// and https:// proxies.`);
|
|
455
462
|
}
|
|
456
463
|
const endpoint = `${url.hostname}:${url.port || (url.protocol === "https:" ? "443" : "80")}`;
|
|
457
464
|
return {
|
|
@@ -547,7 +554,11 @@ function isResearchUserAgent(userAgent) {
|
|
|
547
554
|
return /\bw2l-research\b/.test(userAgent) || userAgent.startsWith(`${SEC_DECLARED_NAME} `);
|
|
548
555
|
}
|
|
549
556
|
function robotsAgent(userAgent) {
|
|
550
|
-
|
|
557
|
+
const declared3 = userAgent.startsWith(`${SEC_DECLARED_NAME} `) ? `${userAgent} ${RESEARCH_PRODUCT_TOKEN}` : userAgent;
|
|
558
|
+
return `${declared3} ${PRODUCT_ROBOTS_TOKEN}`;
|
|
559
|
+
}
|
|
560
|
+
function isOctocrawlRobotsGroup(matchedAgent) {
|
|
561
|
+
return matchedAgent === PRODUCT_ROBOTS_TOKEN || matchedAgent === RESEARCH_PRODUCT_TOKEN;
|
|
551
562
|
}
|
|
552
563
|
function operatorContact(env) {
|
|
553
564
|
const contact = (env["W2L_CONTACT"] ?? "").trim();
|
|
@@ -639,7 +650,7 @@ function checkIdentityHonesty(identity, sent) {
|
|
|
639
650
|
}
|
|
640
651
|
return { honest: mismatches.length === 0, mismatches };
|
|
641
652
|
}
|
|
642
|
-
var RESEARCH_UA_COMMENT, RESEARCH_USER_AGENT, MAX_CONTACT_LENGTH, SEC_DECLARED_NAME, RESEARCH_PRODUCT_TOKEN, CHROME_MAJOR_FLOOR, BROWSER_FINGERPRINT, MOBILE_BROWSER_FINGERPRINT, PREVIEW_PRODUCT_TOKEN, MODE_IDENTITIES;
|
|
653
|
+
var RESEARCH_UA_COMMENT, RESEARCH_USER_AGENT, MAX_CONTACT_LENGTH, SEC_DECLARED_NAME, RESEARCH_PRODUCT_TOKEN, PRODUCT_ROBOTS_TOKEN, CHROME_MAJOR_FLOOR, BROWSER_FINGERPRINT, MOBILE_BROWSER_FINGERPRINT, PREVIEW_PRODUCT_TOKEN, MODE_IDENTITIES;
|
|
643
654
|
var init_compliance = __esm({
|
|
644
655
|
"packages/contracts/dist/compliance.js"() {
|
|
645
656
|
"use strict";
|
|
@@ -648,6 +659,7 @@ var init_compliance = __esm({
|
|
|
648
659
|
MAX_CONTACT_LENGTH = 200;
|
|
649
660
|
SEC_DECLARED_NAME = "W2L Research";
|
|
650
661
|
RESEARCH_PRODUCT_TOKEN = "w2l-research";
|
|
662
|
+
PRODUCT_ROBOTS_TOKEN = "octocrawl";
|
|
651
663
|
CHROME_MAJOR_FLOOR = 128;
|
|
652
664
|
BROWSER_FINGERPRINT = {
|
|
653
665
|
locale: "en-US",
|
|
@@ -1577,11 +1589,11 @@ function readSchema(value, at = "schema") {
|
|
|
1577
1589
|
if (depth > 0)
|
|
1578
1590
|
unsupported(where, key, "only the root may declare it");
|
|
1579
1591
|
} else if (!SCHEMA_KEYS.has(key)) {
|
|
1580
|
-
unsupported(where, key, "
|
|
1592
|
+
unsupported(where, key, "Octocrawl extraction does not support it");
|
|
1581
1593
|
}
|
|
1582
1594
|
}
|
|
1583
1595
|
if (rec.$schema !== void 0 && (typeof rec.$schema !== "string" || !SCHEMA_DIALECTS.test(rec.$schema))) {
|
|
1584
|
-
unsupported(where, "$schema", `
|
|
1596
|
+
unsupported(where, "$schema", `Octocrawl follows JSON Schema draft-07, 2019-09 and 2020-12, not ${JSON.stringify(rec.$schema)}`);
|
|
1585
1597
|
}
|
|
1586
1598
|
if (rec.$id !== void 0 && typeof rec.$id !== "string")
|
|
1587
1599
|
invalid(where, "$id must be a string");
|
|
@@ -1669,7 +1681,7 @@ function readSchema(value, at = "schema") {
|
|
|
1669
1681
|
const union = branches;
|
|
1670
1682
|
const nullable2 = union.length === 2 && union.some(isNullSchema);
|
|
1671
1683
|
if (!nullable2 && !union.every(isPrimitiveSchema))
|
|
1672
|
-
unsupported(where, key, "
|
|
1684
|
+
unsupported(where, key, "Octocrawl maps a schema-or-null union or a union of primitive types, not a union of objects, arrays or references");
|
|
1673
1685
|
union.forEach((branch, index) => visit(branch, depth + 1, `${where}.${key}[${index}]`));
|
|
1674
1686
|
}
|
|
1675
1687
|
};
|
|
@@ -1699,7 +1711,7 @@ function readListFormat(rec, name) {
|
|
|
1699
1711
|
};
|
|
1700
1712
|
if (rec.itemSelector === void 0) {
|
|
1701
1713
|
if (rec.fields !== void 0)
|
|
1702
|
-
throw new RequestError(`${name}.fields needs an itemSelector: without one,
|
|
1714
|
+
throw new RequestError(`${name}.fields needs an itemSelector: without one, Octocrawl finds the list and its fields itself`);
|
|
1703
1715
|
return { type: "list" };
|
|
1704
1716
|
}
|
|
1705
1717
|
const itemSelector = selectorOf2(rec.itemSelector, `${name}.itemSelector`);
|
|
@@ -1945,7 +1957,7 @@ function readSelectors(value, name) {
|
|
|
1945
1957
|
}
|
|
1946
1958
|
function headerRefusal(name) {
|
|
1947
1959
|
if (name === "user-agent" || name.startsWith("sec-ch-") || name.startsWith("sec-fetch-"))
|
|
1948
|
-
return `headers.${name} is refused: the User-Agent and client hints are
|
|
1960
|
+
return `headers.${name} is refused: the User-Agent and client hints are Octocrawl's declared identity`;
|
|
1949
1961
|
if (CREDENTIAL_HEADERS.has(name))
|
|
1950
1962
|
return `headers.${name} is refused: credentials are not sent as headers; mode 'authed' carries your own session on the record`;
|
|
1951
1963
|
if (TRANSPORT_HEADERS.has(name))
|
|
@@ -2011,7 +2023,7 @@ function readParsers(value) {
|
|
|
2011
2023
|
const name = `parsers[${index}]`;
|
|
2012
2024
|
const type = typeof entry3 === "string" ? entry3 : entry3 !== null && typeof entry3 === "object" && !Array.isArray(entry3) ? entry3.type : void 0;
|
|
2013
2025
|
if (type === "image")
|
|
2014
|
-
throw new RequestError(`${name} is refused:
|
|
2026
|
+
throw new RequestError(`${name} is refused: Octocrawl reads no image as a document (no OCR)`, "unsupported_parameter", { parameters: [name] });
|
|
2015
2027
|
if (type !== "pdf")
|
|
2016
2028
|
throw new RequestError(`${name} must be "pdf" or { type: "pdf", mode, maxPages, pages, pageMarkers }`);
|
|
2017
2029
|
if (parsers.length > 0)
|
|
@@ -2023,7 +2035,7 @@ function readParsers(value) {
|
|
|
2023
2035
|
const rec = entry3;
|
|
2024
2036
|
rejectUnknownKeys(rec, PDF_PARSER_KEYS, name);
|
|
2025
2037
|
if (rec.mode === "ocr")
|
|
2026
|
-
throw new RequestError(`${name}.mode "ocr" is refused:
|
|
2038
|
+
throw new RequestError(`${name}.mode "ocr" is refused: Octocrawl reads a PDF's text layer and runs no OCR`, "unsupported_parameter", { parameters: [`${name}.mode`] });
|
|
2027
2039
|
if (rec.mode !== void 0 && rec.mode !== "fast" && rec.mode !== "auto")
|
|
2028
2040
|
throw new RequestError(`${name}.mode must be "fast" or "auto"`);
|
|
2029
2041
|
const maxPages = rec.maxPages;
|
|
@@ -2260,6 +2272,7 @@ function parseCrawlStartRequest(body) {
|
|
|
2260
2272
|
const maxConcurrency = readConcurrency(rec.maxConcurrency);
|
|
2261
2273
|
const idempotencyKey = readIdempotencyKey(rec.idempotencyKey);
|
|
2262
2274
|
const webhook = readWebhook(rec.webhook);
|
|
2275
|
+
const ignoreRobotsTxt = readBoolean(rec.ignoreRobotsTxt, "ignoreRobotsTxt");
|
|
2263
2276
|
const req = {
|
|
2264
2277
|
url: readUrl(rec.url),
|
|
2265
2278
|
mode,
|
|
@@ -2276,6 +2289,7 @@ function parseCrawlStartRequest(body) {
|
|
|
2276
2289
|
...maxConcurrency === void 0 ? {} : { maxConcurrency },
|
|
2277
2290
|
...idempotencyKey === void 0 ? {} : { idempotencyKey },
|
|
2278
2291
|
...webhook === void 0 ? {} : { webhook },
|
|
2292
|
+
...ignoreRobotsTxt === void 0 ? {} : { ignoreRobotsTxt },
|
|
2279
2293
|
...page,
|
|
2280
2294
|
...readAttribution(rec)
|
|
2281
2295
|
};
|
|
@@ -2313,6 +2327,7 @@ function parseMapRequest(body) {
|
|
|
2313
2327
|
}
|
|
2314
2328
|
const includePaths = readPathPatterns(rec.includePaths, "includePaths");
|
|
2315
2329
|
const excludePaths = readPathPatterns(rec.excludePaths, "excludePaths");
|
|
2330
|
+
const ignoreRobotsTxt = readBoolean(rec.ignoreRobotsTxt, "ignoreRobotsTxt");
|
|
2316
2331
|
return {
|
|
2317
2332
|
url: readUrl(rec.url),
|
|
2318
2333
|
...rec.mode === void 0 ? {} : { mode: rec.mode },
|
|
@@ -2323,6 +2338,7 @@ function parseMapRequest(body) {
|
|
|
2323
2338
|
...scope,
|
|
2324
2339
|
...includePaths === void 0 ? {} : { includePaths },
|
|
2325
2340
|
...excludePaths === void 0 ? {} : { excludePaths },
|
|
2341
|
+
...ignoreRobotsTxt === void 0 ? {} : { ignoreRobotsTxt },
|
|
2326
2342
|
...readAttribution(rec)
|
|
2327
2343
|
};
|
|
2328
2344
|
}
|
|
@@ -2488,23 +2504,23 @@ var init_api = __esm({
|
|
|
2488
2504
|
}
|
|
2489
2505
|
};
|
|
2490
2506
|
REFUSAL_HINTS = {
|
|
2491
|
-
stealth: "
|
|
2492
|
-
ignoreRobotsTxt: "robots.txt is always read; a
|
|
2493
|
-
hostedSkipTlsVerification: "a hosted server verifies every certificate; run
|
|
2494
|
-
useIndex: "
|
|
2507
|
+
stealth: "Octocrawl has no stealth option on a request: a provider's stealth or challenge solving runs only on a server started with an access grant that names it (--access-grant, ADR 0005); a proxy or session you own (mode authed) is the other route",
|
|
2508
|
+
ignoreRobotsTxt: "robots.txt is always read and recorded; on a local server a URL a scrape or batch names is fetched whatever it says, and ignoreRobotsTxt on a crawl or map fetches the links it disallows, on the record",
|
|
2509
|
+
hostedSkipTlsVerification: "a hosted server verifies every certificate; run Octocrawl locally to use skipTlsVerification, which is recorded in the trace and a tls_unverified warning",
|
|
2510
|
+
useIndex: "Octocrawl keeps no URL index: a map reads the sitemaps the site declares and its start page, on the record; crawl reads further pages",
|
|
2495
2511
|
actions: "actions run on scrape and batch, where each page named gets the same steps; a crawl or a map does not take them"
|
|
2496
2512
|
};
|
|
2497
2513
|
PAGE_KEYS = ["onlyMainContent", "waitFor", "timeout", "maxFileBytes", "includeTags", "excludeTags", "headers", "mobile", "skipTlsVerification", "fastMode", "blockAds", "removeBase64Images", "parsers", "maxAge", "minAge", "storeInCache", "lockdown"];
|
|
2498
2514
|
ATTRIBUTION_KEYS = ["origin", "integration"];
|
|
2499
2515
|
SCRAPE_KEYS = ["url", "mode", "allowlistedDomains", "formats", "includeLinks", "debug", "robotsOverride", "actions", "handoff", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2500
2516
|
CRAWL_SCOPE_KEYS = ["regexOnFullURL", "ignoreQueryParameters", "deduplicateSimilarURLs", "crawlEntireDomain", "allowSubdomains", "allowExternalLinks"];
|
|
2501
|
-
CRAWL_KEYS = ["url", "mode", "maxPages", "maxDepth", "useCached", "allowlistedDomains", "formats", "includeLinks", "includePaths", "excludePaths", ...CRAWL_SCOPE_KEYS, "sitemap", "maxConcurrency", "idempotencyKey", "webhook", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2517
|
+
CRAWL_KEYS = ["url", "mode", "maxPages", "maxDepth", "useCached", "allowlistedDomains", "formats", "includeLinks", "includePaths", "excludePaths", ...CRAWL_SCOPE_KEYS, "sitemap", "maxConcurrency", "idempotencyKey", "webhook", "ignoreRobotsTxt", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2502
2518
|
BATCH_SCOPE_NOOP_KEYS = { allowExternalLinks: "allowExternalLinks", includeSubdomains: "allowSubdomains" };
|
|
2503
2519
|
BATCH_KEYS = ["urls", "mode", "formats", "includeLinks", "robotsOverrides", "maxConcurrency", "ignoreInvalidURLs", "allowExternalLinks", "includeSubdomains", "idempotencyKey", "appendToId", "webhook", "actions", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2504
2520
|
BATCH_APPEND_KEYS = ["urls", "appendToId", "ignoreInvalidURLs", "allowExternalLinks", "includeSubdomains", "idempotencyKey", "robotsOverrides", ...ATTRIBUTION_KEYS];
|
|
2505
2521
|
ROBOTS_OVERRIDE_KEYS = ["reason", "recordedBy"];
|
|
2506
2522
|
MAP_SCOPE_KEYS = ["includeSubdomains", "ignoreQueryParameters", "regexOnFullURL", "crawlEntireDomain", "deduplicateSimilarURLs"];
|
|
2507
|
-
MAP_KEYS = ["url", "mode", "limit", "timeout", "search", "sitemap", ...MAP_SCOPE_KEYS, "includePaths", "excludePaths", ...ATTRIBUTION_KEYS];
|
|
2523
|
+
MAP_KEYS = ["url", "mode", "limit", "timeout", "search", "sitemap", ...MAP_SCOPE_KEYS, "includePaths", "excludePaths", "ignoreRobotsTxt", ...ATTRIBUTION_KEYS];
|
|
2508
2524
|
WEBHOOK_KEYS = ["url", "headers", "metadata", "events", "secretEnv"];
|
|
2509
2525
|
WEBHOOK_RESERVED_HEADERS = /* @__PURE__ */ new Set(["content-type", "content-length", "host", "connection", "transfer-encoding"]);
|
|
2510
2526
|
WEBHOOK_SECRET_ENV = /^W2L_WEBHOOK_SECRET_[A-Z0-9_]+$/;
|
|
@@ -2585,7 +2601,7 @@ function parseFirecrawlScrapeRequest(body) {
|
|
|
2585
2601
|
function parseFirecrawlCrawlRequest(body) {
|
|
2586
2602
|
const rec = asRecord2(body);
|
|
2587
2603
|
const problems = noProblems();
|
|
2588
|
-
checkShimKeys(rec, "", ["url", ...SHIM_ATTRIBUTION, "limit", "maxDepth", "includePaths", "excludePaths", "ignoreSitemap", "sitemapOnly", "scrapeOptions", ...SHIM_CRAWL_SCOPE_OPTIONS, "allowBackwardLinks", "crawlEntireDomain", "idempotencyKey", "webhook"], problems);
|
|
2604
|
+
checkShimKeys(rec, "", ["url", ...SHIM_ATTRIBUTION, "limit", "maxDepth", "includePaths", "excludePaths", "ignoreSitemap", "sitemapOnly", "scrapeOptions", ...SHIM_CRAWL_SCOPE_OPTIONS, "allowBackwardLinks", "crawlEntireDomain", "idempotencyKey", "webhook", "ignoreRobotsTxt"], problems);
|
|
2589
2605
|
let pageOptions2 = { parsers: shimParsers(void 0, void 0, "scrapeOptions.") };
|
|
2590
2606
|
if (rec.scrapeOptions !== void 0) {
|
|
2591
2607
|
const options = rec.scrapeOptions;
|
|
@@ -2626,6 +2642,8 @@ function parseFirecrawlCrawlRequest(body) {
|
|
|
2626
2642
|
native.crawlEntireDomain = rec.crawlEntireDomain;
|
|
2627
2643
|
if (rec.idempotencyKey !== void 0)
|
|
2628
2644
|
native.idempotencyKey = rec.idempotencyKey;
|
|
2645
|
+
if (rec.ignoreRobotsTxt !== void 0)
|
|
2646
|
+
native.ignoreRobotsTxt = rec.ignoreRobotsTxt;
|
|
2629
2647
|
if (rec.webhook !== void 0)
|
|
2630
2648
|
native.webhook = rec.webhook;
|
|
2631
2649
|
const parsed = parseCrawlStartRequest(native);
|
|
@@ -3017,9 +3035,9 @@ var init_evidenceRecord = __esm({
|
|
|
3017
3035
|
EVIDENCE_SCHEMA_VERSION = "w2l.evidence/1";
|
|
3018
3036
|
keysOf = () => (keys) => keys;
|
|
3019
3037
|
EVIDENCE_RECORD_KEYS = {
|
|
3020
|
-
record: keysOf()(["schemaVersion", "requestedUrl", "finalUrl", "redirectChain", "fetchedAt", "httpStatus", "status", "reason", "lane", "robotsDecision", "rawSha256", "contentEncoding", "outputSha256", "extractor", "fieldEvidence", "artifacts", "proxy", "identity", "pageActions"]),
|
|
3038
|
+
record: keysOf()(["schemaVersion", "requestedUrl", "finalUrl", "redirectChain", "fetchedAt", "httpStatus", "status", "reason", "lane", "robotsDecision", "rawSha256", "contentEncoding", "outputSha256", "extractor", "fieldEvidence", "artifacts", "proxy", "identity", "pageActions", "access"]),
|
|
3021
3039
|
redirectChain: keysOf()(["urls", "complete"]),
|
|
3022
|
-
robotsDecision: keysOf()(["decision", "robotsUrl", "robotsSha256", "unreachable", "crawlDelayMs", "userOverride"]),
|
|
3040
|
+
robotsDecision: keysOf()(["decision", "robotsUrl", "robotsSha256", "unreachable", "crawlDelayMs", "userOverride", "overrideBasis"]),
|
|
3023
3041
|
outputSha256: keysOf()(["markdown", "json"]),
|
|
3024
3042
|
extractor: keysOf()(["name", "version", "commit"]),
|
|
3025
3043
|
fieldEvidence: keysOf()(["source", "locator"]),
|
|
@@ -3027,7 +3045,8 @@ var init_evidenceRecord = __esm({
|
|
|
3027
3045
|
identity: keysOf()(["userAgent", "mode", "contact", "device", "requestHeaders"]),
|
|
3028
3046
|
pageActions: keysOf()(["steps", "scriptRan"]),
|
|
3029
3047
|
pageActionStep: keysOf()(["type", "outcome"]),
|
|
3030
|
-
requestHeader: keysOf()(["name", "valueSha256"])
|
|
3048
|
+
requestHeader: keysOf()(["name", "valueSha256"]),
|
|
3049
|
+
access: keysOf()(["route", "executor", "executorVersion", "profile", "externalCostUsd"])
|
|
3031
3050
|
};
|
|
3032
3051
|
}
|
|
3033
3052
|
});
|
|
@@ -3243,7 +3262,7 @@ function evaluateRobots(robots, userAgent, path) {
|
|
|
3243
3262
|
return { allowed: true, appliedRules: [], matchedAgent: null, crawlDelayMs: null };
|
|
3244
3263
|
}
|
|
3245
3264
|
const ua = userAgent.toLowerCase();
|
|
3246
|
-
const matchedAgent = group.agents.
|
|
3265
|
+
const matchedAgent = group.agents.filter((a) => a !== "*" && ua.includes(a)).sort((a, b) => b.length - a.length)[0] ?? group.agents.find((a) => a === "*") ?? null;
|
|
3247
3266
|
const appliedRules = group.rules.filter((r) => globMatches(r.tokens, path)).sort((a, b) => b.patternLength === a.patternLength ? Number(b.allow) - Number(a.allow) : b.patternLength - a.patternLength);
|
|
3248
3267
|
return {
|
|
3249
3268
|
allowed: isAllowed(group.rules, path),
|
|
@@ -3660,6 +3679,14 @@ var init_resilient = __esm({
|
|
|
3660
3679
|
});
|
|
3661
3680
|
|
|
3662
3681
|
// packages/http-core/dist/gate.js
|
|
3682
|
+
function perimeterXChallenge(head, lower) {
|
|
3683
|
+
if (!lower.includes("_pxappid") || !PX_CAPTCHA_SCRIPT.test(lower))
|
|
3684
|
+
return null;
|
|
3685
|
+
const signals = ["px_captcha_script", "px_app_id"];
|
|
3686
|
+
if (PX_CAPTCHA_CONTAINER.test(head))
|
|
3687
|
+
signals.push("px_captcha_container");
|
|
3688
|
+
return signals;
|
|
3689
|
+
}
|
|
3663
3690
|
function challengePlatformBeyondJsd(lower) {
|
|
3664
3691
|
const marker = "/cdn-cgi/challenge-platform";
|
|
3665
3692
|
for (let at = lower.indexOf(marker); at !== -1; at = lower.indexOf(marker, at + marker.length)) {
|
|
@@ -3692,6 +3719,9 @@ function classifyGate(res) {
|
|
|
3692
3719
|
const lower = head.toLowerCase();
|
|
3693
3720
|
const contentful = res.contentful === true;
|
|
3694
3721
|
if (res.status === 429) {
|
|
3722
|
+
const px2 = perimeterXChallenge(head, lower);
|
|
3723
|
+
if (px2 !== null)
|
|
3724
|
+
return { reason: "captcha", signals: [...px2, "status_429"] };
|
|
3695
3725
|
return { reason: "rate_limit", signals: ["status_429"] };
|
|
3696
3726
|
}
|
|
3697
3727
|
if (res.status === 451) {
|
|
@@ -3718,6 +3748,12 @@ function classifyGate(res) {
|
|
|
3718
3748
|
if (/<form\b[^>]*\bhidden\b/i.test(head) && /\bname\s*=\s*["']js_challenge["']/i.test(head) && /\bname\s*=\s*["']jsc_token["']/i.test(head)) {
|
|
3719
3749
|
return { reason: "bot_detected_generic", signals: ["reddit_js_verification"] };
|
|
3720
3750
|
}
|
|
3751
|
+
if (/<title[^>]*>\s*reddit - prove your humanity\s*<\/title>/.test(lower) && lower.includes("google.com/recaptcha/api.js")) {
|
|
3752
|
+
return { reason: "captcha", signals: ["reddit_captcha_title", "recaptcha_script"] };
|
|
3753
|
+
}
|
|
3754
|
+
const px = perimeterXChallenge(head, lower);
|
|
3755
|
+
if (px !== null)
|
|
3756
|
+
return { reason: "captcha", signals: px };
|
|
3721
3757
|
if (contentful)
|
|
3722
3758
|
return null;
|
|
3723
3759
|
const captcha = matched(lower, CAPTCHA_MARKERS);
|
|
@@ -3792,7 +3828,7 @@ function nextLane(reason2, from) {
|
|
|
3792
3828
|
return from === "browser_proxy" ? null : "browser_proxy";
|
|
3793
3829
|
}
|
|
3794
3830
|
}
|
|
3795
|
-
var HEAD_BYTES, VENDOR_HEADERS, CF_STRONG_MARKERS, CF_INTERSTITIAL_PAIR, CAPTCHA_MARKERS, LOGIN_INTENT_PHRASES, PASSWORD_INPUT, TITLE_OR_H1, BARE_LOGIN_HEADING, GEO_MARKERS, BOT_STRONG_MARKERS, BOT_WEAK_MARKERS, GATE_SHAPED_STATUS, CF_JSD_PATH;
|
|
3831
|
+
var HEAD_BYTES, VENDOR_HEADERS, CF_STRONG_MARKERS, CF_INTERSTITIAL_PAIR, CAPTCHA_MARKERS, LOGIN_INTENT_PHRASES, PASSWORD_INPUT, TITLE_OR_H1, BARE_LOGIN_HEADING, GEO_MARKERS, BOT_STRONG_MARKERS, BOT_WEAK_MARKERS, GATE_SHAPED_STATUS, CF_JSD_PATH, PX_CAPTCHA_SCRIPT, PX_CAPTCHA_CONTAINER;
|
|
3796
3832
|
var init_gate = __esm({
|
|
3797
3833
|
"packages/http-core/dist/gate.js"() {
|
|
3798
3834
|
"use strict";
|
|
@@ -3826,7 +3862,9 @@ var init_gate = __esm({
|
|
|
3826
3862
|
["challenges.cloudflare.com/turnstile", "widget_turnstile_script"],
|
|
3827
3863
|
["funcaptcha", "widget_funcaptcha"],
|
|
3828
3864
|
["arkoselabs", "widget_arkose"],
|
|
3829
|
-
["data-sitekey", "widget_sitekey"]
|
|
3865
|
+
["data-sitekey", "widget_sitekey"],
|
|
3866
|
+
// PerimeterX's press-and-hold prompt, as Walmart words it: the widget's own instruction.
|
|
3867
|
+
["activate and hold the button", "text_activate_and_hold"]
|
|
3830
3868
|
];
|
|
3831
3869
|
LOGIN_INTENT_PHRASES = [
|
|
3832
3870
|
"sign in to continue",
|
|
@@ -3860,6 +3898,7 @@ var init_gate = __esm({
|
|
|
3860
3898
|
["unusual traffic from your computer network", "text_unusual_traffic"],
|
|
3861
3899
|
["automated queries", "text_automated_queries"],
|
|
3862
3900
|
["are you a robot", "text_are_you_a_robot"],
|
|
3901
|
+
["robot or human?", "text_robot_or_human"],
|
|
3863
3902
|
["verify you are human", "text_verify_you_are_human"],
|
|
3864
3903
|
["verifying you are human", "text_verifying_you_are_human"],
|
|
3865
3904
|
["bot detected", "text_bot_detected"],
|
|
@@ -3878,6 +3917,8 @@ var init_gate = __esm({
|
|
|
3878
3917
|
];
|
|
3879
3918
|
GATE_SHAPED_STATUS = [403, 429, 503];
|
|
3880
3919
|
CF_JSD_PATH = "/cdn-cgi/challenge-platform/scripts/jsd/";
|
|
3920
|
+
PX_CAPTCHA_SCRIPT = /\/captcha\/captcha\.js|captcha\.px-(?:cdn|cloud)\.net\/[^"'\s<>]*captcha\.js/;
|
|
3921
|
+
PX_CAPTCHA_CONTAINER = /<[a-z][^>]*\sid\s*=\s*["']?px-captcha(?=["'\s>/])/i;
|
|
3881
3922
|
}
|
|
3882
3923
|
});
|
|
3883
3924
|
|
|
@@ -4149,18 +4190,47 @@ var init_access2 = __esm({
|
|
|
4149
4190
|
});
|
|
4150
4191
|
|
|
4151
4192
|
// packages/http-core/dist/vendor.js
|
|
4193
|
+
function evaluateAccessCapability(capability, granted = []) {
|
|
4194
|
+
const forever = REFUSED_FOREVER.find((e) => e.capability === capability);
|
|
4195
|
+
if (forever !== void 0) {
|
|
4196
|
+
return { capability, decision: "refused", reason: `${capability} is never enabled (ADR 0005): ${forever.principle}` };
|
|
4197
|
+
}
|
|
4198
|
+
const deferred = DEFERRED.find((e) => e.capability === capability);
|
|
4199
|
+
if (deferred !== void 0) {
|
|
4200
|
+
return {
|
|
4201
|
+
capability,
|
|
4202
|
+
decision: "deferred",
|
|
4203
|
+
source: deferred.source,
|
|
4204
|
+
restartWhen: deferred.restartWhen,
|
|
4205
|
+
reason: `${capability} is deferred (ADR 0005; ${deferred.source}, "${deferred.row}"); it restarts when: ${deferred.restartWhen}`
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
if (AUTHORIZABLE.some((e) => e.capability === capability)) {
|
|
4209
|
+
return granted.includes(capability) ? { capability, decision: "granted" } : { capability, decision: "not_granted", reason: `${capability} is off unless a grant names it (ADR 0005)` };
|
|
4210
|
+
}
|
|
4211
|
+
return { capability, decision: "unknown", reason: `${capability} is not an access capability ADR 0005 names` };
|
|
4212
|
+
}
|
|
4152
4213
|
function evaluateVendorPolicy(offers, policy = DEFAULT_VENDOR_POLICY) {
|
|
4153
|
-
const authorized = new Set(policy.authorized ?? []);
|
|
4214
|
+
const authorized = [...new Set(policy.authorized ?? [])];
|
|
4215
|
+
const operational = OPERATIONAL_POLICY_KEYS;
|
|
4216
|
+
const declined = authorized.filter((key) => !operational.includes(key)).map((key) => evaluateAccessCapability(key, authorized)).filter((v) => v.decision === "refused" || v.decision === "deferred" || v.decision === "unknown");
|
|
4154
4217
|
const enabled = [];
|
|
4218
|
+
const withheld = [];
|
|
4155
4219
|
for (const offer of offers) {
|
|
4156
|
-
|
|
4220
|
+
const access = VENDOR_CAPABILITY_ACCESS[offer.capability];
|
|
4221
|
+
if (access !== void 0) {
|
|
4222
|
+
if (evaluateAccessCapability(access, authorized).decision === "granted") {
|
|
4223
|
+
enabled.push({ capability: offer.capability, enableKey: access, optOutRequired: offer.vendorDefaultOn });
|
|
4224
|
+
} else {
|
|
4225
|
+
withheld.push(offer.capability);
|
|
4226
|
+
}
|
|
4157
4227
|
continue;
|
|
4158
4228
|
}
|
|
4159
4229
|
if (offer.enableKey === null) {
|
|
4160
4230
|
enabled.push({ capability: offer.capability, enableKey: null, optOutRequired: offer.vendorDefaultOn });
|
|
4161
4231
|
continue;
|
|
4162
4232
|
}
|
|
4163
|
-
if (authorized.
|
|
4233
|
+
if (authorized.includes(offer.enableKey)) {
|
|
4164
4234
|
enabled.push({
|
|
4165
4235
|
capability: offer.capability,
|
|
4166
4236
|
enableKey: offer.enableKey,
|
|
@@ -4168,20 +4238,99 @@ function evaluateVendorPolicy(offers, policy = DEFAULT_VENDOR_POLICY) {
|
|
|
4168
4238
|
});
|
|
4169
4239
|
}
|
|
4170
4240
|
}
|
|
4171
|
-
const
|
|
4172
|
-
|
|
4241
|
+
const vendorKeys = /* @__PURE__ */ new Set([...operational, ...Object.values(VENDOR_CAPABILITY_ACCESS)]);
|
|
4242
|
+
const offered = new Set(offers.flatMap((o) => [o.enableKey, VENDOR_CAPABILITY_ACCESS[o.capability]]));
|
|
4243
|
+
const declinedKeys = new Set(declined.map((v) => v.capability));
|
|
4244
|
+
const unauthorized = authorized.filter((key) => vendorKeys.has(key) && !declinedKeys.has(key) && !offered.has(key));
|
|
4245
|
+
return { enabled, unauthorized, withheld, declined, refused: REFUSED_FOREVER.map((e) => e.capability) };
|
|
4173
4246
|
}
|
|
4174
|
-
var
|
|
4247
|
+
var REFUSED_FOREVER, DEFERRED, AUTHORIZABLE, VENDOR_CAPABILITY_ACCESS, DEFAULT_VENDOR_POLICY, OPERATIONAL_POLICY_KEYS, AUTHORIZABLE_POLICY_KEYS;
|
|
4175
4248
|
var init_vendor = __esm({
|
|
4176
4249
|
"packages/http-core/dist/vendor.js"() {
|
|
4177
4250
|
"use strict";
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4251
|
+
REFUSED_FOREVER = [
|
|
4252
|
+
{
|
|
4253
|
+
capability: "identity_rotation",
|
|
4254
|
+
principle: "cycling identities to outlast a block, including answering a 429 with another IP and carrying on"
|
|
4255
|
+
},
|
|
4256
|
+
{
|
|
4257
|
+
capability: "patch_user_chrome",
|
|
4258
|
+
principle: "modifying, patching or injecting into the person's own Chrome"
|
|
4259
|
+
},
|
|
4260
|
+
{
|
|
4261
|
+
capability: "in_session_route_switch",
|
|
4262
|
+
principle: "switching executor or egress inside an established interactive session without saying so"
|
|
4263
|
+
},
|
|
4264
|
+
{
|
|
4265
|
+
capability: "secrets_in_records",
|
|
4266
|
+
principle: "writing proxy credentials, cookies, solver tokens or a cdpEndpoint into a record or a log"
|
|
4267
|
+
}
|
|
4268
|
+
];
|
|
4269
|
+
DEFERRED = [
|
|
4270
|
+
{
|
|
4271
|
+
capability: "own_browser_engine",
|
|
4272
|
+
source: "ROADMAP.md:263",
|
|
4273
|
+
row: "An own browser fork or engine, and broad custom-fingerprint research",
|
|
4274
|
+
restartWhen: "A maintained project stops working for a class of tasks that PA's set shows matters"
|
|
4275
|
+
},
|
|
4276
|
+
{
|
|
4277
|
+
capability: "own_fingerprint_patches",
|
|
4278
|
+
source: "ROADMAP.md:263",
|
|
4279
|
+
row: "An own browser fork or engine, and broad custom-fingerprint research",
|
|
4280
|
+
restartWhen: "A maintained project stops working for a class of tasks that PA's set shows matters"
|
|
4281
|
+
},
|
|
4282
|
+
{
|
|
4283
|
+
capability: "own_captcha_model",
|
|
4284
|
+
source: "ROADMAP.md:264",
|
|
4285
|
+
row: "An in-house CAPTCHA model",
|
|
4286
|
+
restartWhen: "Only if solver cost or coverage blocks paying users"
|
|
4287
|
+
},
|
|
4288
|
+
{
|
|
4289
|
+
capability: "own_residential_network",
|
|
4290
|
+
source: "ROADMAP.md:265",
|
|
4291
|
+
row: "An own residential IP network",
|
|
4292
|
+
restartWhen: "Not restarted; PA uses providers"
|
|
4293
|
+
},
|
|
4294
|
+
{
|
|
4295
|
+
capability: "camoufox",
|
|
4296
|
+
source: "ROADMAP.md:266",
|
|
4297
|
+
row: "A second stealth engine (Camoufox)",
|
|
4298
|
+
restartWhen: "Patchright leaves a clear class of PA tasks unsolved"
|
|
4299
|
+
},
|
|
4300
|
+
{
|
|
4301
|
+
capability: "hosted_browser_cluster",
|
|
4302
|
+
source: "ROADMAP.md:267",
|
|
4303
|
+
row: "A hosted browser cluster beyond Cloud Run's instance cap",
|
|
4304
|
+
restartWhen: "PH's phase 2 shows browser-lane demand that the instance cap cannot serve, and a key holder will pay for it"
|
|
4305
|
+
}
|
|
4306
|
+
];
|
|
4307
|
+
AUTHORIZABLE = [
|
|
4308
|
+
{ capability: "compatible_transport", tier: "standard", thirdPartyCost: false },
|
|
4309
|
+
{ capability: "egress_sessions", tier: "standard", thirdPartyCost: false },
|
|
4310
|
+
{ capability: "enhanced_browser", tier: "enhanced", thirdPartyCost: false },
|
|
4311
|
+
{ capability: "vendor_remote_browser", tier: "enhanced", thirdPartyCost: true },
|
|
4312
|
+
{ capability: "vendor_unlock_html", tier: "enhanced", thirdPartyCost: true },
|
|
4313
|
+
{ capability: "vendor_captcha_solving", tier: "enhanced", thirdPartyCost: true },
|
|
4314
|
+
{ capability: "vendor_stealth", tier: "enhanced", thirdPartyCost: true },
|
|
4315
|
+
{ capability: "third_party_captcha_solver", tier: "enhanced", thirdPartyCost: true }
|
|
4183
4316
|
];
|
|
4317
|
+
VENDOR_CAPABILITY_ACCESS = {
|
|
4318
|
+
fingerprint_spoofing: "vendor_stealth",
|
|
4319
|
+
cdp_patching: "vendor_stealth",
|
|
4320
|
+
captcha_solving: "vendor_captcha_solving",
|
|
4321
|
+
identity_rotation: "identity_rotation"
|
|
4322
|
+
};
|
|
4184
4323
|
DEFAULT_VENDOR_POLICY = { authorized: [] };
|
|
4324
|
+
OPERATIONAL_POLICY_KEYS = [
|
|
4325
|
+
"session_persistence",
|
|
4326
|
+
"live_view_handoff",
|
|
4327
|
+
"residential_proxy",
|
|
4328
|
+
"retry_orchestration"
|
|
4329
|
+
];
|
|
4330
|
+
AUTHORIZABLE_POLICY_KEYS = [
|
|
4331
|
+
...OPERATIONAL_POLICY_KEYS,
|
|
4332
|
+
...AUTHORIZABLE.map((e) => e.capability)
|
|
4333
|
+
];
|
|
4185
4334
|
}
|
|
4186
4335
|
});
|
|
4187
4336
|
|
|
@@ -4193,14 +4342,19 @@ function evaluateProviderGate(provider, robots, path, unreachable = null) {
|
|
|
4193
4342
|
appliedRules: [],
|
|
4194
4343
|
refusedCapabilities: []
|
|
4195
4344
|
};
|
|
4196
|
-
const
|
|
4197
|
-
|
|
4345
|
+
const verdicts = provider.capabilities.flatMap((c) => {
|
|
4346
|
+
const access = VENDOR_CAPABILITY_ACCESS[c];
|
|
4347
|
+
return access === void 0 ? [] : [{ capability: c, verdict: evaluateAccessCapability(access, provider.grants ?? []) }];
|
|
4348
|
+
});
|
|
4349
|
+
const blocked = verdicts.filter((v) => v.verdict.decision !== "granted");
|
|
4350
|
+
if (blocked.length > 0) {
|
|
4351
|
+
const forever = blocked.some((v) => v.verdict.decision === "refused");
|
|
4198
4352
|
return {
|
|
4199
4353
|
...base,
|
|
4200
4354
|
allowed: false,
|
|
4201
|
-
refusal: "refused_capability",
|
|
4202
|
-
reason: `Provider ${provider.id} offers ${
|
|
4203
|
-
refusedCapabilities:
|
|
4355
|
+
refusal: forever ? "refused_capability" : "ungranted_capability",
|
|
4356
|
+
reason: `Provider ${provider.id} offers ${blocked.map((v) => v.capability).join(", ")}: ` + blocked.map((v) => "reason" in v.verdict ? v.verdict.reason : v.capability).join("; ") + ". Changing whose machine runs the browser does not change what the browser is doing.",
|
|
4357
|
+
refusedCapabilities: blocked.map((v) => v.capability)
|
|
4204
4358
|
};
|
|
4205
4359
|
}
|
|
4206
4360
|
if (provider.declaredUserAgent === null) {
|
|
@@ -4253,8 +4407,124 @@ var init_provider = __esm({
|
|
|
4253
4407
|
"use strict";
|
|
4254
4408
|
init_robots();
|
|
4255
4409
|
init_vendor();
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
|
|
4413
|
+
// packages/http-core/dist/accessGrant.js
|
|
4414
|
+
function isRecord(v) {
|
|
4415
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4416
|
+
}
|
|
4417
|
+
function usd(v) {
|
|
4418
|
+
if (v === void 0 || v === null)
|
|
4419
|
+
return null;
|
|
4420
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
4421
|
+
}
|
|
4422
|
+
function normalizeAccessGrant(input) {
|
|
4423
|
+
if (!isRecord(input)) {
|
|
4424
|
+
return { ok: false, problems: [{ field: "grant", kind: "invalid", reason: "a grant is a JSON object" }] };
|
|
4425
|
+
}
|
|
4426
|
+
const problems = [];
|
|
4427
|
+
const tier = input.tier ?? "standard";
|
|
4428
|
+
if (!ACCESS_TIERS.includes(tier)) {
|
|
4429
|
+
problems.push({ field: "tier", kind: "invalid", reason: `tier is one of ${ACCESS_TIERS.join(", ")}` });
|
|
4430
|
+
}
|
|
4431
|
+
const names = input.capabilities ?? [];
|
|
4432
|
+
const capabilities = [];
|
|
4433
|
+
if (!Array.isArray(names) || names.some((n) => typeof n !== "string")) {
|
|
4434
|
+
problems.push({ field: "capabilities", kind: "invalid", reason: "capabilities is a list of capability names" });
|
|
4435
|
+
} else {
|
|
4436
|
+
for (const name of new Set(names)) {
|
|
4437
|
+
const field3 = `capabilities.${name}`;
|
|
4438
|
+
const verdict = evaluateAccessCapability(name, [name]);
|
|
4439
|
+
if (verdict.decision === "refused") {
|
|
4440
|
+
problems.push({ field: field3, kind: "refused", reason: verdict.reason });
|
|
4441
|
+
} else if (verdict.decision === "deferred") {
|
|
4442
|
+
problems.push({ field: field3, kind: "deferred", reason: verdict.reason, source: verdict.source, restartWhen: verdict.restartWhen });
|
|
4443
|
+
} else if (verdict.decision === "unknown") {
|
|
4444
|
+
problems.push({ field: field3, kind: "unknown_capability", reason: verdict.reason });
|
|
4445
|
+
} else {
|
|
4446
|
+
const entry3 = AUTHORIZABLE.find((e) => e.capability === name);
|
|
4447
|
+
if (entry3.tier === "enhanced" && tier !== "enhanced") {
|
|
4448
|
+
problems.push({ field: field3, kind: "tier_too_low", reason: `${name} needs tier enhanced; ${String(tier)} adds no third-party cost and no enhanced browser` });
|
|
4449
|
+
}
|
|
4450
|
+
capabilities.push(entry3.capability);
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
const budgetIn = input.budget ?? {};
|
|
4455
|
+
let perRequestUsd = null;
|
|
4456
|
+
let perRunUsd = null;
|
|
4457
|
+
if (!isRecord(budgetIn)) {
|
|
4458
|
+
problems.push({ field: "budget", kind: "invalid", reason: "budget is an object with perRequestUsd and perRunUsd" });
|
|
4459
|
+
} else {
|
|
4460
|
+
const req = usd(budgetIn.perRequestUsd);
|
|
4461
|
+
const run = usd(budgetIn.perRunUsd);
|
|
4462
|
+
if (req === void 0)
|
|
4463
|
+
problems.push({ field: "budget.perRequestUsd", kind: "invalid", reason: "a positive number of US dollars, or null" });
|
|
4464
|
+
if (run === void 0)
|
|
4465
|
+
problems.push({ field: "budget.perRunUsd", kind: "invalid", reason: "a positive number of US dollars, or null" });
|
|
4466
|
+
perRequestUsd = req ?? null;
|
|
4467
|
+
perRunUsd = run ?? null;
|
|
4468
|
+
}
|
|
4469
|
+
const costed = capabilities.filter((c) => AUTHORIZABLE.find((e) => e.capability === c).thirdPartyCost);
|
|
4470
|
+
if (costed.length > 0 && perRunUsd === null && !problems.some((p) => p.field === "budget.perRunUsd")) {
|
|
4471
|
+
problems.push({
|
|
4472
|
+
field: "budget.perRunUsd",
|
|
4473
|
+
kind: "budget_required",
|
|
4474
|
+
reason: `${costed.join(", ")} can spend a third party's money; a grant naming them needs a positive perRunUsd`
|
|
4475
|
+
});
|
|
4476
|
+
}
|
|
4477
|
+
const scopeIn = input.scope ?? {};
|
|
4478
|
+
let hosts = null;
|
|
4479
|
+
if (!isRecord(scopeIn) || scopeIn.hosts !== void 0 && scopeIn.hosts !== null && (!Array.isArray(scopeIn.hosts) || scopeIn.hosts.some((h) => typeof h !== "string" || h.length === 0))) {
|
|
4480
|
+
problems.push({ field: "scope.hosts", kind: "invalid", reason: "scope.hosts is a list of host names, or null" });
|
|
4481
|
+
} else if (Array.isArray(scopeIn.hosts)) {
|
|
4482
|
+
hosts = scopeIn.hosts;
|
|
4483
|
+
}
|
|
4484
|
+
const att = input.attestation;
|
|
4485
|
+
let attestation = null;
|
|
4486
|
+
if (att !== void 0 && att !== null) {
|
|
4487
|
+
if (!isRecord(att) || [att.principal, att.at, att.statement].some((v) => typeof v !== "string" || v.trim().length === 0)) {
|
|
4488
|
+
problems.push({ field: "attestation", kind: "invalid", reason: "attestation needs principal, at and statement" });
|
|
4489
|
+
} else {
|
|
4490
|
+
attestation = { principal: att.principal, at: att.at, statement: att.statement };
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
const enhancedOnly = capabilities.filter((c) => AUTHORIZABLE.find((e) => e.capability === c).tier === "enhanced");
|
|
4494
|
+
if (enhancedOnly.length > 0 && attestation === null && !problems.some((p) => p.field === "attestation")) {
|
|
4495
|
+
problems.push({
|
|
4496
|
+
field: "attestation",
|
|
4497
|
+
kind: "attestation_required",
|
|
4498
|
+
reason: `${enhancedOnly.join(", ")} needs an attestation: who accepted it, when, and the statement they accepted`
|
|
4499
|
+
});
|
|
4500
|
+
}
|
|
4501
|
+
if (problems.length > 0)
|
|
4502
|
+
return { ok: false, problems };
|
|
4503
|
+
return { ok: true, grant: { tier, capabilities, budget: { perRequestUsd, perRunUsd }, scope: { hosts }, attestation } };
|
|
4504
|
+
}
|
|
4505
|
+
function accessGrantFromText(text2) {
|
|
4506
|
+
let input;
|
|
4507
|
+
try {
|
|
4508
|
+
input = JSON.parse(text2);
|
|
4509
|
+
} catch {
|
|
4510
|
+
throw new Error("access grant: not valid JSON");
|
|
4511
|
+
}
|
|
4512
|
+
const result = normalizeAccessGrant(input);
|
|
4513
|
+
if (!result.ok) {
|
|
4514
|
+
throw new Error(`access grant refused (ADR 0005):
|
|
4515
|
+
${result.problems.map((p) => ` - ${p.field}: ${p.reason}`).join("\n")}`);
|
|
4516
|
+
}
|
|
4517
|
+
if (result.grant.scope.hosts !== null) {
|
|
4518
|
+
throw new Error("access grant refused: scope.hosts is not enforced yet, so a grant that names hosts would reach every host; leave scope out");
|
|
4519
|
+
}
|
|
4520
|
+
return result.grant;
|
|
4521
|
+
}
|
|
4522
|
+
var ACCESS_TIERS;
|
|
4523
|
+
var init_accessGrant = __esm({
|
|
4524
|
+
"packages/http-core/dist/accessGrant.js"() {
|
|
4525
|
+
"use strict";
|
|
4256
4526
|
init_vendor();
|
|
4257
|
-
|
|
4527
|
+
ACCESS_TIERS = ["standard", "enhanced", "my_browser"];
|
|
4258
4528
|
}
|
|
4259
4529
|
});
|
|
4260
4530
|
|
|
@@ -4310,12 +4580,12 @@ var init_failureClass = __esm({
|
|
|
4310
4580
|
// packages/http-core/dist/governance.js
|
|
4311
4581
|
function hostMatchesAllowlist(host, entry3) {
|
|
4312
4582
|
const h = host.toLowerCase();
|
|
4313
|
-
const
|
|
4314
|
-
if (
|
|
4315
|
-
const suffix =
|
|
4316
|
-
return h ===
|
|
4583
|
+
const normalized2 = entry3.toLowerCase();
|
|
4584
|
+
if (normalized2.startsWith("*.")) {
|
|
4585
|
+
const suffix = normalized2.slice(1);
|
|
4586
|
+
return h === normalized2.slice(2) || h.endsWith(suffix);
|
|
4317
4587
|
}
|
|
4318
|
-
return h ===
|
|
4588
|
+
return h === normalized2;
|
|
4319
4589
|
}
|
|
4320
4590
|
function evaluateGovernance(url, policy) {
|
|
4321
4591
|
let host;
|
|
@@ -4343,7 +4613,8 @@ var init_governance = __esm({
|
|
|
4343
4613
|
"packages/http-core/dist/governance.js"() {
|
|
4344
4614
|
"use strict";
|
|
4345
4615
|
MODE_CHANNELS = {
|
|
4346
|
-
|
|
4616
|
+
// http_compat: the http lane over the browser-compatible transport (ADR 0005 compatible_transport); its identity is the standard one.
|
|
4617
|
+
standard: ["http", "http_compat", "browser_local"],
|
|
4347
4618
|
research: ["http", "browser_local", "provider"],
|
|
4348
4619
|
authed: ["http", "browser_local", "provider", "authed_session", "handoff"]
|
|
4349
4620
|
};
|
|
@@ -4416,6 +4687,10 @@ function serialize(input) {
|
|
|
4416
4687
|
field(c, "robots.override");
|
|
4417
4688
|
field(c, r.override.reason);
|
|
4418
4689
|
nullableField(c, r.override.recordedBy ?? null);
|
|
4690
|
+
if (r.override.basis !== void 0) {
|
|
4691
|
+
field(c, "robots.override.basis");
|
|
4692
|
+
field(c, r.override.basis);
|
|
4693
|
+
}
|
|
4419
4694
|
}
|
|
4420
4695
|
const total = c.reduce((n, arr) => n + arr.length, 0);
|
|
4421
4696
|
const out = new Uint8Array(total);
|
|
@@ -4621,6 +4896,7 @@ var init_dist2 = __esm({
|
|
|
4621
4896
|
init_access2();
|
|
4622
4897
|
init_provider();
|
|
4623
4898
|
init_vendor();
|
|
4899
|
+
init_accessGrant();
|
|
4624
4900
|
init_failureClass();
|
|
4625
4901
|
init_governance();
|
|
4626
4902
|
init_compliance2();
|
|
@@ -7620,10 +7896,76 @@ function collectDeclaredProductFacts(doc) {
|
|
|
7620
7896
|
function fillPriceFromText(facts, doc) {
|
|
7621
7897
|
fillMissing(facts, factsFromText(doc));
|
|
7622
7898
|
}
|
|
7899
|
+
function isLabelled(el, doc) {
|
|
7900
|
+
if (el.previousElementSibling !== null && tagOf(el.previousElementSibling) === "label")
|
|
7901
|
+
return true;
|
|
7902
|
+
if (el.closest("label") !== null || (el.getAttribute("aria-label") ?? "").trim() !== "")
|
|
7903
|
+
return true;
|
|
7904
|
+
if (["radiogroup", "listbox"].includes((el.getAttribute("role") ?? "").toLowerCase()))
|
|
7905
|
+
return true;
|
|
7906
|
+
const id = el.getAttribute("id");
|
|
7907
|
+
return id !== null && id !== "" && qsa(doc, "label[for]").some((label) => label.getAttribute("for") === id);
|
|
7908
|
+
}
|
|
7909
|
+
function isProductOption(el) {
|
|
7910
|
+
const form = el.closest("form");
|
|
7911
|
+
if (form !== null && CART_FORM.test(`${form.getAttribute("action") ?? ""} ${form.getAttribute("id") ?? ""} ${form.getAttribute("class") ?? ""}`))
|
|
7912
|
+
return true;
|
|
7913
|
+
return [el, el.parentElement, ...Array.from(el.children)].some((node) => node !== null && OPTION_NAME.test(`${node.getAttribute("name") ?? ""} ${node.getAttribute("id") ?? ""} ${node.getAttribute("class") ?? ""}`));
|
|
7914
|
+
}
|
|
7915
|
+
function markOptionGroups(doc, excluded = /* @__PURE__ */ new Set()) {
|
|
7916
|
+
const marked = /* @__PURE__ */ new Map();
|
|
7917
|
+
const outside = (el) => ![...excluded].some((ex) => ex.contains(el));
|
|
7918
|
+
const place = (replaced, values) => {
|
|
7919
|
+
const unique = [...new Set(values.map((v) => v.replace(/\s+/g, " ").trim()).filter((v) => v !== ""))];
|
|
7920
|
+
if (unique.length < 2)
|
|
7921
|
+
return;
|
|
7922
|
+
const placeholder = doc.createElement("span");
|
|
7923
|
+
replaced.parentNode?.insertBefore(placeholder, replaced);
|
|
7924
|
+
marked.set(placeholder, unique.join(", "));
|
|
7925
|
+
};
|
|
7926
|
+
for (const select of qsa(doc, "select")) {
|
|
7927
|
+
if (!outside(select) || !isLabelled(select, doc) || !isProductOption(select))
|
|
7928
|
+
continue;
|
|
7929
|
+
const choices = qsa(select, "option").filter((option) => option.getAttribute("value") !== "").map((option) => textOf(option).trim());
|
|
7930
|
+
const counts = choices.every((choice, at) => /^\d+$/.test(choice) && Number(choice) === Number(choices[0]) + at) && [0, 1].includes(Number(choices[0]));
|
|
7931
|
+
if (!counts)
|
|
7932
|
+
place(select, choices);
|
|
7933
|
+
}
|
|
7934
|
+
for (const group of qsa(doc, "div,span,ul,fieldset,p")) {
|
|
7935
|
+
const kids = Array.from(group.children);
|
|
7936
|
+
if (kids.length < 2 || !kids.every((kid) => tagOf(kid) === "button" && (kid.getAttribute("type") ?? "").toLowerCase() !== "submit"))
|
|
7937
|
+
continue;
|
|
7938
|
+
if (!outside(group) || !isLabelled(group, doc) || !isProductOption(group))
|
|
7939
|
+
continue;
|
|
7940
|
+
place(kids[0], kids.map((kid) => textOf(kid)));
|
|
7941
|
+
}
|
|
7942
|
+
return marked;
|
|
7943
|
+
}
|
|
7944
|
+
function settleOptionGroups(marked, keep) {
|
|
7945
|
+
for (const [placeholder, values] of marked) {
|
|
7946
|
+
if (keep && placeholder.parentNode !== null) {
|
|
7947
|
+
const text2 = placeholder.ownerDocument.createTextNode(` ${values} `);
|
|
7948
|
+
optionValues.add(text2);
|
|
7949
|
+
placeholder.parentNode.replaceChild(text2, placeholder);
|
|
7950
|
+
} else
|
|
7951
|
+
placeholder.parentNode?.removeChild(placeholder);
|
|
7952
|
+
}
|
|
7953
|
+
}
|
|
7623
7954
|
function isRootish(doc, el) {
|
|
7624
7955
|
return el === null || el === doc.body || el === doc.documentElement;
|
|
7625
7956
|
}
|
|
7957
|
+
function describes(block) {
|
|
7958
|
+
const unlabelled = (node) => {
|
|
7959
|
+
if (node.nodeType === 3)
|
|
7960
|
+
return optionValues.has(node) ? "" : node.textContent ?? "";
|
|
7961
|
+
if (node.nodeType !== 1 || tagOf(node) === "label" || tagOf(node) === "a")
|
|
7962
|
+
return "";
|
|
7963
|
+
return Array.from(node.childNodes).map(unlabelled).join("");
|
|
7964
|
+
};
|
|
7965
|
+
return /[\p{L}\p{N}]/u.test(unlabelled(block.el));
|
|
7966
|
+
}
|
|
7626
7967
|
function selectProduct(doc, blocks) {
|
|
7968
|
+
const described2 = blocks.filter(describes);
|
|
7627
7969
|
const scope = microdataProductScope(doc);
|
|
7628
7970
|
if (scope && !isRootish(doc, scope))
|
|
7629
7971
|
return scope;
|
|
@@ -7633,11 +7975,10 @@ function selectProduct(doc, blocks) {
|
|
|
7633
7975
|
const lca = commonAncestor(heading2, price);
|
|
7634
7976
|
if (!isRootish(doc, lca)) {
|
|
7635
7977
|
const region = lca;
|
|
7636
|
-
|
|
7637
|
-
if (prose3.length > 0)
|
|
7978
|
+
if (described2.some((b) => region.contains(b.el)))
|
|
7638
7979
|
return region;
|
|
7639
7980
|
for (let wider = region.parentElement; !isRootish(doc, wider); wider = wider.parentElement) {
|
|
7640
|
-
if (
|
|
7981
|
+
if (described2.some((b) => wider.contains(b.el)))
|
|
7641
7982
|
return wider;
|
|
7642
7983
|
}
|
|
7643
7984
|
return region;
|
|
@@ -7651,7 +7992,7 @@ function selectProduct(doc, blocks) {
|
|
|
7651
7992
|
}
|
|
7652
7993
|
return null;
|
|
7653
7994
|
}
|
|
7654
|
-
var CURRENCY_SYMBOLS, CURRENCY_CODES, AMOUNT, PRICE_RE;
|
|
7995
|
+
var CURRENCY_SYMBOLS, CURRENCY_CODES, AMOUNT, PRICE_RE, CART_FORM, OPTION_NAME, optionValues;
|
|
7655
7996
|
var init_product = __esm({
|
|
7656
7997
|
"packages/extract-tf/dist/product.js"() {
|
|
7657
7998
|
"use strict";
|
|
@@ -7660,6 +8001,9 @@ var init_product = __esm({
|
|
|
7660
8001
|
CURRENCY_CODES = "(?:USD|EUR|GBP|JPY|CNY|RMB|AUD|CAD|CHF|HKD|SGD|INR|KRW|BRL|MXN|SEK|NOK|DKK|PLN|TRY|ZAR)";
|
|
7661
8002
|
AMOUNT = "(?:(?<![\\d.,'\u2019])\\d{1,3}(?:[\\s'\u2019]\\d{3}){1,4}(?:[.,]\\d{1,3})?|\\d{1,12}(?:[.,]\\d{1,3}){0,4})";
|
|
7662
8003
|
PRICE_RE = new RegExp(`(?:${CURRENCY_SYMBOLS}|${CURRENCY_CODES})\\s{0,3}${AMOUNT}|${AMOUNT}\\s{0,3}(?:${CURRENCY_SYMBOLS}|${CURRENCY_CODES})`, "u");
|
|
8004
|
+
CART_FORM = /cart|basket|bag/i;
|
|
8005
|
+
OPTION_NAME = /swatch|variant|variation|attribute/i;
|
|
8006
|
+
optionValues = /* @__PURE__ */ new WeakSet();
|
|
7663
8007
|
}
|
|
7664
8008
|
});
|
|
7665
8009
|
|
|
@@ -7669,10 +8013,10 @@ function hasToken(attr2, tokens) {
|
|
|
7669
8013
|
return tokens.some((t) => lower.split(/[\s_-]+/).includes(t));
|
|
7670
8014
|
}
|
|
7671
8015
|
function hasRecommendationToken(attr2) {
|
|
7672
|
-
const
|
|
8016
|
+
const normalized2 = attr2.toLowerCase().replace(/([a-z])([A-Z])/g, "$1-$2").split(/\s+/).filter((t) => t.length > 0);
|
|
7673
8017
|
const camelSplit = attr2.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
7674
8018
|
const parts = camelSplit.split(/[\s_]+/).filter((t) => t.length > 0);
|
|
7675
|
-
const all = /* @__PURE__ */ new Set([...
|
|
8019
|
+
const all = /* @__PURE__ */ new Set([...normalized2, ...parts]);
|
|
7676
8020
|
return RECOMMENDATION_TOKENS.some((t) => all.has(t));
|
|
7677
8021
|
}
|
|
7678
8022
|
function isRecommendationHeading(text2) {
|
|
@@ -7685,14 +8029,17 @@ function isProductGrid(el) {
|
|
|
7685
8029
|
const kids = Array.from(el.children);
|
|
7686
8030
|
if (kids.length < 3)
|
|
7687
8031
|
return false;
|
|
8032
|
+
const pricedByTag = /* @__PURE__ */ new Map();
|
|
7688
8033
|
let priced = 0;
|
|
7689
8034
|
for (const kid of kids) {
|
|
7690
8035
|
if (qsa(kid, "a").length === 0)
|
|
7691
8036
|
continue;
|
|
7692
|
-
if (looksLikePrice(textOf(kid)))
|
|
7693
|
-
|
|
8037
|
+
if (!looksLikePrice(textOf(kid)))
|
|
8038
|
+
continue;
|
|
8039
|
+
priced++;
|
|
8040
|
+
pricedByTag.set(tagOf(kid), (pricedByTag.get(tagOf(kid)) ?? 0) + 1);
|
|
7694
8041
|
}
|
|
7695
|
-
return
|
|
8042
|
+
return Math.max(0, ...pricedByTag.values()) >= 3 && priced >= kids.length * 0.6;
|
|
7696
8043
|
}
|
|
7697
8044
|
function recommendationRegion(heading2) {
|
|
7698
8045
|
const rank = Number(/^h([1-6])$/.exec(tagOf(heading2))?.[1] ?? "6");
|
|
@@ -7706,6 +8053,8 @@ function recommendationRegion(heading2) {
|
|
|
7706
8053
|
return region;
|
|
7707
8054
|
}
|
|
7708
8055
|
function pruneRecommendations(doc) {
|
|
8056
|
+
const h1s = qsa(doc, "h1");
|
|
8057
|
+
const holdsH1 = (el) => h1s.length === 1 && el.contains(h1s[0]);
|
|
7709
8058
|
for (const heading2 of qsa(doc, "h1,h2,h3,h4,h5,h6")) {
|
|
7710
8059
|
if (!heading2.isConnected)
|
|
7711
8060
|
continue;
|
|
@@ -7718,14 +8067,14 @@ function pruneRecommendations(doc) {
|
|
|
7718
8067
|
for (const el of containers.reverse()) {
|
|
7719
8068
|
if (!el.isConnected)
|
|
7720
8069
|
continue;
|
|
7721
|
-
if (isProductGrid(el))
|
|
8070
|
+
if (!holdsH1(el) && isProductGrid(el))
|
|
7722
8071
|
detach(el);
|
|
7723
8072
|
}
|
|
7724
8073
|
for (const el of qsa(doc, "[id],[class]")) {
|
|
7725
8074
|
if (!el.isConnected)
|
|
7726
8075
|
continue;
|
|
7727
8076
|
const attr2 = `${el.getAttribute("id") ?? ""} ${el.getAttribute("class") ?? ""}`;
|
|
7728
|
-
if (hasRecommendationToken(attr2))
|
|
8077
|
+
if (hasRecommendationToken(attr2) && !holdsH1(el))
|
|
7729
8078
|
detach(el);
|
|
7730
8079
|
}
|
|
7731
8080
|
}
|
|
@@ -7969,18 +8318,93 @@ var init_prune = __esm({
|
|
|
7969
8318
|
function collapsed(text2) {
|
|
7970
8319
|
return text2.replace(/\s+/g, " ").trim();
|
|
7971
8320
|
}
|
|
8321
|
+
function hydrationPassages(data, out) {
|
|
8322
|
+
const stack = [data];
|
|
8323
|
+
while (stack.length > 0 && out.length < HYDRATION_MAX_PASSAGES) {
|
|
8324
|
+
const node = stack.pop();
|
|
8325
|
+
if (typeof node === "string") {
|
|
8326
|
+
const text2 = collapsed(node);
|
|
8327
|
+
if (text2.length >= HYDRATION_TEXT_MIN_CHARS && (text2.match(/ /g) ?? []).length >= HYDRATION_TEXT_MIN_SPACES)
|
|
8328
|
+
out.push(text2);
|
|
8329
|
+
} else if (Array.isArray(node)) {
|
|
8330
|
+
for (const item of node)
|
|
8331
|
+
stack.push(item);
|
|
8332
|
+
} else if (typeof node === "object" && node !== null) {
|
|
8333
|
+
for (const value of Object.values(node))
|
|
8334
|
+
stack.push(value);
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
function recordName(item) {
|
|
8339
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
8340
|
+
return null;
|
|
8341
|
+
const record3 = item;
|
|
8342
|
+
const name = typeof record3.name === "string" ? record3.name : typeof record3.title === "string" ? record3.title : null;
|
|
8343
|
+
if (name === null)
|
|
8344
|
+
return null;
|
|
8345
|
+
const text2 = collapsed(name).toLowerCase();
|
|
8346
|
+
return text2.length >= LIST_NAME_MIN_CHARS ? text2 : null;
|
|
8347
|
+
}
|
|
8348
|
+
function collectRecordLists(data, out) {
|
|
8349
|
+
const stack = [data];
|
|
8350
|
+
while (stack.length > 0 && out.length < LIST_MAX_LISTS) {
|
|
8351
|
+
const node = stack.pop();
|
|
8352
|
+
if (Array.isArray(node)) {
|
|
8353
|
+
const names = /* @__PURE__ */ new Set();
|
|
8354
|
+
for (const item of node) {
|
|
8355
|
+
if (typeof item === "object" && item !== null)
|
|
8356
|
+
stack.push(item);
|
|
8357
|
+
const name = names.size < LIST_MAX_NAMES ? recordName(item) : null;
|
|
8358
|
+
if (name !== null)
|
|
8359
|
+
names.add(name);
|
|
8360
|
+
}
|
|
8361
|
+
if (names.size >= LIST_MIN_RECORDS)
|
|
8362
|
+
out.push([...names]);
|
|
8363
|
+
} else if (typeof node === "object" && node !== null) {
|
|
8364
|
+
for (const value of Object.values(node))
|
|
8365
|
+
if (typeof value === "object" && value !== null)
|
|
8366
|
+
stack.push(value);
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
}
|
|
8370
|
+
function hydrationShown(raw, doc) {
|
|
8371
|
+
const passages = [];
|
|
8372
|
+
for (const text2 of raw.hydrationJson) {
|
|
8373
|
+
try {
|
|
8374
|
+
hydrationPassages(JSON.parse(text2), passages);
|
|
8375
|
+
} catch {
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
if (passages.length === 0 || doc.body === null)
|
|
8379
|
+
return false;
|
|
8380
|
+
const seen = collapsed(textOf(doc.body));
|
|
8381
|
+
return passages.every((passage) => seen.includes(passage));
|
|
8382
|
+
}
|
|
8383
|
+
function recordLists(raw) {
|
|
8384
|
+
const lists = [];
|
|
8385
|
+
for (const text2 of raw.hydrationJson) {
|
|
8386
|
+
try {
|
|
8387
|
+
collectRecordLists(JSON.parse(text2), lists);
|
|
8388
|
+
} catch {
|
|
8389
|
+
}
|
|
8390
|
+
}
|
|
8391
|
+
return lists;
|
|
8392
|
+
}
|
|
7972
8393
|
function rawSignals(doc) {
|
|
7973
8394
|
const markers = /* @__PURE__ */ new Set();
|
|
7974
8395
|
let scriptChars = 0;
|
|
7975
8396
|
let hydration = false;
|
|
8397
|
+
const hydrationJson = [];
|
|
7976
8398
|
for (const script of qsa(doc, "script")) {
|
|
7977
8399
|
const text2 = script.textContent ?? "";
|
|
7978
8400
|
if (script.getAttribute("src") === null)
|
|
7979
8401
|
scriptChars += text2.length;
|
|
7980
8402
|
const type = (script.getAttribute("type") ?? "").toLowerCase();
|
|
7981
|
-
if (script.id === "__NEXT_DATA__" || type === "application/json" && text2.length >= HYDRATION_MIN_CHARS)
|
|
8403
|
+
if (script.id === "__NEXT_DATA__" || type === "application/json" && text2.length >= HYDRATION_MIN_CHARS) {
|
|
7982
8404
|
hydration = true;
|
|
7983
|
-
|
|
8405
|
+
if (text2.length <= HYDRATION_MAX_JSON_CHARS)
|
|
8406
|
+
hydrationJson.push(text2);
|
|
8407
|
+
} else if (text2.length >= 64 && HYDRATION_SCRIPT.test(text2))
|
|
7984
8408
|
hydration = true;
|
|
7985
8409
|
}
|
|
7986
8410
|
if (hydration)
|
|
@@ -8005,7 +8429,7 @@ function rawSignals(doc) {
|
|
|
8005
8429
|
}
|
|
8006
8430
|
if (qsa(doc, '[aria-busy="true"]').length > 0)
|
|
8007
8431
|
markers.add("aria_busy");
|
|
8008
|
-
return { scriptChars, markers: Array.from(markers) };
|
|
8432
|
+
return { scriptChars, markers: Array.from(markers), hydrationJson };
|
|
8009
8433
|
}
|
|
8010
8434
|
function countEmptyTables(doc) {
|
|
8011
8435
|
let empty = 0;
|
|
@@ -8015,8 +8439,20 @@ function countEmptyTables(doc) {
|
|
|
8015
8439
|
}
|
|
8016
8440
|
return empty;
|
|
8017
8441
|
}
|
|
8018
|
-
function
|
|
8019
|
-
|
|
8442
|
+
function partialRecordList(lists, visible) {
|
|
8443
|
+
let partial = null;
|
|
8444
|
+
for (const names of lists) {
|
|
8445
|
+
const shown = names.filter((name) => visible.includes(name)).length;
|
|
8446
|
+
if (shown < LIST_MIN_SHOWN || shown * 2 > names.length)
|
|
8447
|
+
continue;
|
|
8448
|
+
if (partial === null || names.length - shown > partial.declared - partial.shown)
|
|
8449
|
+
partial = { declared: names.length, shown };
|
|
8450
|
+
}
|
|
8451
|
+
return partial;
|
|
8452
|
+
}
|
|
8453
|
+
function detectRenderSignals(raw, cleaned, context = {}) {
|
|
8454
|
+
const visible = cleaned.body ? collapsed(textOf(cleaned.body)) : "";
|
|
8455
|
+
const textChars = visible.length;
|
|
8020
8456
|
const emptyTables = countEmptyTables(cleaned);
|
|
8021
8457
|
const markers = raw.markers;
|
|
8022
8458
|
const has2 = (m) => markers.includes(m);
|
|
@@ -8025,26 +8461,30 @@ function detectRenderSignals(raw, cleaned) {
|
|
|
8025
8461
|
reason2 = "empty_table_with_scripts";
|
|
8026
8462
|
else if (has2("app_root_empty") && raw.scriptChars >= 500)
|
|
8027
8463
|
reason2 = "empty_app_root";
|
|
8028
|
-
else if (textChars < 300 && raw.scriptChars >= 2e3)
|
|
8464
|
+
else if (textChars < 300 && raw.scriptChars >= 2e3 && context.contentShown !== true)
|
|
8029
8465
|
reason2 = "script_shell";
|
|
8030
8466
|
else if (has2("js_fallback_marker") && raw.scriptChars > textChars)
|
|
8031
8467
|
reason2 = "js_fallback";
|
|
8032
8468
|
else if (has2("noscript_notice") && raw.scriptChars > textChars && (textChars < NOTICE_MAX_TEXT || has2("hydration_state")))
|
|
8033
8469
|
reason2 = "js_fallback";
|
|
8034
|
-
else if (has2("hydration_state") && textChars < 1500 && raw.scriptChars > textChars * 2)
|
|
8470
|
+
else if (has2("hydration_state") && textChars < 1500 && raw.scriptChars > textChars * 2 && !(context.productShown === true && context.hydrationShown === true))
|
|
8035
8471
|
reason2 = "hydration_shell";
|
|
8036
8472
|
else if (has2("aria_busy") && raw.scriptChars >= 1e3)
|
|
8037
8473
|
reason2 = "aria_busy";
|
|
8474
|
+
const listRecords = reason2 === null && context.listing === true && raw.hydrationJson.length > 0 ? partialRecordList(recordLists(raw), visible.toLowerCase()) : null;
|
|
8475
|
+
if (listRecords !== null)
|
|
8476
|
+
reason2 = "hydration_list_partial";
|
|
8038
8477
|
return {
|
|
8039
8478
|
textChars,
|
|
8040
8479
|
scriptChars: raw.scriptChars,
|
|
8041
8480
|
emptyTables,
|
|
8042
8481
|
markers,
|
|
8482
|
+
...listRecords === null ? {} : { listRecords },
|
|
8043
8483
|
clientRendered: reason2 !== null,
|
|
8044
8484
|
reason: reason2
|
|
8045
8485
|
};
|
|
8046
8486
|
}
|
|
8047
|
-
var APP_ROOT_SELECTOR, JS_FALLBACK_CLASS, HYDRATION_SCRIPT, APP_ROOT_MAX_TEXT, HYDRATION_MIN_CHARS, NOTICE_MAX_TEXT;
|
|
8487
|
+
var APP_ROOT_SELECTOR, JS_FALLBACK_CLASS, HYDRATION_SCRIPT, APP_ROOT_MAX_TEXT, HYDRATION_MIN_CHARS, NOTICE_MAX_TEXT, HYDRATION_TEXT_MIN_CHARS, HYDRATION_TEXT_MIN_SPACES, HYDRATION_MAX_JSON_CHARS, HYDRATION_MAX_PASSAGES, LIST_MIN_RECORDS, LIST_NAME_MIN_CHARS, LIST_MIN_SHOWN, LIST_MAX_NAMES, LIST_MAX_LISTS;
|
|
8048
8488
|
var init_render = __esm({
|
|
8049
8489
|
"packages/extract-tf/dist/render.js"() {
|
|
8050
8490
|
"use strict";
|
|
@@ -8055,6 +8495,15 @@ var init_render = __esm({
|
|
|
8055
8495
|
APP_ROOT_MAX_TEXT = 200;
|
|
8056
8496
|
HYDRATION_MIN_CHARS = 2e3;
|
|
8057
8497
|
NOTICE_MAX_TEXT = 1500;
|
|
8498
|
+
HYDRATION_TEXT_MIN_CHARS = 80;
|
|
8499
|
+
HYDRATION_TEXT_MIN_SPACES = 8;
|
|
8500
|
+
HYDRATION_MAX_JSON_CHARS = 5e6;
|
|
8501
|
+
HYDRATION_MAX_PASSAGES = 200;
|
|
8502
|
+
LIST_MIN_RECORDS = 10;
|
|
8503
|
+
LIST_NAME_MIN_CHARS = 12;
|
|
8504
|
+
LIST_MIN_SHOWN = 3;
|
|
8505
|
+
LIST_MAX_NAMES = 500;
|
|
8506
|
+
LIST_MAX_LISTS = 50;
|
|
8058
8507
|
}
|
|
8059
8508
|
});
|
|
8060
8509
|
|
|
@@ -8857,6 +9306,8 @@ var init_detectList = __esm({
|
|
|
8857
9306
|
function hasVisibleBuyBox(doc) {
|
|
8858
9307
|
const h1s = qsa(doc, "h1");
|
|
8859
9308
|
const prices = visiblePrices(doc);
|
|
9309
|
+
if (h1s.length === 0)
|
|
9310
|
+
return hasBuyBoxUnderH2(doc, prices);
|
|
8860
9311
|
if (h1s.length !== 1 || prices.length !== 1 || prices[0].closest("li") !== null)
|
|
8861
9312
|
return false;
|
|
8862
9313
|
let heading2 = null;
|
|
@@ -8868,6 +9319,35 @@ function hasVisibleBuyBox(doc) {
|
|
|
8868
9319
|
}
|
|
8869
9320
|
return heading2 === h1s[0];
|
|
8870
9321
|
}
|
|
9322
|
+
function hasBuyBoxUnderH2(doc, prices) {
|
|
9323
|
+
const h2s = qsa(doc, "h2");
|
|
9324
|
+
if (h2s.length !== 1 || h2s[0].closest("a") !== null)
|
|
9325
|
+
return false;
|
|
9326
|
+
const all = qsa(doc, "*");
|
|
9327
|
+
const priceSet = new Set(prices);
|
|
9328
|
+
let price = null;
|
|
9329
|
+
for (const el of all.slice(all.indexOf(h2s[0]) + 1)) {
|
|
9330
|
+
if (/^h[1-6]$/.test(tagOf(el)))
|
|
9331
|
+
return false;
|
|
9332
|
+
if (priceSet.has(el)) {
|
|
9333
|
+
price = el;
|
|
9334
|
+
break;
|
|
9335
|
+
}
|
|
9336
|
+
}
|
|
9337
|
+
if (price === null || price.closest("li") !== null)
|
|
9338
|
+
return false;
|
|
9339
|
+
const block = commonAncestor(h2s[0], price);
|
|
9340
|
+
if (block === null || prices.some((other) => other !== price && block.contains(other)))
|
|
9341
|
+
return false;
|
|
9342
|
+
if (prices.length - 1 >= PRODUCT_CARDS)
|
|
9343
|
+
return false;
|
|
9344
|
+
for (let up = price.parentElement; up !== null && up !== doc.body; up = up.parentElement) {
|
|
9345
|
+
const siblings = Array.from(up.parentElement?.children ?? []).filter((sibling) => sibling !== up && tagOf(sibling) === tagOf(up) && prices.some((p) => sibling.contains(p)));
|
|
9346
|
+
if (siblings.length >= 2)
|
|
9347
|
+
return false;
|
|
9348
|
+
}
|
|
9349
|
+
return true;
|
|
9350
|
+
}
|
|
8871
9351
|
function countAll(doc) {
|
|
8872
9352
|
const textLength = (el) => (el?.textContent ?? "").replace(/\s+/g, " ").trim().length;
|
|
8873
9353
|
const textChars = textLength(doc.body);
|
|
@@ -8895,35 +9375,53 @@ function normalizeTypeName(raw) {
|
|
|
8895
9375
|
const last = /[#/]([^#/]+)$/.exec(trimmed)?.[1] ?? /^([^:]+):(.+)$/.exec(trimmed)?.[2] ?? trimmed;
|
|
8896
9376
|
return last.toLowerCase();
|
|
8897
9377
|
}
|
|
8898
|
-
function
|
|
9378
|
+
function collectPageTypes(root, out) {
|
|
9379
|
+
for (const node of Array.isArray(root) ? root : [root]) {
|
|
9380
|
+
if (typeof node !== "object" || node === null)
|
|
9381
|
+
continue;
|
|
9382
|
+
const record3 = node;
|
|
9383
|
+
const t = record3["@type"];
|
|
9384
|
+
for (const type of typeof t === "string" ? [t] : Array.isArray(t) ? t : [])
|
|
9385
|
+
if (typeof type === "string")
|
|
9386
|
+
out.push(normalizeTypeName(type));
|
|
9387
|
+
if (Array.isArray(record3["@graph"]))
|
|
9388
|
+
collectPageTypes(record3["@graph"], out);
|
|
9389
|
+
}
|
|
9390
|
+
}
|
|
9391
|
+
function collectJsonLdTypes(node, out, listed, inList = false) {
|
|
8899
9392
|
if (Array.isArray(node)) {
|
|
8900
9393
|
for (const item of node)
|
|
8901
|
-
collectJsonLdTypes(item, out);
|
|
9394
|
+
collectJsonLdTypes(item, out, listed, inList);
|
|
8902
9395
|
return;
|
|
8903
9396
|
}
|
|
8904
9397
|
if (typeof node !== "object" || node === null)
|
|
8905
9398
|
return;
|
|
8906
9399
|
const t = node["@type"];
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
9400
|
+
const types = (typeof t === "string" ? [t] : Array.isArray(t) ? t.filter((item) => typeof item === "string") : []).map(normalizeTypeName);
|
|
9401
|
+
for (const type of types) {
|
|
9402
|
+
if (inList && type === "product")
|
|
9403
|
+
listed.products++;
|
|
9404
|
+
else
|
|
9405
|
+
out.push(type);
|
|
8913
9406
|
}
|
|
9407
|
+
const list2 = inList || types.includes("itemlist");
|
|
8914
9408
|
for (const value of Object.values(node)) {
|
|
8915
9409
|
if (typeof value === "object" && value !== null)
|
|
8916
|
-
collectJsonLdTypes(value, out);
|
|
9410
|
+
collectJsonLdTypes(value, out, listed, list2);
|
|
8917
9411
|
}
|
|
8918
9412
|
}
|
|
8919
9413
|
function collectPageSignals(doc) {
|
|
8920
9414
|
const jsonLdTypes = [];
|
|
9415
|
+
const listed = { products: 0 };
|
|
9416
|
+
const pageTypes = [];
|
|
8921
9417
|
for (const el of qsa(doc, 'script[type="application/ld+json"]')) {
|
|
8922
9418
|
const text2 = (el.textContent ?? "").trim();
|
|
8923
9419
|
if (text2.length === 0)
|
|
8924
9420
|
continue;
|
|
8925
9421
|
try {
|
|
8926
|
-
|
|
9422
|
+
const parsed = JSON.parse(text2);
|
|
9423
|
+
collectJsonLdTypes(parsed, jsonLdTypes, listed);
|
|
9424
|
+
collectPageTypes(parsed, pageTypes);
|
|
8927
9425
|
} catch {
|
|
8928
9426
|
}
|
|
8929
9427
|
}
|
|
@@ -8940,7 +9438,9 @@ function collectPageSignals(doc) {
|
|
|
8940
9438
|
itempropTokens,
|
|
8941
9439
|
itemTypeTokens,
|
|
8942
9440
|
postArticles: qsa(doc, "article.post").length,
|
|
8943
|
-
productCards: productCards(doc)
|
|
9441
|
+
productCards: productCards(doc),
|
|
9442
|
+
listedProducts: listed.products,
|
|
9443
|
+
pageTypes
|
|
8944
9444
|
};
|
|
8945
9445
|
}
|
|
8946
9446
|
function productCards(doc) {
|
|
@@ -8953,9 +9453,11 @@ function productCards(doc) {
|
|
|
8953
9453
|
});
|
|
8954
9454
|
if (outer.length < PRODUCT_CARDS)
|
|
8955
9455
|
return false;
|
|
8956
|
-
const classes = (el) => splitTokens(el.getAttribute("class") ?? "")
|
|
8957
|
-
const
|
|
8958
|
-
|
|
9456
|
+
const classes = (el) => splitTokens(el.getAttribute("class") ?? "");
|
|
9457
|
+
const exact = new Set(outer.map((el) => `${tagOf(el)} ${classes(el).sort().join(" ")}`)).size === 1 && classes(outer[0]).length > 0;
|
|
9458
|
+
const siblings = outer.every((el) => el.parentElement === outer[0].parentElement) && new Set(outer.map(tagOf)).size === 1;
|
|
9459
|
+
const shared = classes(outer[0]).some((name) => outer.every((el) => classes(el).includes(name)));
|
|
9460
|
+
if (!(exact || siblings && shared) || outer.some((el) => el.querySelector("h1") !== null))
|
|
8959
9461
|
return false;
|
|
8960
9462
|
for (const card of outer) {
|
|
8961
9463
|
for (let up = card.parentElement; up !== null; up = up.parentElement) {
|
|
@@ -8968,7 +9470,7 @@ function productCards(doc) {
|
|
|
8968
9470
|
if (all.some((el, at) => at < first2 && /^h[2-6]$/.test(tagOf(el)) && isRecommendationHeading(textOf(el))))
|
|
8969
9471
|
return false;
|
|
8970
9472
|
const h1s = qsa(doc, "h1");
|
|
8971
|
-
return !(h1s.length === 1 && visiblePrices(doc).some((price) => !outer.some((card) => card.contains(price))));
|
|
9473
|
+
return !(h1s.length === 1 && visiblePrices(doc).some((price) => price.closest("header, nav") === null && !outer.some((card) => card.contains(price))));
|
|
8972
9474
|
}
|
|
8973
9475
|
function hasToken2(values, needle) {
|
|
8974
9476
|
return values.includes(needle);
|
|
@@ -8982,7 +9484,7 @@ function countTokens(values, needles) {
|
|
|
8982
9484
|
}
|
|
8983
9485
|
function hasProductSignals(s) {
|
|
8984
9486
|
const strong = hasToken2(s.jsonLdTypes, "product") || hasToken2(s.itemTypeTokens, "product") || hasToken2(s.itempropTokens, "product") || hasToken2(s.itempropTokens, "offer");
|
|
8985
|
-
if (strong)
|
|
9487
|
+
if (strong || s.listedProducts > 0)
|
|
8986
9488
|
return true;
|
|
8987
9489
|
return countTokens(s.itempropTokens, PRICE_ITEMPROPS) >= 2;
|
|
8988
9490
|
}
|
|
@@ -8999,8 +9501,100 @@ function hasForumSignals(s) {
|
|
|
8999
9501
|
return true;
|
|
9000
9502
|
return hasToken2(s.jsonLdTypes, "forum") || hasToken2(s.itemTypeTokens, "forum");
|
|
9001
9503
|
}
|
|
9002
|
-
function
|
|
9003
|
-
|
|
9504
|
+
function namedNotListing(el) {
|
|
9505
|
+
const name = `${el.getAttribute("id") ?? ""} ${el.getAttribute("class") ?? ""}`.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
9506
|
+
return name.split(/[\s_-]+/).some((part) => NOT_LISTING_NAMES.some((word) => part.startsWith(word)));
|
|
9507
|
+
}
|
|
9508
|
+
function leadsWithLink(card) {
|
|
9509
|
+
let lead = 0;
|
|
9510
|
+
const walk = (node) => {
|
|
9511
|
+
for (const child of Array.from(node.childNodes)) {
|
|
9512
|
+
if (child.nodeType === 3) {
|
|
9513
|
+
lead += ((child.textContent ?? "").match(/[\p{L}\p{N}]/gu) ?? []).length;
|
|
9514
|
+
if (lead > CARD_LEAD_CHARS)
|
|
9515
|
+
return false;
|
|
9516
|
+
} else if (child.nodeType === 1) {
|
|
9517
|
+
const el = child;
|
|
9518
|
+
const href = tagOf(el) === "a" ? (el.getAttribute("href") ?? "").trim() : "";
|
|
9519
|
+
if (href !== "" && !href.startsWith("#"))
|
|
9520
|
+
return true;
|
|
9521
|
+
const found = walk(el);
|
|
9522
|
+
if (found !== null)
|
|
9523
|
+
return found;
|
|
9524
|
+
}
|
|
9525
|
+
}
|
|
9526
|
+
return null;
|
|
9527
|
+
};
|
|
9528
|
+
return walk(card) === true;
|
|
9529
|
+
}
|
|
9530
|
+
function largestCardGroup(doc) {
|
|
9531
|
+
let best = [];
|
|
9532
|
+
const inNamed = /* @__PURE__ */ new Map();
|
|
9533
|
+
const insideNamed = (el) => {
|
|
9534
|
+
const unsettled = [];
|
|
9535
|
+
let answer = false;
|
|
9536
|
+
for (let up = el; up !== null; up = up.parentElement) {
|
|
9537
|
+
const known = inNamed.get(up);
|
|
9538
|
+
if (known !== void 0) {
|
|
9539
|
+
answer = known;
|
|
9540
|
+
break;
|
|
9541
|
+
}
|
|
9542
|
+
unsettled.push(up);
|
|
9543
|
+
}
|
|
9544
|
+
for (const up of unsettled.reverse()) {
|
|
9545
|
+
answer ||= namedNotListing(up);
|
|
9546
|
+
inNamed.set(up, answer);
|
|
9547
|
+
}
|
|
9548
|
+
return answer;
|
|
9549
|
+
};
|
|
9550
|
+
for (const el of qsa(doc, "*")) {
|
|
9551
|
+
if (el.children.length < GRID_CARDS || insideNamed(el))
|
|
9552
|
+
continue;
|
|
9553
|
+
const templates = /* @__PURE__ */ new Map();
|
|
9554
|
+
for (const kid of Array.from(el.children)) {
|
|
9555
|
+
if (!CARD_TAGS.has(tagOf(kid)) || namedNotListing(kid) || !isCard(kid) || !leadsWithLink(kid))
|
|
9556
|
+
continue;
|
|
9557
|
+
const template = `${tagOf(kid)} ${kid.getAttribute("class") ?? ""}`;
|
|
9558
|
+
const group = templates.get(template);
|
|
9559
|
+
if (group === void 0)
|
|
9560
|
+
templates.set(template, [kid]);
|
|
9561
|
+
else
|
|
9562
|
+
group.push(kid);
|
|
9563
|
+
}
|
|
9564
|
+
for (const group of templates.values())
|
|
9565
|
+
if (group.length > best.length)
|
|
9566
|
+
best = group;
|
|
9567
|
+
}
|
|
9568
|
+
return best;
|
|
9569
|
+
}
|
|
9570
|
+
function hasCardGrid(doc) {
|
|
9571
|
+
const cards = largestCardGroup(doc);
|
|
9572
|
+
if (cards.length < GRID_CARDS)
|
|
9573
|
+
return false;
|
|
9574
|
+
const collapsedLength = (el) => textOf(el).replace(/\s+/g, " ").trim().length;
|
|
9575
|
+
const cardSet = new Set(cards);
|
|
9576
|
+
const blocks = classifyBlocks(doc);
|
|
9577
|
+
const blockSet = new Set(blocks.map((b) => b.el));
|
|
9578
|
+
let outside = 0;
|
|
9579
|
+
for (const block of blocks) {
|
|
9580
|
+
let counted = true;
|
|
9581
|
+
for (let up = block.el.parentElement; up !== null; up = up.parentElement) {
|
|
9582
|
+
if (cardSet.has(up) || blockSet.has(up)) {
|
|
9583
|
+
counted = false;
|
|
9584
|
+
break;
|
|
9585
|
+
}
|
|
9586
|
+
}
|
|
9587
|
+
if (counted && !cardSet.has(block.el) && !STRUCTURAL_BLOCKS.has(tagOf(block.el)))
|
|
9588
|
+
outside += block.length;
|
|
9589
|
+
}
|
|
9590
|
+
const inside = cards.reduce((sum, card) => sum + collapsedLength(card), 0);
|
|
9591
|
+
return inside >= outside * 2;
|
|
9592
|
+
}
|
|
9593
|
+
function routeByCounts(c, s, doc) {
|
|
9594
|
+
let grid;
|
|
9595
|
+
const cardGrid = () => grid ??= hasCardGrid(doc);
|
|
9596
|
+
const listedOnly = s.listedProducts >= PRODUCT_CARDS && COLLECTION_PAGE_TYPES.some((type) => hasToken2(s.pageTypes, type)) && (s.productCards || !hasToken2(s.itemTypeTokens, "product"));
|
|
9597
|
+
if ((s.productCards || listedOnly) && !c.buyBox && !hasToken2(s.jsonLdTypes, "product")) {
|
|
9004
9598
|
return { type: "collection", strategy: "article" };
|
|
9005
9599
|
}
|
|
9006
9600
|
if (hasProductSignals(s)) {
|
|
@@ -9012,8 +9606,8 @@ function routeByCounts(c, s) {
|
|
|
9012
9606
|
if (hasForumSignals(s))
|
|
9013
9607
|
return { type: "forum", strategy: "article" };
|
|
9014
9608
|
if (c.buyBox && !hasArticleSignals(s))
|
|
9015
|
-
return { type: "product", strategy: "product" };
|
|
9016
|
-
if (c.main >= 1 && c.p >= 3 && c.headings >= 2) {
|
|
9609
|
+
return { type: "product", strategy: "product", buyBox: true };
|
|
9610
|
+
if (c.main >= 1 && c.p >= 3 && c.headings >= 2 && !cardGrid()) {
|
|
9017
9611
|
return { type: "article", strategy: "article" };
|
|
9018
9612
|
}
|
|
9019
9613
|
const textInTables = c.tableChars >= c.textChars * 0.5;
|
|
@@ -9035,13 +9629,15 @@ function routeByCounts(c, s) {
|
|
|
9035
9629
|
if (c.textChars > 3e3 && c.headings >= 5 && c.a >= 20) {
|
|
9036
9630
|
return { type: "collection", strategy: "article" };
|
|
9037
9631
|
}
|
|
9632
|
+
if (cardGrid())
|
|
9633
|
+
return { type: "collection", strategy: "article" };
|
|
9038
9634
|
return { type: "article", strategy: "article" };
|
|
9039
9635
|
}
|
|
9040
9636
|
function pageSignalsFor(doc) {
|
|
9041
9637
|
return collectPageSignals(doc);
|
|
9042
9638
|
}
|
|
9043
9639
|
function routePage(doc, signals = collectPageSignals(doc)) {
|
|
9044
|
-
return routeByCounts(countAll(doc), signals);
|
|
9640
|
+
return routeByCounts(countAll(doc), signals, doc);
|
|
9045
9641
|
}
|
|
9046
9642
|
function selectList(doc) {
|
|
9047
9643
|
let best = null;
|
|
@@ -9086,9 +9682,39 @@ function textOutsideLinks(node) {
|
|
|
9086
9682
|
return text2;
|
|
9087
9683
|
}
|
|
9088
9684
|
function isCard(item) {
|
|
9089
|
-
|
|
9685
|
+
const links = qsa(item, "a[href]");
|
|
9686
|
+
if (links.length === 0)
|
|
9090
9687
|
return false;
|
|
9091
|
-
|
|
9688
|
+
if ((textOutsideLinks(item).match(/[\p{L}\p{N}]/gu) ?? []).length >= 3)
|
|
9689
|
+
return true;
|
|
9690
|
+
return links.some((link2) => qsa(link2, "h2,h3,h4,h5,h6").some((heading2) => /[\p{L}\p{N}]/u.test(textOf(heading2))));
|
|
9691
|
+
}
|
|
9692
|
+
function alikeCards(cards) {
|
|
9693
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
9694
|
+
for (const card of cards) {
|
|
9695
|
+
const group = byTag.get(tagOf(card));
|
|
9696
|
+
if (group === void 0)
|
|
9697
|
+
byTag.set(tagOf(card), [card]);
|
|
9698
|
+
else
|
|
9699
|
+
group.push(card);
|
|
9700
|
+
}
|
|
9701
|
+
let most = 0;
|
|
9702
|
+
for (const group of byTag.values()) {
|
|
9703
|
+
const classes = group.map((card) => new Set(splitTokens(card.getAttribute("class") ?? "")));
|
|
9704
|
+
const classless = classes.every((set2) => set2.size === 0);
|
|
9705
|
+
const shared = [...classes[0]].some((name) => classes.every((set2) => set2.has(name)));
|
|
9706
|
+
if (classless || shared) {
|
|
9707
|
+
most = Math.max(most, group.length);
|
|
9708
|
+
continue;
|
|
9709
|
+
}
|
|
9710
|
+
const exact = /* @__PURE__ */ new Map();
|
|
9711
|
+
for (const set2 of classes) {
|
|
9712
|
+
const key = [...set2].sort().join(" ");
|
|
9713
|
+
exact.set(key, (exact.get(key) ?? 0) + 1);
|
|
9714
|
+
}
|
|
9715
|
+
most = Math.max(most, ...exact.values());
|
|
9716
|
+
}
|
|
9717
|
+
return most;
|
|
9092
9718
|
}
|
|
9093
9719
|
function selectCardList(doc) {
|
|
9094
9720
|
const h1s = qsa(doc, "h1");
|
|
@@ -9104,14 +9730,7 @@ function selectCardList(doc) {
|
|
|
9104
9730
|
continue;
|
|
9105
9731
|
if (el.closest("header") !== null)
|
|
9106
9732
|
continue;
|
|
9107
|
-
const
|
|
9108
|
-
for (const kid of Array.from(el.children)) {
|
|
9109
|
-
if (!isCard(kid))
|
|
9110
|
-
continue;
|
|
9111
|
-
const template = `${kid.tagName} ${kid.getAttribute("class") ?? ""}`;
|
|
9112
|
-
templates.set(template, (templates.get(template) ?? 0) + 1);
|
|
9113
|
-
}
|
|
9114
|
-
const cards = Math.max(0, ...templates.values());
|
|
9733
|
+
const cards = alikeCards(Array.from(el.children).filter(isCard));
|
|
9115
9734
|
if (cards >= 3 && cards > bestCards) {
|
|
9116
9735
|
bestCards = cards;
|
|
9117
9736
|
best = el;
|
|
@@ -9235,10 +9854,11 @@ function elementCounts(doc) {
|
|
|
9235
9854
|
}
|
|
9236
9855
|
return counts;
|
|
9237
9856
|
}
|
|
9238
|
-
var PRODUCT_CARDS, PRICE_ITEMPROPS, ARTICLE_TYPES, DETECTED_LIST_ITEMS, DETECTED_ITEM_CHARS;
|
|
9857
|
+
var PRODUCT_CARDS, PRICE_ITEMPROPS, ARTICLE_TYPES, GRID_CARDS, CARD_TAGS, CARD_LEAD_CHARS, STRUCTURAL_BLOCKS, NOT_LISTING_NAMES, COLLECTION_PAGE_TYPES, DETECTED_LIST_ITEMS, DETECTED_ITEM_CHARS;
|
|
9239
9858
|
var init_route = __esm({
|
|
9240
9859
|
"packages/extract-tf/dist/route.js"() {
|
|
9241
9860
|
"use strict";
|
|
9861
|
+
init_classify();
|
|
9242
9862
|
init_detectList();
|
|
9243
9863
|
init_dom();
|
|
9244
9864
|
init_prune();
|
|
@@ -9247,6 +9867,12 @@ var init_route = __esm({
|
|
|
9247
9867
|
PRODUCT_CARDS = 3;
|
|
9248
9868
|
PRICE_ITEMPROPS = ["price", "offers", "sku", "gtin", "mpn", "brand"];
|
|
9249
9869
|
ARTICLE_TYPES = ["article", "newsarticle", "blogposting", "report", "scholarlyarticle", "techarticle"];
|
|
9870
|
+
GRID_CARDS = 4;
|
|
9871
|
+
CARD_TAGS = /* @__PURE__ */ new Set(["li", "div", "article", "section"]);
|
|
9872
|
+
CARD_LEAD_CHARS = 24;
|
|
9873
|
+
STRUCTURAL_BLOCKS = /* @__PURE__ */ new Set(["li", "td", "th"]);
|
|
9874
|
+
NOT_LISTING_NAMES = ["comment", "reply", "replies", "reference", "reflist", "cite", "citation", "footnote", "endnote"];
|
|
9875
|
+
COLLECTION_PAGE_TYPES = ["collectionpage", "searchresultspage"];
|
|
9250
9876
|
DETECTED_LIST_ITEMS = 3;
|
|
9251
9877
|
DETECTED_ITEM_CHARS = 40;
|
|
9252
9878
|
}
|
|
@@ -9322,9 +9948,9 @@ function deliveryLocation(doc) {
|
|
|
9322
9948
|
const raw = text(doc, selector);
|
|
9323
9949
|
if (raw === null || /^update\s+location$/i.test(raw))
|
|
9324
9950
|
continue;
|
|
9325
|
-
const
|
|
9326
|
-
if (
|
|
9327
|
-
return { value:
|
|
9951
|
+
const normalized2 = clean(raw.replace(/^deliver(?:ing|y)?\s+to\s+/i, "").replace(/[–—-]?\s*update\s+location\s*$/i, "").replace(/\b(Singapore)\s*(\d{6})\b/i, "$1 $2"));
|
|
9952
|
+
if (normalized2 !== null)
|
|
9953
|
+
return { value: normalized2, selector };
|
|
9328
9954
|
}
|
|
9329
9955
|
return null;
|
|
9330
9956
|
}
|
|
@@ -9392,8 +10018,8 @@ function verifyProductIdentity(doc, url, selectedVariants) {
|
|
|
9392
10018
|
const requestedId = amazonAsin(url) ?? "";
|
|
9393
10019
|
const witnesses = [];
|
|
9394
10020
|
const asin = (value) => {
|
|
9395
|
-
const
|
|
9396
|
-
return
|
|
10021
|
+
const normalized2 = clean(value)?.toUpperCase();
|
|
10022
|
+
return normalized2 && /^[A-Z0-9]{10}$/.test(normalized2) ? normalized2 : null;
|
|
9397
10023
|
};
|
|
9398
10024
|
const add = (value, path) => {
|
|
9399
10025
|
const id = asin(value);
|
|
@@ -9645,8 +10271,8 @@ function clean2(value) {
|
|
|
9645
10271
|
const next = (value ?? "").replace(/\s+/g, " ").trim();
|
|
9646
10272
|
return next.length > 0 ? next : null;
|
|
9647
10273
|
}
|
|
9648
|
-
function field2(raw, source, path,
|
|
9649
|
-
return { raw, normalized, source, path, status: "confirmed" };
|
|
10274
|
+
function field2(raw, source, path, normalized2 = raw) {
|
|
10275
|
+
return { raw, normalized: normalized2, source, path, status: "confirmed" };
|
|
9650
10276
|
}
|
|
9651
10277
|
function record2(value) {
|
|
9652
10278
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -10234,7 +10860,10 @@ function pickTitle(doc, main) {
|
|
|
10234
10860
|
const docTitle = doc.querySelector("title");
|
|
10235
10861
|
return docTitle ? textOf(docTitle).trim() : null;
|
|
10236
10862
|
}
|
|
10237
|
-
function
|
|
10863
|
+
function showsBuyBox(main, price) {
|
|
10864
|
+
return main.querySelector("h1,h2") !== null && textOf(main).replace(/\s+/g, " ").includes(price.replace(/\s+/g, " ").trim());
|
|
10865
|
+
}
|
|
10866
|
+
function confidenceOf(blocksLen, main, totalBlocks, mainLength, pageType, productShown, favorPrecision, favorRecall, product) {
|
|
10238
10867
|
let conf = totalBlocks > 0 ? blocksLen / totalBlocks : 0;
|
|
10239
10868
|
if (main && ["ARTICLE", "MAIN"].includes(main.tagName))
|
|
10240
10869
|
conf = Math.min(1, conf + 0.15);
|
|
@@ -10244,6 +10873,8 @@ function confidenceOf(blocksLen, main, totalBlocks, mainLength, pageType, favorP
|
|
|
10244
10873
|
conf = Math.min(conf, 0.75);
|
|
10245
10874
|
if (product !== null && main !== null && product.name !== null && product.price !== null && product.name.source !== "text" && product.price.source !== "text") {
|
|
10246
10875
|
conf = Math.max(conf, 0.6);
|
|
10876
|
+
} else if (productShown) {
|
|
10877
|
+
conf = Math.max(conf, BUY_BOX_CONFIDENCE);
|
|
10247
10878
|
}
|
|
10248
10879
|
if (favorPrecision)
|
|
10249
10880
|
conf = Math.min(conf, 0.85);
|
|
@@ -10251,7 +10882,16 @@ function confidenceOf(blocksLen, main, totalBlocks, mainLength, pageType, favorP
|
|
|
10251
10882
|
conf = Math.max(conf, 0.3);
|
|
10252
10883
|
return Math.round(conf * 100) / 100;
|
|
10253
10884
|
}
|
|
10254
|
-
|
|
10885
|
+
function showsDeclaredProduct(main, product) {
|
|
10886
|
+
if (main === null || product === null || product.name === null || product.price === null)
|
|
10887
|
+
return false;
|
|
10888
|
+
if (product.name.source === "text" || product.price.source === "text")
|
|
10889
|
+
return false;
|
|
10890
|
+
const text2 = textOf(main).replace(/\s+/g, " ");
|
|
10891
|
+
const price = product.price.value.replace(/[^\d.,]/g, "");
|
|
10892
|
+
return text2.includes(product.name.value.replace(/\s+/g, " ").trim()) && price !== "" && text2.includes(price);
|
|
10893
|
+
}
|
|
10894
|
+
var DEFAULT_CLASSIFY, BUY_BOX_CONFIDENCE, DESCRIPTION_MIN_CHARS, ExtractTf, extractTf;
|
|
10255
10895
|
var init_extract = __esm({
|
|
10256
10896
|
"packages/extract-tf/dist/extract.js"() {
|
|
10257
10897
|
"use strict";
|
|
@@ -10272,6 +10912,8 @@ var init_extract = __esm({
|
|
|
10272
10912
|
minTextLength: 25,
|
|
10273
10913
|
maxLinkDensity: 0.2
|
|
10274
10914
|
};
|
|
10915
|
+
BUY_BOX_CONFIDENCE = 0.45;
|
|
10916
|
+
DESCRIPTION_MIN_CHARS = 150;
|
|
10275
10917
|
ExtractTf = class {
|
|
10276
10918
|
extract(html2, options = {}) {
|
|
10277
10919
|
const { favorPrecision = false, favorRecall = false, pruneSelectors, includeSelectors, blockAds = true } = options;
|
|
@@ -10291,10 +10933,13 @@ var init_extract = __esm({
|
|
|
10291
10933
|
const fetchPreloads = Array.from(doc.document.querySelectorAll("link[rel][as]")).filter((link2) => (link2.getAttribute("rel") ?? "").toLowerCase().split(/\s+/).includes("preload") && (link2.getAttribute("as") ?? "").trim().toLowerCase() === "fetch").length;
|
|
10292
10934
|
const raw = rawSignals(doc.document);
|
|
10293
10935
|
const excluded = namedBy(doc.document, pruneSelectors ?? []);
|
|
10936
|
+
const optionGroups = markOptionGroups(doc.document, excluded);
|
|
10294
10937
|
cleanTree(doc.document, excluded);
|
|
10295
10938
|
pruneTree(doc.document, { blockAds });
|
|
10296
10939
|
detachAll(excluded);
|
|
10297
10940
|
const decision = amazonProduct ? { type: "product", strategy: "product" } : routePage(doc.document, signals);
|
|
10941
|
+
settleOptionGroups(optionGroups, decision.type === "product");
|
|
10942
|
+
const dataShown = decision.buyBox === true && raw.hydrationJson.length > 0 && hydrationShown(raw, doc.document);
|
|
10298
10943
|
if (decision.type === "product")
|
|
10299
10944
|
pruneRecommendations(doc.document);
|
|
10300
10945
|
const classifyOptions = { ...DEFAULT_CLASSIFY, favorPrecision };
|
|
@@ -10354,6 +10999,7 @@ var init_extract = __esm({
|
|
|
10354
10999
|
}
|
|
10355
11000
|
const blocks = classifyBlocks(doc.document, classifyOptions);
|
|
10356
11001
|
const mainLength = main ? textOf(main).length : 0;
|
|
11002
|
+
const productShown = decision.buyBox === true && main !== null && product !== null && product.price !== null && showsBuyBox(main, product.price.value) && blocks.some((b) => main.contains(b.el) && !/^h[1-6]$/.test(b.el.tagName.toLowerCase()) && b.length >= DESCRIPTION_MIN_CHARS);
|
|
10357
11003
|
const adapter = amazonProduct ? adapterFor(doc.document, options.url, product) : preliminaryAdapter;
|
|
10358
11004
|
const selection = includeSelectors !== void 0 && includeSelectors.length > 0 ? selectionBody(html2, includeSelectors, pruneSelectors) : null;
|
|
10359
11005
|
const selected = selection !== null && !selection.blank;
|
|
@@ -10362,7 +11008,7 @@ var init_extract = __esm({
|
|
|
10362
11008
|
mainHtml: selection !== null ? selection.html : main ? outerHtml(main) : "",
|
|
10363
11009
|
baseUrl,
|
|
10364
11010
|
metadata,
|
|
10365
|
-
confidence: selected ? 1 : confidenceOf(blocks.filter((b) => main?.contains(b.el)).length, main, blocks.length, mainLength, decision.type, favorPrecision, favorRecall, product),
|
|
11011
|
+
confidence: selected ? 1 : confidenceOf(blocks.filter((b) => main?.contains(b.el)).length, main, blocks.length, mainLength, decision.type, productShown, favorPrecision, favorRecall, product),
|
|
10366
11012
|
// Escalate only when a strategy produced nothing at all. Routing to a
|
|
10367
11013
|
// non-article strategy is not by itself an escalation reason.
|
|
10368
11014
|
escalate: main === null,
|
|
@@ -10375,7 +11021,12 @@ var init_extract = __esm({
|
|
|
10375
11021
|
adapterValidation: amazonValidation ?? adapter.validation,
|
|
10376
11022
|
emptyTableShells,
|
|
10377
11023
|
fetchPreloads,
|
|
10378
|
-
render: detectRenderSignals(raw, doc.document
|
|
11024
|
+
render: detectRenderSignals(raw, doc.document, {
|
|
11025
|
+
productShown,
|
|
11026
|
+
hydrationShown: dataShown,
|
|
11027
|
+
contentShown: showsDeclaredProduct(main, product),
|
|
11028
|
+
listing: decision.type === "listing" || decision.type === "collection"
|
|
11029
|
+
}),
|
|
10379
11030
|
labelledValues: main ? collectLabelledValues(main) : [],
|
|
10380
11031
|
timings: { parseMs, extractMs: Math.max(0, performance.now() - extractionStart) }
|
|
10381
11032
|
};
|
|
@@ -11627,11 +12278,25 @@ function errorCode3(error) {
|
|
|
11627
12278
|
return null;
|
|
11628
12279
|
}
|
|
11629
12280
|
function robotsOverrideWarning(decision, override) {
|
|
11630
|
-
const
|
|
12281
|
+
const robotsUrl = decision.robotsUrl ?? "robots.txt";
|
|
12282
|
+
const verdict = decision.unreachable === void 0 ? `${robotsUrl} disallows this URL (rule ${decision.appliedRules.map((rule) => rule.pattern).join(", ")})` : `${robotsUrl} could not be read (${decision.unreachable}), which counts as a complete disallow`;
|
|
11631
12283
|
const who = override.recordedBy === void 0 ? "" : ` by ${override.recordedBy}`;
|
|
12284
|
+
const why = override.basis === "user_named_url" ? "it was fetched because the request named it: robots.txt binds the links a crawl or map discovers, not the URLs a person names" : override.basis === "ignore_robots_txt" ? "it was fetched because the crawl or map was started with ignoreRobotsTxt" : `it was fetched under an override recorded${who}: ${override.reason}`;
|
|
12285
|
+
return { code: "robots_overridden", message: `${verdict}; ${why}` };
|
|
12286
|
+
}
|
|
12287
|
+
function applicableOverride(override, decision) {
|
|
12288
|
+
if (override?.basis !== void 0 && isOctocrawlRobotsGroup(decision.matchedUserAgentGroup))
|
|
12289
|
+
return void 0;
|
|
12290
|
+
return override;
|
|
12291
|
+
}
|
|
12292
|
+
function overriddenDetail(url, decision, override) {
|
|
11632
12293
|
return {
|
|
11633
|
-
|
|
11634
|
-
|
|
12294
|
+
url,
|
|
12295
|
+
appliedRules: decision.appliedRules,
|
|
12296
|
+
...decision.unreachable === void 0 ? {} : { unreachable: decision.unreachable },
|
|
12297
|
+
reason: override.reason,
|
|
12298
|
+
...override.recordedBy === void 0 ? {} : { recordedBy: override.recordedBy },
|
|
12299
|
+
basis: override.basis ?? "robots_override"
|
|
11635
12300
|
};
|
|
11636
12301
|
}
|
|
11637
12302
|
function robotsOverrideApplied(trace, warning) {
|
|
@@ -11809,11 +12474,13 @@ var init_robotsLookup = __esm({
|
|
|
11809
12474
|
path = parsed.pathname + parsed.search;
|
|
11810
12475
|
} catch {
|
|
11811
12476
|
}
|
|
11812
|
-
const
|
|
12477
|
+
const agent = robotsAgent(userAgent);
|
|
12478
|
+
const match = evaluateRobots(cached.robots, agent, path);
|
|
12479
|
+
const own = matchRobotsGroup(cached.robots, agent)?.agents.find((token) => isOctocrawlRobotsGroup(token));
|
|
11813
12480
|
return {
|
|
11814
12481
|
robotsUrl: cached.robotsUrl,
|
|
11815
12482
|
robotsSha256: cached.sha256,
|
|
11816
|
-
matchedUserAgentGroup: match.matchedAgent,
|
|
12483
|
+
matchedUserAgentGroup: own ?? match.matchedAgent,
|
|
11817
12484
|
appliedRules: match.appliedRules.map((r) => ({ pattern: r.pattern, allow: r.allow })),
|
|
11818
12485
|
decision: match.allowed ? "allowed" : "disallowed",
|
|
11819
12486
|
skippedFetch: false,
|
|
@@ -11930,7 +12597,7 @@ var init_provider2 = __esm({
|
|
|
11930
12597
|
{ at: 0, lane: "provider", event: "provider_selected", detail: { provider: this.provider.id } }
|
|
11931
12598
|
];
|
|
11932
12599
|
const preflight = evaluateProviderGate(this.provider, null, "/");
|
|
11933
|
-
if (preflight.refusal === "refused_capability" || preflight.refusal === "undeclared_user_agent") {
|
|
12600
|
+
if (preflight.refusal === "refused_capability" || preflight.refusal === "ungranted_capability" || preflight.refusal === "undeclared_user_agent") {
|
|
11934
12601
|
return this.denied(url, start, trace, preflight, {
|
|
11935
12602
|
robotsUrl: null,
|
|
11936
12603
|
robotsSha256: null,
|
|
@@ -12404,14 +13071,14 @@ var init_provider2 = __esm({
|
|
|
12404
13071
|
});
|
|
12405
13072
|
|
|
12406
13073
|
// packages/cli/src/run.ts
|
|
12407
|
-
import { mkdir as mkdir9, readFile as
|
|
13074
|
+
import { mkdir as mkdir9, readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
|
|
12408
13075
|
import { join as join12 } from "path";
|
|
12409
13076
|
|
|
12410
13077
|
// packages/api/dist/app.js
|
|
12411
13078
|
import { Hono } from "hono";
|
|
12412
13079
|
import { streamSSE } from "hono/streaming";
|
|
12413
13080
|
import { createNodeWebSocket } from "@hono/node-ws";
|
|
12414
|
-
import { createHash as
|
|
13081
|
+
import { createHash as createHash15 } from "crypto";
|
|
12415
13082
|
|
|
12416
13083
|
// packages/runtime/dist/taskStore.js
|
|
12417
13084
|
function assertId(label, value) {
|
|
@@ -13323,6 +13990,7 @@ var CrawlOrchestrator = class {
|
|
|
13323
13990
|
frontierOptions;
|
|
13324
13991
|
workerCount;
|
|
13325
13992
|
signal;
|
|
13993
|
+
scrapeErrorCostUsd;
|
|
13326
13994
|
shutdownSignal;
|
|
13327
13995
|
sitemapSource;
|
|
13328
13996
|
onStep;
|
|
@@ -13335,6 +14003,7 @@ var CrawlOrchestrator = class {
|
|
|
13335
14003
|
this.frontierOptions = options;
|
|
13336
14004
|
this.workerCount = Math.max(1, options.workerCount ?? 4);
|
|
13337
14005
|
this.signal = options.signal;
|
|
14006
|
+
this.scrapeErrorCostUsd = options.scrapeErrorCostUsd === void 0 ? null : options.scrapeErrorCostUsd;
|
|
13338
14007
|
this.shutdownSignal = options.shutdownSignal;
|
|
13339
14008
|
this.sitemapSource = options.sitemapSource;
|
|
13340
14009
|
this.onStep = options.onStep;
|
|
@@ -13552,7 +14221,7 @@ var CrawlOrchestrator = class {
|
|
|
13552
14221
|
} catch (error) {
|
|
13553
14222
|
if (stopped())
|
|
13554
14223
|
throw error;
|
|
13555
|
-
outcome = { result: scrapeErrorResult(item.url, error, Date.now() - scrapeStartedAt), links: [] };
|
|
14224
|
+
outcome = { result: scrapeErrorResult(item.url, error, Date.now() - scrapeStartedAt, this.scrapeErrorCostUsd), links: [] };
|
|
13556
14225
|
}
|
|
13557
14226
|
result = outcome.result;
|
|
13558
14227
|
links = outcome.links.length > 0 ? outcome.links : linksOf(outcome.result);
|
|
@@ -13954,7 +14623,7 @@ function newAttempt(id, taskId, startedAt, recoveredFromAttemptId = null) {
|
|
|
13954
14623
|
recoveredFromAttemptId
|
|
13955
14624
|
};
|
|
13956
14625
|
}
|
|
13957
|
-
function scrapeErrorResult(url, error, wallMs) {
|
|
14626
|
+
function scrapeErrorResult(url, error, wallMs, externalCostUsd) {
|
|
13958
14627
|
const name = error instanceof Error ? error.name : typeof error;
|
|
13959
14628
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
13960
14629
|
return {
|
|
@@ -13971,7 +14640,7 @@ function scrapeErrorResult(url, error, wallMs) {
|
|
|
13971
14640
|
truncatedAt: null,
|
|
13972
14641
|
compliance: null,
|
|
13973
14642
|
evidence: { finalUrl: url, httpStatus: null, redirectChain: [], contentType: null, rawBodySha256: null, artifacts: [] },
|
|
13974
|
-
usage: { ...EMPTY_USAGE, wallMs, bytesWire: null },
|
|
14643
|
+
usage: { ...EMPTY_USAGE, wallMs, bytesWire: null, externalCostUsd },
|
|
13975
14644
|
trace: [{ at: wallMs, lane: "http", event: "scrape_error", detail: { name, error: message2.slice(0, 500) } }]
|
|
13976
14645
|
};
|
|
13977
14646
|
}
|
|
@@ -15657,9 +16326,40 @@ function toEvidenceRecord(result, request7, output, options = {}) {
|
|
|
15657
16326
|
device: requested ? declaredDevice(result) : null,
|
|
15658
16327
|
requestHeaders: requested ? sentCustomHeaders(result) : null
|
|
15659
16328
|
},
|
|
15660
|
-
pageActions: pageActions(result)
|
|
16329
|
+
pageActions: pageActions(result),
|
|
16330
|
+
access: evidenceAccess(result)
|
|
15661
16331
|
};
|
|
15662
16332
|
}
|
|
16333
|
+
function evidenceAccess(result) {
|
|
16334
|
+
const event = (name) => result.trace.find((e) => e.event === name)?.detail;
|
|
16335
|
+
const text2 = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
16336
|
+
const hit = event("cache_hit");
|
|
16337
|
+
const stored = hit?.externalCostUsd;
|
|
16338
|
+
const externalCostUsd = hit === void 0 ? result.usage.externalCostUsd ?? null : typeof stored === "number" ? stored : null;
|
|
16339
|
+
const route = (r, executor, executorVersion = null, profile = null) => ({ route: r, executor, executorVersion, profile, externalCostUsd });
|
|
16340
|
+
const laneRan = result.trace.some((e) => e.event === "identity_sent" || e.event === "identity_declared" || e.event === "provider_selected");
|
|
16341
|
+
if (!laneRan)
|
|
16342
|
+
return route(null, null);
|
|
16343
|
+
switch (result.lane) {
|
|
16344
|
+
case "http": {
|
|
16345
|
+
const transport = event("transport");
|
|
16346
|
+
return transport === void 0 ? route("http", "undici") : route("http_compat", text2(transport.library), text2(transport.version), text2(transport.profile));
|
|
16347
|
+
}
|
|
16348
|
+
case "browser_local":
|
|
16349
|
+
case "browser_proxy": {
|
|
16350
|
+
const engine = event("browser_engine");
|
|
16351
|
+
if (event("session_attached") !== void 0)
|
|
16352
|
+
return route("authed_browser", "playwright");
|
|
16353
|
+
return engine !== void 0 && engine.engine === "patchright" ? route("enhanced_browser", "patchright", text2(engine.version)) : route("browser", "playwright");
|
|
16354
|
+
}
|
|
16355
|
+
case "browser_local_authed": {
|
|
16356
|
+
const read2 = event("user_browser_read");
|
|
16357
|
+
return read2 === void 0 ? route("authed_browser", "playwright") : route("user_browser", text2(read2.browser));
|
|
16358
|
+
}
|
|
16359
|
+
case "provider":
|
|
16360
|
+
return route("vendor", text2(event("provider_selected")?.provider));
|
|
16361
|
+
}
|
|
16362
|
+
}
|
|
15663
16363
|
function pageActions(result) {
|
|
15664
16364
|
if (result.actions === void 0)
|
|
15665
16365
|
return null;
|
|
@@ -15704,11 +16404,13 @@ function robotsDecision(result) {
|
|
|
15704
16404
|
robotsSha256: signed.robotsSha256,
|
|
15705
16405
|
unreachable: signed.unreachable ?? null,
|
|
15706
16406
|
crawlDelayMs: signed.crawlDelayMs ?? null,
|
|
15707
|
-
//
|
|
15708
|
-
userOverride: signed.override !== void 0
|
|
16407
|
+
// An override set the disallow aside; the record carries it and on whose word.
|
|
16408
|
+
userOverride: signed.override !== void 0,
|
|
16409
|
+
overrideBasis: signed.override === void 0 ? null : signed.override.basis ?? "robots_override"
|
|
15709
16410
|
};
|
|
15710
16411
|
}
|
|
15711
16412
|
const detail = [...result.trace].reverse().find((event) => event.event === "robots_checked")?.detail;
|
|
16413
|
+
const overridden = result.trace.find((event) => event.event === "robots_overridden");
|
|
15712
16414
|
const decision = detail?.decision;
|
|
15713
16415
|
if (decision !== "allowed" && decision !== "disallowed" && decision !== "no_robots")
|
|
15714
16416
|
return null;
|
|
@@ -15721,10 +16423,14 @@ function robotsDecision(result) {
|
|
|
15721
16423
|
robotsSha256: typeof detail?.robotsSha256 === "string" ? detail.robotsSha256 : null,
|
|
15722
16424
|
unreachable: typeof detail?.unreachable === "string" ? detail.unreachable : null,
|
|
15723
16425
|
crawlDelayMs: typeof detail?.crawlDelayMs === "number" ? detail.crawlDelayMs : null,
|
|
15724
|
-
// The HTTP lane mints no record; its trace says when
|
|
15725
|
-
userOverride:
|
|
16426
|
+
// The HTTP lane mints no record; its trace says when an override set the disallow aside, and on whose word.
|
|
16427
|
+
userOverride: overridden !== void 0,
|
|
16428
|
+
overrideBasis: overridden === void 0 ? null : overrideBasisOf(overridden.detail?.basis)
|
|
15726
16429
|
};
|
|
15727
16430
|
}
|
|
16431
|
+
function overrideBasisOf(value) {
|
|
16432
|
+
return value === "user_named_url" || value === "ignore_robots_txt" ? value : "robots_override";
|
|
16433
|
+
}
|
|
15728
16434
|
function identityEvent(trace) {
|
|
15729
16435
|
const detail = trace.find((event) => event.event === "identity_sent")?.detail;
|
|
15730
16436
|
if (detail === void 0)
|
|
@@ -15816,7 +16522,7 @@ function pageCacheKey(url, parts) {
|
|
|
15816
16522
|
return requestFingerprint({ v: 1, url: href, ...parts });
|
|
15817
16523
|
}
|
|
15818
16524
|
function cacheHitResult(hit) {
|
|
15819
|
-
const event = { at: 0, lane: hit.result.lane, event: "cache_hit", detail: { cachedAt: hit.fetchedAt, ageMs: hit.ageMs } };
|
|
16525
|
+
const event = { at: 0, lane: hit.result.lane, event: "cache_hit", detail: { cachedAt: hit.fetchedAt, ageMs: hit.ageMs, externalCostUsd: hit.result.usage.externalCostUsd ?? null } };
|
|
15820
16526
|
return { ...hit.result, trace: [...hit.result.trace, event] };
|
|
15821
16527
|
}
|
|
15822
16528
|
function cacheMissResult(url, bounds) {
|
|
@@ -15834,7 +16540,7 @@ function cacheMissResult(url, bounds) {
|
|
|
15834
16540
|
truncatedAt: null,
|
|
15835
16541
|
compliance: null,
|
|
15836
16542
|
evidence: { finalUrl: url, httpStatus: null, redirectChain: [], contentType: null, rawBodySha256: null, artifacts: [] },
|
|
15837
|
-
usage: { wallMs: 0, bytesWire: 0, bytesDecompressed: 0, requestCount: 0, attemptCount: 0, contentTokens: null, browserMs: 0, externalCostUsd:
|
|
16543
|
+
usage: { wallMs: 0, bytesWire: 0, bytesDecompressed: 0, requestCount: 0, attemptCount: 0, contentTokens: null, browserMs: 0, externalCostUsd: 0 },
|
|
15838
16544
|
trace: [{ at: 0, lane: "http", event: "cache_miss", detail: { lockdown: true, ...bounds } }]
|
|
15839
16545
|
};
|
|
15840
16546
|
}
|
|
@@ -15937,6 +16643,8 @@ ${title ?? ""}`.toLowerCase();
|
|
|
15937
16643
|
const verdict = await this.sources.robotsVerdict(canonicalUrl, scope);
|
|
15938
16644
|
if (typeof verdict === "string")
|
|
15939
16645
|
return verdict;
|
|
16646
|
+
if (verdict.octocrawl === true)
|
|
16647
|
+
return "octocrawl";
|
|
15940
16648
|
return verdict.unreachable === void 0 ? "disallowed" : { unreachable: verdict.unreachable };
|
|
15941
16649
|
} catch (error) {
|
|
15942
16650
|
if (cancelled())
|
|
@@ -16024,6 +16732,8 @@ ${title ?? ""}`.toLowerCase();
|
|
|
16024
16732
|
seen.refused++;
|
|
16025
16733
|
}
|
|
16026
16734
|
};
|
|
16735
|
+
const ignoreRobots = spec.ignoreRobotsTxt === true;
|
|
16736
|
+
const linkVerdict = (verdict) => typeof verdict === "object" ? "unreachable" : verdict;
|
|
16027
16737
|
const titleOf = (found) => found.via === "link" ? found.text : found.entry.title;
|
|
16028
16738
|
const offer = async (result, found) => {
|
|
16029
16739
|
if (!result.accepted) {
|
|
@@ -16084,7 +16794,11 @@ ${title ?? ""}`.toLowerCase();
|
|
|
16084
16794
|
refused.robotsUnchecked++;
|
|
16085
16795
|
return false;
|
|
16086
16796
|
}
|
|
16087
|
-
if (verdict === "
|
|
16797
|
+
if (verdict === "octocrawl") {
|
|
16798
|
+
refuseRobots(canonicalUrl, "disallowed");
|
|
16799
|
+
return false;
|
|
16800
|
+
}
|
|
16801
|
+
if ((verdict === "disallowed" || typeof verdict === "object") && !ignoreRobots) {
|
|
16088
16802
|
refuseRobots(canonicalUrl, verdict);
|
|
16089
16803
|
return false;
|
|
16090
16804
|
}
|
|
@@ -16093,22 +16807,25 @@ ${title ?? ""}`.toLowerCase();
|
|
|
16093
16807
|
limitReached = true;
|
|
16094
16808
|
return true;
|
|
16095
16809
|
}
|
|
16096
|
-
add(canonicalUrl, found, verdict);
|
|
16810
|
+
add(canonicalUrl, found, linkVerdict(verdict));
|
|
16097
16811
|
return true;
|
|
16098
16812
|
};
|
|
16099
16813
|
let startPage = null;
|
|
16100
16814
|
let clientRendered = false;
|
|
16101
16815
|
if (sitemapMode !== "only") {
|
|
16102
|
-
const
|
|
16816
|
+
const fullStartVerdict = await verdictFor(startCanonical);
|
|
16817
|
+
const startVerdict = fullStartVerdict === "octocrawl" ? "disallowed" : fullStartVerdict;
|
|
16818
|
+
const startRefused = fullStartVerdict === "octocrawl" || !ignoreRobots;
|
|
16103
16819
|
const blank = { url: spec.url, finalUrl: null, httpStatus: null, lane: "http", rawBodySha256: null, linksFound: 0, title: null, description: null };
|
|
16104
|
-
if (startVerdict === "disallowed" || typeof startVerdict === "object") {
|
|
16820
|
+
if ((startVerdict === "disallowed" || typeof startVerdict === "object") && startRefused) {
|
|
16105
16821
|
refuseRobots(startCanonical, startVerdict);
|
|
16106
16822
|
startPage = typeof startVerdict === "object" ? { ...blank, status: "failed", failureReason: "policy_denied", robots: "unreachable", robotsUnreachable: startVerdict.unreachable } : { ...blank, status: "failed", failureReason: "policy_denied", robots: "disallowed" };
|
|
16107
16823
|
} else if (startVerdict === "unchecked") {
|
|
16108
16824
|
refused.robotsUnchecked++;
|
|
16109
16825
|
startPage = { ...blank, status: "failed", failureReason: "timeout", robots: null };
|
|
16110
16826
|
} else {
|
|
16111
|
-
const startLink = { url: startCanonical, via: ["start"], robots: startVerdict };
|
|
16827
|
+
const startLink = { url: startCanonical, via: ["start"], robots: linkVerdict(startVerdict) };
|
|
16828
|
+
const startRobots = { robots: linkVerdict(startVerdict), ...typeof startVerdict === "object" ? { robotsUnreachable: startVerdict.unreachable } : {} };
|
|
16112
16829
|
links.push(startLink);
|
|
16113
16830
|
byKey.set(keyOf(startCanonical), startLink);
|
|
16114
16831
|
const keepStartIfMatching = () => {
|
|
@@ -16125,7 +16842,7 @@ ${title ?? ""}`.toLowerCase();
|
|
|
16125
16842
|
if (cancelled() || Date.now() < deadlineAt)
|
|
16126
16843
|
throw error;
|
|
16127
16844
|
timedOut = true;
|
|
16128
|
-
startPage = { ...blank, status: "failed", failureReason: "timeout",
|
|
16845
|
+
startPage = { ...blank, status: "failed", failureReason: "timeout", ...startRobots };
|
|
16129
16846
|
keepStartIfMatching();
|
|
16130
16847
|
}
|
|
16131
16848
|
if (read2 !== null) {
|
|
@@ -16139,7 +16856,7 @@ ${title ?? ""}`.toLowerCase();
|
|
|
16139
16856
|
status: result.status,
|
|
16140
16857
|
failureReason: result.failureReason,
|
|
16141
16858
|
lane: "http",
|
|
16142
|
-
|
|
16859
|
+
...startRobots,
|
|
16143
16860
|
rawBodySha256: result.evidence.rawBodySha256,
|
|
16144
16861
|
linksFound: read2.links.length,
|
|
16145
16862
|
title: title ?? null,
|
|
@@ -16269,8 +16986,9 @@ function nonEmpty(value) {
|
|
|
16269
16986
|
}
|
|
16270
16987
|
|
|
16271
16988
|
// packages/api/dist/engine.js
|
|
16272
|
-
import {
|
|
16273
|
-
import {
|
|
16989
|
+
import { createHash as createHash13 } from "crypto";
|
|
16990
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync8, readdirSync, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
|
|
16991
|
+
import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "fs/promises";
|
|
16274
16992
|
import { isIP as isIP4 } from "net";
|
|
16275
16993
|
import { homedir as homedir2 } from "os";
|
|
16276
16994
|
import { join as join10 } from "path";
|
|
@@ -16444,6 +17162,36 @@ ${prose2(5, 23)}
|
|
|
16444
17162
|
<div class="g-recaptcha" data-sitekey="fixture"></div>`
|
|
16445
17163
|
}))
|
|
16446
17164
|
};
|
|
17165
|
+
var ROBOT_OR_HUMAN_FACT = "The exhibit opens with a quiz called Robot or human? that asks visitors to date each portrait.";
|
|
17166
|
+
var staticArticleRobotOrHuman = {
|
|
17167
|
+
truth: {
|
|
17168
|
+
id: "static-article-robot-or-human",
|
|
17169
|
+
target: "/static/article-robot-or-human",
|
|
17170
|
+
kind: "fixture",
|
|
17171
|
+
category: "static",
|
|
17172
|
+
mustContain: [ROBOT_OR_HUMAN_FACT, ARTICLE_FACT],
|
|
17173
|
+
mustNotContain: B,
|
|
17174
|
+
expectedLane: "http",
|
|
17175
|
+
emptyIsLegit: false,
|
|
17176
|
+
expectedMainTokens: { min: 260, max: 900 },
|
|
17177
|
+
budget: budget(2e3),
|
|
17178
|
+
expectedStatus: "success",
|
|
17179
|
+
notes: `Ordinary article on a PerimeterX-protected site: the sensor sets window._pxAppId on every page (Walmart's home page did on 2026-10-05), and the prose says "Robot or human?". Neither is the challenge, so a contentful page must stay success.`
|
|
17180
|
+
},
|
|
17181
|
+
respond: () => html(htmlPage({
|
|
17182
|
+
title: "Kiln temperatures and glaze vitrification",
|
|
17183
|
+
headExtra: "<script>window._pxAppId='PXfixture1'</script>",
|
|
17184
|
+
bodyHtml: `<article>
|
|
17185
|
+
<h1>Kiln temperatures and glaze vitrification</h1>
|
|
17186
|
+
<p>${ROBOT_OR_HUMAN_FACT}</p>
|
|
17187
|
+
<p>${ARTICLE_FACT}</p>
|
|
17188
|
+
${prose2(6, 11)}
|
|
17189
|
+
<h2>Estuary sediment</h2>
|
|
17190
|
+
<p>${ARTICLE_FACT_2}</p>
|
|
17191
|
+
${prose2(5, 23)}
|
|
17192
|
+
</article>`
|
|
17193
|
+
}))
|
|
17194
|
+
};
|
|
16447
17195
|
var CJK_FACT = "\u7A91\u6E29\u8FBE\u5230\u4E00\u5343\u4E8C\u767E\u56DB\u5341\u5EA6\u540E\u91C9\u9762\u5F00\u59CB\u73BB\u5316\u3002";
|
|
16448
17196
|
var staticCjk = {
|
|
16449
17197
|
truth: {
|
|
@@ -17363,6 +18111,71 @@ var blockLoginWallBenefits = {
|
|
|
17363
18111
|
</body></html>`
|
|
17364
18112
|
})
|
|
17365
18113
|
};
|
|
18114
|
+
var blockPerimeterX = {
|
|
18115
|
+
truth: {
|
|
18116
|
+
id: "block-perimeterx",
|
|
18117
|
+
target: "/block/perimeterx",
|
|
18118
|
+
kind: "fixture",
|
|
18119
|
+
category: "blocked",
|
|
18120
|
+
mustContain: [],
|
|
18121
|
+
mustNotContain: ["Robot or human?", "Activate and hold the button"],
|
|
18122
|
+
expectedLane: "http",
|
|
18123
|
+
emptyIsLegit: false,
|
|
18124
|
+
expectedMainTokens: null,
|
|
18125
|
+
budget: budget(500),
|
|
18126
|
+
expectedStatus: "blocked",
|
|
18127
|
+
expectedBlockReason: "captcha",
|
|
18128
|
+
notes: "Shaped like the PerimeterX press-and-hold page Walmart served with HTTP 200 (after a 307 to /blocked) on 2026-10-05, which browser_local returned as success. The widget renders into #px-captcha from the captcha script the page loads, configured by window._pxAppId. captcha, not bot_detected_generic: the button needs a person to press and hold it, and the browser lane had already run the page's JS when it got this page."
|
|
18129
|
+
},
|
|
18130
|
+
respond: () => ({
|
|
18131
|
+
status: 200,
|
|
18132
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
18133
|
+
body: `<!doctype html><html lang="en"><head><title>Robot or human?</title>
|
|
18134
|
+
<script>window._pxAppId = 'PXfixture1'; window._pxJsClientSrc = '/px/' + window._pxAppId + '/init.js'; window._pxFirstPartyEnabled = true;
|
|
18135
|
+
var captchajs = "/px/" + window._pxAppId + "/captcha/captcha.js?a=c&m=0&g=b"</script>
|
|
18136
|
+
</head><body><div id="sign-in-widget"><a class="header-logo" href="/" aria-label="Home Page"></a>
|
|
18137
|
+
<h1 class="heading">Robot or human?</h1>
|
|
18138
|
+
<div class="re-captcha"><p class="bot-message" id="message">Activate and hold the button to confirm that you\u2019re human. Thank You!</p>
|
|
18139
|
+
<div id="px-captcha" style="margin:16px"></div></div>
|
|
18140
|
+
<div class="lite-footer"><a href="/terms">Terms of Use</a> <a href="/privacy">Privacy Policy</a><p>\xA9 Fixture Stores, Inc.</p></div>
|
|
18141
|
+
</div><script id="blockScript"></script><script>document.getElementById('blockScript').src = captchajs</script></body></html>`
|
|
18142
|
+
})
|
|
18143
|
+
};
|
|
18144
|
+
var blockPerimeterX429 = {
|
|
18145
|
+
truth: {
|
|
18146
|
+
id: "block-perimeterx-429",
|
|
18147
|
+
target: "/block/perimeterx-429",
|
|
18148
|
+
kind: "fixture",
|
|
18149
|
+
category: "blocked",
|
|
18150
|
+
mustContain: [],
|
|
18151
|
+
mustNotContain: ["Press & Hold"],
|
|
18152
|
+
expectedLane: "http",
|
|
18153
|
+
emptyIsLegit: false,
|
|
18154
|
+
expectedMainTokens: null,
|
|
18155
|
+
budget: budget(500),
|
|
18156
|
+
expectedStatus: "blocked",
|
|
18157
|
+
expectedBlockReason: "captcha",
|
|
18158
|
+
notes: "Shaped like the PerimeterX stock block template Wayfair (and its sibling stores) served with HTTP 429 and no Retry-After on 2026-10-05. The status says rate limit, the page is the press-and-hold challenge: captcha, because a person can get through it and slowing down does not."
|
|
18159
|
+
},
|
|
18160
|
+
respond: () => ({
|
|
18161
|
+
status: 429,
|
|
18162
|
+
headers: { "content-type": "text/html" },
|
|
18163
|
+
body: `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
|
18164
|
+
<meta name="description" content="px-captcha"><title>Access to this page has been denied</title></head>
|
|
18165
|
+
<body><script>
|
|
18166
|
+
window._pxVid = ''; window._pxUuid = '00000000-0000-4000-8000-000000000000'; window._pxAppId = 'PXfixture2';
|
|
18167
|
+
window._pxHostUrl = '/fixture2/xhr'; window._pxJsClientSrc = '/fixture2/init.js'; window._pxFirstPartyEnabled = true;
|
|
18168
|
+
var pxCaptchaSrc = '/fixture2/captcha/captcha.js?a=c&u=00000000-0000-4000-8000-000000000000&v=&m=0';
|
|
18169
|
+
var script = document.createElement('script'); script.src = pxCaptchaSrc; script.onerror = onScriptError; document.head.appendChild(script);
|
|
18170
|
+
function onScriptError() { script = document.createElement('script');
|
|
18171
|
+
script.src = 'https://captcha.px-cloud.net/PXfixture2/captcha.js?a=c&u=00000000-0000-4000-8000-000000000000&v=&m=0';
|
|
18172
|
+
document.head.appendChild(script); }
|
|
18173
|
+
window._pxOnError = function () { var div = document.createElement('div');
|
|
18174
|
+
div.innerHTML = '<div>Before we continue...</div><div>Press & Hold to confirm you are a human (and not a bot).</div>';
|
|
18175
|
+
document.body.appendChild(div); };
|
|
18176
|
+
</script></body></html>`
|
|
18177
|
+
})
|
|
18178
|
+
};
|
|
17366
18179
|
var SOFT404_TEXT = "We could not find that page";
|
|
17367
18180
|
var soft404 = {
|
|
17368
18181
|
truth: {
|
|
@@ -17784,6 +18597,7 @@ var homePage = {
|
|
|
17784
18597
|
var FIXTURES = [
|
|
17785
18598
|
staticArticle,
|
|
17786
18599
|
staticArticleWithWidget,
|
|
18600
|
+
staticArticleRobotOrHuman,
|
|
17787
18601
|
staticCjk,
|
|
17788
18602
|
staticTable,
|
|
17789
18603
|
t_thead,
|
|
@@ -17821,6 +18635,8 @@ var FIXTURES = [
|
|
|
17821
18635
|
blockRateLimit,
|
|
17822
18636
|
blockLoginWall,
|
|
17823
18637
|
blockLoginWallBenefits,
|
|
18638
|
+
blockPerimeterX,
|
|
18639
|
+
blockPerimeterX429,
|
|
17824
18640
|
soft404,
|
|
17825
18641
|
redirectToHome,
|
|
17826
18642
|
redirectChain,
|
|
@@ -18021,6 +18837,264 @@ var EgressRoute = class {
|
|
|
18021
18837
|
}
|
|
18022
18838
|
};
|
|
18023
18839
|
|
|
18840
|
+
// packages/bench/dist/compatTransport.js
|
|
18841
|
+
init_dist();
|
|
18842
|
+
init_egress();
|
|
18843
|
+
var COMPAT_LIBRARY = { name: "impit", version: "0.14.5" };
|
|
18844
|
+
var COMPAT_PROFILES = {
|
|
18845
|
+
chrome142: {
|
|
18846
|
+
browser: "chrome142",
|
|
18847
|
+
headers: [
|
|
18848
|
+
["sec-ch-ua", '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"'],
|
|
18849
|
+
["sec-ch-ua-mobile", "?0"],
|
|
18850
|
+
["sec-ch-ua-platform", '"macOS"'],
|
|
18851
|
+
["upgrade-insecure-requests", "1"],
|
|
18852
|
+
["user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"],
|
|
18853
|
+
["accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],
|
|
18854
|
+
["sec-fetch-site", "none"],
|
|
18855
|
+
["sec-fetch-mode", "navigate"],
|
|
18856
|
+
["sec-fetch-user", "?1"],
|
|
18857
|
+
["sec-fetch-dest", "document"],
|
|
18858
|
+
["accept-encoding", "gzip, deflate, br, zstd"],
|
|
18859
|
+
["accept-language", "en-US,en;q=0.9"],
|
|
18860
|
+
["priority", "u=0, i"]
|
|
18861
|
+
]
|
|
18862
|
+
}
|
|
18863
|
+
};
|
|
18864
|
+
var DEFAULT_COMPAT_PROFILE = "chrome142";
|
|
18865
|
+
function compatIdentity(profile = DEFAULT_COMPAT_PROFILE) {
|
|
18866
|
+
const sent = new Map(COMPAT_PROFILES[profile].headers);
|
|
18867
|
+
return {
|
|
18868
|
+
mode: "standard",
|
|
18869
|
+
userAgent: sent.get("user-agent"),
|
|
18870
|
+
clientHints: { "sec-ch-ua": sent.get("sec-ch-ua"), "sec-ch-ua-mobile": sent.get("sec-ch-ua-mobile"), "sec-ch-ua-platform": sent.get("sec-ch-ua-platform") },
|
|
18871
|
+
respectsRobots: true,
|
|
18872
|
+
lane: "browser_local",
|
|
18873
|
+
device: "desktop"
|
|
18874
|
+
};
|
|
18875
|
+
}
|
|
18876
|
+
function prepareCompatIdentity(profile = DEFAULT_COMPAT_PROFILE) {
|
|
18877
|
+
const headers = Object.fromEntries(COMPAT_PROFILES[profile].headers.map(([name, value]) => [name, value]));
|
|
18878
|
+
const sentHeaders = { headers: Object.entries(headers).map(([name, value]) => ({ name, value })).sort((a, b) => a.name.localeCompare(b.name)) };
|
|
18879
|
+
return { mode: "standard", identity: compatIdentity(profile), headers, identityHeaders: headers, customHeaders: {}, sentHeaders };
|
|
18880
|
+
}
|
|
18881
|
+
var impit;
|
|
18882
|
+
function loadImpit() {
|
|
18883
|
+
impit ??= import("impit").catch((error) => {
|
|
18884
|
+
impit = void 0;
|
|
18885
|
+
throw new Error(`the compatible transport needs impit ${COMPAT_LIBRARY.version}, which did not load on ${process.platform}-${process.arch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
18886
|
+
});
|
|
18887
|
+
return impit;
|
|
18888
|
+
}
|
|
18889
|
+
var PROXY_VARIABLES = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"];
|
|
18890
|
+
var NO_PROXY_ANY = "*,0.0.0.0/0,::/0";
|
|
18891
|
+
function withLaneProxyOnly(direct, build) {
|
|
18892
|
+
const saved = PROXY_VARIABLES.map((name) => [name, process.env[name]]);
|
|
18893
|
+
for (const name of PROXY_VARIABLES)
|
|
18894
|
+
delete process.env[name];
|
|
18895
|
+
if (direct) {
|
|
18896
|
+
process.env.NO_PROXY = NO_PROXY_ANY;
|
|
18897
|
+
process.env.no_proxy = NO_PROXY_ANY;
|
|
18898
|
+
}
|
|
18899
|
+
try {
|
|
18900
|
+
return build();
|
|
18901
|
+
} finally {
|
|
18902
|
+
for (const [name, value] of saved) {
|
|
18903
|
+
if (value === void 0)
|
|
18904
|
+
delete process.env[name];
|
|
18905
|
+
else
|
|
18906
|
+
process.env[name] = value;
|
|
18907
|
+
}
|
|
18908
|
+
}
|
|
18909
|
+
}
|
|
18910
|
+
var COMPAT_REQUEST_CEILING_MS = 10 * 6e4;
|
|
18911
|
+
function impitTimeoutMs(deadlineAt, now = Date.now()) {
|
|
18912
|
+
return deadlineAt === void 0 ? COMPAT_REQUEST_CEILING_MS : Math.max(1e3, deadlineAt - now + 1e3);
|
|
18913
|
+
}
|
|
18914
|
+
function decodingFault(message2) {
|
|
18915
|
+
return /kind: Decode\b/.test(message2) && !/hyper::Error|h2::|IncompleteBody|TimedOut|ConnectionReset|ConnectionAborted|BrokenPipe/.test(message2);
|
|
18916
|
+
}
|
|
18917
|
+
var IMPIT_DECODES = /* @__PURE__ */ new Set(["gzip", "deflate", "br", "zstd"]);
|
|
18918
|
+
function normalized(error) {
|
|
18919
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
18920
|
+
const name = error instanceof Error ? error.name : "";
|
|
18921
|
+
const as = (n, code) => Object.assign(new Error(message2.split("\n")[0] ?? message2), { name: n, ...code === void 0 ? {} : { code }, cause: error });
|
|
18922
|
+
if (name === "TimeoutError" || name === "AbortError")
|
|
18923
|
+
return error instanceof Error ? error : as("TimeoutError");
|
|
18924
|
+
if (/InvalidCertificate|certificate|handshake|tls|ssl/i.test(message2)) {
|
|
18925
|
+
const code = /expired/i.test(message2) ? "CERT_HAS_EXPIRED" : /not yet valid/i.test(message2) ? "CERT_NOT_YET_VALID" : /NotValidForName|name mismatch|hostname/i.test(message2) ? "HOSTNAME_MISMATCH" : /not trusted|UnknownIssuer|self.signed/i.test(message2) ? "CERT_UNTRUSTED" : "ERR_SSL_COMPAT_TRANSPORT";
|
|
18926
|
+
return as("CompatTlsError", code);
|
|
18927
|
+
}
|
|
18928
|
+
if (/dns error|failed to lookup|nodename nor servname|Name or service not known/i.test(message2))
|
|
18929
|
+
return as("DnsLookupError");
|
|
18930
|
+
return as("ConnectError");
|
|
18931
|
+
}
|
|
18932
|
+
var CompatTransport = class {
|
|
18933
|
+
networkPolicy;
|
|
18934
|
+
profile;
|
|
18935
|
+
clients = /* @__PURE__ */ new Map();
|
|
18936
|
+
constructor(networkPolicy, profile = DEFAULT_COMPAT_PROFILE) {
|
|
18937
|
+
this.networkPolicy = networkPolicy;
|
|
18938
|
+
this.profile = profile;
|
|
18939
|
+
}
|
|
18940
|
+
async clientFor(url, ignoreTlsErrors) {
|
|
18941
|
+
const proxy = proxyFor(url, this.networkPolicy);
|
|
18942
|
+
const auth = proxy?.username === void 0 ? "" : `${encodeURIComponent(proxy.username)}:${encodeURIComponent(proxy.password ?? "")}@`;
|
|
18943
|
+
const proxyUrl = proxy === null ? void 0 : proxy.url.replace("://", `://${auth}`);
|
|
18944
|
+
const key = `${proxyUrl ?? "direct"}|${ignoreTlsErrors}`;
|
|
18945
|
+
let client = this.clients.get(key);
|
|
18946
|
+
if (client === void 0) {
|
|
18947
|
+
const { Impit } = await loadImpit();
|
|
18948
|
+
client = this.clients.get(key) ?? withLaneProxyOnly(proxyUrl === void 0, () => new Impit({ browser: COMPAT_PROFILES[this.profile].browser, followRedirects: false, ...proxyUrl === void 0 ? {} : { proxyUrl }, ...ignoreTlsErrors ? { ignoreTlsErrors: true } : {} }));
|
|
18949
|
+
this.clients.set(key, client);
|
|
18950
|
+
}
|
|
18951
|
+
return client;
|
|
18952
|
+
}
|
|
18953
|
+
async fetch(url, options) {
|
|
18954
|
+
let response;
|
|
18955
|
+
const client = await this.clientFor(url, options.ignoreTlsErrors === true);
|
|
18956
|
+
const cookie = options.cookies === void 0 ? "" : await options.cookies.header(url);
|
|
18957
|
+
const extraHeaders = { ...options.extraHeaders ?? {}, ...cookie === "" ? {} : { cookie } };
|
|
18958
|
+
const controller2 = new AbortController();
|
|
18959
|
+
const abort = () => controller2.abort(options.signal?.reason);
|
|
18960
|
+
if (options.signal?.aborted === true)
|
|
18961
|
+
abort();
|
|
18962
|
+
else
|
|
18963
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
18964
|
+
let headersTimedOut = false;
|
|
18965
|
+
const headersTimer = setTimeout(() => {
|
|
18966
|
+
headersTimedOut = true;
|
|
18967
|
+
controller2.abort();
|
|
18968
|
+
}, options.headersTimeoutMs);
|
|
18969
|
+
try {
|
|
18970
|
+
response = await client.fetch(url, {
|
|
18971
|
+
redirect: "manual",
|
|
18972
|
+
timeout: impitTimeoutMs(options.deadlineAt),
|
|
18973
|
+
signal: controller2.signal,
|
|
18974
|
+
...Object.keys(extraHeaders).length === 0 ? {} : { headers: extraHeaders }
|
|
18975
|
+
});
|
|
18976
|
+
} catch (error) {
|
|
18977
|
+
options.signal?.removeEventListener("abort", abort);
|
|
18978
|
+
if (headersTimedOut && options.signal?.aborted !== true)
|
|
18979
|
+
throw Object.assign(new Error(`no response headers within ${options.headersTimeoutMs} ms`), { name: "HeadersTimeoutError", cause: error });
|
|
18980
|
+
throw normalized(error);
|
|
18981
|
+
} finally {
|
|
18982
|
+
clearTimeout(headersTimer);
|
|
18983
|
+
}
|
|
18984
|
+
if (options.cookies !== void 0) {
|
|
18985
|
+
const headers = response.headers;
|
|
18986
|
+
await options.cookies.keep(url, headers.getSetCookie?.() ?? (headers.get("set-cookie") === null ? [] : [headers.get("set-cookie")]));
|
|
18987
|
+
}
|
|
18988
|
+
const coding = response.headers.get("content-encoding")?.trim() ?? null;
|
|
18989
|
+
const bodiless = response.status < 200 || response.status === 204 || response.status === 304;
|
|
18990
|
+
const decodedFrom = !bodiless && coding !== null && IMPIT_DECODES.has(coding) ? coding : null;
|
|
18991
|
+
if (decodedFrom !== null)
|
|
18992
|
+
options.onDecoded?.(decodedFrom);
|
|
18993
|
+
const header = (name) => decodedFrom !== null && name.toLowerCase() === "content-encoding" ? null : response.headers.get(name);
|
|
18994
|
+
let bytes;
|
|
18995
|
+
const readBody = async () => {
|
|
18996
|
+
const cap = options.capFor(response.headers.get("content-type"), decodedFrom);
|
|
18997
|
+
const reader = response.body.getReader();
|
|
18998
|
+
if (cap === null) {
|
|
18999
|
+
await reader.cancel().catch(() => {
|
|
19000
|
+
});
|
|
19001
|
+
return new Uint8Array();
|
|
19002
|
+
}
|
|
19003
|
+
const declared3 = decodedFrom === null ? Number(response.headers.get("content-length") ?? NaN) : NaN;
|
|
19004
|
+
if (Number.isSafeInteger(declared3) && declared3 > cap) {
|
|
19005
|
+
await reader.cancel().catch(() => {
|
|
19006
|
+
});
|
|
19007
|
+
throw new BodyTooLargeError(cap, declared3);
|
|
19008
|
+
}
|
|
19009
|
+
const started = performance.now();
|
|
19010
|
+
const chunks = [];
|
|
19011
|
+
let total = 0;
|
|
19012
|
+
try {
|
|
19013
|
+
while (true) {
|
|
19014
|
+
let timer;
|
|
19015
|
+
const stalled = new Promise((_, reject) => {
|
|
19016
|
+
timer = setTimeout(() => reject(Object.assign(new Error("body read timed out"), { name: "BodyTimeoutError" })), options.bodyTimeoutMs);
|
|
19017
|
+
});
|
|
19018
|
+
let step;
|
|
19019
|
+
try {
|
|
19020
|
+
step = await Promise.race([reader.read(), stalled]);
|
|
19021
|
+
} finally {
|
|
19022
|
+
clearTimeout(timer);
|
|
19023
|
+
}
|
|
19024
|
+
if (step.done)
|
|
19025
|
+
break;
|
|
19026
|
+
total += step.value.byteLength;
|
|
19027
|
+
if (total > cap)
|
|
19028
|
+
throw new BodyTooLargeError(cap);
|
|
19029
|
+
chunks.push(step.value);
|
|
19030
|
+
}
|
|
19031
|
+
} catch (error) {
|
|
19032
|
+
await reader.cancel().catch(() => {
|
|
19033
|
+
});
|
|
19034
|
+
if (error instanceof BodyTooLargeError || error instanceof Error && error.name === "BodyTimeoutError")
|
|
19035
|
+
throw error;
|
|
19036
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
19037
|
+
throw error;
|
|
19038
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
19039
|
+
if (decodedFrom !== null && decodingFault(message2))
|
|
19040
|
+
throw new ContentDecodingError(decodedFrom, decodedFrom, "impit_decode");
|
|
19041
|
+
const timedOut = error instanceof Error && error.name === "TimeoutError" || /TimedOut/.test(message2);
|
|
19042
|
+
throw Object.assign(new Error(message2.split("\n")[0] ?? message2), { name: timedOut ? "BodyTimeoutError" : "SocketError", cause: error });
|
|
19043
|
+
}
|
|
19044
|
+
options.onBodyRead?.(Math.max(0, performance.now() - started));
|
|
19045
|
+
const out = new Uint8Array(total);
|
|
19046
|
+
let offset = 0;
|
|
19047
|
+
for (const chunk of chunks) {
|
|
19048
|
+
out.set(chunk, offset);
|
|
19049
|
+
offset += chunk.byteLength;
|
|
19050
|
+
}
|
|
19051
|
+
return out;
|
|
19052
|
+
};
|
|
19053
|
+
const bodyBytes = () => bytes ??= readBody().finally(() => options.signal?.removeEventListener("abort", abort));
|
|
19054
|
+
const bodyText = async () => {
|
|
19055
|
+
try {
|
|
19056
|
+
return new TextDecoder().decode(await bodyBytes());
|
|
19057
|
+
} catch (error) {
|
|
19058
|
+
if (error instanceof ContentDecodingError)
|
|
19059
|
+
return "";
|
|
19060
|
+
throw error;
|
|
19061
|
+
}
|
|
19062
|
+
};
|
|
19063
|
+
return { status: response.status, headers: { get: header }, bodyBytes, bodyText };
|
|
19064
|
+
}
|
|
19065
|
+
};
|
|
19066
|
+
function compatHostsChoice(env, grant, hosted) {
|
|
19067
|
+
const raw = (env.W2L_COMPAT_HOSTS ?? "").split(",").map((value) => value.trim().toLowerCase().replace(/\.$/, "")).filter(Boolean);
|
|
19068
|
+
if (raw.length === 0)
|
|
19069
|
+
return [];
|
|
19070
|
+
const invalid = raw.filter((host) => {
|
|
19071
|
+
try {
|
|
19072
|
+
return new URL(`http://${host}/`).hostname !== host || host.includes("*");
|
|
19073
|
+
} catch {
|
|
19074
|
+
return true;
|
|
19075
|
+
}
|
|
19076
|
+
});
|
|
19077
|
+
if (invalid.length > 0)
|
|
19078
|
+
throw new Error(`W2L_COMPAT_HOSTS takes host names, such as example.com: not ${invalid.join(", ")}`);
|
|
19079
|
+
if (hosted)
|
|
19080
|
+
throw new Error("W2L_COMPAT_HOSTS is refused on a hosted server (ADR 0005: the compatible transport resolves host names outside the hosted address check)");
|
|
19081
|
+
if (!(grant?.capabilities ?? []).includes("compatible_transport")) {
|
|
19082
|
+
throw new Error("W2L_COMPAT_HOSTS needs an access grant that names compatible_transport (ADR 0005; --access-grant or W2L_ACCESS_GRANT)");
|
|
19083
|
+
}
|
|
19084
|
+
return [...new Set(raw)].sort();
|
|
19085
|
+
}
|
|
19086
|
+
function compatHostListed(hosts, url) {
|
|
19087
|
+
if (hosts.length === 0)
|
|
19088
|
+
return false;
|
|
19089
|
+
let host;
|
|
19090
|
+
try {
|
|
19091
|
+
host = new URL(url).hostname.toLowerCase().replace(/\.$/, "");
|
|
19092
|
+
} catch {
|
|
19093
|
+
return false;
|
|
19094
|
+
}
|
|
19095
|
+
return hosts.some((listed) => host === listed || host.endsWith(`.${listed}`));
|
|
19096
|
+
}
|
|
19097
|
+
|
|
18024
19098
|
// packages/bench/dist/subjects/resilientHttp.js
|
|
18025
19099
|
init_robotsLookup();
|
|
18026
19100
|
|
|
@@ -18292,6 +19366,7 @@ async function readFileResponse(response, bytes, context) {
|
|
|
18292
19366
|
var ResilientHttpSubject = class {
|
|
18293
19367
|
fileStore;
|
|
18294
19368
|
previewProductToken;
|
|
19369
|
+
compat;
|
|
18295
19370
|
meta = {
|
|
18296
19371
|
id: "resilient-http",
|
|
18297
19372
|
displayName: "Resilient HTTP (redirect+retry \xD7 extract-tf)",
|
|
@@ -18319,12 +19394,16 @@ var ResilientHttpSubject = class {
|
|
|
18319
19394
|
* `fileStore`: where files (PDF, CSV, ...) are saved as received; without one a file is read but not saved.
|
|
18320
19395
|
* `previewProductToken`: the hosted public preview's standard User-Agent carries PREVIEW_PRODUCT_TOKEN (previewIdentity).
|
|
18321
19396
|
* `robotsCache`: a robots.txt cache shared with the crawl's sitemap reader, so one robots.txt read serves both; without one the subject keeps its own.
|
|
19397
|
+
* `compat`: the browser-compatible transport (compatTransport.ts) in place of undici, with its profile's identity; standard mode on a local server only.
|
|
18322
19398
|
*/
|
|
18323
|
-
constructor(mode = "standard", networkPolicy, scheduler, localPreviewProxyUrl, localPreviewRobotsException = false, fileStore = null, previewProductToken = false, robotsCache) {
|
|
19399
|
+
constructor(mode = "standard", networkPolicy, scheduler, localPreviewProxyUrl, localPreviewRobotsException = false, fileStore = null, previewProductToken = false, robotsCache, compat = null) {
|
|
18324
19400
|
this.fileStore = fileStore;
|
|
18325
19401
|
this.previewProductToken = previewProductToken;
|
|
19402
|
+
this.compat = compat;
|
|
18326
19403
|
this.networkPolicy = networkPolicy ?? defaultNetworkPolicy();
|
|
18327
|
-
|
|
19404
|
+
if (compat !== null && (mode !== "standard" || previewProductToken || localPreviewProxyUrl !== void 0))
|
|
19405
|
+
throw new Error(`the compatible transport sends its profile's standard identity: not for ${mode !== "standard" ? `mode ${mode}` : "the hosted preview"}`);
|
|
19406
|
+
this.prepared = compat !== null ? prepareCompatIdentity(compat.profile) : prepareHttpIdentity(mode, this.networkPolicy.contact ?? null, null, "desktop", void 0, previewProductToken);
|
|
18328
19407
|
if (localPreviewRobotsException && !localPreviewProxyUrl)
|
|
18329
19408
|
throw new Error("Local platform exception requires a loopback proxy");
|
|
18330
19409
|
this.localPreviewRobotsException = localPreviewRobotsException;
|
|
@@ -18336,27 +19415,50 @@ var ResilientHttpSubject = class {
|
|
|
18336
19415
|
this.route = new EgressRoute(this.networkPolicy, this.egress, this.localPreviewProxy);
|
|
18337
19416
|
this.robotsCache = robotsCache ?? new RobotsOriginCache(this.networkPolicy, (url) => this.dispatcherFor(url));
|
|
18338
19417
|
const maxBodyBytes = this.networkPolicy.maxBodyBytes;
|
|
18339
|
-
this.fetcherFor = (initialUrl, headers, validators, signal, onBodyRead, onRequestWait, onEnvProxy, maxFileBytes = fileByteCap(this.networkPolicy), wire, routes = null) => async (url, init) => {
|
|
19418
|
+
this.fetcherFor = (initialUrl, headers, validators, signal, onBodyRead, onRequestWait, onEnvProxy, maxFileBytes = fileByteCap(this.networkPolicy), wire, routes = null, onResponseCoding, deadlineAt, cookies) => async (url, init) => {
|
|
18340
19419
|
await this.scheduler.beforeRequest(new URL(url).origin, init.signal ?? signal, onRequestWait);
|
|
18341
19420
|
const envProxy = this.envProxyFor(url);
|
|
18342
19421
|
if (envProxy !== null)
|
|
18343
19422
|
onEnvProxy?.(url, envProxy);
|
|
19423
|
+
if (this.compat !== null) {
|
|
19424
|
+
onResponseCoding?.(null);
|
|
19425
|
+
return this.compat.fetch(url, {
|
|
19426
|
+
...cookies === void 0 ? {} : { cookies },
|
|
19427
|
+
signal: init.signal ?? signal,
|
|
19428
|
+
headersTimeoutMs: init.headersTimeoutMs,
|
|
19429
|
+
bodyTimeoutMs: init.bodyTimeoutMs,
|
|
19430
|
+
capFor: (contentType, decodedFrom) => {
|
|
19431
|
+
const kind = classifyContentType(contentType);
|
|
19432
|
+
return kind === "unsupported" ? null : kind === "page" ? decodedFrom === null ? maxBodyBytes : this.networkPolicy.maxDecompressedBytes : maxFileBytes;
|
|
19433
|
+
},
|
|
19434
|
+
extraHeaders: url === initialUrl ? validators.etag ? { "if-none-match": validators.etag } : validators.lastModified ? { "if-modified-since": validators.lastModified } : {} : {},
|
|
19435
|
+
ignoreTlsErrors: routes !== null,
|
|
19436
|
+
...deadlineAt === void 0 ? {} : { deadlineAt },
|
|
19437
|
+
onDecoded: (coding) => onResponseCoding?.(coding),
|
|
19438
|
+
...onBodyRead === void 0 ? {} : { onBodyRead }
|
|
19439
|
+
});
|
|
19440
|
+
}
|
|
18344
19441
|
const customNames = wire === void 0 ? [] : Object.keys(wire.headers);
|
|
18345
19442
|
const sameOrigin2 = new URL(url).origin === new URL(initialUrl).origin;
|
|
18346
19443
|
if (wire !== void 0 && customNames.length > 0 && !sameOrigin2)
|
|
18347
19444
|
wire.onWithheld(url, customNames);
|
|
19445
|
+
const cookie = cookies === void 0 ? "" : await cookies.header(url);
|
|
18348
19446
|
const response = await request5(url, {
|
|
18349
19447
|
dispatcher: this.dispatcherFor(url, routes),
|
|
18350
19448
|
method: "GET",
|
|
18351
19449
|
headersTimeout: init.headersTimeoutMs,
|
|
18352
19450
|
bodyTimeout: init.bodyTimeoutMs,
|
|
18353
19451
|
// The identity after the custom headers, so it is never overridden. Validators are bound to one representation; never forward on redirects.
|
|
18354
|
-
headers: { ...wire !== void 0 && sameOrigin2 ? wire.headers : {}, ...headers, ...url === initialUrl ? validators.etag ? { "if-none-match": validators.etag } : validators.lastModified ? { "if-modified-since": validators.lastModified } : {} : {} },
|
|
19452
|
+
headers: { ...wire !== void 0 && sameOrigin2 ? wire.headers : {}, ...headers, ...url === initialUrl ? validators.etag ? { "if-none-match": validators.etag } : validators.lastModified ? { "if-modified-since": validators.lastModified } : {} : {}, ...cookie === "" ? {} : { cookie } },
|
|
18355
19453
|
signal: init.signal ?? signal
|
|
18356
19454
|
}).catch((error) => {
|
|
18357
19455
|
throw proxyRefusal(error) ?? error;
|
|
18358
19456
|
});
|
|
18359
19457
|
const responseHeaders = response.headers;
|
|
19458
|
+
if (cookies !== void 0) {
|
|
19459
|
+
const setCookie = responseHeaders["set-cookie"];
|
|
19460
|
+
await cookies.keep(url, typeof setCookie === "string" ? [setCookie] : setCookie ?? []);
|
|
19461
|
+
}
|
|
18360
19462
|
const header = (name) => {
|
|
18361
19463
|
const v = responseHeaders[name.toLowerCase()];
|
|
18362
19464
|
return typeof v === "string" ? v : Array.isArray(v) ? v[0] ?? null : null;
|
|
@@ -18396,6 +19498,13 @@ var ResilientHttpSubject = class {
|
|
|
18396
19498
|
* request's custom headers (`headers`) before it.
|
|
18397
19499
|
*/
|
|
18398
19500
|
preparedFor(url, options) {
|
|
19501
|
+
if (this.compat !== null) {
|
|
19502
|
+
if (options.mobile === true || Object.keys(options.headers ?? {}).length > 0)
|
|
19503
|
+
throw new Error("the compatible transport sends its profile's headers alone: no custom headers, no mobile identity");
|
|
19504
|
+
const prepared2 = prepareCompatIdentity(this.compat.profile);
|
|
19505
|
+
prepared2.identity.respectsRobots = this.prepared.identity.respectsRobots;
|
|
19506
|
+
return prepared2;
|
|
19507
|
+
}
|
|
18399
19508
|
const prepared = prepareHttpIdentity(this.prepared.mode, this.networkPolicy.contact ?? null, new URL(url).hostname, options.mobile === true ? "mobile" : "desktop", options.headers, this.previewProductToken);
|
|
18400
19509
|
prepared.identity.respectsRobots = this.prepared.identity.respectsRobots;
|
|
18401
19510
|
return prepared;
|
|
@@ -18408,10 +19517,11 @@ var ResilientHttpSubject = class {
|
|
|
18408
19517
|
envProxyFor(url) {
|
|
18409
19518
|
return this.route.viaOperatorProxy(url);
|
|
18410
19519
|
}
|
|
18411
|
-
|
|
19520
|
+
/** `cookieSession`: the task's cookies (ADR 0005 `egress_sessions`), sent and kept on every hop; absent, none are. */
|
|
19521
|
+
async fetch(url, deadlineMs, signal, validators = {}, onRetryAfter, options = {}, onRobotsOverride, cookieSession) {
|
|
18412
19522
|
if (this.localPreviewRobotsException && !isLocalPreviewProxyTarget(url))
|
|
18413
19523
|
throw new Error("Local platform exception is limited to fixed platform hosts");
|
|
18414
|
-
const scope = createExecutionScope({ signal, deadlineAt: deadlineMs, onRetryAfter });
|
|
19524
|
+
const scope = createExecutionScope({ signal, deadlineAt: deadlineMs, onRetryAfter, ...cookieSession === void 0 ? {} : { cookieSession } });
|
|
18415
19525
|
const start = Date.now();
|
|
18416
19526
|
const monotonicStart = performance.now();
|
|
18417
19527
|
const origin = new URL(url).origin;
|
|
@@ -18546,18 +19656,18 @@ var ResilientHttpSubject = class {
|
|
|
18546
19656
|
event: "robots_disallowed",
|
|
18547
19657
|
detail: { url, appliedRules: robotsDecision2.appliedRules, ...robotsDecision2.unreachable === void 0 ? {} : { unreachable: robotsDecision2.unreachable } }
|
|
18548
19658
|
});
|
|
18549
|
-
const override = options.robotsOverride;
|
|
19659
|
+
const override = applicableOverride(options.robotsOverride, robotsDecision2);
|
|
18550
19660
|
if (robotsDecision2.unreachable !== void 0 && cached?.error?.tls === true) {
|
|
18551
19661
|
trace.push({ at: Date.now() - start, lane: "http", event: "request_failed", detail: { reason: "tls_error", url: robotsDecision2.robotsUrl, error: cached.error.name, ...cached.error.code === null ? {} : { code: cached.error.code } } });
|
|
18552
19662
|
return timedDenied("tls_error");
|
|
18553
19663
|
}
|
|
18554
|
-
if (override === void 0
|
|
19664
|
+
if (override === void 0)
|
|
18555
19665
|
return timedDenied("policy_denied");
|
|
18556
19666
|
trace.push({
|
|
18557
19667
|
at: Date.now() - start,
|
|
18558
19668
|
lane: "http",
|
|
18559
19669
|
event: "robots_overridden",
|
|
18560
|
-
detail:
|
|
19670
|
+
detail: overriddenDetail(url, robotsDecision2, override)
|
|
18561
19671
|
});
|
|
18562
19672
|
overrideWarning = robotsOverrideWarning(robotsDecision2, override);
|
|
18563
19673
|
onRobotsOverride?.(robotsOverrideApplied(trace, overrideWarning));
|
|
@@ -18568,8 +19678,12 @@ var ResilientHttpSubject = class {
|
|
|
18568
19678
|
const host = new URL(url).origin;
|
|
18569
19679
|
if (cooldownWaitMs > 0)
|
|
18570
19680
|
trace.push({ at: Date.now() - start, lane: "http", event: "host_cooldown_wait", detail: { host, waitMs: cooldownWaitMs } });
|
|
19681
|
+
if (this.compat !== null)
|
|
19682
|
+
trace.push({ at: Date.now() - start, lane: "http", event: "transport", detail: { library: COMPAT_LIBRARY.name, version: COMPAT_LIBRARY.version, profile: this.compat.profile } });
|
|
18571
19683
|
const transportStart = performance.now();
|
|
18572
19684
|
const maxFileBytes = fileByteCap(this.networkPolicy, options.maxFileBytes);
|
|
19685
|
+
const transportCoding = { decoded: null };
|
|
19686
|
+
const cookies = execution.cookieSession === void 0 ? void 0 : new SessionCookies(execution.cookieSession);
|
|
18573
19687
|
const out = await resilientFetch(url, this.fetcherFor(url, prepared.identityHeaders, validators, signal, (ms) => {
|
|
18574
19688
|
bodyReadMs += ms;
|
|
18575
19689
|
}, (intervalMs, cooldownMs) => {
|
|
@@ -18577,11 +19691,13 @@ var ResilientHttpSubject = class {
|
|
|
18577
19691
|
cooldownWaitMs += cooldownMs;
|
|
18578
19692
|
pacingWaitMs += intervalMs + cooldownMs;
|
|
18579
19693
|
}, (target, proxy) => {
|
|
18580
|
-
trace.push({ at: Date.now() - start, lane: "http", event: "egress_proxy", detail: { url: target, proxy, source: "environment" } });
|
|
19694
|
+
trace.push({ at: Date.now() - start, lane: "http", event: "egress_proxy", detail: { url: target, proxy, source: this.networkPolicy.egressProxy?.source ?? "environment" } });
|
|
18581
19695
|
}, maxFileBytes, {
|
|
18582
19696
|
headers: prepared.customHeaders,
|
|
18583
19697
|
onWithheld: (to, names) => trace.push({ at: Date.now() - start, lane: "http", event: "custom_headers_withheld", detail: { to, names: [...names] } })
|
|
18584
|
-
}, relaxed
|
|
19698
|
+
}, relaxed, (coding) => {
|
|
19699
|
+
transportCoding.decoded = coding;
|
|
19700
|
+
}, deadlineAt, cookies), {
|
|
18585
19701
|
signal,
|
|
18586
19702
|
deadlineAt,
|
|
18587
19703
|
onRetryAfter: (target, retryAt2) => {
|
|
@@ -18604,6 +19720,8 @@ var ResilientHttpSubject = class {
|
|
|
18604
19720
|
transportMs = Math.max(0, performance.now() - transportStart - pacingWaitMs);
|
|
18605
19721
|
return null;
|
|
18606
19722
|
});
|
|
19723
|
+
if (cookies !== void 0)
|
|
19724
|
+
trace.push({ at: Date.now() - start, lane: "http", event: "session_cookies", detail: cookies.detail() });
|
|
18607
19725
|
if (out === null)
|
|
18608
19726
|
return timedDenied("timeout", this.scheduler.retryAt(host));
|
|
18609
19727
|
const fetchedAt = out.status === null ? null : (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -18643,7 +19761,10 @@ var ResilientHttpSubject = class {
|
|
|
18643
19761
|
...file2 === void 0 ? {} : { file: file2 }
|
|
18644
19762
|
};
|
|
18645
19763
|
};
|
|
18646
|
-
const contentEncoding = out.kind === "ok" && classifyContentType(contentType) !== "unsupported" ? contentEncodingLabel(out.headers?.get("content-encoding")) : void 0;
|
|
19764
|
+
const contentEncoding = out.kind === "ok" && classifyContentType(contentType) !== "unsupported" ? contentEncodingLabel(out.headers?.get("content-encoding") ?? transportCoding.decoded) : void 0;
|
|
19765
|
+
const transportDecoded = transportCoding.decoded;
|
|
19766
|
+
if (transportDecoded !== null && out.kind === "ok")
|
|
19767
|
+
trace.push({ at: Date.now() - start, lane: "http", event: "transport_decoded", detail: { contentEncoding: transportDecoded, by: COMPAT_LIBRARY.name } });
|
|
18647
19768
|
let wire;
|
|
18648
19769
|
let bytes;
|
|
18649
19770
|
let undecoded = false;
|
|
@@ -18691,7 +19812,7 @@ var ResilientHttpSubject = class {
|
|
|
18691
19812
|
transportMs += Math.max(0, bodyReadMs - bodyReadBeforeFinal);
|
|
18692
19813
|
const file = contentful ? detectFile(contentType, bytes, responseFileName(out.finalUrl, out.headers?.get("content-disposition") ?? null)) : null;
|
|
18693
19814
|
const body = file === null ? new TextDecoder().decode(bytes) : "";
|
|
18694
|
-
const rawBodySha256 = file === null && !undecoded ? sha256Utf8(body) : null;
|
|
19815
|
+
const rawBodySha256 = file === null && !undecoded && out.status !== null ? sha256Utf8(body) : null;
|
|
18695
19816
|
const rawArtifacts = rawBodySha256 === null ? [] : await captureRawHtml(body, rawBodySha256);
|
|
18696
19817
|
const base = {
|
|
18697
19818
|
requestedUrl: url,
|
|
@@ -18717,13 +19838,13 @@ var ResilientHttpSubject = class {
|
|
|
18717
19838
|
},
|
|
18718
19839
|
usage: {
|
|
18719
19840
|
wallMs,
|
|
18720
|
-
bytesWire: wire.byteLength,
|
|
19841
|
+
bytesWire: transportDecoded === null ? wire.byteLength : null,
|
|
18721
19842
|
bytesDecompressed: file === null ? Buffer.byteLength(body) : bytes.byteLength,
|
|
18722
19843
|
requestCount: out.requestCount,
|
|
18723
19844
|
attemptCount: out.attemptCount,
|
|
18724
19845
|
contentTokens: null,
|
|
18725
19846
|
browserMs: 0,
|
|
18726
|
-
externalCostUsd:
|
|
19847
|
+
externalCostUsd: 0
|
|
18727
19848
|
},
|
|
18728
19849
|
trace
|
|
18729
19850
|
};
|
|
@@ -18863,7 +19984,8 @@ var ResilientHttpSubject = class {
|
|
|
18863
19984
|
markers: render.markers,
|
|
18864
19985
|
emptyTables: render.emptyTables,
|
|
18865
19986
|
textChars: render.textChars,
|
|
18866
|
-
scriptChars: render.scriptChars
|
|
19987
|
+
scriptChars: render.scriptChars,
|
|
19988
|
+
...render.listRecords === void 0 ? {} : { listRecords: render.listRecords }
|
|
18867
19989
|
}
|
|
18868
19990
|
});
|
|
18869
19991
|
return [{
|
|
@@ -18991,7 +20113,7 @@ var ResilientHttpSubject = class {
|
|
|
18991
20113
|
attemptCount: 0,
|
|
18992
20114
|
contentTokens: null,
|
|
18993
20115
|
browserMs: 0,
|
|
18994
|
-
externalCostUsd:
|
|
20116
|
+
externalCostUsd: 0
|
|
18995
20117
|
},
|
|
18996
20118
|
trace
|
|
18997
20119
|
};
|
|
@@ -19046,13 +20168,85 @@ function proxyRefusal(error) {
|
|
|
19046
20168
|
refusal.name = "ProxyConnectError";
|
|
19047
20169
|
return refusal;
|
|
19048
20170
|
}
|
|
20171
|
+
var SessionCookies = class {
|
|
20172
|
+
session;
|
|
20173
|
+
requestsWithCookies = 0;
|
|
20174
|
+
kept = 0;
|
|
20175
|
+
constructor(session) {
|
|
20176
|
+
this.session = session;
|
|
20177
|
+
}
|
|
20178
|
+
async header(url) {
|
|
20179
|
+
const header = await this.session.cookieHeader(url);
|
|
20180
|
+
if (header !== "")
|
|
20181
|
+
this.requestsWithCookies++;
|
|
20182
|
+
return header;
|
|
20183
|
+
}
|
|
20184
|
+
async keep(url, setCookies) {
|
|
20185
|
+
if (setCookies.length > 0)
|
|
20186
|
+
this.kept += await this.session.store(url, setCookies);
|
|
20187
|
+
}
|
|
20188
|
+
detail() {
|
|
20189
|
+
return { session: this.session.id, requestsWithCookies: this.requestsWithCookies, kept: this.kept };
|
|
20190
|
+
}
|
|
20191
|
+
};
|
|
19049
20192
|
|
|
19050
20193
|
// packages/bench/dist/subjects/browserLocal.js
|
|
19051
20194
|
init_dist();
|
|
19052
20195
|
init_dist3();
|
|
19053
20196
|
init_dist2();
|
|
19054
|
-
|
|
20197
|
+
import { chromium as chromium2 } from "playwright";
|
|
20198
|
+
|
|
20199
|
+
// packages/bench/dist/subjects/browserEngine.js
|
|
19055
20200
|
import { chromium } from "playwright";
|
|
20201
|
+
var PLAYWRIGHT_ENGINE = { name: "playwright", launch: (options) => chromium.launch(options) };
|
|
20202
|
+
function browserEngineChoice(env, grant, hosted) {
|
|
20203
|
+
const raw = (env.W2L_BROWSER_ENGINE ?? "").trim().toLowerCase();
|
|
20204
|
+
if (raw === "" || raw === "playwright")
|
|
20205
|
+
return "playwright";
|
|
20206
|
+
if (raw !== "patchright")
|
|
20207
|
+
throw new Error(`W2L_BROWSER_ENGINE is playwright or patchright, not ${raw}`);
|
|
20208
|
+
if (hosted)
|
|
20209
|
+
throw new Error("W2L_BROWSER_ENGINE=patchright is refused on a hosted server (ADR 0005: the enhanced browser runs only in a local Octocrawl instance)");
|
|
20210
|
+
if (!(grant?.capabilities ?? []).includes("enhanced_browser")) {
|
|
20211
|
+
throw new Error("W2L_BROWSER_ENGINE=patchright needs an access grant that names enhanced_browser (ADR 0005; --access-grant or W2L_ACCESS_GRANT)");
|
|
20212
|
+
}
|
|
20213
|
+
return "patchright";
|
|
20214
|
+
}
|
|
20215
|
+
var PATCHRIGHT_MODULE = ["patch", "right"].join("");
|
|
20216
|
+
var PATCHRIGHT_INSTALL = "install it where Octocrawl runs: npm install octocrawl patchright (or, with npx, npx -p octocrawl -p patchright octocrawl ...), then npx patchright install chromium";
|
|
20217
|
+
async function loadPatchrightEngine() {
|
|
20218
|
+
const { createRequire: createRequire2 } = await import("module");
|
|
20219
|
+
const { join: join13 } = await import("path");
|
|
20220
|
+
const { pathToFileURL: pathToFileURL3 } = await import("url");
|
|
20221
|
+
let mod;
|
|
20222
|
+
let from = import.meta.url;
|
|
20223
|
+
try {
|
|
20224
|
+
mod = await import(PATCHRIGHT_MODULE);
|
|
20225
|
+
} catch {
|
|
20226
|
+
try {
|
|
20227
|
+
const resolved = createRequire2(join13(process.cwd(), "package.json")).resolve(PATCHRIGHT_MODULE);
|
|
20228
|
+
mod = await import(pathToFileURL3(resolved).href);
|
|
20229
|
+
from = pathToFileURL3(resolved).href;
|
|
20230
|
+
} catch {
|
|
20231
|
+
throw new Error(`W2L_BROWSER_ENGINE=patchright needs the optional patchright package; ${PATCHRIGHT_INSTALL}`);
|
|
20232
|
+
}
|
|
20233
|
+
}
|
|
20234
|
+
const chromiumApi = mod.chromium ?? mod.default?.chromium;
|
|
20235
|
+
if (chromiumApi === void 0)
|
|
20236
|
+
throw new Error("the patchright package found has no chromium export");
|
|
20237
|
+
let version = null;
|
|
20238
|
+
try {
|
|
20239
|
+
version = createRequire2(from)(`${PATCHRIGHT_MODULE}/package.json`).version ?? null;
|
|
20240
|
+
} catch {
|
|
20241
|
+
}
|
|
20242
|
+
return { name: "patchright", version, launch: async (options) => await chromiumApi.launch(options) };
|
|
20243
|
+
}
|
|
20244
|
+
async function browserEngineFor(name) {
|
|
20245
|
+
return name === "patchright" ? await loadPatchrightEngine() : PLAYWRIGHT_ENGINE;
|
|
20246
|
+
}
|
|
20247
|
+
|
|
20248
|
+
// packages/bench/dist/subjects/browserLocal.js
|
|
20249
|
+
init_egress();
|
|
19056
20250
|
init_robotsLookup();
|
|
19057
20251
|
|
|
19058
20252
|
// packages/bench/dist/subjects/browserActions.js
|
|
@@ -19198,10 +20392,10 @@ async function runPageActions(actions, ctx) {
|
|
|
19198
20392
|
const guard = async () => {
|
|
19199
20393
|
const landed = await refusedDocument();
|
|
19200
20394
|
if (landed !== null)
|
|
19201
|
-
throw new StepFailure("navigation_refused", `the step led the page to ${landed.url}, which
|
|
20395
|
+
throw new StepFailure("navigation_refused", `the step led the page to ${landed.url}, which Octocrawl does not fetch (${landed.reason}); it is not read`);
|
|
19202
20396
|
const refused = ctx.takeRefusedNavigation();
|
|
19203
20397
|
if (refused !== null)
|
|
19204
|
-
throw new StepFailure("navigation_refused", `the step led the page to ${refused.url}, which
|
|
20398
|
+
throw new StepFailure("navigation_refused", `the step led the page to ${refused.url}, which Octocrawl does not fetch (${refused.reason}); the request was not sent`);
|
|
19205
20399
|
};
|
|
19206
20400
|
try {
|
|
19207
20401
|
const detail = await runStep(action, ctx, result, artifacts, index, guard);
|
|
@@ -19217,7 +20411,7 @@ async function runPageActions(actions, ctx) {
|
|
|
19217
20411
|
if (failure2.code !== "navigation_refused") {
|
|
19218
20412
|
const landed = await refusedDocument().catch(() => null);
|
|
19219
20413
|
if (landed !== null)
|
|
19220
|
-
failure2 = new StepFailure("navigation_refused", `the step led the page to ${landed.url}, which
|
|
20414
|
+
failure2 = new StepFailure("navigation_refused", `the step led the page to ${landed.url}, which Octocrawl does not fetch (${landed.reason}); it is not read`);
|
|
19221
20415
|
}
|
|
19222
20416
|
if (failure2.code === "navigation_refused")
|
|
19223
20417
|
dropSince(result, artifacts, before);
|
|
@@ -19321,9 +20515,10 @@ async function runStep(action, ctx, result, artifacts, index, guard) {
|
|
|
19321
20515
|
case "executeJavascript": {
|
|
19322
20516
|
let value;
|
|
19323
20517
|
try {
|
|
19324
|
-
|
|
20518
|
+
const expression = `(async () => {
|
|
19325
20519
|
${action.script}
|
|
19326
|
-
})()
|
|
20520
|
+
})()`;
|
|
20521
|
+
value = await bounded(ctx, ctx.evaluateScript === void 0 ? page.evaluate(expression) : ctx.evaluateScript(expression));
|
|
19327
20522
|
} catch (error) {
|
|
19328
20523
|
if (signal?.aborted || error instanceof StepFailure)
|
|
19329
20524
|
throw error;
|
|
@@ -20144,6 +21339,9 @@ var PAGE_CLOSE_MS = 2e3;
|
|
|
20144
21339
|
async function closePage(page, timeoutMs = PAGE_CLOSE_MS) {
|
|
20145
21340
|
return closeWithin(page.close(), timeoutMs);
|
|
20146
21341
|
}
|
|
21342
|
+
function contextCookie(c) {
|
|
21343
|
+
return { name: c.name, value: c.value, domain: c.domain, path: c.path, expires: c.expires, httpOnly: c.httpOnly, secure: c.secure, sameSite: c.sameSite };
|
|
21344
|
+
}
|
|
20147
21345
|
async function closeWithin(closing, timeoutMs) {
|
|
20148
21346
|
const settled2 = closing.then(() => true, () => true);
|
|
20149
21347
|
let timer;
|
|
@@ -20163,6 +21361,7 @@ var BrowserLocalSubject = class {
|
|
|
20163
21361
|
fileStore;
|
|
20164
21362
|
previewProductToken;
|
|
20165
21363
|
adHosts;
|
|
21364
|
+
browserEngine;
|
|
20166
21365
|
meta = {
|
|
20167
21366
|
id: "browser-local",
|
|
20168
21367
|
displayName: "browser-local (playwright chromium \xD7 extract-tf)",
|
|
@@ -20201,7 +21400,9 @@ var BrowserLocalSubject = class {
|
|
|
20201
21400
|
accessConfig;
|
|
20202
21401
|
/** The operator's environment proxy for every context this subject opens (local mode). */
|
|
20203
21402
|
envProxy;
|
|
20204
|
-
|
|
21403
|
+
/** The launched engine's package version, once known (Patchright only). */
|
|
21404
|
+
engineVersion = null;
|
|
21405
|
+
constructor(mode = "standard", access, headed = false, networkPolicy, managedProfileDir = null, scheduler, publicPreferenceState = null, browserAllowedHosts, onRenderedHtml, fileStore = null, previewProductToken = false, adHosts = AD_HOSTS, browserEngine = "playwright") {
|
|
20205
21406
|
this.mode = mode;
|
|
20206
21407
|
this.headed = headed;
|
|
20207
21408
|
this.managedProfileDir = managedProfileDir;
|
|
@@ -20211,6 +21412,10 @@ var BrowserLocalSubject = class {
|
|
|
20211
21412
|
this.fileStore = fileStore;
|
|
20212
21413
|
this.previewProductToken = previewProductToken;
|
|
20213
21414
|
this.adHosts = adHosts;
|
|
21415
|
+
this.browserEngine = browserEngine;
|
|
21416
|
+
if (browserEngine !== "playwright" && (access != null || managedProfileDir !== null)) {
|
|
21417
|
+
throw new Error("the enhanced browser runs only in the public browser lane, never with a saved login or a managed profile");
|
|
21418
|
+
}
|
|
20214
21419
|
if (publicPreferenceState !== null && (mode !== "standard" || access != null || managedProfileDir !== null)) {
|
|
20215
21420
|
throw new Error("anonymous public preference state is only available to the standard public browser");
|
|
20216
21421
|
}
|
|
@@ -20239,8 +21444,9 @@ var BrowserLocalSubject = class {
|
|
|
20239
21444
|
ledger() {
|
|
20240
21445
|
return this.chain.toLedger();
|
|
20241
21446
|
}
|
|
20242
|
-
|
|
20243
|
-
|
|
21447
|
+
/** `cookieSession`: the task's cookies (ADR 0005 `egress_sessions`): the context starts with them and leaves its own there. */
|
|
21448
|
+
async fetch(url, deadlineMs, signal, onRetryAfter, options = {}, onRobotsOverride, cookieSession) {
|
|
21449
|
+
const scope = createExecutionScope({ signal, deadlineAt: deadlineMs, onRetryAfter, ...cookieSession === void 0 ? {} : { cookieSession } });
|
|
20244
21450
|
const start = Date.now();
|
|
20245
21451
|
const monotonicStart = performance.now();
|
|
20246
21452
|
let queueMs = 0;
|
|
@@ -20326,11 +21532,14 @@ var BrowserLocalSubject = class {
|
|
|
20326
21532
|
const routeMatch = "**/*";
|
|
20327
21533
|
let actionsRoute = null;
|
|
20328
21534
|
let popupCloser = null;
|
|
21535
|
+
let taskCookies = null;
|
|
20329
21536
|
try {
|
|
20330
21537
|
throwIfExecutionStopped(execution);
|
|
20331
21538
|
await raceWithSignal(assertSafeUrl(url, this.networkPolicy), signal);
|
|
20332
21539
|
const managedContext = this.managedProfileDir === null ? null : await raceWithSignal(this.getManagedContext(execution), signal);
|
|
20333
21540
|
const browser = managedContext?.browser() ?? await raceWithSignal(this.getBrowser(execution), signal);
|
|
21541
|
+
if (this.browserEngine !== "playwright")
|
|
21542
|
+
trace.push({ at: Date.now() - start, lane: "browser_local", event: "browser_engine", detail: { engine: this.browserEngine, version: this.engineVersion } });
|
|
20334
21543
|
throwIfExecutionStopped(execution);
|
|
20335
21544
|
const version = browser.version();
|
|
20336
21545
|
const major = Number(version.split(".")[0] ?? CHROME_MAJOR_FLOOR);
|
|
@@ -20361,15 +21570,15 @@ var BrowserLocalSubject = class {
|
|
|
20361
21570
|
}
|
|
20362
21571
|
});
|
|
20363
21572
|
const host = this.hostOf(url);
|
|
20364
|
-
const override = options.robotsOverride;
|
|
20365
|
-
const overridden = identity.respectsRobots && robotsDecision2.decision === "disallowed" &&
|
|
21573
|
+
const override = applicableOverride(options.robotsOverride, robotsDecision2);
|
|
21574
|
+
const overridden = identity.respectsRobots && robotsDecision2.decision === "disallowed" && override !== void 0 && !(robotsDecision2.unreachable !== void 0 && cachedRobots?.error?.tls === true);
|
|
20366
21575
|
if (override !== void 0 && overridden) {
|
|
20367
|
-
trace.push({ at: Date.now() - start, lane: "browser_local", event: "robots_disallowed", detail: { url, appliedRules: robotsDecision2.appliedRules } });
|
|
21576
|
+
trace.push({ at: Date.now() - start, lane: "browser_local", event: "robots_disallowed", detail: { url, appliedRules: robotsDecision2.appliedRules, ...robotsDecision2.unreachable === void 0 ? {} : { unreachable: robotsDecision2.unreachable } } });
|
|
20368
21577
|
trace.push({
|
|
20369
21578
|
at: Date.now() - start,
|
|
20370
21579
|
lane: "browser_local",
|
|
20371
21580
|
event: "robots_overridden",
|
|
20372
|
-
detail:
|
|
21581
|
+
detail: overriddenDetail(url, robotsDecision2, override)
|
|
20373
21582
|
});
|
|
20374
21583
|
onRobotsOverride?.(robotsOverrideApplied(trace, robotsOverrideWarning(robotsDecision2, override)));
|
|
20375
21584
|
}
|
|
@@ -20430,7 +21639,7 @@ var BrowserLocalSubject = class {
|
|
|
20430
21639
|
attemptCount: 0,
|
|
20431
21640
|
contentTokens: null,
|
|
20432
21641
|
browserMs: 0,
|
|
20433
|
-
externalCostUsd:
|
|
21642
|
+
externalCostUsd: 0
|
|
20434
21643
|
},
|
|
20435
21644
|
trace
|
|
20436
21645
|
};
|
|
@@ -20549,6 +21758,13 @@ var BrowserLocalSubject = class {
|
|
|
20549
21758
|
detail: { cookieCount: userCookies.length, ...storedOrigins === 0 ? {} : { localStorageOrigins: storedOrigins }, sessionSha256: this.access.sessionSha256 }
|
|
20550
21759
|
});
|
|
20551
21760
|
}
|
|
21761
|
+
if (execution.cookieSession !== void 0 && this.accessConfig?.session === void 0 && context !== this.managedContext) {
|
|
21762
|
+
const cookies = await raceWithSignal(execution.cookieSession.browserCookies(url), signal);
|
|
21763
|
+
if (cookies.length > 0)
|
|
21764
|
+
await context.addCookies(cookies.map((c) => ({ ...c, ...c.expires < 0 ? { expires: -1 } : {} })));
|
|
21765
|
+
const startedWith = cookies.length === 0 ? [] : (await raceWithSignal(context.cookies(), signal)).map(contextCookie);
|
|
21766
|
+
taskCookies = { session: execution.cookieSession, startedWith };
|
|
21767
|
+
}
|
|
20552
21768
|
throwIfExecutionStopped(execution);
|
|
20553
21769
|
const pendingPage = context.newPage();
|
|
20554
21770
|
void pendingPage.then((created) => {
|
|
@@ -20637,7 +21853,7 @@ var BrowserLocalSubject = class {
|
|
|
20637
21853
|
trace.push({ at: Date.now() - start, lane: "browser_local", event: "navigate", detail: { url: navigationUrl, attempt: attemptCount, timeoutMs: navigationTimeoutMs } });
|
|
20638
21854
|
const envProxy = proxyFor(navigationUrl, this.networkPolicy);
|
|
20639
21855
|
if (envProxy !== null)
|
|
20640
|
-
trace.push({ at: Date.now() - start, lane: "browser_local", event: "egress_proxy", detail: { url: navigationUrl, proxy: envProxy.endpoint, source: "environment" } });
|
|
21856
|
+
trace.push({ at: Date.now() - start, lane: "browser_local", event: "egress_proxy", detail: { url: navigationUrl, proxy: envProxy.endpoint, source: this.networkPolicy.egressProxy?.source ?? "environment" } });
|
|
20641
21857
|
try {
|
|
20642
21858
|
documents.restart();
|
|
20643
21859
|
response = await page.goto(navigationUrl, { waitUntil: "domcontentloaded", timeout: navigationTimeoutMs });
|
|
@@ -20768,8 +21984,11 @@ var BrowserLocalSubject = class {
|
|
|
20768
21984
|
});
|
|
20769
21985
|
};
|
|
20770
21986
|
context.on("page", popupCloser);
|
|
21987
|
+
const actionPage = page;
|
|
20771
21988
|
ran.actions = await runPageActions(options.actions, {
|
|
20772
21989
|
page,
|
|
21990
|
+
// Patchright's evaluate takes (pageFunction, arg, options, isolatedContext) and defaults to an isolated world.
|
|
21991
|
+
evaluateScript: this.browserEngine === "patchright" ? (expression) => actionPage.evaluate(expression, void 0, void 0, false) : (expression) => actionPage.evaluate(expression),
|
|
20773
21992
|
execution,
|
|
20774
21993
|
trace,
|
|
20775
21994
|
at: () => Date.now() - start,
|
|
@@ -20823,7 +22042,7 @@ var BrowserLocalSubject = class {
|
|
|
20823
22042
|
const late = refusedNavigations.shift();
|
|
20824
22043
|
const last = options.actions.length - 1;
|
|
20825
22044
|
if (late !== void 0 && ran.actions.result.failed === void 0) {
|
|
20826
|
-
ran.actions.result.failed = { index: last, type: options.actions[last].type, code: "navigation_refused", message: `after the steps, the page tried to go to ${late.url}, which
|
|
22045
|
+
ran.actions.result.failed = { index: last, type: options.actions[last].type, code: "navigation_refused", message: `after the steps, the page tried to go to ${late.url}, which Octocrawl does not fetch (${late.reason}); the page stayed where it was` };
|
|
20827
22046
|
dropLastStep(ran.actions);
|
|
20828
22047
|
}
|
|
20829
22048
|
for (const loadedUrl of documents.loaded.slice(ran.actions.checkedDocuments)) {
|
|
@@ -20832,7 +22051,7 @@ var BrowserLocalSubject = class {
|
|
|
20832
22051
|
continue;
|
|
20833
22052
|
trace.push({ at: Date.now() - start, lane: "browser_local", event: "navigation_refused", detail: { url: loadedUrl, reason: landed } });
|
|
20834
22053
|
if (ran.actions.result.failed === void 0) {
|
|
20835
|
-
ran.actions.result.failed = { index: last, type: options.actions[last].type, code: "navigation_refused", message: `after the steps, the page loaded ${loadedUrl}, which
|
|
22054
|
+
ran.actions.result.failed = { index: last, type: options.actions[last].type, code: "navigation_refused", message: `after the steps, the page loaded ${loadedUrl}, which Octocrawl does not fetch (${landed}); it is not read` };
|
|
20836
22055
|
dropLastStep(ran.actions);
|
|
20837
22056
|
}
|
|
20838
22057
|
return this.notRead(url, start, trace, finalUrl);
|
|
@@ -20915,7 +22134,7 @@ var BrowserLocalSubject = class {
|
|
|
20915
22134
|
navigationFollowupCount: variantFollowups,
|
|
20916
22135
|
contentTokens: null,
|
|
20917
22136
|
browserMs,
|
|
20918
|
-
externalCostUsd:
|
|
22137
|
+
externalCostUsd: 0,
|
|
20919
22138
|
timings: { retryWaitMs, totalMs: wallMs }
|
|
20920
22139
|
},
|
|
20921
22140
|
trace
|
|
@@ -21087,7 +22306,7 @@ var BrowserLocalSubject = class {
|
|
|
21087
22306
|
attemptCount: 1,
|
|
21088
22307
|
contentTokens: null,
|
|
21089
22308
|
browserMs: wallMs,
|
|
21090
|
-
externalCostUsd:
|
|
22309
|
+
externalCostUsd: 0
|
|
21091
22310
|
},
|
|
21092
22311
|
trace
|
|
21093
22312
|
};
|
|
@@ -21095,6 +22314,11 @@ var BrowserLocalSubject = class {
|
|
|
21095
22314
|
signal?.removeEventListener("abort", onAbort);
|
|
21096
22315
|
if (page !== void 0)
|
|
21097
22316
|
await closePage(page);
|
|
22317
|
+
if (taskCookies !== null && context !== void 0) {
|
|
22318
|
+
const held = await Promise.race([context.cookies().catch(() => null), new Promise((done) => setTimeout(() => done(null), PAGE_CLOSE_MS))]);
|
|
22319
|
+
const changes = held === null ? { kept: 0, removed: 0 } : await taskCookies.session.storeBrowserChanges(taskCookies.startedWith, held.map(contextCookie));
|
|
22320
|
+
trace.push({ at: Date.now() - start, lane: "browser_local", event: "session_cookies", detail: { session: taskCookies.session.id, startedWith: taskCookies.startedWith.length, ...changes, read: held !== null } });
|
|
22321
|
+
}
|
|
21098
22322
|
await session?.detach().catch(() => {
|
|
21099
22323
|
});
|
|
21100
22324
|
if (context !== void 0 && requestRoute !== null)
|
|
@@ -21161,7 +22385,7 @@ var BrowserLocalSubject = class {
|
|
|
21161
22385
|
...this.networkPolicy.egressProxy ? { envProxy: proxyFor(finalUrl, this.networkPolicy)?.endpoint ?? null } : {}
|
|
21162
22386
|
},
|
|
21163
22387
|
// The file's bytes are counted; what crossed the wire (compressed) is not measured.
|
|
21164
|
-
usage: { wallMs: at(), bytesWire: null, bytesDecompressed: 0, requestCount: attemptCount, attemptCount, contentTokens: null, browserMs: at(), externalCostUsd:
|
|
22388
|
+
usage: { wallMs: at(), bytesWire: null, bytesDecompressed: 0, requestCount: attemptCount, attemptCount, contentTokens: null, browserMs: at(), externalCostUsd: 0 },
|
|
21165
22389
|
trace
|
|
21166
22390
|
};
|
|
21167
22391
|
};
|
|
@@ -21248,7 +22472,7 @@ var BrowserLocalSubject = class {
|
|
|
21248
22472
|
artifacts: [],
|
|
21249
22473
|
...this.networkPolicy.egressProxy ? { envProxy: proxyFor(finalUrl, this.networkPolicy)?.endpoint ?? null } : {}
|
|
21250
22474
|
},
|
|
21251
|
-
usage: { wallMs, bytesWire: null, bytesDecompressed: 0, requestCount: attemptCount, attemptCount, contentTokens: null, browserMs: wallMs, externalCostUsd:
|
|
22475
|
+
usage: { wallMs, bytesWire: null, bytesDecompressed: 0, requestCount: attemptCount, attemptCount, contentTokens: null, browserMs: wallMs, externalCostUsd: 0 },
|
|
21252
22476
|
trace
|
|
21253
22477
|
};
|
|
21254
22478
|
}
|
|
@@ -21284,7 +22508,7 @@ var BrowserLocalSubject = class {
|
|
|
21284
22508
|
truncatedAt: null,
|
|
21285
22509
|
compliance: null,
|
|
21286
22510
|
evidence: { finalUrl, httpStatus: null, redirectChain: [], contentType: null, rawBodySha256: null, artifacts: [] },
|
|
21287
|
-
usage: { wallMs: Date.now() - start, bytesWire: 0, bytesDecompressed: 0, requestCount: 1, attemptCount: 1, contentTokens: null, browserMs: Date.now() - start, externalCostUsd:
|
|
22511
|
+
usage: { wallMs: Date.now() - start, bytesWire: 0, bytesDecompressed: 0, requestCount: 1, attemptCount: 1, contentTokens: null, browserMs: Date.now() - start, externalCostUsd: 0 },
|
|
21288
22512
|
trace
|
|
21289
22513
|
};
|
|
21290
22514
|
}
|
|
@@ -21325,7 +22549,7 @@ var BrowserLocalSubject = class {
|
|
|
21325
22549
|
attemptCount: 0,
|
|
21326
22550
|
contentTokens: null,
|
|
21327
22551
|
browserMs: wallMs,
|
|
21328
|
-
externalCostUsd:
|
|
22552
|
+
externalCostUsd: 0
|
|
21329
22553
|
},
|
|
21330
22554
|
trace
|
|
21331
22555
|
};
|
|
@@ -21351,7 +22575,10 @@ var BrowserLocalSubject = class {
|
|
|
21351
22575
|
];
|
|
21352
22576
|
if (this.activeExecutions === 0)
|
|
21353
22577
|
throw new DOMException("Browser startup abandoned", "AbortError");
|
|
21354
|
-
|
|
22578
|
+
const engine = await browserEngineFor(this.browserEngine);
|
|
22579
|
+
if (engine.version !== void 0)
|
|
22580
|
+
this.engineVersion = engine.version;
|
|
22581
|
+
return engine.launch({ headless: !this.headed, timeout: 3e4, ...args.length === 0 ? chromiumProxyLaunchOptions(this.envProxy) : { args } });
|
|
21355
22582
|
};
|
|
21356
22583
|
const pending = launch().then(async (browser) => {
|
|
21357
22584
|
if (this.activeExecutions === 0) {
|
|
@@ -21377,7 +22604,7 @@ var BrowserLocalSubject = class {
|
|
|
21377
22604
|
if (this.managedContext !== null)
|
|
21378
22605
|
return this.managedContext;
|
|
21379
22606
|
if (this.managedContextPromise === null) {
|
|
21380
|
-
const pending =
|
|
22607
|
+
const pending = chromium2.launchPersistentContext(this.managedProfileDir, { headless: !this.headed, timeout: 3e4, ...chromiumProxyLaunchOptions(this.envProxy) }).then(async (context) => {
|
|
21381
22608
|
if (this.activeExecutions === 0) {
|
|
21382
22609
|
if (this.managedContextPromise === pending)
|
|
21383
22610
|
this.managedContextPromise = null;
|
|
@@ -21491,6 +22718,7 @@ function freshEntry() {
|
|
|
21491
22718
|
latencyTotalMs: 0,
|
|
21492
22719
|
latencySamplesMs: [],
|
|
21493
22720
|
costTotalUsd: 0,
|
|
22721
|
+
costUnknownAttempts: 0,
|
|
21494
22722
|
lastFailureClass: null
|
|
21495
22723
|
};
|
|
21496
22724
|
}
|
|
@@ -21504,7 +22732,10 @@ function applyOutcome(entry3, outcome) {
|
|
|
21504
22732
|
if (entry3.latencySamplesMs.length > MAX_LATENCY_SAMPLES) {
|
|
21505
22733
|
entry3.latencySamplesMs = entry3.latencySamplesMs.slice(-MAX_LATENCY_SAMPLES);
|
|
21506
22734
|
}
|
|
21507
|
-
|
|
22735
|
+
if (outcome.costUsd === null)
|
|
22736
|
+
entry3.costUnknownAttempts += 1;
|
|
22737
|
+
else
|
|
22738
|
+
entry3.costTotalUsd += outcome.costUsd;
|
|
21508
22739
|
entry3.lastFailureClass = outcome.failureClass;
|
|
21509
22740
|
}
|
|
21510
22741
|
var MemoryRoutingHistory = class {
|
|
@@ -21532,7 +22763,8 @@ function rankVendors(domainHistory, vendorIds) {
|
|
|
21532
22763
|
score: EXPLORE_BONUS,
|
|
21533
22764
|
successRate: 0,
|
|
21534
22765
|
medianLatencyMs: null,
|
|
21535
|
-
totalCostUsd: 0
|
|
22766
|
+
totalCostUsd: 0,
|
|
22767
|
+
costUnknown: false
|
|
21536
22768
|
});
|
|
21537
22769
|
continue;
|
|
21538
22770
|
}
|
|
@@ -21544,13 +22776,15 @@ function rankVendors(domainHistory, vendorIds) {
|
|
|
21544
22776
|
mid = samples.length % 2 === 0 ? (samples[half - 1] + samples[half]) / 2 : samples[half];
|
|
21545
22777
|
}
|
|
21546
22778
|
const cost = entry3.costTotalUsd;
|
|
21547
|
-
const
|
|
22779
|
+
const costUnknown = entry3.costUnknownAttempts > 0;
|
|
22780
|
+
const score = successRate + 0.3 / (1 + mid / 1e3) + (costUnknown ? 0 : 0.1 / (1 + cost / 0.1));
|
|
21548
22781
|
scores.push({
|
|
21549
22782
|
vendorId: id,
|
|
21550
22783
|
score,
|
|
21551
22784
|
successRate,
|
|
21552
22785
|
medianLatencyMs: mid,
|
|
21553
|
-
totalCostUsd: cost
|
|
22786
|
+
totalCostUsd: cost,
|
|
22787
|
+
costUnknown
|
|
21554
22788
|
});
|
|
21555
22789
|
}
|
|
21556
22790
|
return scores.sort((a, b) => b.score - a.score);
|
|
@@ -21661,6 +22895,10 @@ var FileSessionStore = class _FileSessionStore {
|
|
|
21661
22895
|
};
|
|
21662
22896
|
|
|
21663
22897
|
// packages/bench/dist/routing/ladder.js
|
|
22898
|
+
function attemptOf(channel, id, result, ordinal, startedAt) {
|
|
22899
|
+
return { channel: id, result, ordinal, startedAt, endedAt: (/* @__PURE__ */ new Date()).toISOString(), ...channel.vendorId === void 0 ? {} : { vendorId: channel.vendorId } };
|
|
22900
|
+
}
|
|
22901
|
+
var HTTP_CHANNELS = /* @__PURE__ */ new Set(["http", "http_compat"]);
|
|
21664
22902
|
function summarize(channelsTried, attempts) {
|
|
21665
22903
|
const cost = {
|
|
21666
22904
|
knownSubtotal: attempts.reduce((sum, item) => sum + (item.result.usage.externalCostUsd ?? 0), 0),
|
|
@@ -21716,7 +22954,7 @@ function withLowContentYield(result, channelsTried) {
|
|
|
21716
22954
|
return result;
|
|
21717
22955
|
if (result.warnings?.some((warning) => warning.code === "low_content_yield") === true)
|
|
21718
22956
|
return result;
|
|
21719
|
-
return { ...result, warnings: [...result.warnings ?? [], lowContentYieldWarning(result, channelsTried.some((channel) => channel
|
|
22957
|
+
return { ...result, warnings: [...result.warnings ?? [], lowContentYieldWarning(result, channelsTried.some((channel) => !HTTP_CHANNELS.has(channel)))] };
|
|
21720
22958
|
}
|
|
21721
22959
|
function withRenderedLowYield(result) {
|
|
21722
22960
|
if (!CONTENTFUL_STATUS.has(result.status) || result.warnings?.some((warning) => warning.code === "low_content_yield") === true)
|
|
@@ -21811,11 +23049,11 @@ var LadderRunner = class {
|
|
|
21811
23049
|
execution.onRobotsOverride?.(applied);
|
|
21812
23050
|
} };
|
|
21813
23051
|
try {
|
|
21814
|
-
return carryRobotsOverride(await this.runWithinBudget(url, session, rungs, options, progress), progress.robotsOverrides);
|
|
23052
|
+
return withRunCost(carryRobotsOverride(await this.runWithinBudget(url, session, rungs, options, progress), progress.robotsOverrides));
|
|
21815
23053
|
} catch (error) {
|
|
21816
23054
|
if (!deadlineReached(scope))
|
|
21817
23055
|
throw error;
|
|
21818
|
-
return carryRobotsOverride(deadlineOutcome(url, progress, null), progress.robotsOverrides);
|
|
23056
|
+
return withRunCost(carryRobotsOverride(deadlineOutcome(url, progress, null), progress.robotsOverrides));
|
|
21819
23057
|
} finally {
|
|
21820
23058
|
scope.dispose();
|
|
21821
23059
|
}
|
|
@@ -21866,7 +23104,7 @@ var LadderRunner = class {
|
|
|
21866
23104
|
return finish2(this.governanceRefusal(url, decision.reason ?? "governance refused this url"), false);
|
|
21867
23105
|
}
|
|
21868
23106
|
const permitted = new Set(decision.permittedChannels);
|
|
21869
|
-
const local = this.channels.filter((c) => c.vendorId === void 0 && permitted.has(c.id));
|
|
23107
|
+
const local = this.channels.filter((c) => c.vendorId === void 0 && permitted.has(c.id) && (c.serves?.(url, options) ?? true));
|
|
21870
23108
|
const providers = this.channels.filter((c) => c.vendorId !== void 0 && permitted.has(c.id));
|
|
21871
23109
|
let ordered = [...local, ...await raceWithSignal(this.orderProviders(url, providers), execution.signal)];
|
|
21872
23110
|
const sessionFirst = effectiveSession !== null && effectiveSession.vendor === "browser_local_authed" && sessionCoversHost(effectiveSession.domain, safeHost(url)) ? ordered.find((c) => c.id === "authed_session") ?? null : null;
|
|
@@ -21911,8 +23149,9 @@ var LadderRunner = class {
|
|
|
21911
23149
|
return finish2(identityBlock, false);
|
|
21912
23150
|
}
|
|
21913
23151
|
channelsTried.push(channel.id);
|
|
23152
|
+
const startedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
21914
23153
|
const result = await raceWithSignal(channel.fetch(url, effectiveSession, execution, options), execution.signal);
|
|
21915
|
-
attempts.push(
|
|
23154
|
+
attempts.push(attemptOf(channel, channel.id, result, attempts.length + 1, startedAt2));
|
|
21916
23155
|
if (result.usage.deadlineExceeded === true || result.failureReason === "timeout" && deadlineReached(execution)) {
|
|
21917
23156
|
return deadlineOutcome(url, progress, result);
|
|
21918
23157
|
}
|
|
@@ -21955,7 +23194,7 @@ var LadderRunner = class {
|
|
|
21955
23194
|
detail: { domain: snapshot.domain, vendor: channel.vendorId }
|
|
21956
23195
|
});
|
|
21957
23196
|
}
|
|
21958
|
-
const qualityEvent = channel.id
|
|
23197
|
+
const qualityEvent = HTTP_CHANNELS.has(channel.id) ? qualityEscalationEvent(result) : null;
|
|
21959
23198
|
const thinHttp = qualityEvent !== null;
|
|
21960
23199
|
const emptyConfirmed = !thinHttp && worseThanBest && bestSize === 0;
|
|
21961
23200
|
if (thinHttp || worseThanBest) {
|
|
@@ -22080,7 +23319,7 @@ var LadderRunner = class {
|
|
|
22080
23319
|
const outcome = {
|
|
22081
23320
|
contentful: CONTENTFUL_STATUS.has(result.status),
|
|
22082
23321
|
wallMs: result.usage.wallMs,
|
|
22083
|
-
costUsd: result.usage.externalCostUsd
|
|
23322
|
+
costUsd: result.usage.externalCostUsd,
|
|
22084
23323
|
failureClass: cls
|
|
22085
23324
|
};
|
|
22086
23325
|
await this.history?.record(safeHost(url), vendorId, outcome);
|
|
@@ -22148,8 +23387,9 @@ var LadderRunner = class {
|
|
|
22148
23387
|
summary: summarize([...channelsTried, `${channel.id}(retry)`], attempts)
|
|
22149
23388
|
};
|
|
22150
23389
|
}
|
|
23390
|
+
const retryStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
22151
23391
|
const retry = await raceWithSignal(channel.fetch(url, snapshot, execution, options), execution.signal);
|
|
22152
|
-
attempts.push(
|
|
23392
|
+
attempts.push(attemptOf(channel, `${channel.id}(retry)`, retry, attempts.length + 1, retryStartedAt));
|
|
22153
23393
|
ladderTrace.push({
|
|
22154
23394
|
at: retry.usage.wallMs,
|
|
22155
23395
|
event: "ladder_step",
|
|
@@ -22199,7 +23439,7 @@ var LadderRunner = class {
|
|
|
22199
23439
|
attemptCount: 0,
|
|
22200
23440
|
contentTokens: null,
|
|
22201
23441
|
browserMs: 0,
|
|
22202
|
-
externalCostUsd:
|
|
23442
|
+
externalCostUsd: 0
|
|
22203
23443
|
},
|
|
22204
23444
|
trace: [{ at: 0, lane: "http", event: "governance_refusal", detail: { reason: reason2 } }]
|
|
22205
23445
|
};
|
|
@@ -22262,7 +23502,13 @@ function deadlineOutcome(url, progress, returned) {
|
|
|
22262
23502
|
const event = { at, lane: base.lane, event: "deadline_exceeded", detail: evidence === null ? detail : { ...detail, evidence: evidence.channel } };
|
|
22263
23503
|
result = { ...base, status: "failed", failureReason: "timeout", blockReason: null, budgetExceeded: null, markdown: evidence?.result.markdown ?? null, usage: { ...base.usage, contentTokens: null, deadlineExceeded: true }, trace: [...base.trace, event] };
|
|
22264
23504
|
}
|
|
22265
|
-
|
|
23505
|
+
const summary = summarize(channelsTried, attempts);
|
|
23506
|
+
const providerUnmeasured = interrupted !== null && attempts.length < channelsTried.length && laneOf(interrupted) === "provider";
|
|
23507
|
+
const costed = providerUnmeasured ? { ...summary, externalCostUsd: null, externalCost: { ...summary.externalCost, unknown: true } } : summary;
|
|
23508
|
+
return { result, channelsTried, handoffRequested: false, ladderTrace, summary: { ...costed, totalMs: at } };
|
|
23509
|
+
}
|
|
23510
|
+
function withRunCost(run) {
|
|
23511
|
+
return { ...run, result: { ...run.result, usage: { ...run.result.usage, externalCostUsd: run.summary.externalCostUsd } } };
|
|
22266
23512
|
}
|
|
22267
23513
|
function noMainContentEvidence(attempts) {
|
|
22268
23514
|
for (let i = attempts.length - 1; i >= 0; i--) {
|
|
@@ -22288,7 +23534,8 @@ function deadlineFailure(url, lane, wallMs) {
|
|
|
22288
23534
|
compliance: null,
|
|
22289
23535
|
evidence: { finalUrl: url, httpStatus: null, redirectChain: [], contentType: null, rawBodySha256: null, artifacts: [] },
|
|
22290
23536
|
// The interrupted rung's traffic was not measured; the ladder summary holds what was.
|
|
22291
|
-
|
|
23537
|
+
// A vendor rung may have billed before the deadline; a local one calls no third party.
|
|
23538
|
+
usage: { wallMs, bytesWire: null, bytesDecompressed: 0, requestCount: 0, attemptCount: 0, contentTokens: null, browserMs: 0, externalCostUsd: lane === "provider" ? null : 0 },
|
|
22292
23539
|
trace: []
|
|
22293
23540
|
};
|
|
22294
23541
|
}
|
|
@@ -22335,7 +23582,7 @@ function identityRefusedResult(url, channel, event, issues) {
|
|
|
22335
23582
|
attemptCount: 0,
|
|
22336
23583
|
contentTokens: null,
|
|
22337
23584
|
browserMs: 0,
|
|
22338
|
-
externalCostUsd:
|
|
23585
|
+
externalCostUsd: 0
|
|
22339
23586
|
},
|
|
22340
23587
|
trace: [
|
|
22341
23588
|
{
|
|
@@ -22401,6 +23648,10 @@ function sessionRejection(url, result) {
|
|
|
22401
23648
|
return { ...result, status: "blocked", blockReason: "login_wall", failureReason: null };
|
|
22402
23649
|
}
|
|
22403
23650
|
|
|
23651
|
+
// packages/bench/dist/ladderCli.js
|
|
23652
|
+
init_dist2();
|
|
23653
|
+
import { readFileSync } from "fs";
|
|
23654
|
+
|
|
22404
23655
|
// packages/bench/dist/fileStore.js
|
|
22405
23656
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
22406
23657
|
import { mkdir as mkdir4, rename as rename3, rm, stat, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -22441,9 +23692,9 @@ init_provider2();
|
|
|
22441
23692
|
init_dist2();
|
|
22442
23693
|
var playwrightConnector = async (wsUrl, deadlineMs, signal) => {
|
|
22443
23694
|
throwIfExecutionStopped({ signal, deadlineAt: deadlineMs });
|
|
22444
|
-
const { chromium:
|
|
23695
|
+
const { chromium: chromium4 } = await import("playwright");
|
|
22445
23696
|
const remaining = deadlineMs === void 0 ? 3e4 : Math.max(1, deadlineMs - Date.now());
|
|
22446
|
-
const pending =
|
|
23697
|
+
const pending = chromium4.connectOverCDP(wsUrl, { timeout: remaining });
|
|
22447
23698
|
void pending.then((browser) => {
|
|
22448
23699
|
if (signal?.aborted)
|
|
22449
23700
|
void browser.close().catch(() => {
|
|
@@ -22734,7 +23985,7 @@ var CdpVendorTransport = class {
|
|
|
22734
23985
|
};
|
|
22735
23986
|
|
|
22736
23987
|
// packages/bench/dist/vendors/connect.js
|
|
22737
|
-
async function connectVendor(ops, connector = playwrightConnector, resume, deadlineMs, signal) {
|
|
23988
|
+
async function connectVendor(ops, connector = playwrightConnector, resume, deadlineMs, signal, grants = []) {
|
|
22738
23989
|
const transport = new CdpVendorTransport(ops, connector);
|
|
22739
23990
|
if (resume !== void 0 && resume !== null) {
|
|
22740
23991
|
transport.useResumedSession(resume);
|
|
@@ -22747,6 +23998,7 @@ async function connectVendor(ops, connector = playwrightConnector, resume, deadl
|
|
|
22747
23998
|
// here: the three-layer split means the vendor adapter declares what it
|
|
22748
23999
|
// CAN do, policy decides what we WILL use, and this list is the result.
|
|
22749
24000
|
capabilities: ops.decision.enabled.map((c) => c.capability),
|
|
24001
|
+
grants,
|
|
22750
24002
|
honoursCallerUserAgent: false
|
|
22751
24003
|
};
|
|
22752
24004
|
return { declaration, transport };
|
|
@@ -22760,20 +24012,22 @@ var BROWSERBASE_CAPABILITIES = [
|
|
|
22760
24012
|
{ capability: "retry_orchestration", vendorDefaultOn: false, enableKey: "retry_orchestration" },
|
|
22761
24013
|
{ capability: "session_persistence", vendorDefaultOn: false, enableKey: "session_persistence" },
|
|
22762
24014
|
{ capability: "live_view_handoff", vendorDefaultOn: true, enableKey: "live_view_handoff" },
|
|
22763
|
-
{ capability: "captcha_solving", vendorDefaultOn: true, enableKey: "
|
|
22764
|
-
{ capability: "fingerprint_spoofing", vendorDefaultOn: false, enableKey: "
|
|
24015
|
+
{ capability: "captcha_solving", vendorDefaultOn: true, enableKey: "vendor_captcha_solving" },
|
|
24016
|
+
{ capability: "fingerprint_spoofing", vendorDefaultOn: false, enableKey: "vendor_stealth" }
|
|
22765
24017
|
];
|
|
22766
24018
|
function browserbaseSessionBody(decision, projectId, resume) {
|
|
22767
24019
|
const persistEnabled = decision.enabled.some((c) => c.capability === "session_persistence");
|
|
24020
|
+
const solveEnabled = decision.enabled.some((c) => c.capability === "captcha_solving");
|
|
24021
|
+
const stealthEnabled = decision.enabled.some((c) => c.capability === "fingerprint_spoofing");
|
|
22768
24022
|
return {
|
|
22769
24023
|
...projectId === void 0 ? {} : { projectId },
|
|
22770
24024
|
browserSettings: {
|
|
22771
|
-
//
|
|
22772
|
-
//
|
|
22773
|
-
|
|
22774
|
-
|
|
22775
|
-
//
|
|
22776
|
-
advancedStealth: false,
|
|
24025
|
+
// Browserbase defaults solveCaptchas to true, so the off state is an
|
|
24026
|
+
// explicit false, never an omission.
|
|
24027
|
+
solveCaptchas: solveEnabled,
|
|
24028
|
+
// Without the grant the body keeps the opt-out it has always sent;
|
|
24029
|
+
// with it, the current field (`verified` replaced `advancedStealth`).
|
|
24030
|
+
...stealthEnabled ? { verified: true } : { advancedStealth: false },
|
|
22777
24031
|
...persistEnabled && resume?.browserbaseContextId !== void 0 ? { context: { id: resume.browserbaseContextId, persist: true } } : {}
|
|
22778
24032
|
}
|
|
22779
24033
|
};
|
|
@@ -22876,23 +24130,25 @@ var STEEL_CAPABILITIES = [
|
|
|
22876
24130
|
{ capability: "retry_orchestration", vendorDefaultOn: false, enableKey: "retry_orchestration" },
|
|
22877
24131
|
{ capability: "session_persistence", vendorDefaultOn: false, enableKey: "session_persistence" },
|
|
22878
24132
|
{ capability: "live_view_handoff", vendorDefaultOn: true, enableKey: "live_view_handoff" },
|
|
22879
|
-
{ capability: "captcha_solving", vendorDefaultOn: false, enableKey: "
|
|
22880
|
-
{ capability: "fingerprint_spoofing", vendorDefaultOn: true, enableKey: "
|
|
24133
|
+
{ capability: "captcha_solving", vendorDefaultOn: false, enableKey: "vendor_captcha_solving" },
|
|
24134
|
+
{ capability: "fingerprint_spoofing", vendorDefaultOn: true, enableKey: "vendor_stealth" }
|
|
22881
24135
|
];
|
|
22882
24136
|
function steelSessionBody(decision, resume) {
|
|
22883
24137
|
const persistEnabled = decision.enabled.some((c) => c.capability === "session_persistence");
|
|
24138
|
+
const solveEnabled = decision.enabled.some((c) => c.capability === "captcha_solving");
|
|
24139
|
+
const stealthEnabled = decision.enabled.some((c) => c.capability === "fingerprint_spoofing");
|
|
22884
24140
|
return {
|
|
22885
24141
|
// Steel injects a synthetic fingerprint BY DEFAULT. skipFingerprintInjection
|
|
22886
|
-
// is the opt-out
|
|
22887
|
-
//
|
|
22888
|
-
//
|
|
22889
|
-
//
|
|
24142
|
+
// is the opt-out, sent unless the grant names `vendor_stealth` (ADR 0005);
|
|
24143
|
+
// a body without it would buy the capability by omission. Solving follows
|
|
24144
|
+
// `vendor_captcha_solving`. humanizeInteractions stays off under any
|
|
24145
|
+
// grant: ADR 0005 grants no behaviour simulation.
|
|
22890
24146
|
stealthConfig: {
|
|
22891
|
-
skipFingerprintInjection:
|
|
22892
|
-
autoCaptchaSolving:
|
|
24147
|
+
skipFingerprintInjection: !stealthEnabled,
|
|
24148
|
+
autoCaptchaSolving: solveEnabled,
|
|
22893
24149
|
humanizeInteractions: false
|
|
22894
24150
|
},
|
|
22895
|
-
solveCaptcha:
|
|
24151
|
+
solveCaptcha: solveEnabled,
|
|
22896
24152
|
// First use persists the profile so the create response's profileId can
|
|
22897
24153
|
// be saved and resumed later; a later run passes the saved profileId.
|
|
22898
24154
|
...persistEnabled ? { persistProfile: true } : {},
|
|
@@ -22963,8 +24219,11 @@ function buildChannels(mode, opts = {}) {
|
|
|
22963
24219
|
if (preview && mode !== "standard")
|
|
22964
24220
|
throw new Error(`the preview product token is for standard mode, not ${mode}`);
|
|
22965
24221
|
const http = new ResilientHttpSubject(mode, opts.networkPolicy, originScheduler, opts.localPreviewProxyUrl, opts.localPreviewRobotsException === true, fileStore, preview, opts.robotsCache);
|
|
22966
|
-
const plainBrowser = new BrowserLocalSubject(mode, null, opts.headed === true, opts.networkPolicy, null, originScheduler, opts.publicPreferenceState ?? null, opts.browserAllowedHosts, opts.onRenderedHtml, fileStore, preview);
|
|
24222
|
+
const plainBrowser = new BrowserLocalSubject(mode, null, opts.headed === true, opts.networkPolicy, null, originScheduler, opts.publicPreferenceState ?? null, opts.browserAllowedHosts, opts.onRenderedHtml, fileStore, preview, void 0, opts.browserEngine ?? "playwright");
|
|
22967
24223
|
const declared3 = preview ? identityBundleFrom(previewIdentity(modeIdentity(mode))) : identityForRoute(mode);
|
|
24224
|
+
if (opts.compatTransport === true && (preview || opts.localPreviewProxyUrl !== void 0))
|
|
24225
|
+
throw new Error("the compatible transport is for a local server, not the hosted preview");
|
|
24226
|
+
const compat = opts.compatTransport === true && mode === "standard" ? new ResilientHttpSubject(mode, opts.networkPolicy, originScheduler, void 0, false, fileStore, false, opts.robotsCache, new CompatTransport(opts.networkPolicy ?? defaultNetworkPolicy())) : null;
|
|
22968
24227
|
const authedSubjects = /* @__PURE__ */ new Map();
|
|
22969
24228
|
const authedSubjectFor = (session) => {
|
|
22970
24229
|
const key = `${session.domain}\0${sessionFingerprint(session)}`;
|
|
@@ -22991,18 +24250,24 @@ function buildChannels(mode, opts = {}) {
|
|
|
22991
24250
|
{
|
|
22992
24251
|
id: "http",
|
|
22993
24252
|
identity: declared3,
|
|
22994
|
-
fetch: (url, _session, execution, options) => opts.localSubjects?.http !== void 0 ? opts.localSubjects.http.fetch(url, execution?.deadlineAt, execution?.signal, execution, options) : http.fetch(url, execution?.deadlineAt, execution?.signal, {}, execution?.onRetryAfter, options, execution?.onRobotsOverride),
|
|
24253
|
+
fetch: (url, _session, execution, options) => opts.localSubjects?.http !== void 0 ? opts.localSubjects.http.fetch(url, execution?.deadlineAt, execution?.signal, execution, options) : http.fetch(url, execution?.deadlineAt, execution?.signal, {}, execution?.onRetryAfter, options, execution?.onRobotsOverride, execution?.cookieSession),
|
|
22995
24254
|
close: async () => {
|
|
22996
24255
|
await http.teardown();
|
|
22997
24256
|
await opts.localSubjects?.http?.teardown?.();
|
|
22998
24257
|
}
|
|
22999
24258
|
},
|
|
24259
|
+
...compat === null ? [] : [{
|
|
24260
|
+
id: "http_compat",
|
|
24261
|
+
identity: identityBundleFrom(compatIdentity()),
|
|
24262
|
+
fetch: (url, _session, execution, options) => compat.fetch(url, execution?.deadlineAt, execution?.signal, {}, execution?.onRetryAfter, options, execution?.onRobotsOverride, execution?.cookieSession),
|
|
24263
|
+
close: () => compat.teardown()
|
|
24264
|
+
}],
|
|
23000
24265
|
{
|
|
23001
24266
|
id: "browser_local",
|
|
23002
24267
|
identity: declared3,
|
|
23003
24268
|
waitsFor: true,
|
|
23004
24269
|
// No session here, ever: the plain rung is the public browser.
|
|
23005
|
-
fetch: (url, _session, execution, options) => opts.localSubjects?.browser_local !== void 0 ? opts.localSubjects.browser_local.fetch(url, execution?.deadlineAt, execution?.signal, execution, options) : plainBrowser.fetch(url, execution?.deadlineAt, execution?.signal, execution?.onRetryAfter, options, execution?.onRobotsOverride),
|
|
24270
|
+
fetch: (url, _session, execution, options) => opts.localSubjects?.browser_local !== void 0 ? opts.localSubjects.browser_local.fetch(url, execution?.deadlineAt, execution?.signal, execution, options) : plainBrowser.fetch(url, execution?.deadlineAt, execution?.signal, execution?.onRetryAfter, options, execution?.onRobotsOverride, execution?.cookieSession),
|
|
23006
24271
|
close: async () => {
|
|
23007
24272
|
await plainBrowser.teardown();
|
|
23008
24273
|
await opts.localSubjects?.browser_local?.teardown?.();
|
|
@@ -23056,7 +24321,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
23056
24321
|
attemptCount: 0,
|
|
23057
24322
|
contentTokens: null,
|
|
23058
24323
|
browserMs: 0,
|
|
23059
|
-
externalCostUsd:
|
|
24324
|
+
externalCostUsd: 0
|
|
23060
24325
|
},
|
|
23061
24326
|
trace
|
|
23062
24327
|
};
|
|
@@ -23072,6 +24337,8 @@ function buildChannels(mode, opts = {}) {
|
|
|
23072
24337
|
}
|
|
23073
24338
|
if (mode === "standard")
|
|
23074
24339
|
return channels;
|
|
24340
|
+
if (!(opts.vendorPolicy?.authorized ?? []).includes("vendor_remote_browser"))
|
|
24341
|
+
return channels;
|
|
23075
24342
|
const named = new Set((process.env.W2L_VENDORS ?? "").split(",").map((name) => name.trim().toLowerCase()).filter((name) => name !== ""));
|
|
23076
24343
|
const bbKey = opts.keys?.browserbase ?? (named.has("browserbase") ? process.env.BROWSERBASE_API_KEY : void 0) ?? "";
|
|
23077
24344
|
const steelKey = opts.keys?.steel ?? (named.has("steel") ? process.env.STEEL_API_KEY : void 0) ?? "";
|
|
@@ -23099,7 +24366,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
23099
24366
|
return connected;
|
|
23100
24367
|
if (pending === null) {
|
|
23101
24368
|
opts.onVendorConnect?.(vendorId);
|
|
23102
|
-
pending = connectVendor(ops, opts.vendorConnector, pendingResume ?? null, execution?.deadlineAt, execution?.signal).then((c) => {
|
|
24369
|
+
pending = connectVendor(ops, opts.vendorConnector, pendingResume ?? null, execution?.deadlineAt, execution?.signal, opts.vendorPolicy?.authorized ?? []).then((c) => {
|
|
23103
24370
|
connected = c;
|
|
23104
24371
|
return c;
|
|
23105
24372
|
}).catch((error) => {
|
|
@@ -23148,7 +24415,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
23148
24415
|
attemptCount: 0,
|
|
23149
24416
|
contentTokens: null,
|
|
23150
24417
|
browserMs: 0,
|
|
23151
|
-
externalCostUsd:
|
|
24418
|
+
externalCostUsd: 0
|
|
23152
24419
|
},
|
|
23153
24420
|
trace: [
|
|
23154
24421
|
{
|
|
@@ -23250,7 +24517,7 @@ function pageFromUserBrowser(read2, prior, options) {
|
|
|
23250
24517
|
attemptCount: 0,
|
|
23251
24518
|
contentTokens: null,
|
|
23252
24519
|
browserMs: wallMs,
|
|
23253
|
-
externalCostUsd:
|
|
24520
|
+
externalCostUsd: 0
|
|
23254
24521
|
},
|
|
23255
24522
|
trace
|
|
23256
24523
|
};
|
|
@@ -23535,6 +24802,10 @@ function validateCdpEndpoint(value) {
|
|
|
23535
24802
|
throw new Error("invalid CDP discovery endpoint");
|
|
23536
24803
|
return u.href;
|
|
23537
24804
|
}
|
|
24805
|
+
function publicSession(session) {
|
|
24806
|
+
const { profileDir: _dir, cdpEndpoint: _endpoint, ...publicFields } = session;
|
|
24807
|
+
return publicFields;
|
|
24808
|
+
}
|
|
23538
24809
|
|
|
23539
24810
|
// packages/bench/dist/recipeExecutor.js
|
|
23540
24811
|
import { createHash as createHash10 } from "crypto";
|
|
@@ -23543,7 +24814,7 @@ import { createHash as createHash10 } from "crypto";
|
|
|
23543
24814
|
import { createHash as createHash9 } from "crypto";
|
|
23544
24815
|
import { mkdir as mkdir6, rmdir as rmdir2 } from "fs/promises";
|
|
23545
24816
|
import { join as join8, resolve as resolve3 } from "path";
|
|
23546
|
-
import { chromium as
|
|
24817
|
+
import { chromium as chromium3 } from "playwright";
|
|
23547
24818
|
init_egress();
|
|
23548
24819
|
|
|
23549
24820
|
// packages/bench/dist/recipeExecutor.js
|
|
@@ -23566,7 +24837,7 @@ import { mkdir as mkdir7, writeFile as writeFile6 } from "fs/promises";
|
|
|
23566
24837
|
init_dist();
|
|
23567
24838
|
|
|
23568
24839
|
// packages/sdk/dist/version.js
|
|
23569
|
-
var SDK_VERSION = "0.3.
|
|
24840
|
+
var SDK_VERSION = "0.3.1";
|
|
23570
24841
|
|
|
23571
24842
|
// packages/sdk/dist/watcher.js
|
|
23572
24843
|
init_dist();
|
|
@@ -23575,9 +24846,170 @@ init_dist();
|
|
|
23575
24846
|
var SDK_ORIGIN = `js-sdk@${SDK_VERSION}`;
|
|
23576
24847
|
|
|
23577
24848
|
// packages/bench/dist/benchmarkGate.js
|
|
23578
|
-
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
24849
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
23579
24850
|
import { execFileSync } from "child_process";
|
|
23580
24851
|
|
|
24852
|
+
// packages/bench/dist/cookieSession.js
|
|
24853
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
24854
|
+
import { readFile as readFile4, rename as rename5, rm as rm2, writeFile as writeFile7 } from "fs/promises";
|
|
24855
|
+
import { CookieJar } from "tough-cookie";
|
|
24856
|
+
var SAME_SITE = { strict: "Strict", lax: "Lax", none: "None" };
|
|
24857
|
+
var TaskCookieSession = class {
|
|
24858
|
+
file;
|
|
24859
|
+
sessionId = randomUUID3();
|
|
24860
|
+
// Lenient about what a site sends: a cookie it would not keep is skipped, never an error.
|
|
24861
|
+
jar = new CookieJar(void 0, { looseMode: true });
|
|
24862
|
+
/** Done once the file, if any, has been read; every method waits for it. */
|
|
24863
|
+
ready;
|
|
24864
|
+
writing = Promise.resolve();
|
|
24865
|
+
/** Set when the task ended: a page still finishing afterwards changes the session in memory alone. */
|
|
24866
|
+
closed = false;
|
|
24867
|
+
/** `file`: where the session lives between runs of its task; without one it lives in memory alone. */
|
|
24868
|
+
constructor(file) {
|
|
24869
|
+
this.file = file;
|
|
24870
|
+
this.ready = file === void 0 ? Promise.resolve() : this.load(file);
|
|
24871
|
+
}
|
|
24872
|
+
/** An opaque id for the record, unrelated to any cookie value; a session read from its file keeps the id it had. */
|
|
24873
|
+
get id() {
|
|
24874
|
+
return this.sessionId;
|
|
24875
|
+
}
|
|
24876
|
+
/** Deletes a task's session file: the task has ended, and its cookies go with it. */
|
|
24877
|
+
static async remove(file) {
|
|
24878
|
+
await rm2(file, { force: true });
|
|
24879
|
+
}
|
|
24880
|
+
/**
|
|
24881
|
+
* The task has ended: deletes the file after the writes already queued, and writes nothing again,
|
|
24882
|
+
* so a page that finishes after the task (one a cancel or a deadline stopped waiting for) cannot put it back.
|
|
24883
|
+
*/
|
|
24884
|
+
async close() {
|
|
24885
|
+
this.closed = true;
|
|
24886
|
+
const file = this.file;
|
|
24887
|
+
if (file === void 0)
|
|
24888
|
+
return;
|
|
24889
|
+
this.writing = this.writing.then(() => rm2(file, { force: true })).catch(() => {
|
|
24890
|
+
});
|
|
24891
|
+
await this.writing;
|
|
24892
|
+
}
|
|
24893
|
+
async load(file) {
|
|
24894
|
+
let text2;
|
|
24895
|
+
try {
|
|
24896
|
+
text2 = await readFile4(file, "utf8");
|
|
24897
|
+
} catch {
|
|
24898
|
+
return;
|
|
24899
|
+
}
|
|
24900
|
+
try {
|
|
24901
|
+
const saved = JSON.parse(text2);
|
|
24902
|
+
if (saved.version !== 1 || typeof saved.id !== "string" || saved.jar === void 0)
|
|
24903
|
+
return;
|
|
24904
|
+
this.jar = await CookieJar.deserialize({ ...saved.jar, looseMode: true });
|
|
24905
|
+
this.sessionId = saved.id;
|
|
24906
|
+
} catch {
|
|
24907
|
+
}
|
|
24908
|
+
}
|
|
24909
|
+
/** Writes the session to its file after a change, one write at a time. */
|
|
24910
|
+
persist() {
|
|
24911
|
+
const file = this.file;
|
|
24912
|
+
if (file === void 0)
|
|
24913
|
+
return Promise.resolve();
|
|
24914
|
+
this.writing = this.writing.then(async () => {
|
|
24915
|
+
if (this.closed)
|
|
24916
|
+
return;
|
|
24917
|
+
const temporary = `${file}.${randomUUID3()}.tmp`;
|
|
24918
|
+
try {
|
|
24919
|
+
await writeFile7(temporary, JSON.stringify({ version: 1, id: this.sessionId, jar: await this.jar.serialize() }), { mode: 384 });
|
|
24920
|
+
await rename5(temporary, file);
|
|
24921
|
+
} catch {
|
|
24922
|
+
await rm2(temporary, { force: true }).catch(() => {
|
|
24923
|
+
});
|
|
24924
|
+
}
|
|
24925
|
+
}).catch(() => {
|
|
24926
|
+
});
|
|
24927
|
+
return this.writing;
|
|
24928
|
+
}
|
|
24929
|
+
async cookieHeader(url) {
|
|
24930
|
+
await this.ready;
|
|
24931
|
+
return this.jar.getCookieString(url).catch(() => "");
|
|
24932
|
+
}
|
|
24933
|
+
async store(url, setCookies) {
|
|
24934
|
+
await this.ready;
|
|
24935
|
+
let kept = 0;
|
|
24936
|
+
for (const line of setCookies) {
|
|
24937
|
+
const cookie = await this.jar.setCookie(line, url, { ignoreError: true }).catch(() => void 0);
|
|
24938
|
+
if (cookie !== void 0 && cookie.TTL() > 0)
|
|
24939
|
+
kept++;
|
|
24940
|
+
}
|
|
24941
|
+
if (setCookies.length > 0)
|
|
24942
|
+
await this.persist();
|
|
24943
|
+
return kept;
|
|
24944
|
+
}
|
|
24945
|
+
async browserCookies(url) {
|
|
24946
|
+
await this.ready;
|
|
24947
|
+
const cookies = await this.jar.getCookies(url).catch(() => []);
|
|
24948
|
+
return cookies.map((cookie) => ({
|
|
24949
|
+
name: cookie.key,
|
|
24950
|
+
value: cookie.value,
|
|
24951
|
+
// A host-only cookie names its host alone; a domain cookie, with the leading dot, its subdomains too.
|
|
24952
|
+
domain: cookie.hostOnly === true ? cookie.domain ?? "" : `.${cookie.domain ?? ""}`,
|
|
24953
|
+
path: cookie.path ?? "/",
|
|
24954
|
+
expires: cookie.expires instanceof Date ? Math.floor(cookie.expires.getTime() / 1e3) : -1,
|
|
24955
|
+
httpOnly: cookie.httpOnly,
|
|
24956
|
+
secure: cookie.secure,
|
|
24957
|
+
sameSite: SAME_SITE[(cookie.sameSite ?? "").toLowerCase()] ?? "Lax"
|
|
24958
|
+
}));
|
|
24959
|
+
}
|
|
24960
|
+
async storeBrowserChanges(startedWith, held) {
|
|
24961
|
+
const key = (c) => `${c.name}\0${c.domain}\0${c.path}`;
|
|
24962
|
+
const before = new Map(startedWith.map((c) => [key(c), c]));
|
|
24963
|
+
const after = new Map(held.map((c) => [key(c), c]));
|
|
24964
|
+
await this.ready;
|
|
24965
|
+
let kept = 0;
|
|
24966
|
+
let removed = 0;
|
|
24967
|
+
for (const c of held) {
|
|
24968
|
+
const was = before.get(key(c));
|
|
24969
|
+
if (was !== void 0 && was.value === c.value) {
|
|
24970
|
+
if (was.expires === c.expires || await this.currentValue(c) !== was.value)
|
|
24971
|
+
continue;
|
|
24972
|
+
}
|
|
24973
|
+
if (await this.setLine(c, false))
|
|
24974
|
+
kept++;
|
|
24975
|
+
}
|
|
24976
|
+
for (const c of startedWith) {
|
|
24977
|
+
if (after.has(key(c)))
|
|
24978
|
+
continue;
|
|
24979
|
+
if (await this.currentValue(c) !== c.value)
|
|
24980
|
+
continue;
|
|
24981
|
+
await this.setLine(c, true);
|
|
24982
|
+
removed++;
|
|
24983
|
+
}
|
|
24984
|
+
if (kept + removed > 0)
|
|
24985
|
+
await this.persist();
|
|
24986
|
+
return { kept, removed };
|
|
24987
|
+
}
|
|
24988
|
+
/** The session's value now for the cookie with this name, domain and path; undefined when it holds none. */
|
|
24989
|
+
async currentValue(c) {
|
|
24990
|
+
const same = (now) => now.name === c.name && now.domain === c.domain && now.path === c.path;
|
|
24991
|
+
return (await this.browserCookies(`${c.secure ? "https" : "http"}://${c.domain.replace(/^\./, "")}${c.path}`)).find(same)?.value;
|
|
24992
|
+
}
|
|
24993
|
+
/** The cookie as the site would have set it (or deleted it): a host-only one without Domain, which an IP address cannot take. */
|
|
24994
|
+
async setLine(c, remove) {
|
|
24995
|
+
const host = c.domain.replace(/^\./, "");
|
|
24996
|
+
if (host === "")
|
|
24997
|
+
return false;
|
|
24998
|
+
const path = c.path === "" ? "/" : c.path;
|
|
24999
|
+
const line = [
|
|
25000
|
+
`${c.name}=${remove ? "" : c.value}`,
|
|
25001
|
+
`Path=${path}`,
|
|
25002
|
+
...c.domain.startsWith(".") ? [`Domain=${host}`] : [],
|
|
25003
|
+
...remove ? ["Expires=Thu, 01 Jan 1970 00:00:00 GMT"] : c.expires > 0 ? [`Expires=${new Date(c.expires * 1e3).toUTCString()}`] : [],
|
|
25004
|
+
...c.httpOnly ? ["HttpOnly"] : [],
|
|
25005
|
+
...c.secure ? ["Secure"] : [],
|
|
25006
|
+
`SameSite=${c.sameSite}`
|
|
25007
|
+
].join("; ");
|
|
25008
|
+
const stored = await this.jar.setCookie(line, `${c.secure ? "https" : "http"}://${host}${path}`, { ignoreError: true }).catch(() => void 0);
|
|
25009
|
+
return stored !== void 0 && stored.TTL() > 0;
|
|
25010
|
+
}
|
|
25011
|
+
};
|
|
25012
|
+
|
|
23581
25013
|
// packages/bench/dist/scrapeAtom.js
|
|
23582
25014
|
var LadderScrapeAtom = class {
|
|
23583
25015
|
runner;
|
|
@@ -23611,6 +25043,7 @@ init_egress();
|
|
|
23611
25043
|
init_robotsLookup();
|
|
23612
25044
|
|
|
23613
25045
|
// packages/bench/dist/sitemapSource.js
|
|
25046
|
+
init_dist();
|
|
23614
25047
|
init_dist2();
|
|
23615
25048
|
import { createHash as createHash12 } from "crypto";
|
|
23616
25049
|
import { gunzipSync } from "zlib";
|
|
@@ -23628,6 +25061,7 @@ var HttpSitemapSource = class {
|
|
|
23628
25061
|
route;
|
|
23629
25062
|
robots;
|
|
23630
25063
|
ownRobots;
|
|
25064
|
+
ignoreRobotsTxt;
|
|
23631
25065
|
/** The Crawl-delay of each origin whose robots.txt a file of this source was judged by (the strictest one seen). */
|
|
23632
25066
|
crawlDelays = /* @__PURE__ */ new Map();
|
|
23633
25067
|
/** When this source's last request to each origin left (monotonic). */
|
|
@@ -23641,6 +25075,7 @@ var HttpSitemapSource = class {
|
|
|
23641
25075
|
this.routes = new EgressRoutes(this.policy);
|
|
23642
25076
|
this.route = new EgressRoute(this.policy, this.routes);
|
|
23643
25077
|
this.ownRobots = options.robots === void 0;
|
|
25078
|
+
this.ignoreRobotsTxt = options.ignoreRobotsTxt === true;
|
|
23644
25079
|
this.robots = options.robots ?? new RobotsOriginCache(this.policy, (url) => this.route.dispatcherFor(url));
|
|
23645
25080
|
}
|
|
23646
25081
|
async load(request7, context = {}) {
|
|
@@ -23752,7 +25187,7 @@ var HttpSitemapSource = class {
|
|
|
23752
25187
|
this.crawlDelays.set(origin, Math.max(this.crawlDelays.get(origin) ?? 0, decision.crawlDelayMs));
|
|
23753
25188
|
}
|
|
23754
25189
|
record3.robots = decision.decision === "allowed" ? "allowed" : decision.decision === "no_robots" ? "no_robots" : "disallowed";
|
|
23755
|
-
if (decision.decision === "disallowed") {
|
|
25190
|
+
if (decision.decision === "disallowed" && (!this.ignoreRobotsTxt || isOctocrawlRobotsGroup(decision.matchedUserAgentGroup))) {
|
|
23756
25191
|
return { record: { ...record3, kind: "refused", error: decision.unreachable === void 0 ? null : `robots_unreachable_${decision.unreachable}` }, locs: null };
|
|
23757
25192
|
}
|
|
23758
25193
|
const response = await this.fetch(url, identity.headers, scope);
|
|
@@ -23907,6 +25342,95 @@ init_dist3();
|
|
|
23907
25342
|
init_dist();
|
|
23908
25343
|
init_dist2();
|
|
23909
25344
|
|
|
25345
|
+
// packages/api/dist/egressPool.js
|
|
25346
|
+
init_dist();
|
|
25347
|
+
import { connect as netConnect } from "net";
|
|
25348
|
+
import { connect as tlsConnect } from "tls";
|
|
25349
|
+
var EGRESS_COOLDOWN_MS = 10 * 6e4;
|
|
25350
|
+
var MAX_EGRESS_SWITCHES = 2;
|
|
25351
|
+
var EgressPool = class {
|
|
25352
|
+
now;
|
|
25353
|
+
egresses;
|
|
25354
|
+
cooldownUntil = /* @__PURE__ */ new Map();
|
|
25355
|
+
next = 0;
|
|
25356
|
+
constructor(servers, policy, now = Date.now) {
|
|
25357
|
+
this.now = now;
|
|
25358
|
+
if (servers.length === 0)
|
|
25359
|
+
throw new Error("an egress pool needs at least one proxy");
|
|
25360
|
+
this.egresses = servers.map((server) => ({ id: server.endpoint, policy: withPoolProxy(policy, server), server }));
|
|
25361
|
+
}
|
|
25362
|
+
get size() {
|
|
25363
|
+
return this.egresses.length;
|
|
25364
|
+
}
|
|
25365
|
+
/** The egress with this id, when it is in the pool. */
|
|
25366
|
+
byId(id) {
|
|
25367
|
+
return this.egresses.find((egress) => egress.id === id);
|
|
25368
|
+
}
|
|
25369
|
+
/** Whether this egress is cooling down after a failure. */
|
|
25370
|
+
cooling(id) {
|
|
25371
|
+
return (this.cooldownUntil.get(id) ?? 0) > this.now();
|
|
25372
|
+
}
|
|
25373
|
+
/**
|
|
25374
|
+
* The next egress in turn that is not cooling down and is not `except`; when every other one is
|
|
25375
|
+
* cooling, the one whose cooldown ends first, so a task always has an egress.
|
|
25376
|
+
*/
|
|
25377
|
+
pick(except) {
|
|
25378
|
+
const candidates2 = this.egresses.filter((egress) => egress.id !== except);
|
|
25379
|
+
const pool = candidates2.length === 0 ? this.egresses : candidates2;
|
|
25380
|
+
for (let i = 0; i < this.egresses.length; i++) {
|
|
25381
|
+
const egress = this.egresses[(this.next + i) % this.egresses.length];
|
|
25382
|
+
if (!pool.includes(egress) || this.cooling(egress.id))
|
|
25383
|
+
continue;
|
|
25384
|
+
this.next = (this.next + i + 1) % this.egresses.length;
|
|
25385
|
+
return egress;
|
|
25386
|
+
}
|
|
25387
|
+
return [...pool].sort((a, b) => (this.cooldownUntil.get(a.id) ?? 0) - (this.cooldownUntil.get(b.id) ?? 0))[0];
|
|
25388
|
+
}
|
|
25389
|
+
/** The egress failed its probe: it cools down. */
|
|
25390
|
+
fail(id) {
|
|
25391
|
+
this.cooldownUntil.set(id, this.now() + EGRESS_COOLDOWN_MS);
|
|
25392
|
+
}
|
|
25393
|
+
};
|
|
25394
|
+
function egressInDoubt(outcome) {
|
|
25395
|
+
const results = [outcome.result, ...(outcome.audit?.summary.attempts ?? []).map((attempt) => attempt.result)];
|
|
25396
|
+
return outcome.result.status === "failed" && results.every((result) => (result.evidence.httpStatus ?? null) === null);
|
|
25397
|
+
}
|
|
25398
|
+
async function probeEgress(server, timeoutMs = 5e3) {
|
|
25399
|
+
const url = new URL(server.url);
|
|
25400
|
+
const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
25401
|
+
return new Promise((resolve4) => {
|
|
25402
|
+
let settled2 = false;
|
|
25403
|
+
let socket;
|
|
25404
|
+
const done = (verdict) => {
|
|
25405
|
+
if (settled2)
|
|
25406
|
+
return;
|
|
25407
|
+
settled2 = true;
|
|
25408
|
+
clearTimeout(timer);
|
|
25409
|
+
socket?.destroy();
|
|
25410
|
+
resolve4(verdict);
|
|
25411
|
+
};
|
|
25412
|
+
const timer = setTimeout(() => done("unreachable"), timeoutMs);
|
|
25413
|
+
const auth = server.username === void 0 ? "" : `Proxy-Authorization: Basic ${Buffer.from(`${server.username}:${server.password ?? ""}`).toString("base64")}\r
|
|
25414
|
+
`;
|
|
25415
|
+
const onConnect = () => socket.write(`CONNECT octocrawl-egress-probe.invalid:443 HTTP/1.1\r
|
|
25416
|
+
Host: octocrawl-egress-probe.invalid:443\r
|
|
25417
|
+
${auth}\r
|
|
25418
|
+
`);
|
|
25419
|
+
socket = url.protocol === "https:" ? tlsConnect({ host: url.hostname, port, servername: url.hostname }, onConnect) : netConnect({ host: url.hostname, port }, onConnect);
|
|
25420
|
+
let head = "";
|
|
25421
|
+
socket.on("data", (chunk) => {
|
|
25422
|
+
head += chunk.toString("latin1");
|
|
25423
|
+
const status = /^HTTP\/\d(?:\.\d)? (\d{3})/.exec(head);
|
|
25424
|
+
if (status !== null)
|
|
25425
|
+
done(status[1] === "407" ? "credentials_refused" : "works");
|
|
25426
|
+
else if (head.length > 4096)
|
|
25427
|
+
done("works");
|
|
25428
|
+
});
|
|
25429
|
+
socket.on("error", () => done("unreachable"));
|
|
25430
|
+
socket.on("close", () => done(head === "" ? "unreachable" : "works"));
|
|
25431
|
+
});
|
|
25432
|
+
}
|
|
25433
|
+
|
|
23910
25434
|
// packages/api/dist/hints.js
|
|
23911
25435
|
init_dist();
|
|
23912
25436
|
var FAST_MODE_DECLINED_HINT = "the http lane asked for the browser lane; fastMode declined it; retry without fastMode";
|
|
@@ -23945,13 +25469,13 @@ function egressHint(result, host) {
|
|
|
23945
25469
|
if (denied2 === void 0)
|
|
23946
25470
|
return null;
|
|
23947
25471
|
const why = denied2.detail?.error ?? denied2.detail?.reason;
|
|
23948
|
-
return `the egress policy refused ${host}${typeof why === "string" ? ` (${why})` : ""} and nothing was fetched;
|
|
25472
|
+
return `the egress policy refused ${host}${typeof why === "string" ? ` (${why})` : ""} and nothing was fetched; Octocrawl reaches public addresses, and a local server the addresses its policy allowlists`;
|
|
23949
25473
|
}
|
|
23950
25474
|
function laneEscalatedHint(run, host) {
|
|
23951
25475
|
const { result } = run;
|
|
23952
|
-
if (result.status !== "success" && result.status !== "partial" || !BROWSER_LANES.has(result.lane) || !run.channelsTried.
|
|
25476
|
+
if (result.status !== "success" && result.status !== "partial" || !BROWSER_LANES.has(result.lane) || !run.channelsTried.some((channel) => HTTP_CHANNELS.has(channel)))
|
|
23953
25477
|
return null;
|
|
23954
|
-
const http = run.summary?.attempts.find((attempt) => attempt.channel
|
|
25478
|
+
const http = run.summary?.attempts.find((attempt) => HTTP_CHANNELS.has(attempt.channel))?.result;
|
|
23955
25479
|
if (http === void 0)
|
|
23956
25480
|
return null;
|
|
23957
25481
|
const reason2 = http.status === "blocked" ? http.blockReason : http.status === "failed" && http.failureReason === "http_error" ? "http_error" : null;
|
|
@@ -23979,11 +25503,16 @@ function robotsHint(result, host) {
|
|
|
23979
25503
|
return null;
|
|
23980
25504
|
const detail = disallowed.detail ?? {};
|
|
23981
25505
|
if (typeof detail.unreachable === "string") {
|
|
23982
|
-
return `robots.txt of ${host} could not be read (${detail.unreachable}), which counts as a complete disallow;
|
|
25506
|
+
return `robots.txt of ${host} could not be read (${detail.unreachable}), which counts as a complete disallow; Octocrawl asks for it again after five minutes. ${ROBOTS_ROUTE}`;
|
|
23983
25507
|
}
|
|
23984
25508
|
const rules = (Array.isArray(detail.appliedRules) ? detail.appliedRules : []).filter((rule) => rule !== null && typeof rule === "object" && typeof rule.pattern === "string").filter((rule) => rule.allow === false).map((rule) => rule.pattern);
|
|
23985
|
-
|
|
25509
|
+
const group = result.trace.find((event) => event.event === "robots_checked")?.detail?.matchedGroup;
|
|
25510
|
+
if (typeof group === "string" && isOctocrawlRobotsGroup(group)) {
|
|
25511
|
+
return `robots.txt of ${host} disallows this URL for Octocrawl by name (User-agent: ${group}; rule ${rules.length === 0 ? "unknown" : rules.join(", ")}): the site owner's opt-out, which a named URL and ignoreRobotsTxt do not set aside; only a robotsOverride with your recorded reason does, on a local server`;
|
|
25512
|
+
}
|
|
25513
|
+
return `robots.txt of ${host} disallows this URL for Octocrawl's identity (rule ${rules.length === 0 ? "unknown" : rules.join(", ")}). ${ROBOTS_ROUTE}`;
|
|
23986
25514
|
}
|
|
25515
|
+
var ROBOTS_ROUTE = "A local Octocrawl server fetches a URL a scrape or batch names whatever robots.txt says, and a crawl or map started there with ignoreRobotsTxt fetches the links it disallows, each on the record; a hosted server obeys robots.txt for every URL";
|
|
23987
25516
|
function agentHintsFor(req, run) {
|
|
23988
25517
|
const { result } = run;
|
|
23989
25518
|
const hints = [];
|
|
@@ -23998,16 +25527,16 @@ function agentHintsFor(req, run) {
|
|
|
23998
25527
|
}
|
|
23999
25528
|
if (result.status === "blocked" && result.blockReason === "login_wall") {
|
|
24000
25529
|
const rejected = run.ladderTrace?.find((event) => event.event === "ladder_session_rejected");
|
|
24001
|
-
hints.push(rejected === void 0 ? "the page asks for a login;
|
|
25530
|
+
hints.push(rejected === void 0 ? "the page asks for a login; Octocrawl does not create accounts; use mode authed with your own session" : `${host} refused your saved login for ${String(rejected.detail?.domain ?? host)} (expired or signed out); sign in to it again in Chrome and run octocrawl login import ${String(rejected.detail?.domain ?? host)}`);
|
|
24002
25531
|
}
|
|
24003
25532
|
if (result.status === "blocked" && result.blockReason !== null && GATES.has(result.blockReason)) {
|
|
24004
|
-
hints.push(`${host} gates automated access on the lanes tried (${run.channelsTried.join(", ")});
|
|
25533
|
+
hints.push(`${host} gates automated access on the lanes tried (${run.channelsTried.join(", ")}); Octocrawl does not solve challenges or change its identity unless this server's access grant names a provider that does (--access-grant, ADR 0005); a proxy or session you own is the supported route, or, on your own machine, getting through the check yourself in your own Chrome: handoff: true on a scrape (octocrawl scrape --handoff), or a batch handoff (octocrawl batch --handoff, POST /v1/batches/:id/handoff)`);
|
|
24005
25534
|
}
|
|
24006
25535
|
const escalated = laneEscalatedHint(run, host);
|
|
24007
25536
|
if (escalated !== null)
|
|
24008
25537
|
hints.push(escalated);
|
|
24009
25538
|
if (result.status === "failed" && result.failureReason === "tls_error") {
|
|
24010
|
-
hints.push(`the certificate of ${host} did not verify and
|
|
25539
|
+
hints.push(`the certificate of ${host} did not verify and Octocrawl keeps verification on; a local server takes skipTlsVerification for one request, recorded in the trace and a tls_unverified warning, and a hosted server refuses it`);
|
|
24011
25540
|
}
|
|
24012
25541
|
if (result.status === "failed" && result.failureReason === "timeout") {
|
|
24013
25542
|
hints.push(`no lane answered within the request's deadline; raise timeout (up to ${DEFAULT_SCRAPE_TIMEOUT_MS} ms)`);
|
|
@@ -24024,7 +25553,7 @@ function agentHintsFor(req, run) {
|
|
|
24024
25553
|
hints.push(`the content was cut at character ${result.truncatedAt ?? "unknown"}; ask for rawHtml or a narrower includeTags`);
|
|
24025
25554
|
}
|
|
24026
25555
|
const fastModeDeclined = req.fastMode === true && result.lane === "http" && httpLaneAskedForBrowser(result);
|
|
24027
|
-
const browserTried = run.channelsTried.some((channel) => channel
|
|
25556
|
+
const browserTried = run.channelsTried.some((channel) => !HTTP_CHANNELS.has(channel));
|
|
24028
25557
|
if (!fastModeDeclined && result.warnings?.some((warning) => warning.code === "client_rendered_suspected")) {
|
|
24029
25558
|
hints.push(`the page fills its data with JavaScript; the browser lane ${browserTried ? "was tried" : "was not tried"}`);
|
|
24030
25559
|
}
|
|
@@ -24037,9 +25566,9 @@ function agentHintsFor(req, run) {
|
|
|
24037
25566
|
const shellHinted = result.warnings?.some((warning) => warning.code === "client_rendered_suspected" || warning.code === "low_content_yield") === true;
|
|
24038
25567
|
if (result.status === "failed" && result.failureReason === "empty_unverified" && !shellHinted && !fastModeDeclined) {
|
|
24039
25568
|
if (result.file?.kind === "pdf")
|
|
24040
|
-
hints.push("the PDF has no text layer, and
|
|
25569
|
+
hints.push("the PDF has no text layer, and Octocrawl runs no OCR");
|
|
24041
25570
|
else if (result.file === void 0)
|
|
24042
|
-
hints.push("
|
|
25571
|
+
hints.push("Octocrawl found no main content on the page; onlyMainContent: false returns the whole page's Markdown as content, and includeTags names the elements to read instead");
|
|
24043
25572
|
}
|
|
24044
25573
|
if (result.status === "failed" && result.failureReason === "http_error") {
|
|
24045
25574
|
const status = result.evidence.httpStatus;
|
|
@@ -24069,7 +25598,7 @@ init_dist3();
|
|
|
24069
25598
|
init_dist2();
|
|
24070
25599
|
|
|
24071
25600
|
// packages/api/dist/chromeLogin.js
|
|
24072
|
-
import { readFile as
|
|
25601
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
24073
25602
|
import { homedir, userInfo } from "os";
|
|
24074
25603
|
import { join as join9 } from "path";
|
|
24075
25604
|
init_dist2();
|
|
@@ -24087,7 +25616,7 @@ function chromeUserDataDir(platform = process.platform, env = process.env, home
|
|
|
24087
25616
|
async function chromeEndpoint(userDataDir) {
|
|
24088
25617
|
let text2;
|
|
24089
25618
|
try {
|
|
24090
|
-
text2 = await
|
|
25619
|
+
text2 = await readFile5(join9(userDataDir, "DevToolsActivePort"), "utf8");
|
|
24091
25620
|
} catch {
|
|
24092
25621
|
throw new ChromeLoginError(`Chrome is not accepting a connection (no DevToolsActivePort in ${userDataDir}): ${ENABLE_HINT}`);
|
|
24093
25622
|
}
|
|
@@ -24398,7 +25927,7 @@ async function readPage(connection, browser, url, options) {
|
|
|
24398
25927
|
const host = new URL(url).hostname;
|
|
24399
25928
|
const started = Date.now();
|
|
24400
25929
|
let sawGate = null;
|
|
24401
|
-
const gone = (error) => error instanceof ChromeLoginError && !/navigated or closed/i.test(error.message) && /Session with given id not found|No session with given id|No target with given id|closed the connection|Target closed|target not found|did not answer Target\.getTargetInfo/i.test(error.message) ? new HandoffNotThrough(`the tab for ${url} was closed, or Chrome quit, before
|
|
25930
|
+
const gone = (error) => error instanceof ChromeLoginError && !/navigated or closed/i.test(error.message) && /Session with given id not found|No session with given id|No target with given id|closed the connection|Target closed|target not found|did not answer Target\.getTargetInfo/i.test(error.message) ? new HandoffNotThrough(`the tab for ${url} was closed, or Chrome quit, before Octocrawl read it`, sawGate) : null;
|
|
24402
25931
|
const ended = (error) => gone(error) ?? (error instanceof ChromeLoginError ? new HandoffNotThrough(`${url} was not read: ${error.message}`, sawGate) : error);
|
|
24403
25932
|
let targetId;
|
|
24404
25933
|
try {
|
|
@@ -24454,7 +25983,7 @@ async function readPage(connection, browser, url, options) {
|
|
|
24454
25983
|
while (Date.now() - started < waitMs) {
|
|
24455
25984
|
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
24456
25985
|
if (options.signal?.aborted === true)
|
|
24457
|
-
throw new HandoffNotThrough(`the handoff of ${url} was cancelled before
|
|
25986
|
+
throw new HandoffNotThrough(`the handoff of ${url} was cancelled before Octocrawl read it`, sawGate);
|
|
24458
25987
|
if (navigation !== null && gone(navigation) !== null)
|
|
24459
25988
|
throw gone(navigation);
|
|
24460
25989
|
let state;
|
|
@@ -24542,7 +26071,7 @@ async function readPage(connection, browser, url, options) {
|
|
|
24542
26071
|
browser
|
|
24543
26072
|
};
|
|
24544
26073
|
}
|
|
24545
|
-
const where = last === null ? "it never loaded" : !sameSite(last.state.href, host) ? `it was on ${safeHost2(last.state.href)}, not ${host}` : stillGated(last, options) !== null ? `it still showed a check (${stillGated(last, options).reason}: ${stillGated(last, options).signals.join(", ")})` : clear >= CLEAR_READS && heard.act === null ? "the page showed no check, and you did not click on it to have it read (
|
|
26074
|
+
const where = last === null ? "it never loaded" : !sameSite(last.state.href, host) ? `it was on ${safeHost2(last.state.href)}, not ${host}` : stillGated(last, options) !== null ? `it still showed a check (${stillGated(last, options).reason}: ${stillGated(last, options).signals.join(", ")})` : clear >= CLEAR_READS && heard.act === null ? "the page showed no check, and you did not click on it to have it read (Octocrawl reads a page in your Chrome only once you act in its tab; a site you are signed into is read with your login through octocrawl login import and mode authed)" : "it was not yet the page: still loading, at a sign-in step, or not answering 2xx";
|
|
24546
26075
|
throw new HandoffNotThrough(`${url} was not through within ${Math.round(waitMs / 1e3)} s: ${where}`, sawGate);
|
|
24547
26076
|
} catch (error) {
|
|
24548
26077
|
throw ended(error);
|
|
@@ -25673,7 +27202,7 @@ async function extractStructured(result, format, execution = {}, modelConfig = s
|
|
|
25673
27202
|
return { status: "incomplete", data, schemaSha256, evidence, issues, modelUsage: null };
|
|
25674
27203
|
}
|
|
25675
27204
|
if (modelConfig === null) {
|
|
25676
|
-
issues.push({ code: "model_unavailable", message: "model fallback requested but
|
|
27205
|
+
issues.push({ code: "model_unavailable", message: "model fallback requested but Octocrawl extraction model is not configured" }, ...missingIssues);
|
|
25677
27206
|
return { status: "incomplete", data, schemaSha256, evidence, issues, modelUsage: null };
|
|
25678
27207
|
}
|
|
25679
27208
|
const errors = deterministicValid ? [] : validate.errors ?? [];
|
|
@@ -25943,6 +27472,9 @@ function compactScrapeResponse(next, req, formats = requestedFormats(req, next),
|
|
|
25943
27472
|
var LoginsUnavailableError = class extends Error {
|
|
25944
27473
|
name = "LoginsUnavailableError";
|
|
25945
27474
|
};
|
|
27475
|
+
var SessionsUnavailableError = class extends Error {
|
|
27476
|
+
name = "SessionsUnavailableError";
|
|
27477
|
+
};
|
|
25946
27478
|
var HandoffUnavailableError = class extends Error {
|
|
25947
27479
|
name = "HandoffUnavailableError";
|
|
25948
27480
|
};
|
|
@@ -25960,7 +27492,45 @@ var NO_SITEMAP = {
|
|
|
25960
27492
|
close: async () => {
|
|
25961
27493
|
}
|
|
25962
27494
|
};
|
|
27495
|
+
var EGRESS_FILE = "egress.json";
|
|
27496
|
+
var COOKIE_SESSION_NAME = /^cookie-session(?:\.[0-9a-f]+)?\.json(?:\..+\.tmp)?$/;
|
|
27497
|
+
function cookieSessionFileFor(taskDir, route) {
|
|
27498
|
+
return join10(taskDir, `cookie-session.${createHash13("sha256").update(route).digest("hex").slice(0, 16)}.json`);
|
|
27499
|
+
}
|
|
27500
|
+
function removeCookieSessions(taskDir, keep) {
|
|
27501
|
+
let names;
|
|
27502
|
+
try {
|
|
27503
|
+
names = readdirSync(taskDir);
|
|
27504
|
+
} catch {
|
|
27505
|
+
return;
|
|
27506
|
+
}
|
|
27507
|
+
for (const name of names)
|
|
27508
|
+
if (COOKIE_SESSION_NAME.test(name) && join10(taskDir, name) !== keep)
|
|
27509
|
+
rmSync(join10(taskDir, name), { force: true });
|
|
27510
|
+
}
|
|
27511
|
+
function bindTaskEgress(pool, file) {
|
|
27512
|
+
let saved;
|
|
27513
|
+
try {
|
|
27514
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8")).egress;
|
|
27515
|
+
if (typeof parsed === "string")
|
|
27516
|
+
saved = parsed;
|
|
27517
|
+
} catch {
|
|
27518
|
+
}
|
|
27519
|
+
const kept = saved === void 0 ? void 0 : pool.byId(saved);
|
|
27520
|
+
if (kept !== void 0 && !pool.cooling(kept.id))
|
|
27521
|
+
return kept;
|
|
27522
|
+
const egress = pool.pick(saved);
|
|
27523
|
+
writeTaskEgress(file, egress.id);
|
|
27524
|
+
return egress;
|
|
27525
|
+
}
|
|
27526
|
+
function writeTaskEgress(file, id) {
|
|
27527
|
+
writeFileSync(file, `${JSON.stringify({ egress: id })}
|
|
27528
|
+
`, { mode: 384 });
|
|
27529
|
+
}
|
|
25963
27530
|
var DEFAULT_WORKER_COUNT = 4;
|
|
27531
|
+
function fetchedPastRobots(result) {
|
|
27532
|
+
return result.compliance?.robots.override !== void 0 || result.trace.some((event) => event.event === "robots_overridden");
|
|
27533
|
+
}
|
|
25964
27534
|
function createApiEngine(options = {}) {
|
|
25965
27535
|
const taskRoot = options.taskRoot ?? ".w2l/api";
|
|
25966
27536
|
prepareTaskRoot(taskRoot);
|
|
@@ -25969,6 +27539,10 @@ function createApiEngine(options = {}) {
|
|
|
25969
27539
|
const shutdownController = new AbortController();
|
|
25970
27540
|
const monitorControllers = /* @__PURE__ */ new Map();
|
|
25971
27541
|
const sessionBroker = new SessionBroker(new FileSessionBrokerStore(join10(taskRoot, "b3-sessions.json")));
|
|
27542
|
+
const managedSessionsServed = () => {
|
|
27543
|
+
if (options.hosted === true)
|
|
27544
|
+
throw new SessionsUnavailableError("this server keeps no managed sessions: run Octocrawl on your own machine (octocrawl serve) for them");
|
|
27545
|
+
};
|
|
25972
27546
|
const userChrome = options.hosted === true ? null : options.userChrome ?? null;
|
|
25973
27547
|
const handoffs = /* @__PURE__ */ new Set();
|
|
25974
27548
|
const handoffClosing = new AbortController();
|
|
@@ -25976,6 +27550,29 @@ function createApiEngine(options = {}) {
|
|
|
25976
27550
|
const savedLogins = options.hosted === true || (options.sessionsFile ?? null) === null ? null : readOnlySessions(new FileSessionStore(options.sessionsFile));
|
|
25977
27551
|
const sessionsFor = (mode) => mode === "authed" ? savedLogins : null;
|
|
25978
27552
|
const headed = options.headed === true;
|
|
27553
|
+
const accessGrant = options.accessGrant ?? null;
|
|
27554
|
+
const compatHosts = options.compatHosts ?? [];
|
|
27555
|
+
const egressSessions = (accessGrant?.capabilities ?? []).includes("egress_sessions");
|
|
27556
|
+
const egressProxyList = options.egressProxies ?? [];
|
|
27557
|
+
if (egressProxyList.length > 0 && options.hosted === true)
|
|
27558
|
+
throw new Error("egress proxies are refused on a hosted engine (ADR 0005)");
|
|
27559
|
+
if (egressProxyList.length > 0 && !egressSessions)
|
|
27560
|
+
throw new Error("egress proxies need an access grant that names egress_sessions (ADR 0005)");
|
|
27561
|
+
if (compatHosts.length > 0 && options.hosted === true)
|
|
27562
|
+
throw new Error("the compatible transport is refused on a hosted engine (ADR 0005)");
|
|
27563
|
+
if (compatHosts.length > 0 && !(accessGrant?.capabilities ?? []).includes("compatible_transport"))
|
|
27564
|
+
throw new Error("the compatible transport needs an access grant that names compatible_transport (ADR 0005)");
|
|
27565
|
+
const compatOptionsRefusal = (page) => {
|
|
27566
|
+
const wire = [...page.headers !== void 0 && Object.keys(page.headers).length > 0 ? ["headers"] : [], ...page.mobile === true ? ["mobile"] : []];
|
|
27567
|
+
return wire.length === 0 ? null : wire.join(", ");
|
|
27568
|
+
};
|
|
27569
|
+
const compatTakes = (url, page) => compatHostListed(compatHosts, url) && compatOptionsRefusal(page) === null;
|
|
27570
|
+
const grantedBudget = (budget2) => {
|
|
27571
|
+
const cap = accessGrant?.budget.perRunUsd ?? null;
|
|
27572
|
+
if (cap === null)
|
|
27573
|
+
return budget2;
|
|
27574
|
+
return { ...budget2, maxCostUsd: budget2.maxCostUsd === null ? cap : Math.min(budget2.maxCostUsd, cap) };
|
|
27575
|
+
};
|
|
25979
27576
|
const basePolicy = options.networkPolicy ?? localNetworkPolicy();
|
|
25980
27577
|
const networkPolicy = {
|
|
25981
27578
|
...basePolicy,
|
|
@@ -25984,9 +27581,10 @@ function createApiEngine(options = {}) {
|
|
|
25984
27581
|
// The operator's file cap: the policy's own, else W2L_MAX_FILE_BYTES, else the default.
|
|
25985
27582
|
maxFileBytes: basePolicy.maxFileBytes ?? maxFileBytesFromEnv(process.env)
|
|
25986
27583
|
};
|
|
27584
|
+
const egressPool = egressProxyList.length === 0 ? null : new EgressPool(egressProxyList, networkPolicy);
|
|
25987
27585
|
const fileStore = new FileStore(join10(taskRoot, "files"));
|
|
25988
27586
|
const pageCache = PageCache.open(taskRoot);
|
|
25989
|
-
const cachePlanFor = (url, mode, page, formats, channels, robotsOverride) => {
|
|
27587
|
+
const cachePlanFor = (url, mode, page, formats, channels, robotsOverride, robotsSetAside2) => {
|
|
25990
27588
|
if (mode === "authed")
|
|
25991
27589
|
return null;
|
|
25992
27590
|
if (page.actions !== void 0)
|
|
@@ -25999,14 +27597,15 @@ function createApiEngine(options = {}) {
|
|
|
25999
27597
|
const { timeout: _timeout, ...shape } = fetchOptions(page, formats);
|
|
26000
27598
|
const rungs = channels.map((channel) => channel.vendorId === void 0 ? channel.id : `${channel.id}(${channel.vendorId})`);
|
|
26001
27599
|
const build = { extractor: EXTRACTOR_VERSION, pdf: PDF_TEXT_VERSION, file: FILE_TEXT_VERSION, commit: sourceCommitFromEnv() };
|
|
26002
|
-
const
|
|
26003
|
-
|
|
27600
|
+
const access = accessGrant === null ? void 0 : { tier: accessGrant.tier, capabilities: [...accessGrant.capabilities].sort() };
|
|
27601
|
+
const key = pageCacheKey(url, { mode, fetch: shape, rungs, fastMode: page.fastMode === true, robotsOverride: robotsOverride ?? null, build, ...access === void 0 ? {} : { access } });
|
|
27602
|
+
return { key, bounds: lookup3 ? { minAgeMs: page.minAge ?? 0, maxAgeMs: page.maxAge ?? null } : null, lockdown: page.lockdown === true, store, robotsSetAside: robotsSetAside2 };
|
|
26004
27603
|
};
|
|
26005
27604
|
const consultCache = (plan, url, policy) => {
|
|
26006
27605
|
if (plan === null || plan.bounds === null || !evaluateGovernance(url, policy).allowed)
|
|
26007
27606
|
return { kind: "fetch", missed: false };
|
|
26008
27607
|
const hit = pageCache.lookup(plan.key, plan.bounds);
|
|
26009
|
-
if (hit !== null)
|
|
27608
|
+
if (hit !== null && (plan.robotsSetAside || !fetchedPastRobots(hit.result)))
|
|
26010
27609
|
return { kind: "hit", result: cacheHitResult(hit) };
|
|
26011
27610
|
if (plan.lockdown)
|
|
26012
27611
|
return { kind: "lockdown_miss", result: cacheMissResult(url, plan.bounds) };
|
|
@@ -26040,7 +27639,7 @@ function createApiEngine(options = {}) {
|
|
|
26040
27639
|
}
|
|
26041
27640
|
if (refusal.kind === "syntax")
|
|
26042
27641
|
throw new RequestError(`${name} entry is not a valid CSS selector: ${selector}`);
|
|
26043
|
-
throw new RequestError(`${name} entry uses ${refusal.reason}, which
|
|
27642
|
+
throw new RequestError(`${name} entry uses ${refusal.reason}, which Octocrawl does not match: ${selector} (supported: ${SUPPORTED_SELECTORS})`, "unsupported_parameter", { parameters: [`${name}[${index}]`] });
|
|
26044
27643
|
}
|
|
26045
27644
|
if (parts > MAX_SELECTOR_PARTS) {
|
|
26046
27645
|
throw new RequestError(`${name} must hold at most ${MAX_SELECTOR_PARTS} selector parts in all, and holds ${parts} (a tag name, *, a class, an id, an attribute test and a pseudo-class each count as one)`);
|
|
@@ -26061,7 +27660,7 @@ function createApiEngine(options = {}) {
|
|
|
26061
27660
|
}
|
|
26062
27661
|
if (refusal.kind === "syntax")
|
|
26063
27662
|
throw new RequestError(`attributes selectors[${index}].selector is not a valid CSS selector: ${selector}`);
|
|
26064
|
-
throw new RequestError(`attributes selectors[${index}].selector uses ${refusal.reason}, which
|
|
27663
|
+
throw new RequestError(`attributes selectors[${index}].selector uses ${refusal.reason}, which Octocrawl does not match: ${selector} (supported: ${SUPPORTED_SELECTORS})`, "unsupported_parameter", { parameters: [`formats[${at}].selectors[${index}]`] });
|
|
26065
27664
|
}
|
|
26066
27665
|
if (parts > MAX_SELECTOR_PARTS) {
|
|
26067
27666
|
throw new RequestError(`attributes selectors must hold at most ${MAX_SELECTOR_PARTS} selector parts in all, and hold ${parts} (a tag name, *, a class, an id, an attribute test and a pseudo-class each count as one)`);
|
|
@@ -26082,22 +27681,25 @@ function createApiEngine(options = {}) {
|
|
|
26082
27681
|
}
|
|
26083
27682
|
if (refusal.kind === "syntax")
|
|
26084
27683
|
throw new RequestError(`list ${path} is not a valid CSS selector: ${selector}`);
|
|
26085
|
-
throw new RequestError(`list ${path} uses ${refusal.reason}, which
|
|
27684
|
+
throw new RequestError(`list ${path} uses ${refusal.reason}, which Octocrawl does not match: ${selector} (supported: ${SUPPORTED_SELECTORS})`, "unsupported_parameter", { parameters: [`formats[${at}].${path}`] });
|
|
26086
27685
|
}
|
|
26087
27686
|
if (parts > MAX_SELECTOR_PARTS)
|
|
26088
27687
|
throw new RequestError(`list selectors must hold at most ${MAX_SELECTOR_PARTS} selector parts in all, and hold ${parts}`);
|
|
26089
27688
|
};
|
|
27689
|
+
const hosted = options.hosted === true;
|
|
27690
|
+
const robotsSetAside = !hosted && options.allowRobotsOverride !== false;
|
|
26090
27691
|
const checkRobotsOverride = (parameter, value) => {
|
|
26091
|
-
if (
|
|
26092
|
-
throw new RequestError(`unsupported parameter: ${parameter} (this server
|
|
27692
|
+
if (!robotsSetAside && value !== void 0 && value !== false) {
|
|
27693
|
+
throw new RequestError(`unsupported parameter: ${parameter} (this server obeys robots.txt for every URL; setting it aside is for a local Octocrawl server)`, "unsupported_parameter", { parameters: [parameter] });
|
|
26093
27694
|
}
|
|
26094
27695
|
};
|
|
26095
|
-
const
|
|
27696
|
+
const namedUrlOverride = robotsSetAside ? { reason: "the request named this URL", basis: "user_named_url" } : void 0;
|
|
27697
|
+
const ignoreRobotsOverride = (ignore) => ignore === true && robotsSetAside ? { reason: "the crawl or map was started with ignoreRobotsTxt", basis: "ignore_robots_txt" } : void 0;
|
|
26096
27698
|
const checkHostedOptions = (req) => {
|
|
26097
27699
|
if (hosted && req.skipTlsVerification === true)
|
|
26098
27700
|
throw new RequestError("skipTlsVerification is not available in hosted mode", "invalid_request", void 0, [REFUSAL_HINTS.hostedSkipTlsVerification]);
|
|
26099
27701
|
if (hosted && req.actions !== void 0)
|
|
26100
|
-
throw new RequestError("actions are not available in hosted mode: run
|
|
27702
|
+
throw new RequestError("actions are not available in hosted mode: run Octocrawl locally to use them");
|
|
26101
27703
|
};
|
|
26102
27704
|
const jobEvents = new JobEventHub();
|
|
26103
27705
|
const jobWebhooks = new JobWebhooks(deliveryStore, { hosted, allowHttpLoopback: options.webhookPolicy?.allowHttpLoopback ?? !hosted });
|
|
@@ -26112,7 +27714,7 @@ function createApiEngine(options = {}) {
|
|
|
26112
27714
|
throw error;
|
|
26113
27715
|
});
|
|
26114
27716
|
await scrapesDirReady;
|
|
26115
|
-
await
|
|
27717
|
+
await writeFile8(join10(scrapesDir, `${record3.scrapeId}.json`), JSON.stringify(record3, null, 2));
|
|
26116
27718
|
};
|
|
26117
27719
|
const mapsDir = join10(taskRoot, "maps");
|
|
26118
27720
|
let mapsDirReady = null;
|
|
@@ -26122,7 +27724,7 @@ function createApiEngine(options = {}) {
|
|
|
26122
27724
|
throw error;
|
|
26123
27725
|
});
|
|
26124
27726
|
await mapsDirReady;
|
|
26125
|
-
await
|
|
27727
|
+
await writeFile8(join10(mapsDir, `${record3.response.id}.json`), JSON.stringify(record3));
|
|
26126
27728
|
};
|
|
26127
27729
|
const mapMaxLimit = options.mapMaxLimit ?? (hosted ? HOSTED_MAP_MAX_LIMIT : MAX_MAP_LIMIT);
|
|
26128
27730
|
const mapMaxTimeoutMs = options.mapMaxTimeoutMs ?? (hosted ? HOSTED_MAP_MAX_TIMEOUT_MS : MAX_MAP_TIMEOUT_MS);
|
|
@@ -26131,12 +27733,13 @@ function createApiEngine(options = {}) {
|
|
|
26131
27733
|
const defaultMaxPages = options.defaultMaxPages ?? null;
|
|
26132
27734
|
const workerCount = Math.max(1, options.workerCount ?? DEFAULT_WORKER_COUNT);
|
|
26133
27735
|
const robotsCaches = /* @__PURE__ */ new Map();
|
|
26134
|
-
const robotsCacheFor = (mode) => {
|
|
26135
|
-
const
|
|
27736
|
+
const robotsCacheFor = (mode, egress) => {
|
|
27737
|
+
const key = egress === void 0 ? mode : `${mode}|${egress.id}`;
|
|
27738
|
+
const existing = robotsCaches.get(key);
|
|
26136
27739
|
if (existing !== void 0)
|
|
26137
27740
|
return existing;
|
|
26138
|
-
const cache = new RobotsOriginCache(networkPolicy);
|
|
26139
|
-
robotsCaches.set(
|
|
27741
|
+
const cache = new RobotsOriginCache(egress?.policy ?? networkPolicy);
|
|
27742
|
+
robotsCaches.set(key, cache);
|
|
26140
27743
|
return cache;
|
|
26141
27744
|
};
|
|
26142
27745
|
const inflight = /* @__PURE__ */ new Map();
|
|
@@ -26161,24 +27764,27 @@ function createApiEngine(options = {}) {
|
|
|
26161
27764
|
const activeScrapes = /* @__PURE__ */ new Set();
|
|
26162
27765
|
const crawlControllers = /* @__PURE__ */ new Map();
|
|
26163
27766
|
const runningCrawls = /* @__PURE__ */ new Map();
|
|
26164
|
-
const createChannels = options.channelsFor ?? ((mode) => {
|
|
26165
|
-
const channels = buildChannels(mode, { headed, networkPolicy, originScheduler, publicPreferenceState: options.publicPreferenceState, browserAllowedHosts: options.browserAllowedHosts, fileStore, robotsCache: robotsCacheFor(mode) });
|
|
26166
|
-
return options.httpOnly ? channels.filter((channel) => channel.id
|
|
27767
|
+
const createChannels = options.channelsFor ?? ((mode, egress) => {
|
|
27768
|
+
const channels = buildChannels(mode, { headed, networkPolicy: egress?.policy ?? networkPolicy, originScheduler, publicPreferenceState: options.publicPreferenceState, browserAllowedHosts: options.browserAllowedHosts, fileStore, robotsCache: robotsCacheFor(mode, egress), vendorPolicy: { authorized: accessGrant?.capabilities ?? [] }, browserEngine: options.browserEngine ?? "playwright", compatTransport: compatHosts.length > 0 });
|
|
27769
|
+
return options.httpOnly ? channels.filter((channel) => HTTP_CHANNELS.has(channel.id)) : channels;
|
|
26167
27770
|
});
|
|
26168
27771
|
const channelsByMode = /* @__PURE__ */ new Map();
|
|
26169
27772
|
const historiesByMode = /* @__PURE__ */ new Map();
|
|
26170
|
-
const channelsFor = (mode) => {
|
|
26171
|
-
const
|
|
27773
|
+
const channelsFor = (mode, egress) => {
|
|
27774
|
+
const key = egress === void 0 ? mode : `${mode}|${egress.id}`;
|
|
27775
|
+
const existing = channelsByMode.get(key);
|
|
26172
27776
|
if (existing !== void 0)
|
|
26173
27777
|
return existing;
|
|
26174
|
-
const channels = createChannels(mode);
|
|
26175
|
-
channelsByMode.set(
|
|
27778
|
+
const channels = createChannels(mode, egress);
|
|
27779
|
+
channelsByMode.set(key, channels);
|
|
26176
27780
|
return channels;
|
|
26177
27781
|
};
|
|
26178
|
-
const channelsForUrl = (mode, url, page = {}, formats = []) => {
|
|
26179
|
-
const channels = channelsFor(mode);
|
|
27782
|
+
const channelsForUrl = (mode, url, page = {}, formats = [], oneUrl = true, compat = true, egress) => {
|
|
27783
|
+
const channels = channelsFor(mode, egress);
|
|
26180
27784
|
const policy = options.channelPolicy?.(url) ?? "ladder";
|
|
26181
|
-
let selected = policy === "ladder" ? channels : channels.filter((channel) =>
|
|
27785
|
+
let selected = policy === "ladder" ? channels : channels.filter((channel) => policy === "http_only" ? HTTP_CHANNELS.has(channel.id) : channel.id === "browser_local");
|
|
27786
|
+
if (!compat || oneUrl && !compatHostListed(compatHosts, url))
|
|
27787
|
+
selected = selected.filter((channel) => channel.id !== "http_compat");
|
|
26182
27788
|
if (selected.length === 0)
|
|
26183
27789
|
throw new RequestError(`capture channel unavailable for ${policy}`);
|
|
26184
27790
|
const filtered = [];
|
|
@@ -26208,10 +27814,10 @@ function createApiEngine(options = {}) {
|
|
|
26208
27814
|
if (page.fastMode === true) {
|
|
26209
27815
|
if (policy === "browser_only")
|
|
26210
27816
|
throw new RequestError("fastMode is not available for this URL: it is served by the browser lane only");
|
|
26211
|
-
const kept = selected.filter((channel) => channel.id
|
|
27817
|
+
const kept = selected.filter((channel) => HTTP_CHANNELS.has(channel.id));
|
|
26212
27818
|
if (kept.length === 0)
|
|
26213
27819
|
throw new RequestError("fastMode is not available for this URL: no http rung is configured for it");
|
|
26214
|
-
const dropped = selected.filter((channel) => channel.id
|
|
27820
|
+
const dropped = selected.filter((channel) => !HTTP_CHANNELS.has(channel.id)).map(name);
|
|
26215
27821
|
if (dropped.length > 0)
|
|
26216
27822
|
filtered.push({ reason: "fastMode", dropped });
|
|
26217
27823
|
selected = kept;
|
|
@@ -26223,6 +27829,19 @@ function createApiEngine(options = {}) {
|
|
|
26223
27829
|
filtered.push({ reason: wire.join(", "), dropped });
|
|
26224
27830
|
selected = selected.filter((channel) => channel.vendorId === void 0);
|
|
26225
27831
|
}
|
|
27832
|
+
if (selected.some((channel) => channel.id === "http_compat")) {
|
|
27833
|
+
const refusal = compatOptionsRefusal(page);
|
|
27834
|
+
if (refusal !== null) {
|
|
27835
|
+
filtered.push({ reason: refusal, dropped: ["http_compat"] });
|
|
27836
|
+
selected = selected.filter((channel) => channel.id !== "http_compat");
|
|
27837
|
+
} else if (oneUrl && selected.some((channel) => channel.id === "http")) {
|
|
27838
|
+
filtered.push({ reason: "compatible_transport", dropped: ["http"] });
|
|
27839
|
+
selected = selected.filter((channel) => channel.id !== "http");
|
|
27840
|
+
}
|
|
27841
|
+
}
|
|
27842
|
+
if (selected.some((channel) => channel.id === "http_compat") && selected.some((channel) => channel.id === "http")) {
|
|
27843
|
+
selected = selected.map((channel) => channel.id === "http" ? { ...channel, serves: (target, fetch2) => !compatTakes(target, fetch2) } : channel.id === "http_compat" ? { ...channel, serves: (target, fetch2) => compatTakes(target, fetch2) } : channel);
|
|
27844
|
+
}
|
|
26226
27845
|
return { channels: selected, filtered };
|
|
26227
27846
|
};
|
|
26228
27847
|
const historyFor = (mode) => {
|
|
@@ -26369,7 +27988,7 @@ function createApiEngine(options = {}) {
|
|
|
26369
27988
|
const read2 = await chrome.read(url, { ...waitMs === void 0 ? {} : { waitMs }, ...hooks.onWaiting === void 0 ? {} : { onWaiting: hooks.onWaiting }, ...hooks.onConfirm === void 0 ? {} : { onConfirm: hooks.onConfirm }, ...hooks.onHidden === void 0 ? {} : { onHidden: hooks.onHidden }, signal, ...judged });
|
|
26370
27989
|
const result = pageFromUserBrowser(read2, prior, fetchOpts);
|
|
26371
27990
|
if (!CONTENTFUL_STATUS.has(result.status))
|
|
26372
|
-
return { reason: `the page
|
|
27991
|
+
return { reason: `the page Octocrawl read in Chrome was ${result.status} (${result.blockReason ?? result.failureReason ?? "no reason"}), not the page` };
|
|
26373
27992
|
return { result };
|
|
26374
27993
|
} catch (error) {
|
|
26375
27994
|
if (!(error instanceof HandoffNotThrough))
|
|
@@ -26381,14 +28000,14 @@ function createApiEngine(options = {}) {
|
|
|
26381
28000
|
if (req.handoff === void 0)
|
|
26382
28001
|
return;
|
|
26383
28002
|
if (userChrome === null)
|
|
26384
|
-
throw new RequestError("handoff: this server does not hand pages to a person; run
|
|
28003
|
+
throw new RequestError("handoff: this server does not hand pages to a person; run Octocrawl on your own machine (octocrawl serve, the local MCP host, or the octocrawl CLI)", "unsupported_parameter", { parameters: ["handoff"] });
|
|
26385
28004
|
const unread = unreadByPerson(fetchOptions(req, req.formats));
|
|
26386
28005
|
if (unread !== null)
|
|
26387
28006
|
throw new RequestError(`handoff: the request asks for ${unread}, which a page read in your own Chrome cannot give`, "unsupported_parameter", { parameters: ["handoff"] });
|
|
26388
28007
|
}
|
|
26389
28008
|
async function handOffScrape(req, prior, context, hooks) {
|
|
26390
28009
|
if (handoffClosing.signal.aborted)
|
|
26391
|
-
return { reason: "
|
|
28010
|
+
return { reason: "Octocrawl is shutting down" };
|
|
26392
28011
|
const signal = AbortSignal.any([...context.signal === void 0 ? [] : [context.signal], ...hooks.signal === void 0 ? [] : [hooks.signal], shutdownController.signal, handoffClosing.signal]);
|
|
26393
28012
|
let chrome;
|
|
26394
28013
|
try {
|
|
@@ -26406,17 +28025,17 @@ function createApiEngine(options = {}) {
|
|
|
26406
28025
|
if (userChrome === null || !handoffResult(result) || unreadByPerson(fetchOptions(req, req.formats)) !== null)
|
|
26407
28026
|
return result;
|
|
26408
28027
|
const blockReason = result.blockReason;
|
|
26409
|
-
const stopped = `${result.requestedUrl} stopped at a ${blockReason.replace(/_/g, " ")}
|
|
26410
|
-
const rationale = tried ? `${stopped}, and handed to you in your own Chrome it was not read there (the handoff_not_through warning says why): send the request again with handoff to try once more, with a longer handoff.waitMs if you needed more time` : `${stopped}: send the request again with handoff: true (octocrawl scrape --handoff) to get through it yourself in your own Chrome, and
|
|
28028
|
+
const stopped = `${result.requestedUrl} stopped at a ${blockReason.replace(/_/g, " ")} Octocrawl does not pass`;
|
|
28029
|
+
const rationale = tried ? `${stopped}, and handed to you in your own Chrome it was not read there (the handoff_not_through warning says why): send the request again with handoff to try once more, with a longer handoff.waitMs if you needed more time` : `${stopped}: send the request again with handoff: true (octocrawl scrape --handoff) to get through it yourself in your own Chrome, and Octocrawl reads the page there`;
|
|
26411
28030
|
return { ...result, handoff: { reason: HANDOFF_REASONS[blockReason], liveViewUrl: null, rationale } };
|
|
26412
28031
|
}
|
|
26413
28032
|
async function handOff2(taskId, req, hooks) {
|
|
26414
28033
|
if (!UUID.test(taskId))
|
|
26415
28034
|
return null;
|
|
26416
28035
|
if (userChrome === null)
|
|
26417
|
-
throw new HandoffUnavailableError("this server does not hand pages to a person: run
|
|
28036
|
+
throw new HandoffUnavailableError("this server does not hand pages to a person: run Octocrawl on your own machine (octocrawl serve, the local MCP host, or the octocrawl CLI) to open them in your Chrome");
|
|
26418
28037
|
if (handoffClosing.signal.aborted)
|
|
26419
|
-
throw new HandoffUnavailableError("
|
|
28038
|
+
throw new HandoffUnavailableError("Octocrawl is shutting down");
|
|
26420
28039
|
if (!existsSync3(join10(taskRoot, taskId, "checkpoint.sqlite")))
|
|
26421
28040
|
return null;
|
|
26422
28041
|
const store = SqliteTaskStore.open(join10(taskRoot, taskId));
|
|
@@ -26469,7 +28088,7 @@ function createApiEngine(options = {}) {
|
|
|
26469
28088
|
...json2 === void 0 ? {} : { json: json2 }
|
|
26470
28089
|
};
|
|
26471
28090
|
if (shutdownController.signal.aborted || handoffClosing.signal.aborted) {
|
|
26472
|
-
items.push({ id: step.id, url: step.url, through: false, status: step.status, reason: "
|
|
28091
|
+
items.push({ id: step.id, url: step.url, through: false, status: step.status, reason: "Octocrawl shut down before the page was stored" });
|
|
26473
28092
|
continue;
|
|
26474
28093
|
}
|
|
26475
28094
|
const { audit: _stoppedAudit, ...kept } = step;
|
|
@@ -26504,23 +28123,55 @@ function createApiEngine(options = {}) {
|
|
|
26504
28123
|
const mode = defaultApiMode(task.mode);
|
|
26505
28124
|
const stored = task.batch ?? task.crawl;
|
|
26506
28125
|
const selection = stored === void 0 || !hosted ? stored : { ...stored, skipTlsVerification: false };
|
|
26507
|
-
const
|
|
26508
|
-
const
|
|
26509
|
-
|
|
26510
|
-
const
|
|
28126
|
+
const egressFile = join10(task.taskDir, EGRESS_FILE);
|
|
28127
|
+
const firstEgress = egressPool === null || mode === "authed" ? void 0 : bindTaskEgress(egressPool, egressFile);
|
|
28128
|
+
let switches = 0;
|
|
28129
|
+
const lanes = [];
|
|
28130
|
+
const routeOf = (egress) => {
|
|
28131
|
+
const env = networkPolicy.egressProxy ?? null;
|
|
28132
|
+
return egress !== void 0 ? `pool:${egress.id}` : env === null ? "direct" : `env:${(env.https ?? env.http)?.endpoint ?? ""}`;
|
|
28133
|
+
};
|
|
28134
|
+
const endCookieSession = async (ended) => {
|
|
28135
|
+
if (!ended)
|
|
28136
|
+
return;
|
|
28137
|
+
await Promise.all(lanes.map((lane) => lane.cookieSession?.close()));
|
|
28138
|
+
removeCookieSessions(task.taskDir);
|
|
28139
|
+
rmSync(egressFile, { force: true });
|
|
28140
|
+
};
|
|
28141
|
+
const runnerFor = (lane) => new LadderRunner(lane.channels, { mode, ...req.policyAllowlist.length ? { allowlistedDomains: req.policyAllowlist } : {} }, historyFor(mode), null, task.batch === void 0 ? null : sessionsFor(mode), { channelsFiltered: lane.filtered });
|
|
28142
|
+
const recordedOverrideFor = !robotsSetAside || task.batch?.robotsOverrides === void 0 ? null : robotsOverrideLookup(task.batch.robotsOverrides);
|
|
28143
|
+
const namedOverride = task.batch === void 0 ? ignoreRobotsOverride(task.crawl?.ignoreRobotsTxt) : namedUrlOverride;
|
|
28144
|
+
const robotsOverrideFor = recordedOverrideFor === null && namedOverride === void 0 ? null : (url) => recordedOverrideFor?.(url) ?? namedOverride;
|
|
28145
|
+
const ladderFor = (lane) => new LadderScrapeAtom(runnerFor(lane), robotsOverrideFor === null ? fetchOptions(selection, selection?.formats) : (url) => {
|
|
26511
28146
|
const robotsOverride = robotsOverrideFor(url);
|
|
26512
28147
|
return { ...fetchOptions(selection, selection?.formats), ...robotsOverride === void 0 ? {} : { robotsOverride } };
|
|
26513
28148
|
});
|
|
26514
|
-
const
|
|
28149
|
+
const laneFor = (egress) => {
|
|
28150
|
+
const rungs = channelsForUrl(mode, task.seedUrl, selection ?? {}, selection?.formats ?? [], false, true, egress);
|
|
28151
|
+
const sessionFile = cookieSessionFileFor(task.taskDir, routeOf(egress));
|
|
28152
|
+
return { egress, rungs, ladder: ladderFor(rungs), cookieSession: egressSessions && mode !== "authed" ? new TaskCookieSession(sessionFile) : void 0 };
|
|
28153
|
+
};
|
|
28154
|
+
removeCookieSessions(task.taskDir, cookieSessionFileFor(task.taskDir, routeOf(firstEgress)));
|
|
28155
|
+
let current = laneFor(firstEgress);
|
|
28156
|
+
lanes.push(current);
|
|
28157
|
+
const closeLadders = async () => {
|
|
28158
|
+
await Promise.all(lanes.map((lane) => lane.ladder.close()));
|
|
28159
|
+
};
|
|
28160
|
+
const pageAtom = selection === void 0 ? { scrape: (url, context) => {
|
|
28161
|
+
const lane = current;
|
|
28162
|
+
return lane.ladder.scrape(url, lane.cookieSession === void 0 ? context : { ...context, cookieSession: lane.cookieSession });
|
|
28163
|
+
}, close: closeLadders } : {
|
|
26515
28164
|
async scrape(url, context) {
|
|
26516
|
-
const
|
|
28165
|
+
const lane = current;
|
|
28166
|
+
const page = createExecutionScope({ ...context, deadlineAt: Math.min(context?.deadlineAt ?? Infinity, Date.now() + (selection.timeout ?? DEFAULT_SCRAPE_TIMEOUT_MS)), ...lane.cookieSession === void 0 ? {} : { cookieSession: lane.cookieSession } });
|
|
26517
28167
|
const formats = selection.formats ?? ["markdown"];
|
|
26518
28168
|
const wants = (name) => hasFormat(formats, name);
|
|
26519
28169
|
const custom = customJsonFormat(formats);
|
|
26520
|
-
const
|
|
28170
|
+
const cachePlan = cachePlanFor(url, mode, selection, selection.formats, lane.rungs.channels, recordedOverrideFor?.(url), robotsOverrideFor?.(url) !== void 0);
|
|
28171
|
+
const plan = lane.cookieSession === void 0 || cachePlan?.lockdown === true ? cachePlan : null;
|
|
26521
28172
|
const answer = consultCache(plan, url, { mode, ...req.policyAllowlist.length ? { allowlistedDomains: req.policyAllowlist } : {} });
|
|
26522
28173
|
const { outcome, json: json2 } = await (async () => {
|
|
26523
|
-
const outcome2 = answer.kind === "fetch" ? await ladder.scrape(url, page).then((fetched) => ({ ...fetched, result: afterFetch(plan, answer, fetched.result) })) : { result: answer.result, links: answer.result.links ?? [], cached: answer.kind === "hit" };
|
|
28174
|
+
const outcome2 = answer.kind === "fetch" ? await lane.ladder.scrape(url, page).then((fetched) => ({ ...fetched, result: afterFetch(plan, answer, fetched.result) })) : { result: answer.result, links: answer.result.links ?? [], cached: answer.kind === "hit" };
|
|
26524
28175
|
const json3 = wants("json") ? await extractStructured(extractionInput(outcome2.result), custom, page, structuredModelConfigFromEnv()) : void 0;
|
|
26525
28176
|
return { outcome: outcome2, json: json3 };
|
|
26526
28177
|
})().finally(() => page.dispose());
|
|
@@ -26542,11 +28193,45 @@ function createApiEngine(options = {}) {
|
|
|
26542
28193
|
...json2 === void 0 ? {} : { json: json2 }
|
|
26543
28194
|
} };
|
|
26544
28195
|
},
|
|
26545
|
-
close:
|
|
28196
|
+
close: closeLadders
|
|
28197
|
+
};
|
|
28198
|
+
const atom = egressPool === null || firstEgress === void 0 ? pageAtom : {
|
|
28199
|
+
async scrape(url, context) {
|
|
28200
|
+
let used = current;
|
|
28201
|
+
let outcome = await pageAtom.scrape(url, context);
|
|
28202
|
+
const events = [];
|
|
28203
|
+
while (egressInDoubt(outcome)) {
|
|
28204
|
+
let reason2;
|
|
28205
|
+
if (current === used) {
|
|
28206
|
+
if (switches >= MAX_EGRESS_SWITCHES)
|
|
28207
|
+
break;
|
|
28208
|
+
const verdict = await probeEgress(used.egress.server);
|
|
28209
|
+
if (verdict === "works")
|
|
28210
|
+
break;
|
|
28211
|
+
reason2 = verdict;
|
|
28212
|
+
if (current === used) {
|
|
28213
|
+
egressPool.fail(used.egress.id);
|
|
28214
|
+
switches++;
|
|
28215
|
+
const next = egressPool.pick(used.egress.id);
|
|
28216
|
+
writeTaskEgress(egressFile, next.id);
|
|
28217
|
+
current = laneFor(next);
|
|
28218
|
+
lanes.push(current);
|
|
28219
|
+
await used.cookieSession?.close();
|
|
28220
|
+
}
|
|
28221
|
+
} else {
|
|
28222
|
+
reason2 = "moved_with_task";
|
|
28223
|
+
}
|
|
28224
|
+
events.push({ at: 0, lane: outcome.result.lane, event: "egress_switched", detail: { from: used.egress.id, to: current.egress.id, reason: reason2, switches } });
|
|
28225
|
+
used = current;
|
|
28226
|
+
outcome = await pageAtom.scrape(url, context);
|
|
28227
|
+
}
|
|
28228
|
+
return events.length === 0 ? outcome : { ...outcome, result: { ...outcome.result, trace: [...outcome.result.trace, ...events] } };
|
|
28229
|
+
},
|
|
28230
|
+
close: () => pageAtom.close()
|
|
26546
28231
|
};
|
|
26547
28232
|
const controller2 = new AbortController();
|
|
26548
28233
|
crawlControllers.set(task.id, controller2);
|
|
26549
|
-
const sitemapSource = req.sitemap === "skip" ? void 0 : new HttpSitemapSource({ mode, device: selection?.mobile === true ? "mobile" : "desktop", networkPolicy, scheduler: originScheduler, robots: robotsCacheFor(mode) });
|
|
28234
|
+
const sitemapSource = req.sitemap === "skip" ? void 0 : new HttpSitemapSource({ mode, device: selection?.mobile === true ? "mobile" : "desktop", networkPolicy: firstEgress?.policy ?? networkPolicy, scheduler: originScheduler, robots: robotsCacheFor(mode, firstEgress), ignoreRobotsTxt: ignoreRobotsOverride(task.crawl?.ignoreRobotsTxt) !== void 0 });
|
|
26550
28235
|
const webhook = webhookOf(task);
|
|
26551
28236
|
const kind = jobKindOf(task);
|
|
26552
28237
|
const onStep = async (step) => {
|
|
@@ -26563,6 +28248,8 @@ function createApiEngine(options = {}) {
|
|
|
26563
28248
|
const orchestrator = new CrawlOrchestrator({
|
|
26564
28249
|
store,
|
|
26565
28250
|
atom,
|
|
28251
|
+
// A page whose scrape threw called a provider only if this mode has one: otherwise its third-party cost is a known 0.
|
|
28252
|
+
scrapeErrorCostUsd: channelsFor(mode).some((channel) => channel.vendorId !== void 0) ? null : 0,
|
|
26566
28253
|
workerCount,
|
|
26567
28254
|
perHostConcurrency: Math.min(4, networkPolicy.perHostConcurrency),
|
|
26568
28255
|
perHostMinDelayMs: networkPolicy.perHostMinDelayMs,
|
|
@@ -26585,7 +28272,7 @@ function createApiEngine(options = {}) {
|
|
|
26585
28272
|
...task.batch ? { seedUrls: task.batch.urls } : {},
|
|
26586
28273
|
taskDir: task.taskDir,
|
|
26587
28274
|
mode,
|
|
26588
|
-
budget: task.budget,
|
|
28275
|
+
budget: grantedBudget(task.budget),
|
|
26589
28276
|
maxDepth: req.maxDepth,
|
|
26590
28277
|
allowlistedDomains: req.allowlistedDomains,
|
|
26591
28278
|
resumeFrom: req.resume ? task.id : null,
|
|
@@ -26605,6 +28292,8 @@ function createApiEngine(options = {}) {
|
|
|
26605
28292
|
}
|
|
26606
28293
|
await finishJob(task, store);
|
|
26607
28294
|
inflight.delete(task.id);
|
|
28295
|
+
const after = await store.getTask(task.id);
|
|
28296
|
+
await endCookieSession(after !== null && isTerminalStatus(after.status));
|
|
26608
28297
|
await store.close();
|
|
26609
28298
|
}).catch(async (error) => {
|
|
26610
28299
|
inflight.delete(task.id);
|
|
@@ -26612,6 +28301,7 @@ function createApiEngine(options = {}) {
|
|
|
26612
28301
|
runningCrawls.delete(task.id);
|
|
26613
28302
|
await markCrawlFailed(store, task.id);
|
|
26614
28303
|
await finishJob(task, store, error instanceof Error ? error.message : String(error));
|
|
28304
|
+
await endCookieSession(true);
|
|
26615
28305
|
await store.close();
|
|
26616
28306
|
});
|
|
26617
28307
|
runningCrawls.set(task.id, orchestrator);
|
|
@@ -26631,6 +28321,10 @@ function createApiEngine(options = {}) {
|
|
|
26631
28321
|
} else {
|
|
26632
28322
|
if (task !== null && webhookOf(task) !== void 0 && isTerminalStatus(task.status))
|
|
26633
28323
|
await reconcileFinished(task, store);
|
|
28324
|
+
if (task !== null && isTerminalStatus(task.status)) {
|
|
28325
|
+
removeCookieSessions(taskDir);
|
|
28326
|
+
rmSync(join10(taskDir, EGRESS_FILE), { force: true });
|
|
28327
|
+
}
|
|
26634
28328
|
void store.close();
|
|
26635
28329
|
}
|
|
26636
28330
|
}).catch(() => {
|
|
@@ -26651,26 +28345,29 @@ function createApiEngine(options = {}) {
|
|
|
26651
28345
|
const deadlineAt = req.timeout === void 0 ? context.deadlineAt ?? Date.now() + DEFAULT_SCRAPE_TIMEOUT_MS : Math.min(context.deadlineAt ?? Infinity, Date.now() + req.timeout);
|
|
26652
28346
|
const scope = createExecutionScope({ ...context, signal: context.signal ? AbortSignal.any([context.signal, shutdownController.signal]) : shutdownController.signal, deadlineAt });
|
|
26653
28347
|
const mode = defaultApiMode(req.mode);
|
|
26654
|
-
const rungs = channelsForUrl(mode, req.url, req, req.formats ?? []);
|
|
28348
|
+
const rungs = channelsForUrl(mode, req.url, req, req.formats ?? [], true, true, mode === "authed" ? void 0 : egressPool?.pick());
|
|
26655
28349
|
const policy = {
|
|
26656
28350
|
mode,
|
|
26657
28351
|
...req.allowlistedDomains !== void 0 && req.allowlistedDomains.length > 0 ? { allowlistedDomains: req.allowlistedDomains } : {}
|
|
26658
28352
|
};
|
|
26659
28353
|
const runner = new LadderRunner(rungs.channels, policy, historyFor(mode), null, sessionsFor(mode), { channelsFiltered: rungs.filtered });
|
|
26660
|
-
const
|
|
28354
|
+
const robotsOverride = req.robotsOverride ?? (record3 ? namedUrlOverride : void 0);
|
|
28355
|
+
const plan = record3 ? cachePlanFor(req.url, mode, req, req.formats, rungs.channels, req.robotsOverride, robotsOverride !== void 0) : null;
|
|
26661
28356
|
const operation = (async () => {
|
|
26662
28357
|
const answer = consultCache(plan, req.url, policy);
|
|
26663
|
-
const run = answer.kind === "fetch" ? await runner.run(req.url, void 0, scope, { ...fetchOptions(req, req.formats), ...
|
|
28358
|
+
const run = answer.kind === "fetch" ? await runner.run(req.url, void 0, scope, { ...fetchOptions(req, req.formats), ...robotsOverride === void 0 ? {} : { robotsOverride } }).then((fetched) => ({ ...fetched, result: afterFetch(plan, answer, fetched.result) })) : { result: answer.result, ...untriedAudit(Math.round(performance.now() - overallStart)) };
|
|
26664
28359
|
const handed = req.handoff !== void 0 && answer.kind === "fetch" && handoffResult(run.result) ? await handOffScrape(req, run.result, context, hooks) : null;
|
|
26665
28360
|
const read2 = handed !== null && "result" in handed ? handed.result : null;
|
|
26666
28361
|
const result = handed === null ? withHandoffHint(run.result, req) : "reason" in handed ? withHandoffHint({ ...run.result, warnings: [...run.result.warnings ?? [], { code: "handoff_not_through", message: `Handed to you in your Chrome, the page was not read: ${handed.reason}.` }] }, req, true) : answer.kind === "fetch" && answer.missed && plan?.bounds != null ? withCacheMiss(handed.result, plan.bounds) : handed.result;
|
|
26667
28362
|
const agentHints = read2 !== null ? agentHintsFor(req, { channelsTried: [result.lane], result }) : agentHintsFor(req, { ...run, result });
|
|
28363
|
+
const summary = read2 === null ? run.summary : { ...run.summary, ...summarize(run.channelsTried, [...run.summary.attempts, { channel: read2.lane, result: read2, ordinal: run.summary.attempts.length + 1 }]) };
|
|
26668
28364
|
const full = {
|
|
26669
28365
|
...result,
|
|
28366
|
+
// The answer's third-party spend is the whole call's: a page read in the person's Chrome after a provider tried it still cost what the provider charged.
|
|
28367
|
+
usage: { ...result.usage, externalCostUsd: summary.externalCostUsd },
|
|
26670
28368
|
channelsTried: run.channelsTried,
|
|
26671
28369
|
ladderTrace: run.ladderTrace,
|
|
26672
|
-
|
|
26673
|
-
summary: read2 === null ? run.summary : { ...run.summary, ...summarize(run.channelsTried, [...run.summary.attempts, { channel: read2.lane, result: read2 }]) },
|
|
28370
|
+
summary,
|
|
26674
28371
|
...agentHints.length === 0 ? {} : { agentHints }
|
|
26675
28372
|
};
|
|
26676
28373
|
const shapeScope = handed !== null && "result" in handed ? createExecutionScope({ signal: AbortSignal.any([...context.signal === void 0 ? [] : [context.signal], shutdownController.signal]) }) : scope;
|
|
@@ -26700,6 +28397,8 @@ function createApiEngine(options = {}) {
|
|
|
26700
28397
|
}
|
|
26701
28398
|
}
|
|
26702
28399
|
async function runMap(req, context) {
|
|
28400
|
+
checkRobotsOverride("ignoreRobotsTxt", req.ignoreRobotsTxt);
|
|
28401
|
+
const robotsOverride = ignoreRobotsOverride(req.ignoreRobotsTxt);
|
|
26703
28402
|
if (req.limit !== void 0 && req.limit > mapMaxLimit)
|
|
26704
28403
|
throw new RequestError(`limit must be at most ${mapMaxLimit} on this server`);
|
|
26705
28404
|
if (req.timeout !== void 0 && req.timeout > mapMaxTimeoutMs)
|
|
@@ -26708,20 +28407,21 @@ function createApiEngine(options = {}) {
|
|
|
26708
28407
|
if (readsPage && options.channelPolicy?.(req.url) === "browser_only")
|
|
26709
28408
|
throw new RequestError("map is not available for this URL: this server reads it with the browser lane only");
|
|
26710
28409
|
const mode = req.mode ?? "standard";
|
|
26711
|
-
const
|
|
28410
|
+
const egress = egressPool?.pick();
|
|
28411
|
+
const rungs = readsPage ? channelsForUrl(mode, req.url, { fastMode: true }, ["rawHtml"], true, false, egress) : null;
|
|
26712
28412
|
const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
26713
28413
|
const id = crypto.randomUUID();
|
|
26714
28414
|
const signal = context.signal ? AbortSignal.any([context.signal, shutdownController.signal]) : shutdownController.signal;
|
|
26715
28415
|
const userAgentFor = (url) => prepareHttpIdentity(mode, networkPolicy.contact ?? null, new URL(url).hostname).identity.userAgent;
|
|
26716
|
-
const robots = robotsCacheFor(mode);
|
|
28416
|
+
const robots = robotsCacheFor(mode, egress);
|
|
26717
28417
|
const lookups = /* @__PURE__ */ new Map();
|
|
26718
28418
|
const runner = rungs === null ? null : new LadderRunner(rungs.channels, { mode }, historyFor(mode), null, null, { channelsFiltered: rungs.filtered });
|
|
26719
|
-
const sitemap = req.sitemap === "skip" ? null : new HttpSitemapSource({ mode, networkPolicy, scheduler: originScheduler, robots });
|
|
28419
|
+
const sitemap = req.sitemap === "skip" ? null : new HttpSitemapSource({ mode, networkPolicy: egress?.policy ?? networkPolicy, scheduler: originScheduler, robots, ignoreRobotsTxt: robotsOverride !== void 0 });
|
|
26720
28420
|
const sources = {
|
|
26721
28421
|
async readStartPage(url, scope) {
|
|
26722
28422
|
if (runner === null)
|
|
26723
28423
|
throw new Error("a sitemap-only map reads no page");
|
|
26724
|
-
const run = await runner.run(url, void 0, scope, fetchOptions(void 0, ["rawHtml"]));
|
|
28424
|
+
const run = await runner.run(url, void 0, scope, { ...fetchOptions(void 0, ["rawHtml"]), ...robotsOverride === void 0 ? {} : { robotsOverride } });
|
|
26725
28425
|
const result = run.result;
|
|
26726
28426
|
const links = typeof result.rawHtml === "string" ? collectLinkDetails(result.rawHtml, result.evidence.finalUrl || url) : [];
|
|
26727
28427
|
const clientRendered = result.warnings?.some((warning) => warning.code === "client_rendered_suspected") === true || httpLaneAskedForBrowser(result);
|
|
@@ -26739,7 +28439,7 @@ function createApiEngine(options = {}) {
|
|
|
26739
28439
|
lookups.set(origin, lookup3);
|
|
26740
28440
|
}
|
|
26741
28441
|
const decision = robots.decision(await lookup3, url, userAgent);
|
|
26742
|
-
return decision.decision === "disallowed" ? { disallowed: true, ...decision.unreachable === void 0 ? {} : { unreachable: decision.unreachable } } : decision.decision;
|
|
28442
|
+
return decision.decision === "disallowed" ? { disallowed: true, ...decision.unreachable === void 0 ? {} : { unreachable: decision.unreachable }, ...isOctocrawlRobotsGroup(decision.matchedUserAgentGroup) ? { octocrawl: true } : {} } : decision.decision;
|
|
26743
28443
|
},
|
|
26744
28444
|
identity: { mode, userAgent: userAgentFor(req.url) }
|
|
26745
28445
|
};
|
|
@@ -26758,7 +28458,8 @@ function createApiEngine(options = {}) {
|
|
|
26758
28458
|
excludePaths: req.excludePaths,
|
|
26759
28459
|
regexOnFullURL: req.regexOnFullURL,
|
|
26760
28460
|
crawlEntireDomain: req.crawlEntireDomain,
|
|
26761
|
-
deduplicateSimilarURLs: req.deduplicateSimilarURLs
|
|
28461
|
+
deduplicateSimilarURLs: req.deduplicateSimilarURLs,
|
|
28462
|
+
ignoreRobotsTxt: robotsOverride !== void 0
|
|
26762
28463
|
}, { signal });
|
|
26763
28464
|
const agentHints = mapAgentHints(run, mapMaxTimeoutMs);
|
|
26764
28465
|
const { elapsedMs, ...rest } = run;
|
|
@@ -26788,7 +28489,7 @@ function createApiEngine(options = {}) {
|
|
|
26788
28489
|
if (!UUID.test(id))
|
|
26789
28490
|
return null;
|
|
26790
28491
|
try {
|
|
26791
|
-
return JSON.parse(await
|
|
28492
|
+
return JSON.parse(await readFile6(join10(mapsDir, `${id}.json`), "utf8"));
|
|
26792
28493
|
} catch (error) {
|
|
26793
28494
|
if (error.code === "ENOENT")
|
|
26794
28495
|
return null;
|
|
@@ -26799,7 +28500,7 @@ function createApiEngine(options = {}) {
|
|
|
26799
28500
|
if (!UUID.test(scrapeId))
|
|
26800
28501
|
return null;
|
|
26801
28502
|
try {
|
|
26802
|
-
return JSON.parse(await
|
|
28503
|
+
return JSON.parse(await readFile6(join10(scrapesDir, `${scrapeId}.json`), "utf8"));
|
|
26803
28504
|
} catch (error) {
|
|
26804
28505
|
if (error.code === "ENOENT")
|
|
26805
28506
|
return null;
|
|
@@ -26807,6 +28508,7 @@ function createApiEngine(options = {}) {
|
|
|
26807
28508
|
}
|
|
26808
28509
|
},
|
|
26809
28510
|
async startCrawl(req) {
|
|
28511
|
+
checkRobotsOverride("ignoreRobotsTxt", req.ignoreRobotsTxt);
|
|
26810
28512
|
checkFileCap(req);
|
|
26811
28513
|
checkSelectors(req);
|
|
26812
28514
|
checkAttributeSelectors(req.formats);
|
|
@@ -26840,7 +28542,7 @@ function createApiEngine(options = {}) {
|
|
|
26840
28542
|
budget: {
|
|
26841
28543
|
maxPages: req.maxPages ?? defaultMaxPages,
|
|
26842
28544
|
maxWallMs: null,
|
|
26843
|
-
maxCostUsd: null,
|
|
28545
|
+
maxCostUsd: accessGrant?.budget.perRunUsd ?? null,
|
|
26844
28546
|
maxTokens: null
|
|
26845
28547
|
},
|
|
26846
28548
|
crawl: {
|
|
@@ -26859,6 +28561,7 @@ function createApiEngine(options = {}) {
|
|
|
26859
28561
|
allowExternalLinks: req.allowExternalLinks === true,
|
|
26860
28562
|
sitemap: req.sitemap ?? "include",
|
|
26861
28563
|
maxConcurrency: req.maxConcurrency ?? null,
|
|
28564
|
+
...req.ignoreRobotsTxt === true ? { ignoreRobotsTxt: true } : {},
|
|
26862
28565
|
...pageOptions(req),
|
|
26863
28566
|
// The destination is registered first (`job:<taskId>`); the task stores everything but the header values.
|
|
26864
28567
|
...webhookConfig === void 0 ? {} : { webhook: jobWebhooks.register(taskId, webhookConfig, req.webhookPayloadFormat) }
|
|
@@ -26939,7 +28642,7 @@ function createApiEngine(options = {}) {
|
|
|
26939
28642
|
taskDir,
|
|
26940
28643
|
mode: defaultApiMode(req.mode),
|
|
26941
28644
|
status: "pending",
|
|
26942
|
-
budget: { maxPages: null, maxWallMs: options.batchMaxWallMs ?? null, maxCostUsd: null, maxTokens: null },
|
|
28645
|
+
budget: { maxPages: null, maxWallMs: options.batchMaxWallMs ?? null, maxCostUsd: accessGrant?.budget.perRunUsd ?? null, maxTokens: null },
|
|
26943
28646
|
batch,
|
|
26944
28647
|
...attributionOf(req),
|
|
26945
28648
|
createdAt: now,
|
|
@@ -27078,8 +28781,11 @@ function createApiEngine(options = {}) {
|
|
|
27078
28781
|
if (latest?.status === "running")
|
|
27079
28782
|
await store.putAttempt({ ...latest, status: "cancelled", endedAt: now });
|
|
27080
28783
|
crawlControllers.get(taskId)?.abort();
|
|
27081
|
-
if (!inflight.has(taskId))
|
|
28784
|
+
if (!inflight.has(taskId)) {
|
|
27082
28785
|
await finishJob(task, store);
|
|
28786
|
+
removeCookieSessions(join10(taskRoot, taskId));
|
|
28787
|
+
rmSync(join10(taskRoot, taskId, EGRESS_FILE), { force: true });
|
|
28788
|
+
}
|
|
27083
28789
|
}
|
|
27084
28790
|
} finally {
|
|
27085
28791
|
await store.close();
|
|
@@ -27117,7 +28823,7 @@ function createApiEngine(options = {}) {
|
|
|
27117
28823
|
},
|
|
27118
28824
|
async importLogin(req) {
|
|
27119
28825
|
if (options.hosted === true || (options.sessionsFile ?? null) === null || userChrome === null)
|
|
27120
|
-
throw new LoginsUnavailableError("this server does not save logins: run
|
|
28826
|
+
throw new LoginsUnavailableError("this server does not save logins: run Octocrawl on your own machine (octocrawl serve, the local MCP host, or octocrawl login import)");
|
|
27121
28827
|
try {
|
|
27122
28828
|
loginDomain(req.site);
|
|
27123
28829
|
} catch (error) {
|
|
@@ -27242,25 +28948,32 @@ function createApiEngine(options = {}) {
|
|
|
27242
28948
|
},
|
|
27243
28949
|
retryDelivery: (id) => deliveryStore.replayDeadLetter(id),
|
|
27244
28950
|
async createManagedSession(input) {
|
|
28951
|
+
managedSessionsServed();
|
|
27245
28952
|
const profileDir = join10(taskRoot, "profiles", crypto.randomUUID());
|
|
27246
|
-
return sessionBroker.createManagedSession({ ...input, profileDir });
|
|
28953
|
+
return publicSession(await sessionBroker.createManagedSession({ ...input, profileDir }));
|
|
27247
28954
|
},
|
|
27248
28955
|
async authorizeManagedSession(sessionRef, accountRef) {
|
|
27249
|
-
|
|
28956
|
+
managedSessionsServed();
|
|
28957
|
+
return publicSession(await sessionBroker.markAuthorized(sessionRef, accountRef));
|
|
27250
28958
|
},
|
|
27251
28959
|
async revokeManagedSession(sessionRef) {
|
|
28960
|
+
managedSessionsServed();
|
|
27252
28961
|
await sessionBroker.revoke(sessionRef);
|
|
27253
28962
|
},
|
|
27254
28963
|
async getManagedSession(sessionRef) {
|
|
27255
|
-
|
|
28964
|
+
managedSessionsServed();
|
|
28965
|
+
return publicSession(await sessionBroker.getSession(sessionRef));
|
|
27256
28966
|
},
|
|
27257
28967
|
async renewManagedSession(sessionRef, expiresAt) {
|
|
27258
|
-
|
|
28968
|
+
managedSessionsServed();
|
|
28969
|
+
return publicSession(await sessionBroker.renewExpired(sessionRef, expiresAt));
|
|
27259
28970
|
},
|
|
27260
28971
|
async requestManagedHandoff(sessionRef, reason2, expiresAt) {
|
|
27261
|
-
|
|
28972
|
+
managedSessionsServed();
|
|
28973
|
+
return publicSession(await sessionBroker.requestHandoff(sessionRef, reason2, expiresAt));
|
|
27262
28974
|
},
|
|
27263
28975
|
async captureManagedSession(input) {
|
|
28976
|
+
managedSessionsServed();
|
|
27264
28977
|
const access = await sessionBroker.grant({ ...input, origin: input.url });
|
|
27265
28978
|
if (access.kind !== "granted")
|
|
27266
28979
|
return access;
|
|
@@ -27399,7 +29112,7 @@ function withoutFetchUsage(response) {
|
|
|
27399
29112
|
...timings.modelMs === void 0 ? {} : { modelMs: timings.modelMs },
|
|
27400
29113
|
...timings.totalMs === void 0 ? {} : { totalMs: timings.totalMs }
|
|
27401
29114
|
};
|
|
27402
|
-
return { ...response, usage: { ...usage2, bytesWire: 0, bytesDecompressed: 0, requestCount: 0, attemptCount: 0, browserMs: 0, externalCostUsd:
|
|
29115
|
+
return { ...response, usage: { ...usage2, bytesWire: 0, bytesDecompressed: 0, requestCount: 0, attemptCount: 0, browserMs: 0, externalCostUsd: 0, ...own === void 0 ? {} : { timings: own } } };
|
|
27403
29116
|
}
|
|
27404
29117
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27405
29118
|
function prepareTaskRoot(taskRoot) {
|
|
@@ -27466,7 +29179,7 @@ async function appendedWithoutStep(taskId, store) {
|
|
|
27466
29179
|
return await store.countCompletedSteps(taskId) < batch.urls.length ? { ...task, batch } : null;
|
|
27467
29180
|
}
|
|
27468
29181
|
function activeCrawlOptions(task) {
|
|
27469
|
-
const { formats, includeLinks, includePaths, excludePaths, maxDepth, allowlistedDomains, useCached, regexOnFullURL, ignoreQueryParameters, deduplicateSimilarURLs, crawlEntireDomain, allowSubdomains, allowExternalLinks, sitemap, maxConcurrency, webhook: _webhook, ...page } = task.crawl ?? {};
|
|
29182
|
+
const { formats, includeLinks, includePaths, excludePaths, maxDepth, allowlistedDomains, useCached, regexOnFullURL, ignoreQueryParameters, deduplicateSimilarURLs, crawlEntireDomain, allowSubdomains, allowExternalLinks, sitemap, maxConcurrency, ignoreRobotsTxt, webhook: _webhook, ...page } = task.crawl ?? {};
|
|
27470
29183
|
return {
|
|
27471
29184
|
maxPages: task.budget.maxPages,
|
|
27472
29185
|
maxDepth: maxDepth ?? null,
|
|
@@ -27482,6 +29195,7 @@ function activeCrawlOptions(task) {
|
|
|
27482
29195
|
allowExternalLinks: allowExternalLinks ?? false,
|
|
27483
29196
|
regexOnFullURL: regexOnFullURL ?? false,
|
|
27484
29197
|
maxConcurrency: maxConcurrency ?? null,
|
|
29198
|
+
ignoreRobotsTxt: ignoreRobotsTxt === true,
|
|
27485
29199
|
scrapeOptions: { formats: formats ?? ["markdown"], includeLinks: includeLinks === true, ...page }
|
|
27486
29200
|
};
|
|
27487
29201
|
}
|
|
@@ -27530,7 +29244,7 @@ function handoffRequestOf(step) {
|
|
|
27530
29244
|
return {
|
|
27531
29245
|
reason: HANDOFF_REASONS[blockReason],
|
|
27532
29246
|
liveViewUrl: null,
|
|
27533
|
-
rationale: `${step.url} stopped at a ${blockReason.replace(/_/g, " ")}
|
|
29247
|
+
rationale: `${step.url} stopped at a ${blockReason.replace(/_/g, " ")} Octocrawl does not pass: POST /v1/batches/${step.taskId}/handoff (MCP hand_off_batch, or octocrawl batch --handoff) opens it in your own Chrome, where you get through it, and Octocrawl reads the page there`
|
|
27534
29248
|
};
|
|
27535
29249
|
}
|
|
27536
29250
|
async function stepsOf(store, taskId, kind) {
|
|
@@ -27663,7 +29377,7 @@ function readOnlySessions(store) {
|
|
|
27663
29377
|
}
|
|
27664
29378
|
|
|
27665
29379
|
// packages/api/dist/auth.js
|
|
27666
|
-
import { createHash as
|
|
29380
|
+
import { createHash as createHash14, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
27667
29381
|
function bearerTokenMatcher(tokens) {
|
|
27668
29382
|
const accepted = tokens.map(digest);
|
|
27669
29383
|
return (presented) => {
|
|
@@ -27675,7 +29389,7 @@ function bearerTokenMatcher(tokens) {
|
|
|
27675
29389
|
};
|
|
27676
29390
|
}
|
|
27677
29391
|
function digest(token) {
|
|
27678
|
-
return
|
|
29392
|
+
return createHash14("sha256").update(token, "utf8").digest();
|
|
27679
29393
|
}
|
|
27680
29394
|
|
|
27681
29395
|
// packages/api/dist/jobStream.js
|
|
@@ -27910,7 +29624,7 @@ function createApp(engine, options = {}) {
|
|
|
27910
29624
|
app.use("*", async (c, next) => {
|
|
27911
29625
|
if (c.req.method !== "POST" || !RATE_LIMITED_POSTS.has(c.req.path))
|
|
27912
29626
|
return next();
|
|
27913
|
-
const key = tokens.length === 0 ? "local" :
|
|
29627
|
+
const key = tokens.length === 0 ? "local" : createHash15("sha256").update(presentedToken(c), "utf8").digest("hex");
|
|
27914
29628
|
const retryAfterSeconds = limiter.retryAfterSeconds(key);
|
|
27915
29629
|
if (retryAfterSeconds === null)
|
|
27916
29630
|
return next();
|
|
@@ -28237,7 +29951,13 @@ function createApp(engine, options = {}) {
|
|
|
28237
29951
|
const body = await c.req.json();
|
|
28238
29952
|
if (typeof body.workspaceId !== "string" || typeof body.accountRef !== "string" || typeof body.originScope !== "string")
|
|
28239
29953
|
return fail(c, "invalid_request", "workspaceId, accountRef, and originScope are required");
|
|
28240
|
-
|
|
29954
|
+
try {
|
|
29955
|
+
return c.json(await engine.createManagedSession({ workspaceId: body.workspaceId, accountRef: body.accountRef, originScope: body.originScope, expiresAt: typeof body.expiresAt === "string" ? body.expiresAt : null }), 201);
|
|
29956
|
+
} catch (error) {
|
|
29957
|
+
if (error instanceof SessionsUnavailableError)
|
|
29958
|
+
return fail(c, "conflict", error.message);
|
|
29959
|
+
throw error;
|
|
29960
|
+
}
|
|
28241
29961
|
});
|
|
28242
29962
|
app.post("/v1/sessions/:id/authorize", async (c) => {
|
|
28243
29963
|
const body = await c.req.json();
|
|
@@ -28260,8 +29980,8 @@ function createApp(engine, options = {}) {
|
|
|
28260
29980
|
app.get("/v1/sessions/:id", async (c) => {
|
|
28261
29981
|
try {
|
|
28262
29982
|
return c.json(await engine.getManagedSession(c.req.param("id")), 200);
|
|
28263
|
-
} catch {
|
|
28264
|
-
return fail(c, "not_found", "session not found");
|
|
29983
|
+
} catch (error) {
|
|
29984
|
+
return error instanceof SessionsUnavailableError ? fail(c, "conflict", error.message) : fail(c, "not_found", "session not found");
|
|
28265
29985
|
}
|
|
28266
29986
|
});
|
|
28267
29987
|
app.post("/v1/sessions/:id/renew", async (c) => {
|
|
@@ -28286,7 +30006,13 @@ function createApp(engine, options = {}) {
|
|
|
28286
30006
|
const body = await c.req.json();
|
|
28287
30007
|
if (typeof body.workspaceId !== "string" || typeof body.accountRef !== "string" || typeof body.url !== "string")
|
|
28288
30008
|
return fail(c, "invalid_request", "workspaceId, accountRef, and url are required");
|
|
28289
|
-
|
|
30009
|
+
try {
|
|
30010
|
+
return c.json(await engine.captureManagedSession({ sessionRef: c.req.param("id"), workspaceId: body.workspaceId, accountRef: body.accountRef, url: body.url }), 200);
|
|
30011
|
+
} catch (error) {
|
|
30012
|
+
if (error instanceof SessionsUnavailableError)
|
|
30013
|
+
return fail(c, "conflict", error.message);
|
|
30014
|
+
throw error;
|
|
30015
|
+
}
|
|
28290
30016
|
});
|
|
28291
30017
|
app.post("/fc/v1/scrape", async (c) => {
|
|
28292
30018
|
const req = parseFirecrawlScrapeRequest(await c.req.json());
|
|
@@ -28342,8 +30068,40 @@ function createApp(engine, options = {}) {
|
|
|
28342
30068
|
}
|
|
28343
30069
|
|
|
28344
30070
|
// packages/api/dist/listen.js
|
|
30071
|
+
init_dist2();
|
|
30072
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
28345
30073
|
init_dist();
|
|
28346
30074
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
30075
|
+
function readEgressProxies(env, grant, hosted) {
|
|
30076
|
+
const proxies = egressProxies(env.W2L_EGRESS_PROXIES);
|
|
30077
|
+
if (proxies.length === 0)
|
|
30078
|
+
return [];
|
|
30079
|
+
if (hosted)
|
|
30080
|
+
throw new Error("W2L_EGRESS_PROXIES is refused on a hosted server (ADR 0005: a hosted server connects direct, to addresses it checked)");
|
|
30081
|
+
if (!(grant?.capabilities ?? []).includes("egress_sessions"))
|
|
30082
|
+
throw new Error("W2L_EGRESS_PROXIES needs an access grant that names egress_sessions (ADR 0005; --access-grant or W2L_ACCESS_GRANT)");
|
|
30083
|
+
return proxies;
|
|
30084
|
+
}
|
|
30085
|
+
function readAccessGrant(argv, env, hosted) {
|
|
30086
|
+
const source = (readFlag(argv, "--access-grant") ?? env["W2L_ACCESS_GRANT"] ?? "").trim();
|
|
30087
|
+
if (source === "")
|
|
30088
|
+
return null;
|
|
30089
|
+
let text2;
|
|
30090
|
+
try {
|
|
30091
|
+
text2 = source.startsWith("{") ? source : readFileSync4(source, "utf8");
|
|
30092
|
+
} catch (error) {
|
|
30093
|
+
throw new Error(`access grant: cannot read ${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
30094
|
+
}
|
|
30095
|
+
const grant = accessGrantFromText(text2);
|
|
30096
|
+
if (hosted && grant.tier === "my_browser") {
|
|
30097
|
+
throw new Error("access grant refused: tier my_browser reads the person's own Chrome, which a hosted server does not have");
|
|
30098
|
+
}
|
|
30099
|
+
return grant;
|
|
30100
|
+
}
|
|
30101
|
+
function accessGrantNotice(grant) {
|
|
30102
|
+
const usd2 = (value) => value === null ? "none" : `${value} USD`;
|
|
30103
|
+
return `access grant (ADR 0005): tier ${grant.tier}; ${grant.capabilities.length === 0 ? "no capabilities" : grant.capabilities.join(", ")}; run budget ${usd2(grant.budget.perRunUsd)}; per-request budget ${usd2(grant.budget.perRequestUsd)} (not enforced yet)${grant.attestation === null ? "" : `; accepted by ${grant.attestation.principal}`}`;
|
|
30104
|
+
}
|
|
28347
30105
|
function jobStreamsEnabled(env) {
|
|
28348
30106
|
return (env["W2L_JOB_STREAMS"] ?? "").trim().toLowerCase() !== "off";
|
|
28349
30107
|
}
|
|
@@ -28354,6 +30112,13 @@ function parseListen(argv, env = process.env) {
|
|
|
28354
30112
|
const tokens = readTokens(argv, env);
|
|
28355
30113
|
const rateLimit = parseRateLimit(argv, env);
|
|
28356
30114
|
const workerCount = parseWorkerCount(env);
|
|
30115
|
+
const accessGrant = readAccessGrant(argv, env, hosted);
|
|
30116
|
+
const browserEngine = browserEngineChoice(env, accessGrant, hosted);
|
|
30117
|
+
const engineNotice = browserEngine === "playwright" ? [] : [`browser engine: ${browserEngine} on the public browser rung (ADR 0005 enhanced_browser); saved logins and managed sessions keep stock Playwright`];
|
|
30118
|
+
const compatHosts = compatHostsChoice(env, accessGrant, hosted);
|
|
30119
|
+
const egressList = readEgressProxies(env, accessGrant, hosted);
|
|
30120
|
+
const egressNotice = egressList.length === 0 ? [] : [`egress proxies (ADR 0005 egress_sessions): ${egressList.map((proxy) => proxy.endpoint).join(", ")}; a batch or crawl keeps one for its run and moves on only when the proxy itself fails its probe (at most 2 times), never after a block, a challenge, a 429 or a site's reset; a scrape takes the next healthy one`];
|
|
30121
|
+
const compatNotice = compatHosts.length === 0 ? [] : [`compatible transport (ADR 0005 compatible_transport): ${COMPAT_LIBRARY.name} ${COMPAT_LIBRARY.version}, profile ${DEFAULT_COMPAT_PROFILE}, in place of the http rung for standard-mode pages on ${compatHosts.join(", ")} and their subdomains; a request with custom headers or mobile keeps the http rung`];
|
|
28357
30122
|
if (hosted) {
|
|
28358
30123
|
if (tokens.length === 0) {
|
|
28359
30124
|
throw new Error("hosted mode requires --token, W2L_API_TOKEN or W2L_API_TOKENS");
|
|
@@ -28370,10 +30135,14 @@ function parseListen(argv, env = process.env) {
|
|
|
28370
30135
|
mapMaxLimit: HOSTED_MAP_MAX_LIMIT,
|
|
28371
30136
|
mapMaxTimeoutMs: HOSTED_MAP_MAX_TIMEOUT_MS,
|
|
28372
30137
|
allowRobotsOverride: false,
|
|
28373
|
-
notices: [hostedProxyNotice(env), ...jobStreamsEnabled(env) ? [] : [JOB_STREAMS_OFF_NOTICE]].filter((notice) => notice !== null),
|
|
30138
|
+
notices: [hostedProxyNotice(env), ...jobStreamsEnabled(env) ? [] : [JOB_STREAMS_OFF_NOTICE], ...accessGrant === null ? [] : [accessGrantNotice(accessGrant)]].filter((notice) => notice !== null),
|
|
28374
30139
|
...rateLimit === void 0 ? {} : { rateLimit },
|
|
28375
30140
|
delivery: deliveryConfig("hosted", env),
|
|
28376
|
-
jobStreams: jobStreamsEnabled(env)
|
|
30141
|
+
jobStreams: jobStreamsEnabled(env),
|
|
30142
|
+
accessGrant,
|
|
30143
|
+
browserEngine,
|
|
30144
|
+
compatHosts,
|
|
30145
|
+
egressProxies: egressList
|
|
28377
30146
|
};
|
|
28378
30147
|
}
|
|
28379
30148
|
const networkPolicy = withOperatorContact(withEnvironmentProxy(tunedPolicy(localNetworkPolicy(), env), env), env);
|
|
@@ -28392,10 +30161,14 @@ function parseListen(argv, env = process.env) {
|
|
|
28392
30161
|
mapMaxLimit: MAX_MAP_LIMIT,
|
|
28393
30162
|
mapMaxTimeoutMs: MAX_MAP_TIMEOUT_MS,
|
|
28394
30163
|
allowRobotsOverride: true,
|
|
28395
|
-
notices: [...networkPolicy.egressProxy ? [describeEgressProxy(networkPolicy.egressProxy)] : [], ...jobStreamsEnabled(env) ? [] : [JOB_STREAMS_OFF_NOTICE]],
|
|
30164
|
+
notices: [...networkPolicy.egressProxy ? [describeEgressProxy(networkPolicy.egressProxy)] : [], ...jobStreamsEnabled(env) ? [] : [JOB_STREAMS_OFF_NOTICE], ...accessGrant === null ? [] : [accessGrantNotice(accessGrant)], ...engineNotice, ...compatNotice, ...egressNotice],
|
|
28396
30165
|
...rateLimit === void 0 ? {} : { rateLimit },
|
|
28397
30166
|
delivery: deliveryConfig("local", env),
|
|
28398
|
-
jobStreams: jobStreamsEnabled(env)
|
|
30167
|
+
jobStreams: jobStreamsEnabled(env),
|
|
30168
|
+
accessGrant,
|
|
30169
|
+
browserEngine,
|
|
30170
|
+
compatHosts,
|
|
30171
|
+
egressProxies: egressList
|
|
28399
30172
|
};
|
|
28400
30173
|
}
|
|
28401
30174
|
function deliveryConfig(mode, env = process.env) {
|
|
@@ -28480,12 +30253,18 @@ function readFlag(argv, name) {
|
|
|
28480
30253
|
|
|
28481
30254
|
// packages/api/dist/cli.js
|
|
28482
30255
|
import { serve } from "@hono/node-server";
|
|
28483
|
-
import { readFileSync as
|
|
30256
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
28484
30257
|
import { join as join11 } from "path";
|
|
28485
30258
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
28486
30259
|
async function runApiServer(argv, env = process.env, options = {}) {
|
|
28487
30260
|
const log = options.log ?? ((line) => console.log(line));
|
|
28488
30261
|
const listen = parseListen([...argv], env);
|
|
30262
|
+
if (listen.browserEngine === "patchright") {
|
|
30263
|
+
const engine2 = await loadPatchrightEngine();
|
|
30264
|
+
log(`w2l-api: patchright ${engine2.version ?? "(version unknown)"} loaded`);
|
|
30265
|
+
}
|
|
30266
|
+
if (listen.compatHosts.length > 0)
|
|
30267
|
+
await loadImpit();
|
|
28489
30268
|
const taskRoot = env.W2L_TASK_ROOT ?? ".w2l/api";
|
|
28490
30269
|
const servesLogins = listen.mode === "local" && isLoopbackAuthority(listen.host.includes(":") && !listen.host.startsWith("[") ? `[${listen.host}]` : listen.host, false);
|
|
28491
30270
|
if (listen.mode === "local" && !servesLogins)
|
|
@@ -28502,7 +30281,11 @@ async function runApiServer(argv, env = process.env, options = {}) {
|
|
|
28502
30281
|
// A page a check stopped is handed to the person in their own Chrome by a server that answers them alone.
|
|
28503
30282
|
userChrome: servesLogins ? {} : null,
|
|
28504
30283
|
webhookPolicy: { allowHttpLoopback: listen.delivery.allowHttpLoopback },
|
|
28505
|
-
workerCount: listen.workerCount
|
|
30284
|
+
workerCount: listen.workerCount,
|
|
30285
|
+
accessGrant: listen.accessGrant,
|
|
30286
|
+
browserEngine: listen.browserEngine,
|
|
30287
|
+
compatHosts: listen.compatHosts,
|
|
30288
|
+
egressProxies: listen.egressProxies
|
|
28506
30289
|
});
|
|
28507
30290
|
const app = createApp(engine, { loopbackOnly: servesLogins, tokens: listen.tokens, exposeInternalErrors: listen.mode === "local", jobStreams: listen.jobStreams, ...listen.rateLimit === void 0 ? {} : { rateLimit: listen.rateLimit } });
|
|
28508
30291
|
const deliveryStore = DeliveryStore.open(join11(taskRoot, "section-b-control.sqlite"));
|
|
@@ -28510,7 +30293,7 @@ async function runApiServer(argv, env = process.env, options = {}) {
|
|
|
28510
30293
|
networkPolicy: listen.delivery.networkPolicy,
|
|
28511
30294
|
allowHttpLoopback: listen.delivery.allowHttpLoopback,
|
|
28512
30295
|
...listen.delivery.proxyUrl === void 0 ? {} : { proxyUrl: listen.delivery.proxyUrl },
|
|
28513
|
-
...listen.delivery.caFile === void 0 ? {} : { ca:
|
|
30296
|
+
...listen.delivery.caFile === void 0 ? {} : { ca: readFileSync5(listen.delivery.caFile) }
|
|
28514
30297
|
});
|
|
28515
30298
|
const workerController = new AbortController();
|
|
28516
30299
|
const workerLoop = worker.run(workerController.signal).catch((error) => {
|
|
@@ -28588,6 +30371,7 @@ var KINDS = {
|
|
|
28588
30371
|
allowExternalLinks: "boolean",
|
|
28589
30372
|
includeSubdomains: "boolean",
|
|
28590
30373
|
handoff: "boolean",
|
|
30374
|
+
ignoreRobotsTxt: "boolean",
|
|
28591
30375
|
waitFor: "int",
|
|
28592
30376
|
timeout: "int",
|
|
28593
30377
|
maxFileBytes: "int",
|
|
@@ -28772,8 +30556,8 @@ function usage(command) {
|
|
|
28772
30556
|
}
|
|
28773
30557
|
const synopsis = {
|
|
28774
30558
|
crawl: "usage: octocrawl crawl <url> [options] [--out <dir>] | octocrawl crawl --resume <taskId>",
|
|
28775
|
-
scrape: "usage: octocrawl scrape <url> [options] [--markdown] [--out <dir>] [--handoff]\n\n--handoff: a page a captcha, a challenge or a login wall stops opens in a new tab of your own Chrome\n(remote debugging on at chrome://inspect/#remote-debugging; click Allow); get through it there and click on the page,\nand
|
|
28776
|
-
batch: "usage: octocrawl batch <url>... [--urls-file <file>] [options] [--out <dir>] [--handoff]\n\n--handoff: when the batch ends, each page a captcha, a challenge or a login wall stopped opens in a new tab\nof your own Chrome (remote debugging on at chrome://inspect/#remote-debugging; click Allow once); get through\nit there and
|
|
30559
|
+
scrape: "usage: octocrawl scrape <url> [options] [--markdown] [--out <dir>] [--handoff]\n\n--handoff: a page a captcha, a challenge or a login wall stops opens in a new tab of your own Chrome\n(remote debugging on at chrome://inspect/#remote-debugging; click Allow); get through it there and click on the page,\nand Octocrawl answers with it. Octocrawl passes no check itself.",
|
|
30560
|
+
batch: "usage: octocrawl batch <url>... [--urls-file <file>] [options] [--out <dir>] [--handoff]\n\n--handoff: when the batch ends, each page a captcha, a challenge or a login wall stopped opens in a new tab\nof your own Chrome (remote debugging on at chrome://inspect/#remote-debugging; click Allow once); get through\nit there and Octocrawl reads the page. Octocrawl passes no check itself.",
|
|
28777
30561
|
map: "usage: octocrawl map <url> [options]"
|
|
28778
30562
|
};
|
|
28779
30563
|
const lines = optionKeys(command).map((key) => {
|
|
@@ -28805,7 +30589,7 @@ var LOGIN_USAGE = [
|
|
|
28805
30589
|
"`octocrawl scrape <url> --mode authed` (and the API and MCP in mode authed) read the page signed in as you.",
|
|
28806
30590
|
"First open chrome://inspect/#remote-debugging in Chrome 144 or later and turn on",
|
|
28807
30591
|
'"Allow remote debugging for this browser instance"; Chrome then asks "Allow remote debugging?": click Allow.',
|
|
28808
|
-
"
|
|
30592
|
+
"Octocrawl connects once, reads that site's cookies and the localStorage of its tabs you have open",
|
|
28809
30593
|
"(a site that keeps its login there needs a tab of it open), disconnects, and loads nothing in your tabs.",
|
|
28810
30594
|
"Logins are kept in W2L_SESSIONS_FILE, else ~/.w2l/sessions.json (readable by you alone)."
|
|
28811
30595
|
].join("\n");
|
|
@@ -28890,7 +30674,7 @@ ${LOGIN_USAGE}`);
|
|
|
28890
30674
|
}
|
|
28891
30675
|
|
|
28892
30676
|
// packages/cli/src/run.ts
|
|
28893
|
-
var CLI_VERSION = "0.3.
|
|
30677
|
+
var CLI_VERSION = "0.3.1";
|
|
28894
30678
|
async function runCli(argv, io) {
|
|
28895
30679
|
const [name, ...rest] = argv;
|
|
28896
30680
|
if (name === void 0 || name === "--help" || name === "help") {
|
|
@@ -28918,7 +30702,14 @@ ${usage(null)}`);
|
|
|
28918
30702
|
}
|
|
28919
30703
|
if (command === "batch" && line.cli.urlsFile !== void 0) line.urls.push(...await urlsFrom(line.cli.urlsFile));
|
|
28920
30704
|
const taskRoot = line.cli.taskRoot ?? io.env.W2L_TASK_ROOT ?? ".w2l/cli";
|
|
28921
|
-
|
|
30705
|
+
let listen;
|
|
30706
|
+
try {
|
|
30707
|
+
listen = parseListen([], { ...io.env, W2L_API_HOST: "127.0.0.1" });
|
|
30708
|
+
if (listen.browserEngine === "patchright") await loadPatchrightEngine();
|
|
30709
|
+
if (listen.compatHosts.length > 0) await loadImpit();
|
|
30710
|
+
} catch (error) {
|
|
30711
|
+
throw new UsageError(error instanceof Error ? error.message : String(error));
|
|
30712
|
+
}
|
|
28922
30713
|
for (const notice of listen.notices) io.stderr(`octocrawl: ${notice}`);
|
|
28923
30714
|
const engine = createApiEngine({
|
|
28924
30715
|
taskRoot,
|
|
@@ -28929,7 +30720,11 @@ ${usage(null)}`);
|
|
|
28929
30720
|
userChrome: {},
|
|
28930
30721
|
webhookPolicy: { allowHttpLoopback: listen.delivery.allowHttpLoopback },
|
|
28931
30722
|
workerCount: listen.workerCount,
|
|
28932
|
-
resumeOnStart: false
|
|
30723
|
+
resumeOnStart: false,
|
|
30724
|
+
accessGrant: listen.accessGrant,
|
|
30725
|
+
browserEngine: listen.browserEngine,
|
|
30726
|
+
compatHosts: listen.compatHosts,
|
|
30727
|
+
egressProxies: listen.egressProxies
|
|
28933
30728
|
});
|
|
28934
30729
|
try {
|
|
28935
30730
|
return await runCommand(engine, command, line.urls, line.body, line.cli, io);
|
|
@@ -29025,8 +30820,8 @@ async function finish(engine, kind, taskId, cli, io) {
|
|
|
29025
30820
|
function handoffPrompts(command, io) {
|
|
29026
30821
|
return {
|
|
29027
30822
|
onWaiting: (url, check) => io.stderr(`octocrawl ${command}: ${url} shows a ${check.replace(/_/g, " ")}: get through it in the Chrome tab that opened (click Allow if Chrome asks)`),
|
|
29028
|
-
onConfirm: (url) => io.stderr(`octocrawl ${command}: ${url} shows no check in your Chrome: click on the page if it is the one to read (
|
|
29029
|
-
onHidden: (url) => io.stderr(`octocrawl ${command}: the Chrome tab
|
|
30823
|
+
onConfirm: (url) => io.stderr(`octocrawl ${command}: ${url} shows no check in your Chrome: click on the page if it is the one to read (Octocrawl reads it only once you act in its tab)`),
|
|
30824
|
+
onHidden: (url) => io.stderr(`octocrawl ${command}: the Chrome tab Octocrawl opened for ${url} is not in front: switch to it (clicks in another tab or window are not seen)`),
|
|
29030
30825
|
...io.signal === void 0 ? {} : { signal: io.signal }
|
|
29031
30826
|
};
|
|
29032
30827
|
}
|
|
@@ -29056,8 +30851,8 @@ async function settled(engine, kind, taskId, signal) {
|
|
|
29056
30851
|
}
|
|
29057
30852
|
async function writeOut(dir, pages, report, io) {
|
|
29058
30853
|
await mkdir9(dir, { recursive: true });
|
|
29059
|
-
await
|
|
29060
|
-
if (report !== null) await
|
|
30854
|
+
await writeFile9(join12(dir, "results.jsonl"), pages.map((page) => JSON.stringify(page)).join("\n") + (pages.length > 0 ? "\n" : ""));
|
|
30855
|
+
if (report !== null) await writeFile9(join12(dir, "report.json"), JSON.stringify(report, null, 2));
|
|
29061
30856
|
let markdowns = 0;
|
|
29062
30857
|
let tables = 0;
|
|
29063
30858
|
const rows = [EVIDENCE_COLUMNS.join(",")];
|
|
@@ -29065,17 +30860,17 @@ async function writeOut(dir, pages, report, io) {
|
|
|
29065
30860
|
const stem = `${String(n + 1).padStart(4, "0")}-${slug(page.url ?? page.requestedUrl ?? "")}`;
|
|
29066
30861
|
const markdownFile = typeof page.markdown === "string" ? `${stem}.md` : null;
|
|
29067
30862
|
if (markdownFile !== null) {
|
|
29068
|
-
await
|
|
30863
|
+
await writeFile9(join12(dir, markdownFile), page.markdown);
|
|
29069
30864
|
markdowns++;
|
|
29070
30865
|
}
|
|
29071
30866
|
rows.push(evidenceRow(page, markdownFile).map(csvField).join(","));
|
|
29072
30867
|
for (const table of page.tables ?? []) {
|
|
29073
30868
|
if (table.omitted !== void 0) continue;
|
|
29074
|
-
await
|
|
30869
|
+
await writeFile9(join12(dir, `${stem}.table-${table.tableIndex}.csv`), table.csv);
|
|
29075
30870
|
tables++;
|
|
29076
30871
|
}
|
|
29077
30872
|
}
|
|
29078
|
-
await
|
|
30873
|
+
await writeFile9(join12(dir, "results.csv"), rows.map((row) => `${row}\r
|
|
29079
30874
|
`).join(""));
|
|
29080
30875
|
io.stderr(`octocrawl: wrote ${pages.length} results (results.jsonl, results.csv), ${markdowns} Markdown files and ${tables} CSV tables to ${dir}`);
|
|
29081
30876
|
}
|
|
@@ -29132,11 +30927,11 @@ function slug(url) {
|
|
|
29132
30927
|
return text2.replace(/[^A-Za-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "page";
|
|
29133
30928
|
}
|
|
29134
30929
|
async function urlsFrom(file) {
|
|
29135
|
-
return (await
|
|
30930
|
+
return (await readFile7(file, "utf8")).split(/\r?\n/).map((line) => line.replace(/#.*/, "").trim()).filter((line) => line !== "");
|
|
29136
30931
|
}
|
|
29137
30932
|
async function serve2(argv, io) {
|
|
29138
30933
|
if (argv.includes("--help")) {
|
|
29139
|
-
io.stdout("usage: octocrawl serve [--port <n>] [--host <addr>] [--hosted --token <t>] [--rate-limit-per-minute <n>] [--task-root <dir>]\nRuns the local API (as w2l-api does) on --task-root, else W2L_TASK_ROOT, else .w2l/api, until SIGINT.");
|
|
30934
|
+
io.stdout("usage: octocrawl serve [--port <n>] [--host <addr>] [--hosted --token <t>] [--rate-limit-per-minute <n>] [--task-root <dir>] [--access-grant <file>]\nRuns the local API (as w2l-api does) on --task-root, else W2L_TASK_ROOT, else .w2l/api, until SIGINT. --access-grant (or W2L_ACCESS_GRANT) names what enhanced access may do (ADR 0005).");
|
|
29140
30935
|
return 0;
|
|
29141
30936
|
}
|
|
29142
30937
|
const rest = [...argv];
|