octocrawl 0.3.1 → 0.3.2
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 +1964 -359
- package/package.json +19 -2
package/dist/cli.js
CHANGED
|
@@ -10,14 +10,32 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// packages/contracts/dist/status.js
|
|
13
|
-
var CONTENTFUL_STATUS;
|
|
13
|
+
var RESULT_STATUS, CONTENTFUL_STATUS, BLOCK_REASON;
|
|
14
14
|
var init_status = __esm({
|
|
15
15
|
"packages/contracts/dist/status.js"() {
|
|
16
16
|
"use strict";
|
|
17
|
+
RESULT_STATUS = [
|
|
18
|
+
"success",
|
|
19
|
+
"partial",
|
|
20
|
+
"empty_verified",
|
|
21
|
+
"blocked",
|
|
22
|
+
"failed",
|
|
23
|
+
"cancelled",
|
|
24
|
+
"budget_exceeded",
|
|
25
|
+
"duplicate"
|
|
26
|
+
];
|
|
17
27
|
CONTENTFUL_STATUS = /* @__PURE__ */ new Set([
|
|
18
28
|
"success",
|
|
19
29
|
"partial"
|
|
20
30
|
]);
|
|
31
|
+
BLOCK_REASON = [
|
|
32
|
+
"cloudflare_challenge",
|
|
33
|
+
"captcha",
|
|
34
|
+
"rate_limit",
|
|
35
|
+
"login_wall",
|
|
36
|
+
"geo_restricted",
|
|
37
|
+
"bot_detected_generic"
|
|
38
|
+
];
|
|
21
39
|
}
|
|
22
40
|
});
|
|
23
41
|
|
|
@@ -46,6 +64,21 @@ var init_tokens = __esm({
|
|
|
46
64
|
});
|
|
47
65
|
|
|
48
66
|
// packages/contracts/dist/result.js
|
|
67
|
+
function givenUpPaidCalls(trace) {
|
|
68
|
+
return trace.filter((event) => event.event === "paid_calls").map((event) => {
|
|
69
|
+
const calls = Array.isArray(event.detail?.calls) ? event.detail.calls : [];
|
|
70
|
+
return { ...event, detail: { ...event.detail, calls: calls.map((call) => ({ ...call, answer: false })) } };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function carryPaidCalls(error, event) {
|
|
74
|
+
if (typeof error === "object" && error !== null && Object.isExtensible(error))
|
|
75
|
+
Object.defineProperty(error, "paidCalls", { value: event, enumerable: false, configurable: true });
|
|
76
|
+
return error;
|
|
77
|
+
}
|
|
78
|
+
function paidCallsOfError(error) {
|
|
79
|
+
const event = typeof error === "object" && error !== null ? error.paidCalls : void 0;
|
|
80
|
+
return typeof event === "object" && event !== null && event.event === "paid_calls" ? event : null;
|
|
81
|
+
}
|
|
49
82
|
var init_result = __esm({
|
|
50
83
|
"packages/contracts/dist/result.js"() {
|
|
51
84
|
"use strict";
|
|
@@ -1268,6 +1301,20 @@ function cacheStateOf(trace) {
|
|
|
1268
1301
|
const cachedAt = event.detail?.cachedAt;
|
|
1269
1302
|
return { cacheState: "hit", ...typeof cachedAt === "string" ? { cachedAt } : {} };
|
|
1270
1303
|
}
|
|
1304
|
+
function readAccessChoice(rec, takesMyBrowser) {
|
|
1305
|
+
if (rec.lane !== void 0 && !REQUEST_LANES.includes(rec.lane))
|
|
1306
|
+
throw new RequestError(`lane must be one of: ${REQUEST_LANES.join(", ")}`);
|
|
1307
|
+
if (rec.access === void 0)
|
|
1308
|
+
return rec.lane === void 0 ? {} : { lane: rec.lane };
|
|
1309
|
+
if (!ACCESS_CHOICES.includes(rec.access))
|
|
1310
|
+
throw new RequestError(`access must be one of: ${ACCESS_CHOICES.join(", ")}`);
|
|
1311
|
+
const access = rec.access;
|
|
1312
|
+
if (access === "my-browser" && !takesMyBrowser)
|
|
1313
|
+
throw new RequestError("access my-browser reads pages in your own Chrome, one you name at a time: a crawl does not take it; list the pages and send them as a batch", "unsupported_parameter", { parameters: ["access"] });
|
|
1314
|
+
if (rec.lane !== void 0 && access !== "my-browser")
|
|
1315
|
+
throw new RequestError(`lane my-browser and access ${access} ask for two different routes: send one`, "unsupported_parameter", { parameters: ["lane", "access"] });
|
|
1316
|
+
return access === "my-browser" ? { access, lane: "my-browser" } : { access };
|
|
1317
|
+
}
|
|
1271
1318
|
function warningOf(warnings) {
|
|
1272
1319
|
return warnings === void 0 || warnings.length === 0 ? void 0 : warnings.map((warning) => warning.message).join(" ");
|
|
1273
1320
|
}
|
|
@@ -2224,6 +2271,7 @@ function parseScrapeRequest(body) {
|
|
|
2224
2271
|
if (rec.handoff !== void 0 && typeof rec.handoff !== "boolean" && (rec.handoff === null || typeof rec.handoff !== "object" || Array.isArray(rec.handoff)))
|
|
2225
2272
|
throw new RequestError("handoff must be true or { waitMs }");
|
|
2226
2273
|
const handoff = rec.handoff === void 0 || rec.handoff === false ? void 0 : rec.handoff === true ? {} : parseBatchHandoffRequest(rec.handoff);
|
|
2274
|
+
const choice = readAccessChoice(rec, true);
|
|
2227
2275
|
const mode = readMode(rec.mode);
|
|
2228
2276
|
const page = readPageOptions(rec, mode);
|
|
2229
2277
|
checkMobileMode(mode, page.mobile);
|
|
@@ -2237,6 +2285,7 @@ function parseScrapeRequest(body) {
|
|
|
2237
2285
|
...page,
|
|
2238
2286
|
...robotsOverride === void 0 ? {} : { robotsOverride },
|
|
2239
2287
|
...handoff === void 0 ? {} : { handoff },
|
|
2288
|
+
...choice,
|
|
2240
2289
|
...readAttribution(rec)
|
|
2241
2290
|
};
|
|
2242
2291
|
checkScreenshotViewport(req.mobile, req.formats, req.actions);
|
|
@@ -2273,9 +2322,11 @@ function parseCrawlStartRequest(body) {
|
|
|
2273
2322
|
const idempotencyKey = readIdempotencyKey(rec.idempotencyKey);
|
|
2274
2323
|
const webhook = readWebhook(rec.webhook);
|
|
2275
2324
|
const ignoreRobotsTxt = readBoolean(rec.ignoreRobotsTxt, "ignoreRobotsTxt");
|
|
2325
|
+
const { access } = readAccessChoice(rec, false);
|
|
2276
2326
|
const req = {
|
|
2277
2327
|
url: readUrl(rec.url),
|
|
2278
2328
|
mode,
|
|
2329
|
+
...access === void 0 ? {} : { access },
|
|
2279
2330
|
maxPages: readBound(rec.maxPages, "maxPages", 1),
|
|
2280
2331
|
maxDepth: readBound(rec.maxDepth, "maxDepth", 0),
|
|
2281
2332
|
useCached,
|
|
@@ -2384,6 +2435,9 @@ function parseBatchStartRequest(body) {
|
|
|
2384
2435
|
const mode = readMode(rec.mode);
|
|
2385
2436
|
if (mode === "authed" && webhook !== void 0)
|
|
2386
2437
|
throw new RequestError("webhook is not available in mode 'authed': pages read with your session are not sent to another address; read them from the batch");
|
|
2438
|
+
const choice = readAccessChoice(rec, true);
|
|
2439
|
+
if (choice.lane !== void 0 && webhook !== void 0)
|
|
2440
|
+
throw new RequestError("webhook is not available on lane my-browser: pages read in your own Chrome are not sent to another address; read them from the batch", "unsupported_parameter", { parameters: ["lane", "webhook"] });
|
|
2387
2441
|
const page = readPageOptions(rec, mode);
|
|
2388
2442
|
checkMobileMode(mode, page.mobile);
|
|
2389
2443
|
const req = {
|
|
@@ -2401,6 +2455,7 @@ function parseBatchStartRequest(body) {
|
|
|
2401
2455
|
...idempotencyKey === void 0 ? {} : { idempotencyKey },
|
|
2402
2456
|
...appendToId === void 0 ? {} : { appendToId },
|
|
2403
2457
|
...webhook === void 0 ? {} : { webhook },
|
|
2458
|
+
...choice,
|
|
2404
2459
|
...readAttribution(rec)
|
|
2405
2460
|
};
|
|
2406
2461
|
checkScreenshotViewport(req.mobile, req.formats, req.actions);
|
|
@@ -2437,7 +2492,7 @@ function parseCrawlPageQuery(query) {
|
|
|
2437
2492
|
...query.includeDuplicates === void 0 ? {} : { includeDuplicates: query.includeDuplicates === "true" }
|
|
2438
2493
|
};
|
|
2439
2494
|
}
|
|
2440
|
-
var CRAWL_MODES, DEFAULT_SCRAPE_TIMEOUT_MS, MIN_SCRAPE_TIMEOUT_MS, MAX_WAIT_FOR_MS, MAX_CACHE_AGE_MS, WEBHOOK_EVENTS, MAX_WEBHOOK_URL_LENGTH, MAX_WEBHOOK_HEADERS, MAX_WEBHOOK_HEADERS_BYTES, MAX_WEBHOOK_METADATA_ENTRIES, MAX_WEBHOOK_METADATA_VALUE_LENGTH, MAX_WEBHOOK_METADATA_BYTES, DEFAULT_MAP_LIMIT, MAX_MAP_LIMIT, DEFAULT_MAP_TIMEOUT_MS, MAX_MAP_TIMEOUT_MS, HOSTED_MAP_MAX_LIMIT, HOSTED_MAP_MAX_TIMEOUT_MS, MAP_SEARCH_MAX_CHARS, MAP_SEARCH_MAX_WORDS, IDEMPOTENCY_KEY_HEADERS, MAX_IDEMPOTENCY_KEY_LENGTH, HANDOFF_REASONS, MAX_HANDOFF_WAIT_MS, BATCH_ERRORS_MAX_LIMIT, WS_TOKEN_PROTOCOL_PREFIX, API_ERROR_STATUS, RATE_LIMITED_CODE, RATE_LIMITED_STATUS, RequestError, REFUSAL_HINTS, PAGE_KEYS, ATTRIBUTION_KEYS, SCRAPE_KEYS, CRAWL_SCOPE_KEYS, CRAWL_KEYS, BATCH_SCOPE_NOOP_KEYS, BATCH_KEYS, BATCH_APPEND_KEYS, ROBOTS_OVERRIDE_KEYS, MAP_SCOPE_KEYS, MAP_KEYS, WEBHOOK_KEYS, WEBHOOK_RESERVED_HEADERS, WEBHOOK_SECRET_ENV, WEBHOOK_HEADERS_MESSAGE, WEBHOOK_METADATA_MESSAGE, WEBHOOK_EVENTS_MESSAGE, utf8Bytes, SCHEMA_STRUCTURE, SCHEMA_NUMBERS, SCHEMA_COUNTS, SCHEMA_ANNOTATIONS, SCHEMA_ROOT, SCHEMA_KEYS, SCHEMA_TYPES, PRIMITIVE_TYPES, SCHEMA_DIALECTS, STRING_FORMATS, FORMAT_NAMES, SCREENSHOT_FULL_PAGE_ALIAS, JSON_FORMAT_KEYS, ATTRIBUTES_FORMAT_KEYS, ATTRIBUTE_SELECTOR_KEYS, SCREENSHOT_FORMAT_KEYS, SCREENSHOT_VIEWPORT_KEYS, MIN_SCREENSHOT_VIEWPORT, ATTRIBUTE_NAME, ATTRIBUTES_SELECTORS_MESSAGE, SCREENSHOT_VIEWPORT_MESSAGE, SCREENSHOT_ENTRIES_MESSAGE, FORMAT_ENTRY_MESSAGE, LIST_FORMAT_KEYS, LIST_FIELD_KEYS, LIST_ATTRIBUTE, MAX_REQUEST_HEADERS, MAX_REQUEST_HEADER_VALUE_LENGTH, HEADER_NAME, CREDENTIAL_HEADERS, TRANSPORT_HEADERS, ATTRIBUTION_LABEL, PDF_PARSER_KEYS, ACTION_KEYS;
|
|
2495
|
+
var CRAWL_MODES, DEFAULT_SCRAPE_TIMEOUT_MS, MIN_SCRAPE_TIMEOUT_MS, MAX_WAIT_FOR_MS, MAX_CACHE_AGE_MS, ACCESS_CHOICES, WEBHOOK_EVENTS, MAX_WEBHOOK_URL_LENGTH, MAX_WEBHOOK_HEADERS, MAX_WEBHOOK_HEADERS_BYTES, MAX_WEBHOOK_METADATA_ENTRIES, MAX_WEBHOOK_METADATA_VALUE_LENGTH, MAX_WEBHOOK_METADATA_BYTES, DEFAULT_MAP_LIMIT, MAX_MAP_LIMIT, DEFAULT_MAP_TIMEOUT_MS, MAX_MAP_TIMEOUT_MS, HOSTED_MAP_MAX_LIMIT, HOSTED_MAP_MAX_TIMEOUT_MS, MAP_SEARCH_MAX_CHARS, MAP_SEARCH_MAX_WORDS, IDEMPOTENCY_KEY_HEADERS, MAX_IDEMPOTENCY_KEY_LENGTH, HANDOFF_REASONS, MAX_HANDOFF_WAIT_MS, BATCH_ERRORS_MAX_LIMIT, WS_TOKEN_PROTOCOL_PREFIX, API_ERROR_STATUS, RATE_LIMITED_CODE, RATE_LIMITED_STATUS, RequestError, REFUSAL_HINTS, PAGE_KEYS, ATTRIBUTION_KEYS, SCRAPE_KEYS, REQUEST_LANES, CRAWL_SCOPE_KEYS, CRAWL_KEYS, BATCH_SCOPE_NOOP_KEYS, BATCH_KEYS, BATCH_APPEND_KEYS, ROBOTS_OVERRIDE_KEYS, MAP_SCOPE_KEYS, MAP_KEYS, WEBHOOK_KEYS, WEBHOOK_RESERVED_HEADERS, WEBHOOK_SECRET_ENV, WEBHOOK_HEADERS_MESSAGE, WEBHOOK_METADATA_MESSAGE, WEBHOOK_EVENTS_MESSAGE, utf8Bytes, SCHEMA_STRUCTURE, SCHEMA_NUMBERS, SCHEMA_COUNTS, SCHEMA_ANNOTATIONS, SCHEMA_ROOT, SCHEMA_KEYS, SCHEMA_TYPES, PRIMITIVE_TYPES, SCHEMA_DIALECTS, STRING_FORMATS, FORMAT_NAMES, SCREENSHOT_FULL_PAGE_ALIAS, JSON_FORMAT_KEYS, ATTRIBUTES_FORMAT_KEYS, ATTRIBUTE_SELECTOR_KEYS, SCREENSHOT_FORMAT_KEYS, SCREENSHOT_VIEWPORT_KEYS, MIN_SCREENSHOT_VIEWPORT, ATTRIBUTE_NAME, ATTRIBUTES_SELECTORS_MESSAGE, SCREENSHOT_VIEWPORT_MESSAGE, SCREENSHOT_ENTRIES_MESSAGE, FORMAT_ENTRY_MESSAGE, LIST_FORMAT_KEYS, LIST_FIELD_KEYS, LIST_ATTRIBUTE, MAX_REQUEST_HEADERS, MAX_REQUEST_HEADER_VALUE_LENGTH, HEADER_NAME, CREDENTIAL_HEADERS, TRANSPORT_HEADERS, ATTRIBUTION_LABEL, PDF_PARSER_KEYS, ACTION_KEYS;
|
|
2441
2496
|
var init_api = __esm({
|
|
2442
2497
|
"packages/contracts/dist/api.js"() {
|
|
2443
2498
|
"use strict";
|
|
@@ -2452,6 +2507,7 @@ var init_api = __esm({
|
|
|
2452
2507
|
MIN_SCRAPE_TIMEOUT_MS = 1e3;
|
|
2453
2508
|
MAX_WAIT_FOR_MS = 6e4;
|
|
2454
2509
|
MAX_CACHE_AGE_MS = 31536e7;
|
|
2510
|
+
ACCESS_CHOICES = ["standard", "enhanced", "my-browser"];
|
|
2455
2511
|
WEBHOOK_EVENTS = ["started", "page", "completed", "failed", "cancelled"];
|
|
2456
2512
|
MAX_WEBHOOK_URL_LENGTH = 2048;
|
|
2457
2513
|
MAX_WEBHOOK_HEADERS = 32;
|
|
@@ -2512,11 +2568,12 @@ var init_api = __esm({
|
|
|
2512
2568
|
};
|
|
2513
2569
|
PAGE_KEYS = ["onlyMainContent", "waitFor", "timeout", "maxFileBytes", "includeTags", "excludeTags", "headers", "mobile", "skipTlsVerification", "fastMode", "blockAds", "removeBase64Images", "parsers", "maxAge", "minAge", "storeInCache", "lockdown"];
|
|
2514
2570
|
ATTRIBUTION_KEYS = ["origin", "integration"];
|
|
2515
|
-
SCRAPE_KEYS = ["url", "mode", "allowlistedDomains", "formats", "includeLinks", "debug", "robotsOverride", "actions", "handoff", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2571
|
+
SCRAPE_KEYS = ["url", "mode", "allowlistedDomains", "formats", "includeLinks", "debug", "robotsOverride", "actions", "handoff", "lane", "access", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2572
|
+
REQUEST_LANES = ["my-browser"];
|
|
2516
2573
|
CRAWL_SCOPE_KEYS = ["regexOnFullURL", "ignoreQueryParameters", "deduplicateSimilarURLs", "crawlEntireDomain", "allowSubdomains", "allowExternalLinks"];
|
|
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];
|
|
2574
|
+
CRAWL_KEYS = ["url", "mode", "access", "maxPages", "maxDepth", "useCached", "allowlistedDomains", "formats", "includeLinks", "includePaths", "excludePaths", ...CRAWL_SCOPE_KEYS, "sitemap", "maxConcurrency", "idempotencyKey", "webhook", "ignoreRobotsTxt", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2518
2575
|
BATCH_SCOPE_NOOP_KEYS = { allowExternalLinks: "allowExternalLinks", includeSubdomains: "allowSubdomains" };
|
|
2519
|
-
BATCH_KEYS = ["urls", "mode", "formats", "includeLinks", "robotsOverrides", "maxConcurrency", "ignoreInvalidURLs", "allowExternalLinks", "includeSubdomains", "idempotencyKey", "appendToId", "webhook", "actions", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2576
|
+
BATCH_KEYS = ["urls", "mode", "lane", "access", "formats", "includeLinks", "robotsOverrides", "maxConcurrency", "ignoreInvalidURLs", "allowExternalLinks", "includeSubdomains", "idempotencyKey", "appendToId", "webhook", "actions", ...PAGE_KEYS, ...ATTRIBUTION_KEYS];
|
|
2520
2577
|
BATCH_APPEND_KEYS = ["urls", "appendToId", "ignoreInvalidURLs", "allowExternalLinks", "includeSubdomains", "idempotencyKey", "robotsOverrides", ...ATTRIBUTION_KEYS];
|
|
2521
2578
|
ROBOTS_OVERRIDE_KEYS = ["reason", "recordedBy"];
|
|
2522
2579
|
MAP_SCOPE_KEYS = ["includeSubdomains", "ignoreQueryParameters", "regexOnFullURL", "crawlEntireDomain", "deduplicateSimilarURLs"];
|
|
@@ -2694,8 +2751,16 @@ function wrapJobWebhook(envelope, result) {
|
|
|
2694
2751
|
}
|
|
2695
2752
|
function readShimScrapeOptions(rec, prefix, keys, problems) {
|
|
2696
2753
|
const takesActions = prefix === "";
|
|
2697
|
-
checkShimKeys(rec, prefix, [...keys, "formats", "parsers", "parsePDF", ...SHIM_PAGE_OPTIONS, ...takesActions ? ["actions"] : []], problems);
|
|
2754
|
+
checkShimKeys(rec, prefix, [...keys, "formats", "parsers", "parsePDF", "proxy", ...SHIM_PAGE_OPTIONS, ...takesActions ? ["actions"] : []], problems);
|
|
2698
2755
|
const mapped = {};
|
|
2756
|
+
if (rec.proxy !== void 0) {
|
|
2757
|
+
if (rec.proxy === "basic")
|
|
2758
|
+
mapped.access = "standard";
|
|
2759
|
+
else if (rec.proxy === "stealth" || rec.proxy === "auto" || rec.proxy === "enhanced")
|
|
2760
|
+
mapped.access = "enhanced";
|
|
2761
|
+
else
|
|
2762
|
+
throw new RequestError(`${prefix}proxy must be one of: basic, stealth, auto, enhanced`);
|
|
2763
|
+
}
|
|
2699
2764
|
for (const key of SHIM_PAGE_OPTIONS)
|
|
2700
2765
|
if (rec[key] !== void 0)
|
|
2701
2766
|
mapped[key] = rec[key];
|
|
@@ -3046,7 +3111,12 @@ var init_evidenceRecord = __esm({
|
|
|
3046
3111
|
pageActions: keysOf()(["steps", "scriptRan"]),
|
|
3047
3112
|
pageActionStep: keysOf()(["type", "outcome"]),
|
|
3048
3113
|
requestHeader: keysOf()(["name", "valueSha256"]),
|
|
3049
|
-
access: keysOf()(["route", "executor", "executorVersion", "profile", "externalCostUsd"])
|
|
3114
|
+
access: keysOf()(["route", "executor", "executorVersion", "profile", "externalCostUsd", "completion", "egress", "session", "paidCalls", "grant"]),
|
|
3115
|
+
accessEgress: keysOf()(["proxy", "source", "switchedFrom", "exit"]),
|
|
3116
|
+
accessEgressExit: keysOf()(["ip", "country", "observedAt"]),
|
|
3117
|
+
accessSession: keysOf()(["id"]),
|
|
3118
|
+
accessPaidCall: keysOf()(["provider", "rung", "capabilities", "ceilingUsd", "chargedUsd", "reportedCostUsd", "outcome", "reason", "answer"]),
|
|
3119
|
+
accessGrant: keysOf()(["sha256", "tier", "attestedAt"])
|
|
3050
3120
|
};
|
|
3051
3121
|
}
|
|
3052
3122
|
});
|
|
@@ -3751,11 +3821,15 @@ function classifyGate(res) {
|
|
|
3751
3821
|
if (/<title[^>]*>\s*reddit - prove your humanity\s*<\/title>/.test(lower) && lower.includes("google.com/recaptcha/api.js")) {
|
|
3752
3822
|
return { reason: "captcha", signals: ["reddit_captcha_title", "recaptcha_script"] };
|
|
3753
3823
|
}
|
|
3824
|
+
if (/<title[^>]*>\s*best buy international: select your country/.test(lower) && lower.includes("intl=nosplash")) {
|
|
3825
|
+
return { reason: "geo_restricted", signals: ["bestbuy_country_selector"] };
|
|
3826
|
+
}
|
|
3754
3827
|
const px = perimeterXChallenge(head, lower);
|
|
3755
3828
|
if (px !== null)
|
|
3756
3829
|
return { reason: "captcha", signals: px };
|
|
3830
|
+
const wall = res.status >= 200 && res.status < 300 ? shortWallPage(res.body) : null;
|
|
3757
3831
|
if (contentful)
|
|
3758
|
-
return null;
|
|
3832
|
+
return wall === null ? null : { reason: "bot_detected_generic", signals: wall };
|
|
3759
3833
|
const captcha = matched(lower, CAPTCHA_MARKERS);
|
|
3760
3834
|
if (captcha.length > 0) {
|
|
3761
3835
|
return { reason: "captcha", signals: captcha };
|
|
@@ -3771,6 +3845,8 @@ function classifyGate(res) {
|
|
|
3771
3845
|
signals.push("heading_sign_in");
|
|
3772
3846
|
return { reason: "login_wall", signals };
|
|
3773
3847
|
}
|
|
3848
|
+
if (wall !== null)
|
|
3849
|
+
return { reason: "bot_detected_generic", signals: wall };
|
|
3774
3850
|
const geo = matched(lower, GEO_MARKERS);
|
|
3775
3851
|
if (geo.length > 0) {
|
|
3776
3852
|
return { reason: "geo_restricted", signals: geo };
|
|
@@ -3799,6 +3875,104 @@ function classifyGate(res) {
|
|
|
3799
3875
|
}
|
|
3800
3876
|
return null;
|
|
3801
3877
|
}
|
|
3878
|
+
function shortWallPage(body) {
|
|
3879
|
+
if (!/\b(?:reference|incident|unusual|suspicious)\b/i.test(body))
|
|
3880
|
+
return null;
|
|
3881
|
+
const page = pageText(body);
|
|
3882
|
+
if (page === null)
|
|
3883
|
+
return null;
|
|
3884
|
+
if (AKAMAI_REFERENCE.test(page.text))
|
|
3885
|
+
return ["akamai_reference", "short_page"];
|
|
3886
|
+
if (page.headings.some((h) => UNUSUAL_ACTIVITY.test(h)) && AUTOMATED_TRAFFIC.test(page.text))
|
|
3887
|
+
return ["heading_unusual_activity", "text_automated_traffic", "short_page"];
|
|
3888
|
+
return null;
|
|
3889
|
+
}
|
|
3890
|
+
function pageText(body) {
|
|
3891
|
+
const visible = [];
|
|
3892
|
+
let visibleLength = 0;
|
|
3893
|
+
const headings = [];
|
|
3894
|
+
let heading2 = null;
|
|
3895
|
+
let inHead = false;
|
|
3896
|
+
let at = 0;
|
|
3897
|
+
const text2 = (raw) => {
|
|
3898
|
+
if (raw.length === 0)
|
|
3899
|
+
return;
|
|
3900
|
+
if (heading2 !== null)
|
|
3901
|
+
heading2.parts.push(raw);
|
|
3902
|
+
if (inHead)
|
|
3903
|
+
return;
|
|
3904
|
+
visible.push(raw);
|
|
3905
|
+
visibleLength += collapse(raw).length + 1;
|
|
3906
|
+
};
|
|
3907
|
+
while (at < body.length) {
|
|
3908
|
+
const open = body.indexOf("<", at);
|
|
3909
|
+
if (open < 0) {
|
|
3910
|
+
text2(body.slice(at));
|
|
3911
|
+
break;
|
|
3912
|
+
}
|
|
3913
|
+
text2(body.slice(at, open));
|
|
3914
|
+
if (visibleLength > WALL_PAGE_MAX_TEXT * 4)
|
|
3915
|
+
return null;
|
|
3916
|
+
if (body.startsWith("<!--", open)) {
|
|
3917
|
+
const close = body.indexOf("-->", open + 4);
|
|
3918
|
+
if (close < 0)
|
|
3919
|
+
break;
|
|
3920
|
+
at = close + 3;
|
|
3921
|
+
continue;
|
|
3922
|
+
}
|
|
3923
|
+
const end = body.indexOf(">", open + 1);
|
|
3924
|
+
if (end < 0)
|
|
3925
|
+
break;
|
|
3926
|
+
const tag = /^<(\/?)([a-z][a-z0-9-]*)(?=[\s/>])/i.exec(body.slice(open, Math.min(end + 1, open + 64)));
|
|
3927
|
+
at = end + 1;
|
|
3928
|
+
if (tag === null)
|
|
3929
|
+
continue;
|
|
3930
|
+
const [, closing, tagName] = tag;
|
|
3931
|
+
const name = tagName.toLowerCase();
|
|
3932
|
+
if (name === "head") {
|
|
3933
|
+
inHead = closing === "";
|
|
3934
|
+
continue;
|
|
3935
|
+
}
|
|
3936
|
+
if (name === "body") {
|
|
3937
|
+
inHead = false;
|
|
3938
|
+
continue;
|
|
3939
|
+
}
|
|
3940
|
+
if (HEADING_ELEMENTS.has(name)) {
|
|
3941
|
+
if (closing === "")
|
|
3942
|
+
heading2 = { name, parts: [] };
|
|
3943
|
+
else if (heading2 !== null && heading2.name === name) {
|
|
3944
|
+
headings.push(collapse(decodeEntities(heading2.parts.join(" "))));
|
|
3945
|
+
heading2 = null;
|
|
3946
|
+
}
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
if (closing === "" && HIDDEN_ELEMENTS.has(name) && body[end - 1] !== "/") {
|
|
3950
|
+
const closer = new RegExp(`</${name}`, "gi");
|
|
3951
|
+
closer.lastIndex = at;
|
|
3952
|
+
const close = closer.exec(body)?.index ?? -1;
|
|
3953
|
+
if (close < 0)
|
|
3954
|
+
break;
|
|
3955
|
+
const closeEnd = body.indexOf(">", close);
|
|
3956
|
+
if (closeEnd < 0)
|
|
3957
|
+
break;
|
|
3958
|
+
at = closeEnd + 1;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
const visibleText = collapse(decodeEntities(visible.join(" ")));
|
|
3962
|
+
return visibleText.length > WALL_PAGE_MAX_TEXT ? null : { text: visibleText, headings };
|
|
3963
|
+
}
|
|
3964
|
+
function collapse(text2) {
|
|
3965
|
+
return text2.replace(/\s+/g, " ").trim();
|
|
3966
|
+
}
|
|
3967
|
+
function decodeEntities(text2) {
|
|
3968
|
+
const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
|
|
3969
|
+
return text2.replace(/&(?:#(\d+)|#x([0-9a-f]+)|([a-z]+));/gi, (whole, dec, hex, name) => {
|
|
3970
|
+
const code = dec !== void 0 ? Number(dec) : hex !== void 0 ? parseInt(hex, 16) : null;
|
|
3971
|
+
if (code !== null)
|
|
3972
|
+
return code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
3973
|
+
return named[name.toLowerCase()] ?? whole;
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3802
3976
|
function escalationForBlock(reason2, from) {
|
|
3803
3977
|
const trigger = `blocked:${reason2}`;
|
|
3804
3978
|
const to = nextLane(reason2, from);
|
|
@@ -3828,7 +4002,7 @@ function nextLane(reason2, from) {
|
|
|
3828
4002
|
return from === "browser_proxy" ? null : "browser_proxy";
|
|
3829
4003
|
}
|
|
3830
4004
|
}
|
|
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;
|
|
4005
|
+
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, WALL_PAGE_MAX_TEXT, AKAMAI_REFERENCE, UNUSUAL_ACTIVITY, AUTOMATED_TRAFFIC, HIDDEN_ELEMENTS, HEADING_ELEMENTS;
|
|
3832
4006
|
var init_gate = __esm({
|
|
3833
4007
|
"packages/http-core/dist/gate.js"() {
|
|
3834
4008
|
"use strict";
|
|
@@ -3919,6 +4093,12 @@ var init_gate = __esm({
|
|
|
3919
4093
|
CF_JSD_PATH = "/cdn-cgi/challenge-platform/scripts/jsd/";
|
|
3920
4094
|
PX_CAPTCHA_SCRIPT = /\/captcha\/captcha\.js|captcha\.px-(?:cdn|cloud)\.net\/[^"'\s<>]*captcha\.js/;
|
|
3921
4095
|
PX_CAPTCHA_CONTAINER = /<[a-z][^>]*\sid\s*=\s*["']?px-captcha(?=["'\s>/])/i;
|
|
4096
|
+
WALL_PAGE_MAX_TEXT = 1500;
|
|
4097
|
+
AKAMAI_REFERENCE = /\b(?:reference|incident)\b\s*(?:number|id|no\.?)?\s*[:#]?\s*#?\s*\d{1,3}\.[0-9a-f]{6,8}\.\d{10}\.[0-9a-f]{6,12}\b/i;
|
|
4098
|
+
UNUSUAL_ACTIVITY = /\b(?:unusual|suspicious) (?:activity|traffic)\b/i;
|
|
4099
|
+
AUTOMATED_TRAFFIC = /\bautomated (?:traffic|access|requests|queries|browsing)\b/i;
|
|
4100
|
+
HIDDEN_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "svg"]);
|
|
4101
|
+
HEADING_ELEMENTS = /* @__PURE__ */ new Set(["h1", "h2", "h3", "title"]);
|
|
3922
4102
|
}
|
|
3923
4103
|
});
|
|
3924
4104
|
|
|
@@ -4411,6 +4591,55 @@ var init_provider = __esm({
|
|
|
4411
4591
|
});
|
|
4412
4592
|
|
|
4413
4593
|
// packages/http-core/dist/accessGrant.js
|
|
4594
|
+
function accessGrantRef(grant) {
|
|
4595
|
+
return { sha256: grant.sha256 ?? null, tier: grant.tier, attestedAt: grant.attestation?.at ?? null };
|
|
4596
|
+
}
|
|
4597
|
+
function tariffCeilingUsd(tariff, floorMs = 0) {
|
|
4598
|
+
const sessionMs = Math.max(tariff.maxSessionMs ?? 0, tariff.minBilledMs, tariff.maxSessionMs === null ? 0 : floorMs);
|
|
4599
|
+
const step = tariff.billingIncrementMs > 0 ? tariff.billingIncrementMs : 1;
|
|
4600
|
+
const billedMs = Math.ceil(sessionMs / step) * step;
|
|
4601
|
+
return tariff.perCallUsd + tariff.perHourUsd * billedMs / 36e5;
|
|
4602
|
+
}
|
|
4603
|
+
function readTariff(vendor, input, problems) {
|
|
4604
|
+
const field3 = `tariffs.${vendor}`;
|
|
4605
|
+
if (!isRecord(input)) {
|
|
4606
|
+
problems.push({ field: field3, kind: "invalid", reason: "a tariff is an object of prices and limits" });
|
|
4607
|
+
return null;
|
|
4608
|
+
}
|
|
4609
|
+
const known = /* @__PURE__ */ new Set([...TARIFF_PRICES, ...TARIFF_LIMITS]);
|
|
4610
|
+
if (input.perGbUsd !== void 0 || input.maxBytes !== void 0) {
|
|
4611
|
+
problems.push({ field: `${field3}.perGbUsd`, kind: "invalid", reason: "a per-GB price (a provider's proxy bandwidth) has no ceiling: the bytes its browser receives across every target cannot be counted from here; turn the provider's proxies off and leave perGbUsd and maxBytes out" });
|
|
4612
|
+
}
|
|
4613
|
+
for (const key of Object.keys(input))
|
|
4614
|
+
if (!known.has(key) && key !== "perGbUsd" && key !== "maxBytes")
|
|
4615
|
+
problems.push({ field: `${field3}.${key}`, kind: "invalid", reason: `unknown tariff field; one of ${[...known].join(", ")}` });
|
|
4616
|
+
const price = (key) => {
|
|
4617
|
+
const v = input[key];
|
|
4618
|
+
if (v === void 0 || v === null)
|
|
4619
|
+
return 0;
|
|
4620
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
|
|
4621
|
+
problems.push({ field: `${field3}.${key}`, kind: "invalid", reason: "a non-negative number of US dollars" });
|
|
4622
|
+
return 0;
|
|
4623
|
+
}
|
|
4624
|
+
return v;
|
|
4625
|
+
};
|
|
4626
|
+
const limit = (key) => {
|
|
4627
|
+
const v = input[key];
|
|
4628
|
+
if (v === void 0 || v === null)
|
|
4629
|
+
return null;
|
|
4630
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0) {
|
|
4631
|
+
problems.push({ field: `${field3}.${key}`, kind: "invalid", reason: "a positive whole number" });
|
|
4632
|
+
return null;
|
|
4633
|
+
}
|
|
4634
|
+
return v;
|
|
4635
|
+
};
|
|
4636
|
+
const tariff = { perCallUsd: price("perCallUsd"), perHourUsd: price("perHourUsd"), maxSessionMs: limit("maxSessionMs"), minBilledMs: limit("minBilledMs") ?? 0, billingIncrementMs: limit("billingIncrementMs") ?? 1 };
|
|
4637
|
+
if (TARIFF_PRICES.every((key) => input[key] === void 0 || input[key] === null))
|
|
4638
|
+
problems.push({ field: field3, kind: "invalid", reason: "a tariff names at least one price: perCallUsd or perHourUsd" });
|
|
4639
|
+
if (tariff.perHourUsd > 0 && tariff.maxSessionMs === null)
|
|
4640
|
+
problems.push({ field: `${field3}.maxSessionMs`, kind: "invalid", reason: "a per-hour price needs the longest session a call may hold, or its cost has no ceiling" });
|
|
4641
|
+
return tariff;
|
|
4642
|
+
}
|
|
4414
4643
|
function isRecord(v) {
|
|
4415
4644
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4416
4645
|
}
|
|
@@ -4498,9 +4727,20 @@ function normalizeAccessGrant(input) {
|
|
|
4498
4727
|
reason: `${enhancedOnly.join(", ")} needs an attestation: who accepted it, when, and the statement they accepted`
|
|
4499
4728
|
});
|
|
4500
4729
|
}
|
|
4730
|
+
const tariffsIn = input.tariffs ?? {};
|
|
4731
|
+
const tariffs = {};
|
|
4732
|
+
if (!isRecord(tariffsIn)) {
|
|
4733
|
+
problems.push({ field: "tariffs", kind: "invalid", reason: "tariffs is an object of provider ids to their prices" });
|
|
4734
|
+
} else {
|
|
4735
|
+
for (const [vendor, value] of Object.entries(tariffsIn)) {
|
|
4736
|
+
const tariff = readTariff(vendor, value, problems);
|
|
4737
|
+
if (tariff !== null)
|
|
4738
|
+
tariffs[vendor] = tariff;
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4501
4741
|
if (problems.length > 0)
|
|
4502
4742
|
return { ok: false, problems };
|
|
4503
|
-
return { ok: true, grant: { tier, capabilities, budget: { perRequestUsd, perRunUsd }, scope: { hosts }, attestation } };
|
|
4743
|
+
return { ok: true, grant: { tier, capabilities, budget: { perRequestUsd, perRunUsd }, scope: { hosts }, attestation, tariffs } };
|
|
4504
4744
|
}
|
|
4505
4745
|
function accessGrantFromText(text2) {
|
|
4506
4746
|
let input;
|
|
@@ -4517,14 +4757,17 @@ ${result.problems.map((p) => ` - ${p.field}: ${p.reason}`).join("\n")}`);
|
|
|
4517
4757
|
if (result.grant.scope.hosts !== null) {
|
|
4518
4758
|
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
4759
|
}
|
|
4520
|
-
return result.grant;
|
|
4760
|
+
return { ...result.grant, sha256: sha256Utf8(text2) };
|
|
4521
4761
|
}
|
|
4522
|
-
var ACCESS_TIERS;
|
|
4762
|
+
var ACCESS_TIERS, TARIFF_PRICES, TARIFF_LIMITS;
|
|
4523
4763
|
var init_accessGrant = __esm({
|
|
4524
4764
|
"packages/http-core/dist/accessGrant.js"() {
|
|
4525
4765
|
"use strict";
|
|
4766
|
+
init_hash();
|
|
4526
4767
|
init_vendor();
|
|
4527
4768
|
ACCESS_TIERS = ["standard", "enhanced", "my_browser"];
|
|
4769
|
+
TARIFF_PRICES = ["perCallUsd", "perHourUsd"];
|
|
4770
|
+
TARIFF_LIMITS = ["maxSessionMs", "minBilledMs", "billingIncrementMs"];
|
|
4528
4771
|
}
|
|
4529
4772
|
});
|
|
4530
4773
|
|
|
@@ -4606,7 +4849,7 @@ function evaluateGovernance(url, policy) {
|
|
|
4606
4849
|
if (permitted === void 0) {
|
|
4607
4850
|
return { allowed: false, permittedChannels: [], reason: `unknown mode: ${policy.mode}` };
|
|
4608
4851
|
}
|
|
4609
|
-
return { allowed: true, permittedChannels: permitted, reason: null };
|
|
4852
|
+
return { allowed: true, permittedChannels: policy.enhanced === true && !permitted.includes("provider") ? [...permitted, "provider"] : permitted, reason: null };
|
|
4610
4853
|
}
|
|
4611
4854
|
var MODE_CHANNELS;
|
|
4612
4855
|
var init_governance = __esm({
|
|
@@ -4885,6 +5128,67 @@ var init_sitemap = __esm({
|
|
|
4885
5128
|
}
|
|
4886
5129
|
});
|
|
4887
5130
|
|
|
5131
|
+
// packages/http-core/dist/spend.js
|
|
5132
|
+
function createSpendLedger(capUsd, alreadySettledUsd = 0) {
|
|
5133
|
+
if (capUsd !== null && (!Number.isFinite(capUsd) || capUsd < 0))
|
|
5134
|
+
throw new Error("a spend cap is a non-negative number of US dollars, or null");
|
|
5135
|
+
const ledger = new Ledger(capUsd, null);
|
|
5136
|
+
if (Number.isFinite(alreadySettledUsd) && alreadySettledUsd > 0)
|
|
5137
|
+
ledger.settledUsd = alreadySettledUsd;
|
|
5138
|
+
return ledger;
|
|
5139
|
+
}
|
|
5140
|
+
var EPSILON, Ledger;
|
|
5141
|
+
var init_spend = __esm({
|
|
5142
|
+
"packages/http-core/dist/spend.js"() {
|
|
5143
|
+
"use strict";
|
|
5144
|
+
EPSILON = 1e-9;
|
|
5145
|
+
Ledger = class _Ledger {
|
|
5146
|
+
capUsd;
|
|
5147
|
+
parent;
|
|
5148
|
+
settledUsd = 0;
|
|
5149
|
+
reservedUsd = 0;
|
|
5150
|
+
constructor(capUsd, parent) {
|
|
5151
|
+
this.capUsd = capUsd;
|
|
5152
|
+
this.parent = parent;
|
|
5153
|
+
}
|
|
5154
|
+
fits(amountUsd) {
|
|
5155
|
+
const own = this.capUsd === null || this.settledUsd + this.reservedUsd + amountUsd <= this.capUsd + EPSILON;
|
|
5156
|
+
return own && (this.parent === null || this.parent.fits(amountUsd));
|
|
5157
|
+
}
|
|
5158
|
+
add(field3, amountUsd) {
|
|
5159
|
+
this[field3] += amountUsd;
|
|
5160
|
+
this.parent?.add(field3, amountUsd);
|
|
5161
|
+
}
|
|
5162
|
+
reserve(ceilingUsd) {
|
|
5163
|
+
if (!Number.isFinite(ceilingUsd) || ceilingUsd < 0 || !this.fits(ceilingUsd))
|
|
5164
|
+
return null;
|
|
5165
|
+
this.add("reservedUsd", ceilingUsd);
|
|
5166
|
+
let open = true;
|
|
5167
|
+
return {
|
|
5168
|
+
settle: (reportedUsd) => {
|
|
5169
|
+
if (!open)
|
|
5170
|
+
return 0;
|
|
5171
|
+
open = false;
|
|
5172
|
+
const charged = typeof reportedUsd === "number" && Number.isFinite(reportedUsd) && reportedUsd >= 0 ? reportedUsd : ceilingUsd;
|
|
5173
|
+
this.add("reservedUsd", -ceilingUsd);
|
|
5174
|
+
this.add("settledUsd", charged);
|
|
5175
|
+
return charged;
|
|
5176
|
+
},
|
|
5177
|
+
release: () => {
|
|
5178
|
+
if (!open)
|
|
5179
|
+
return;
|
|
5180
|
+
open = false;
|
|
5181
|
+
this.add("reservedUsd", -ceilingUsd);
|
|
5182
|
+
}
|
|
5183
|
+
};
|
|
5184
|
+
}
|
|
5185
|
+
child(capUsd) {
|
|
5186
|
+
return new _Ledger(capUsd, this);
|
|
5187
|
+
}
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
5190
|
+
});
|
|
5191
|
+
|
|
4888
5192
|
// packages/http-core/dist/index.js
|
|
4889
5193
|
var init_dist2 = __esm({
|
|
4890
5194
|
"packages/http-core/dist/index.js"() {
|
|
@@ -4903,6 +5207,7 @@ var init_dist2 = __esm({
|
|
|
4903
5207
|
init_ledger();
|
|
4904
5208
|
init_execution2();
|
|
4905
5209
|
init_sitemap();
|
|
5210
|
+
init_spend();
|
|
4906
5211
|
}
|
|
4907
5212
|
});
|
|
4908
5213
|
|
|
@@ -6208,12 +6513,12 @@ function collectLinkDetails(html2, baseUrl) {
|
|
|
6208
6513
|
doc.close();
|
|
6209
6514
|
return [...byUrl.values()];
|
|
6210
6515
|
}
|
|
6211
|
-
function
|
|
6516
|
+
function collapse2(text2) {
|
|
6212
6517
|
return (text2 ?? "").replace(/\s+/g, " ").trim();
|
|
6213
6518
|
}
|
|
6214
6519
|
function anchorText(a) {
|
|
6215
|
-
const imageAlt = qsa(a, "img[alt]").map((img) =>
|
|
6216
|
-
for (const candidate of [
|
|
6520
|
+
const imageAlt = qsa(a, "img[alt]").map((img) => collapse2(img.getAttribute("alt"))).find((alt) => alt.length > 0);
|
|
6521
|
+
for (const candidate of [collapse2(a.textContent), collapse2(a.getAttribute("aria-label")), collapse2(a.getAttribute("title")), imageAlt ?? ""]) {
|
|
6217
6522
|
if (candidate.length === 0)
|
|
6218
6523
|
continue;
|
|
6219
6524
|
const points = Array.from(candidate);
|
|
@@ -8211,7 +8516,7 @@ function pruneTree(doc, options = {}) {
|
|
|
8211
8516
|
detachAll(namedBy(doc, options.selectors ?? []));
|
|
8212
8517
|
void tagOf;
|
|
8213
8518
|
}
|
|
8214
|
-
var MANUALLY_CLEANED, CLEANED_SELECTOR, CMP_ID_TOKENS, CMP_CLASS_TOKENS, AD_TOKENS, RECOMMENDATION_HEADINGS, RECOMMENDATION_TOKENS, NEVER_SHOWN, FORM_CONTENT_MIN_TEXT, FORM_CONTROLS;
|
|
8519
|
+
var MANUALLY_CLEANED, IN_CONTENT_NAVIGATION, SITE_HIDDEN, CLEANED_SELECTOR, CMP_ID_TOKENS, CMP_CLASS_TOKENS, AD_TOKENS, RECOMMENDATION_HEADINGS, RECOMMENDATION_TOKENS, NEVER_SHOWN, FORM_CONTENT_MIN_TEXT, FORM_CONTROLS;
|
|
8215
8520
|
var init_prune = __esm({
|
|
8216
8521
|
"packages/extract-tf/dist/prune.js"() {
|
|
8217
8522
|
"use strict";
|
|
@@ -8244,7 +8549,9 @@ var init_prune = __esm({
|
|
|
8244
8549
|
// filing keeps off the page.
|
|
8245
8550
|
"ix\\:header"
|
|
8246
8551
|
];
|
|
8247
|
-
|
|
8552
|
+
IN_CONTENT_NAVIGATION = ['[role="navigation"]', "#p-lang-btn", ".mw-portlet", ".vector-menu"];
|
|
8553
|
+
SITE_HIDDEN = [".mw-hidden-catlinks"];
|
|
8554
|
+
CLEANED_SELECTOR = [...MANUALLY_CLEANED, ...IN_CONTENT_NAVIGATION, ...SITE_HIDDEN].join(",");
|
|
8248
8555
|
CMP_ID_TOKENS = [
|
|
8249
8556
|
"cookie",
|
|
8250
8557
|
"consent",
|
|
@@ -8709,19 +9016,19 @@ function extractListRecords(html2, url, spec, page = 1, budget2 = { records: MAX
|
|
|
8709
9016
|
doc.close();
|
|
8710
9017
|
return records;
|
|
8711
9018
|
}
|
|
8712
|
-
function resolveListSpec(html2,
|
|
8713
|
-
if (
|
|
8714
|
-
return { spec: { type: "list", itemSelector:
|
|
8715
|
-
if (lastResolved !== null && lastResolved.html === html2 && lastResolved.request ===
|
|
9019
|
+
function resolveListSpec(html2, request8) {
|
|
9020
|
+
if (request8.itemSelector !== void 0 && request8.fields !== void 0)
|
|
9021
|
+
return { spec: { type: "list", itemSelector: request8.itemSelector, fields: request8.fields } };
|
|
9022
|
+
if (lastResolved !== null && lastResolved.html === html2 && lastResolved.request === request8)
|
|
8716
9023
|
return lastResolved.resolved;
|
|
8717
|
-
const resolved = detectSpec(html2,
|
|
8718
|
-
lastResolved = { html: html2, request:
|
|
9024
|
+
const resolved = detectSpec(html2, request8);
|
|
9025
|
+
lastResolved = { html: html2, request: request8, resolved };
|
|
8719
9026
|
return resolved;
|
|
8720
9027
|
}
|
|
8721
|
-
function detectSpec(html2,
|
|
8722
|
-
if (
|
|
8723
|
-
const fields = detectFields(html2,
|
|
8724
|
-
return { spec: { type: "list", itemSelector:
|
|
9028
|
+
function detectSpec(html2, request8) {
|
|
9029
|
+
if (request8.itemSelector !== void 0) {
|
|
9030
|
+
const fields = detectFields(html2, request8.itemSelector) ?? [];
|
|
9031
|
+
return { spec: { type: "list", itemSelector: request8.itemSelector, fields }, detected: { fields, alternatives: [] } };
|
|
8725
9032
|
}
|
|
8726
9033
|
const [best, ...rest] = detectLists(html2);
|
|
8727
9034
|
if (best === void 0)
|
|
@@ -10173,10 +10480,10 @@ function collectAmazonProductFacts(doc, url, declared3) {
|
|
|
10173
10480
|
const offersRaw = jsonNode?.offers;
|
|
10174
10481
|
const offer = record(Array.isArray(offersRaw) ? offersRaw[0] : offersRaw);
|
|
10175
10482
|
const titleSelector = qs(doc, "#productTitle") ? "#productTitle" : "#title";
|
|
10176
|
-
const
|
|
10483
|
+
const pageText2 = text(doc, "body") ?? "";
|
|
10177
10484
|
const documentTitle = clean(text(doc, "title")) ?? "";
|
|
10178
|
-
const subscriptionPlan =
|
|
10179
|
-
const subscription = subscriptionPlan !== null || /\bsubscription plan\b/i.test(
|
|
10485
|
+
const subscriptionPlan = pageText2.match(/\bPlan:\s*(Blink\s+(?:AI\s+)?(?:basic|plus))\b/i)?.[1] ?? documentTitle.match(/\b(Blink\s+(?:AI\s+)?(?:basic|plus))\s+plan\b/i)?.[1] ?? null;
|
|
10486
|
+
const subscription = subscriptionPlan !== null || /\bsubscription plan\b/i.test(pageText2) && /\bBilling:\s*(?:Monthly|Annual)\b/i.test(pageText2);
|
|
10180
10487
|
const extractedVariants = variants(doc);
|
|
10181
10488
|
const identity = verifyProductIdentity(doc, url, extractedVariants);
|
|
10182
10489
|
if (!identity.identityMatch) {
|
|
@@ -10206,7 +10513,7 @@ function collectAmazonProductFacts(doc, url, declared3) {
|
|
|
10206
10513
|
const store = first(doc, ["#bylineInfo", '[data-feature-name="bylineInfo"]']);
|
|
10207
10514
|
const brand = store === null ? null : domFact(store.value.replace(/^Brand:\s*/i, "").replace(/^Visit the\s+/i, "").replace(/\s+Store$/i, ""), store.selector);
|
|
10208
10515
|
const sellerHit = first(doc, ["#sellerProfileTriggerId", "#merchant-info", '.tabular-buybox-text[tabular-attribute-name="Sold by"]', "#merchantInfoFeature_feature_div .offer-display-feature-text-message"]);
|
|
10209
|
-
const seller = sellerHit === null ? /\bSold\s*by\s*Blink\b/i.test(
|
|
10516
|
+
const seller = sellerHit === null ? /\bSold\s*by\s*Blink\b/i.test(pageText2) || /SoldbyBlink/i.test(pageText2.replace(/\s+/g, "")) ? domFact("Blink", "body:Sold by") : null : domFact(sellerHit.value, sellerHit.selector);
|
|
10210
10517
|
const availabilityHit = first(doc, ["#availability span", "#availability", "#outOfStock"]);
|
|
10211
10518
|
const deliveryHit = deliveryLocation(doc);
|
|
10212
10519
|
const ratingRaw = attr(doc, "#acrPopover", "title") ?? text(doc, "#acrPopover .a-icon-alt") ?? text(doc, '[data-hook="rating-out-of-text"]');
|
|
@@ -10710,7 +11017,7 @@ function dublinCore(metas) {
|
|
|
10710
11017
|
for (const [field3, name] of DC_FIELDS) {
|
|
10711
11018
|
const value = metaNamed(metas, name);
|
|
10712
11019
|
if (value !== null)
|
|
10713
|
-
out[field3] =
|
|
11020
|
+
out[field3] = collapse3(value);
|
|
10714
11021
|
}
|
|
10715
11022
|
return out;
|
|
10716
11023
|
}
|
|
@@ -10741,7 +11048,7 @@ function metaProperties(metas, name) {
|
|
|
10741
11048
|
continue;
|
|
10742
11049
|
const value = declared(attribute(meta, "content"));
|
|
10743
11050
|
if (value !== null)
|
|
10744
|
-
values.push(
|
|
11051
|
+
values.push(collapse3(value));
|
|
10745
11052
|
}
|
|
10746
11053
|
}
|
|
10747
11054
|
return values;
|
|
@@ -10753,7 +11060,7 @@ function resolvedUrl(value, baseUrl) {
|
|
|
10753
11060
|
return value;
|
|
10754
11061
|
}
|
|
10755
11062
|
}
|
|
10756
|
-
function
|
|
11063
|
+
function collapse3(value) {
|
|
10757
11064
|
return value.replace(ASCII_WHITESPACE, " ");
|
|
10758
11065
|
}
|
|
10759
11066
|
function pageTitle(document2) {
|
|
@@ -10891,11 +11198,45 @@ function showsDeclaredProduct(main, product) {
|
|
|
10891
11198
|
const price = product.price.value.replace(/[^\d.,]/g, "");
|
|
10892
11199
|
return text2.includes(product.name.value.replace(/\s+/g, " ").trim()) && price !== "" && text2.includes(price);
|
|
10893
11200
|
}
|
|
10894
|
-
|
|
11201
|
+
function headingsOnly(region) {
|
|
11202
|
+
const isHeading = (el) => /^h[1-6]$/.test(el.tagName.toLowerCase());
|
|
11203
|
+
let headingChars = 0;
|
|
11204
|
+
let prose3 = 0;
|
|
11205
|
+
const stack = [{ node: region, inHeading: isHeading(region) }];
|
|
11206
|
+
while (stack.length > 0) {
|
|
11207
|
+
const { node, inHeading } = stack.pop();
|
|
11208
|
+
for (const child of Array.from(node.childNodes)) {
|
|
11209
|
+
if (child.nodeType === 3) {
|
|
11210
|
+
const chars = (child.textContent ?? "").replace(/\s+/g, " ").trim().length;
|
|
11211
|
+
if (inHeading)
|
|
11212
|
+
headingChars += chars;
|
|
11213
|
+
else
|
|
11214
|
+
prose3 += chars;
|
|
11215
|
+
if (prose3 >= MIN_PROSE_CHARS || headingChars >= HEADINGS_ONLY_MAX_CHARS)
|
|
11216
|
+
return false;
|
|
11217
|
+
continue;
|
|
11218
|
+
}
|
|
11219
|
+
if (child.nodeType !== 1)
|
|
11220
|
+
continue;
|
|
11221
|
+
const el = child;
|
|
11222
|
+
const tag = el.tagName.toLowerCase();
|
|
11223
|
+
if (tag === "script" || tag === "style" || tag === "template" || tag === "noscript")
|
|
11224
|
+
continue;
|
|
11225
|
+
if (tag === "a" && (el.getAttribute("href") ?? "").startsWith("#"))
|
|
11226
|
+
continue;
|
|
11227
|
+
if (!inHeading && tag === "img" && linkTarget(el.getAttribute("src") ?? "", null) !== null)
|
|
11228
|
+
return false;
|
|
11229
|
+
stack.push({ node: el, inHeading: inHeading || isHeading(el) });
|
|
11230
|
+
}
|
|
11231
|
+
}
|
|
11232
|
+
return true;
|
|
11233
|
+
}
|
|
11234
|
+
var DEFAULT_CLASSIFY, BUY_BOX_CONFIDENCE, DESCRIPTION_MIN_CHARS, ExtractTf, extractTf, HEADINGS_ONLY_MAX_CHARS, MIN_PROSE_CHARS;
|
|
10895
11235
|
var init_extract = __esm({
|
|
10896
11236
|
"packages/extract-tf/dist/extract.js"() {
|
|
10897
11237
|
"use strict";
|
|
10898
11238
|
init_dom();
|
|
11239
|
+
init_markdown();
|
|
10899
11240
|
init_prune();
|
|
10900
11241
|
init_render();
|
|
10901
11242
|
init_selectors();
|
|
@@ -11009,9 +11350,11 @@ var init_extract = __esm({
|
|
|
11009
11350
|
baseUrl,
|
|
11010
11351
|
metadata,
|
|
11011
11352
|
confidence: selected ? 1 : confidenceOf(blocks.filter((b) => main?.contains(b.el)).length, main, blocks.length, mainLength, decision.type, productShown, favorPrecision, favorRecall, product),
|
|
11012
|
-
// Escalate
|
|
11013
|
-
//
|
|
11014
|
-
|
|
11353
|
+
// Escalate when a strategy produced nothing at all, or the article
|
|
11354
|
+
// cascade a region that only names the page (headingsOnly; a region the
|
|
11355
|
+
// last resort found is a list's). Routing to a non-article strategy is
|
|
11356
|
+
// not by itself an escalation reason.
|
|
11357
|
+
escalate: main === null || strategy === "article" && headingsOnly(main),
|
|
11015
11358
|
...lastResort ? { lastResort: true } : {},
|
|
11016
11359
|
pageType: decision.type,
|
|
11017
11360
|
strategy,
|
|
@@ -11035,6 +11378,8 @@ var init_extract = __esm({
|
|
|
11035
11378
|
}
|
|
11036
11379
|
};
|
|
11037
11380
|
extractTf = new ExtractTf();
|
|
11381
|
+
HEADINGS_ONLY_MAX_CHARS = 100;
|
|
11382
|
+
MIN_PROSE_CHARS = 3;
|
|
11038
11383
|
}
|
|
11039
11384
|
});
|
|
11040
11385
|
|
|
@@ -11043,7 +11388,7 @@ var EXTRACTOR_VERSION;
|
|
|
11043
11388
|
var init_version = __esm({
|
|
11044
11389
|
"packages/extract-tf/dist/version.js"() {
|
|
11045
11390
|
"use strict";
|
|
11046
|
-
EXTRACTOR_VERSION = "extract-tf/
|
|
11391
|
+
EXTRACTOR_VERSION = "extract-tf/16";
|
|
11047
11392
|
}
|
|
11048
11393
|
});
|
|
11049
11394
|
|
|
@@ -11972,7 +12317,7 @@ var init_errorPage = __esm({
|
|
|
11972
12317
|
// packages/bench/dist/egress.js
|
|
11973
12318
|
import { lookup as lookup2 } from "dns/promises";
|
|
11974
12319
|
import { isIP as isIP3 } from "net";
|
|
11975
|
-
import { Agent as Agent2, Pool, ProxyAgent } from "undici";
|
|
12320
|
+
import { Agent as Agent2, Pool, ProxyAgent, request as request5 } from "undici";
|
|
11976
12321
|
function defaultNetworkPolicy() {
|
|
11977
12322
|
return localNetworkPolicy();
|
|
11978
12323
|
}
|
|
@@ -12086,6 +12431,16 @@ function createGuardedDispatcher(policy, resolve4 = lookup2, tls = {}) {
|
|
|
12086
12431
|
maxHeaderSize: MAX_RESPONSE_HEADER_BYTES
|
|
12087
12432
|
});
|
|
12088
12433
|
}
|
|
12434
|
+
async function fetchThroughProxy(server, url, timeoutMs = 1e4, maxBytes = 64 * 1024) {
|
|
12435
|
+
const dispatcher = createProxyDispatcher(server);
|
|
12436
|
+
try {
|
|
12437
|
+
const answer = await request5(url, { method: "GET", dispatcher, signal: AbortSignal.timeout(timeoutMs), headers: { accept: "application/json, text/plain;q=0.9" } });
|
|
12438
|
+
const body = await readCappedBody(answer.body, maxBytes);
|
|
12439
|
+
return { status: answer.statusCode, body: Buffer.from(body).toString("utf8") };
|
|
12440
|
+
} finally {
|
|
12441
|
+
await dispatcher.close();
|
|
12442
|
+
}
|
|
12443
|
+
}
|
|
12089
12444
|
function createProxyDispatcher(server, tls = {}) {
|
|
12090
12445
|
return new ProxyAgent({
|
|
12091
12446
|
uri: server.url,
|
|
@@ -12354,7 +12709,7 @@ var init_robotsLookup = __esm({
|
|
|
12354
12709
|
let pending = this.pending.get(key);
|
|
12355
12710
|
if (pending === void 0) {
|
|
12356
12711
|
const controller2 = new AbortController();
|
|
12357
|
-
const
|
|
12712
|
+
const request8 = (async () => {
|
|
12358
12713
|
const scope = createExecutionScope({ signal: controller2.signal, deadlineAt: Date.now() + (this.networkPolicy.robotsTimeoutMs ?? ROBOTS_TIMEOUT_MS) });
|
|
12359
12714
|
let entry3 = { robotsUrl, robots: null, sha256: null, absent: false };
|
|
12360
12715
|
try {
|
|
@@ -12433,10 +12788,10 @@ var init_robotsLookup = __esm({
|
|
|
12433
12788
|
this.byOrigin.set(key, { entry: entry3, expiresAt });
|
|
12434
12789
|
return entry3;
|
|
12435
12790
|
})();
|
|
12436
|
-
pending = { promise:
|
|
12791
|
+
pending = { promise: request8, controller: controller2, users: 0 };
|
|
12437
12792
|
this.pending.set(key, pending);
|
|
12438
12793
|
const current = pending;
|
|
12439
|
-
void
|
|
12794
|
+
void request8.finally(() => {
|
|
12440
12795
|
if (this.pending.get(key) === current)
|
|
12441
12796
|
this.pending.delete(key);
|
|
12442
12797
|
}).catch(() => {
|
|
@@ -12497,6 +12852,123 @@ var init_robotsLookup = __esm({
|
|
|
12497
12852
|
}
|
|
12498
12853
|
});
|
|
12499
12854
|
|
|
12855
|
+
// packages/bench/dist/browserSettle.js
|
|
12856
|
+
async function waitForRenderedStability(page, options = {}) {
|
|
12857
|
+
const maxMs = options.maxMs ?? 1500;
|
|
12858
|
+
const minMs = Math.min(options.minMs ?? 500, maxMs);
|
|
12859
|
+
const sampleMs = options.sampleMs ?? 100;
|
|
12860
|
+
const started = Date.now();
|
|
12861
|
+
const deadline = started + maxMs;
|
|
12862
|
+
const loadingDeadline = started + Math.max(maxMs, options.loadingMaxMs ?? maxMs);
|
|
12863
|
+
let observedSince = Date.now();
|
|
12864
|
+
let previous = "";
|
|
12865
|
+
let stableRounds = 0;
|
|
12866
|
+
let loading = false;
|
|
12867
|
+
let loadingSeen = false;
|
|
12868
|
+
const extends_ = loadingDeadline > deadline;
|
|
12869
|
+
const outcome = () => ({ loadingSeen, stillLoading: loading, waitedMs: Date.now() - started });
|
|
12870
|
+
const bound = () => extends_ && loading ? loadingDeadline : deadline;
|
|
12871
|
+
while (Date.now() < bound()) {
|
|
12872
|
+
let snapshot;
|
|
12873
|
+
try {
|
|
12874
|
+
snapshot = await page.evaluate(`(() => {
|
|
12875
|
+
const root = document.documentElement
|
|
12876
|
+
const body = document.body
|
|
12877
|
+
return JSON.stringify({
|
|
12878
|
+
size: root?.outerHTML.length ?? 0,
|
|
12879
|
+
text: body?.innerText ?? '',
|
|
12880
|
+
loading: ${LOADING_PROBE},
|
|
12881
|
+
})
|
|
12882
|
+
})()`);
|
|
12883
|
+
} catch (error) {
|
|
12884
|
+
if (!isNavigationError(error))
|
|
12885
|
+
throw error;
|
|
12886
|
+
observedSince = Date.now();
|
|
12887
|
+
previous = "";
|
|
12888
|
+
stableRounds = 0;
|
|
12889
|
+
if (extends_) {
|
|
12890
|
+
loading = true;
|
|
12891
|
+
loadingSeen = true;
|
|
12892
|
+
}
|
|
12893
|
+
await page.waitForTimeout(Math.min(sampleMs, Math.max(1, bound() - Date.now())));
|
|
12894
|
+
continue;
|
|
12895
|
+
}
|
|
12896
|
+
const current = typeof snapshot === "string" ? snapshot : JSON.stringify(snapshot);
|
|
12897
|
+
loading = isLoading(current);
|
|
12898
|
+
loadingSeen ||= loading;
|
|
12899
|
+
if (current === previous)
|
|
12900
|
+
stableRounds++;
|
|
12901
|
+
else
|
|
12902
|
+
stableRounds = 0;
|
|
12903
|
+
previous = current;
|
|
12904
|
+
if (Date.now() - observedSince >= minMs && stableRounds >= 2 && !(extends_ && loading))
|
|
12905
|
+
return outcome();
|
|
12906
|
+
await page.waitForTimeout(Math.min(sampleMs, Math.max(1, bound() - Date.now())));
|
|
12907
|
+
}
|
|
12908
|
+
return outcome();
|
|
12909
|
+
}
|
|
12910
|
+
function loadingWaitEvent(outcome, at, lane) {
|
|
12911
|
+
if (outcome === void 0 || !outcome.loadingSeen)
|
|
12912
|
+
return null;
|
|
12913
|
+
return { at, lane, event: "loading_wait", detail: { waitedMs: outcome.waitedMs, cleared: !outcome.stillLoading } };
|
|
12914
|
+
}
|
|
12915
|
+
function withStillLoadingWarning(result) {
|
|
12916
|
+
if (result.status !== "success" && result.status !== "partial")
|
|
12917
|
+
return result;
|
|
12918
|
+
const waited = result.trace.find((event) => event.event === "loading_wait" && event.detail?.cleared === false);
|
|
12919
|
+
if (waited === void 0)
|
|
12920
|
+
return result;
|
|
12921
|
+
const seconds = Math.round(Number(waited.detail?.waitedMs ?? 0) / 100) / 10;
|
|
12922
|
+
const warning = { code: "page_still_loading", message: `The page still showed a loading indicator when it was read, after ${seconds} s: its data may not be in this answer.` };
|
|
12923
|
+
return { ...result, warnings: [...result.warnings ?? [], warning] };
|
|
12924
|
+
}
|
|
12925
|
+
function isLoading(snapshot) {
|
|
12926
|
+
try {
|
|
12927
|
+
return JSON.parse(snapshot).loading === true;
|
|
12928
|
+
} catch {
|
|
12929
|
+
return false;
|
|
12930
|
+
}
|
|
12931
|
+
}
|
|
12932
|
+
function isNavigationError(error) {
|
|
12933
|
+
return error instanceof Error && /Execution context was destroyed|because of a navigation|page is navigating and changing the content|Cannot find context with specified id/i.test(error.message);
|
|
12934
|
+
}
|
|
12935
|
+
var LOADING_PAGE_TEXT_MAX, LOADING_PROBE, LOADING_WAIT_MAX_MS;
|
|
12936
|
+
var init_browserSettle = __esm({
|
|
12937
|
+
"packages/bench/dist/browserSettle.js"() {
|
|
12938
|
+
"use strict";
|
|
12939
|
+
LOADING_PAGE_TEXT_MAX = 4e3;
|
|
12940
|
+
LOADING_PROBE = `(() => {
|
|
12941
|
+
try {
|
|
12942
|
+
const body = document.body
|
|
12943
|
+
if (body === null || (body.innerText ?? '').length > ${LOADING_PAGE_TEXT_MAX}) return false
|
|
12944
|
+
const shown = (el) => {
|
|
12945
|
+
const box = el.getBoundingClientRect()
|
|
12946
|
+
if (box.width < 1 || box.height < 1 || box.right <= 0 || box.bottom <= 0) return false
|
|
12947
|
+
if (typeof el.checkVisibility === 'function') return el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
|
|
12948
|
+
const style = getComputedStyle(el)
|
|
12949
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
|
12950
|
+
}
|
|
12951
|
+
for (const el of body.querySelectorAll('[aria-busy="true"]')) if (shown(el)) return true
|
|
12952
|
+
const message = /^(?:(?:loading|fetching|please wait|one moment|searching|retrieving)(?:\\s+[\\w-]+){0,3}\\s*(?:\\.{2,3}|\\u2026)|loading|fetching|please wait)$/i
|
|
12953
|
+
const trailing = /\\b(?:loading|fetching)\\s*(?:\\.{2,3}|\\u2026)$/i
|
|
12954
|
+
// 4 is NodeFilter.SHOW_TEXT, named so that a page replacing NodeFilter cannot break the probe.
|
|
12955
|
+
const walker = document.createTreeWalker(body, 4)
|
|
12956
|
+
let seen = 0
|
|
12957
|
+
for (let node = walker.nextNode(); node !== null && seen < 20000; node = walker.nextNode(), seen++) {
|
|
12958
|
+
const text = (node.textContent ?? '').trim()
|
|
12959
|
+
if (text.length === 0 || text.length > 48 || !(message.test(text) || trailing.test(text))) continue
|
|
12960
|
+
const parent = node.parentElement
|
|
12961
|
+
if (parent !== null && parent.closest('button, a') === null && shown(parent)) return true
|
|
12962
|
+
}
|
|
12963
|
+
return false
|
|
12964
|
+
} catch {
|
|
12965
|
+
return false
|
|
12966
|
+
}
|
|
12967
|
+
})()`;
|
|
12968
|
+
LOADING_WAIT_MAX_MS = 8e3;
|
|
12969
|
+
}
|
|
12970
|
+
});
|
|
12971
|
+
|
|
12500
12972
|
// packages/bench/dist/routing/identity.js
|
|
12501
12973
|
function identityCompromised(trace) {
|
|
12502
12974
|
return trace.some((t) => t.event === "identity_mismatch" || t.event === "identity_unobserved");
|
|
@@ -12552,6 +13024,7 @@ var init_provider2 = __esm({
|
|
|
12552
13024
|
init_robotsLookup();
|
|
12553
13025
|
init_identity();
|
|
12554
13026
|
init_errorPage();
|
|
13027
|
+
init_browserSettle();
|
|
12555
13028
|
defaultRobotsFetcher = robotsFetcherVia();
|
|
12556
13029
|
ProviderSubject = class {
|
|
12557
13030
|
provider;
|
|
@@ -12585,7 +13058,7 @@ var init_provider2 = __esm({
|
|
|
12585
13058
|
async fetch(url, deadlineMs, signal, onRetryAfter, options = {}) {
|
|
12586
13059
|
const scope = createExecutionScope({ signal, deadlineAt: deadlineMs, onRetryAfter });
|
|
12587
13060
|
try {
|
|
12588
|
-
return await this.fetchWithinBudget(url, scope, options);
|
|
13061
|
+
return withStillLoadingWarning(await this.fetchWithinBudget(url, scope, options));
|
|
12589
13062
|
} finally {
|
|
12590
13063
|
scope.dispose();
|
|
12591
13064
|
}
|
|
@@ -12658,6 +13131,9 @@ var init_provider2 = __esm({
|
|
|
12658
13131
|
try {
|
|
12659
13132
|
res = await raceWithSignal(this.transport.fetch(url, execution.deadlineAt, execution.signal), execution.signal);
|
|
12660
13133
|
fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13134
|
+
const waited = loadingWaitEvent(res.settle, Date.now() - start, "provider");
|
|
13135
|
+
if (waited !== null)
|
|
13136
|
+
trace.push(waited);
|
|
12661
13137
|
if (res.status === 429 || res.status === 503) {
|
|
12662
13138
|
const delay = parseRetryAfterMs(res.headers["retry-after"] ?? null);
|
|
12663
13139
|
if (delay !== null)
|
|
@@ -13139,7 +13615,8 @@ CREATE TABLE IF NOT EXISTS attempts (
|
|
|
13139
13615
|
content_tokens_unknown INTEGER NOT NULL DEFAULT 0,
|
|
13140
13616
|
budget_exceeded TEXT,
|
|
13141
13617
|
recovered_from_attempt_id TEXT,
|
|
13142
|
-
discovery_json TEXT
|
|
13618
|
+
discovery_json TEXT,
|
|
13619
|
+
charged_usd REAL
|
|
13143
13620
|
);
|
|
13144
13621
|
|
|
13145
13622
|
CREATE TABLE IF NOT EXISTS steps (
|
|
@@ -13164,6 +13641,21 @@ CREATE INDEX IF NOT EXISTS steps_attempt ON steps(attempt_id);
|
|
|
13164
13641
|
CREATE INDEX IF NOT EXISTS steps_task_created_id ON steps(task_id, created_at, id);
|
|
13165
13642
|
CREATE INDEX IF NOT EXISTS steps_task_status_created_id ON steps(task_id, status, created_at, id);
|
|
13166
13643
|
CREATE INDEX IF NOT EXISTS attempts_task ON attempts(task_id);
|
|
13644
|
+
|
|
13645
|
+
CREATE TABLE IF NOT EXISTS list_pages (
|
|
13646
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
13647
|
+
canonical_url TEXT NOT NULL,
|
|
13648
|
+
step INTEGER NOT NULL,
|
|
13649
|
+
page INTEGER NOT NULL,
|
|
13650
|
+
page_url TEXT NOT NULL,
|
|
13651
|
+
html TEXT NOT NULL,
|
|
13652
|
+
state TEXT NOT NULL,
|
|
13653
|
+
items TEXT,
|
|
13654
|
+
item_refs TEXT,
|
|
13655
|
+
count INTEGER,
|
|
13656
|
+
created_at TEXT NOT NULL,
|
|
13657
|
+
PRIMARY KEY (task_id, canonical_url, step, page)
|
|
13658
|
+
);
|
|
13167
13659
|
`;
|
|
13168
13660
|
var SqliteTaskStore = class _SqliteTaskStore {
|
|
13169
13661
|
db;
|
|
@@ -13202,6 +13694,10 @@ var SqliteTaskStore = class _SqliteTaskStore {
|
|
|
13202
13694
|
this.db.exec("ALTER TABLE attempts ADD COLUMN discovery_json TEXT");
|
|
13203
13695
|
} catch {
|
|
13204
13696
|
}
|
|
13697
|
+
try {
|
|
13698
|
+
this.db.exec("ALTER TABLE attempts ADD COLUMN charged_usd REAL");
|
|
13699
|
+
} catch {
|
|
13700
|
+
}
|
|
13205
13701
|
try {
|
|
13206
13702
|
this.db.exec("ALTER TABLE tasks ADD COLUMN batch_json TEXT");
|
|
13207
13703
|
} catch {
|
|
@@ -13260,9 +13756,9 @@ var SqliteTaskStore = class _SqliteTaskStore {
|
|
|
13260
13756
|
throw new Error(`putAttempt: unknown task ${attempt.taskId}`);
|
|
13261
13757
|
}
|
|
13262
13758
|
this.db.prepare(`INSERT INTO attempts (
|
|
13263
|
-
id, task_id, status, started_at, ended_at, pages_fetched, wall_ms, cost_usd, cost_unknown, content_tokens, content_tokens_unknown, budget_exceeded, recovered_from_attempt_id, discovery_json
|
|
13759
|
+
id, task_id, status, started_at, ended_at, pages_fetched, wall_ms, cost_usd, cost_unknown, content_tokens, content_tokens_unknown, budget_exceeded, recovered_from_attempt_id, discovery_json, charged_usd
|
|
13264
13760
|
) VALUES (
|
|
13265
|
-
@id, @task_id, @status, @started_at, @ended_at, @pages_fetched, @wall_ms, @cost_usd, @cost_unknown, @content_tokens, @content_tokens_unknown, @budget_exceeded, @recovered_from_attempt_id, @discovery_json
|
|
13761
|
+
@id, @task_id, @status, @started_at, @ended_at, @pages_fetched, @wall_ms, @cost_usd, @cost_unknown, @content_tokens, @content_tokens_unknown, @budget_exceeded, @recovered_from_attempt_id, @discovery_json, @charged_usd
|
|
13266
13762
|
)
|
|
13267
13763
|
ON CONFLICT(id) DO UPDATE SET
|
|
13268
13764
|
task_id = excluded.task_id,
|
|
@@ -13277,7 +13773,8 @@ var SqliteTaskStore = class _SqliteTaskStore {
|
|
|
13277
13773
|
content_tokens_unknown = excluded.content_tokens_unknown,
|
|
13278
13774
|
budget_exceeded = excluded.budget_exceeded,
|
|
13279
13775
|
recovered_from_attempt_id = excluded.recovered_from_attempt_id,
|
|
13280
|
-
discovery_json = excluded.discovery_json
|
|
13776
|
+
discovery_json = excluded.discovery_json,
|
|
13777
|
+
charged_usd = excluded.charged_usd`).run({
|
|
13281
13778
|
id: attempt.id,
|
|
13282
13779
|
task_id: attempt.taskId,
|
|
13283
13780
|
status: attempt.status,
|
|
@@ -13291,7 +13788,8 @@ var SqliteTaskStore = class _SqliteTaskStore {
|
|
|
13291
13788
|
content_tokens_unknown: attempt.contentTokensUnknown === true ? 1 : 0,
|
|
13292
13789
|
budget_exceeded: attempt.budgetExceeded,
|
|
13293
13790
|
recovered_from_attempt_id: attempt.recoveredFromAttemptId ?? null,
|
|
13294
|
-
discovery_json: attempt.discovery === void 0 || attempt.discovery === null ? null : JSON.stringify(attempt.discovery)
|
|
13791
|
+
discovery_json: attempt.discovery === void 0 || attempt.discovery === null ? null : JSON.stringify(attempt.discovery),
|
|
13792
|
+
charged_usd: attempt.chargedUsd ?? null
|
|
13295
13793
|
});
|
|
13296
13794
|
}
|
|
13297
13795
|
async getAttempt(attemptId) {
|
|
@@ -13411,6 +13909,20 @@ var SqliteTaskStore = class _SqliteTaskStore {
|
|
|
13411
13909
|
LIMIT 1`).get(taskId, canonicalUrl);
|
|
13412
13910
|
return row === void 0 ? null : stepFromRow(row);
|
|
13413
13911
|
}
|
|
13912
|
+
async putPageRead(taskId, canonicalUrl, page) {
|
|
13913
|
+
assertId("taskId", taskId);
|
|
13914
|
+
this.db.prepare(`INSERT INTO list_pages (task_id, canonical_url, step, page, page_url, html, state, items, item_refs, count, created_at)
|
|
13915
|
+
VALUES (@task_id, @canonical_url, @step, @page, @page_url, @html, @state, @items, @item_refs, @count, @created_at)
|
|
13916
|
+
ON CONFLICT(task_id, canonical_url, step, page) DO UPDATE SET
|
|
13917
|
+
page_url = excluded.page_url, html = excluded.html, state = excluded.state, items = excluded.items, item_refs = excluded.item_refs, count = excluded.count, created_at = excluded.created_at`).run({ task_id: taskId, canonical_url: canonicalUrl, step: page.step, page: page.page, page_url: page.url, html: page.html, state: page.state, items: page.items, item_refs: page.itemRefs, count: page.count, created_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13918
|
+
}
|
|
13919
|
+
async listPagesRead(taskId, canonicalUrl) {
|
|
13920
|
+
const rows = this.db.prepare(`SELECT step, page, page_url, html, state, items, item_refs, count FROM list_pages WHERE task_id = ? AND canonical_url = ? ORDER BY step, page`).all(taskId, canonicalUrl);
|
|
13921
|
+
return rows.map((row) => ({ step: row.step, page: row.page, url: row.page_url, html: row.html, state: row.state, items: row.items, itemRefs: row.item_refs, count: row.count }));
|
|
13922
|
+
}
|
|
13923
|
+
async clearPagesRead(taskId, canonicalUrl) {
|
|
13924
|
+
this.db.prepare(`DELETE FROM list_pages WHERE task_id = ? AND canonical_url = ?`).run(taskId, canonicalUrl);
|
|
13925
|
+
}
|
|
13414
13926
|
async close() {
|
|
13415
13927
|
this.db.close();
|
|
13416
13928
|
}
|
|
@@ -13445,7 +13957,8 @@ function attemptFromRow(row) {
|
|
|
13445
13957
|
...row.content_tokens_unknown === 1 ? { contentTokensUnknown: true } : {},
|
|
13446
13958
|
budgetExceeded: row.budget_exceeded,
|
|
13447
13959
|
...row.recovered_from_attempt_id ? { recoveredFromAttemptId: row.recovered_from_attempt_id } : {},
|
|
13448
|
-
...row.discovery_json ? { discovery: JSON.parse(row.discovery_json) } : {}
|
|
13960
|
+
...row.discovery_json ? { discovery: JSON.parse(row.discovery_json) } : {},
|
|
13961
|
+
...typeof row.charged_usd === "number" ? { chargedUsd: row.charged_usd } : {}
|
|
13449
13962
|
};
|
|
13450
13963
|
}
|
|
13451
13964
|
function stepFromRow(row) {
|
|
@@ -14036,6 +14549,10 @@ var CrawlOrchestrator = class {
|
|
|
14036
14549
|
let cachedPages = 0;
|
|
14037
14550
|
let costUsd = 0;
|
|
14038
14551
|
let costUnknown = false;
|
|
14552
|
+
let spend;
|
|
14553
|
+
let priorChargedUsd = 0;
|
|
14554
|
+
let chargedUsd = 0;
|
|
14555
|
+
let unboundedCost = false;
|
|
14039
14556
|
let contentTokens = 0;
|
|
14040
14557
|
let contentTokensUnknown = false;
|
|
14041
14558
|
let budgetExceeded = null;
|
|
@@ -14048,6 +14565,10 @@ var CrawlOrchestrator = class {
|
|
|
14048
14565
|
wallMs: this.clock.now() - startedAtMs,
|
|
14049
14566
|
costUsd: costUnknown ? null : costUsd,
|
|
14050
14567
|
costUnknown,
|
|
14568
|
+
// What the ledger settled and holds reserved in this run, calls of pages never written (interrupted, cut) and
|
|
14569
|
+
// reservations in flight included, so a crash between steps leaves the next run no room it did not have; at least
|
|
14570
|
+
// what the written pages were charged, costs reported outside the ledger included.
|
|
14571
|
+
...spend === void 0 ? {} : { chargedUsd: Math.max(chargedUsd, spend.settledUsd + spend.reservedUsd - priorChargedUsd) },
|
|
14051
14572
|
contentTokens,
|
|
14052
14573
|
contentTokensUnknown,
|
|
14053
14574
|
budgetExceeded,
|
|
@@ -14070,6 +14591,11 @@ var CrawlOrchestrator = class {
|
|
|
14070
14591
|
const opened = await this.openRun(spec, startedAt);
|
|
14071
14592
|
task = opened.task;
|
|
14072
14593
|
attempt = opened.attempt;
|
|
14594
|
+
if (spec.budget.maxCostUsd !== null || (spec.budget.maxCostPerPageUsd ?? null) !== null) {
|
|
14595
|
+
const openedId = attempt.id;
|
|
14596
|
+
priorChargedUsd = (await this.store.listAttempts(task.id)).filter((earlier) => earlier.id !== openedId).reduce((sum, earlier) => sum + (earlier.chargedUsd ?? 0), 0);
|
|
14597
|
+
spend = createSpendLedger(spec.budget.maxCostUsd, priorChargedUsd);
|
|
14598
|
+
}
|
|
14073
14599
|
const stored = task.batch === void 0 ? task.crawl : void 0;
|
|
14074
14600
|
if (task.batch === void 0)
|
|
14075
14601
|
discovery = { ...EMPTY_CRAWL_DISCOVERY };
|
|
@@ -14162,8 +14688,8 @@ var CrawlOrchestrator = class {
|
|
|
14162
14688
|
stopping = true;
|
|
14163
14689
|
break;
|
|
14164
14690
|
}
|
|
14165
|
-
const spent = { wallMs: now - startedAtMs, costUsd, costUnknown, tokens: contentTokens, tokensUnknown: contentTokensUnknown };
|
|
14166
|
-
const hit = budgetHit(spec.budget, spent);
|
|
14691
|
+
const spent = { wallMs: now - startedAtMs, costUsd, costUnknown, chargedUsd: priorChargedUsd + chargedUsd, unboundedCost, tokens: contentTokens, tokensUnknown: contentTokensUnknown };
|
|
14692
|
+
const hit = budgetHit(spec.budget, spent, spend);
|
|
14167
14693
|
if (hit !== null) {
|
|
14168
14694
|
budgetExceeded = hit;
|
|
14169
14695
|
if (hit === "time")
|
|
@@ -14208,6 +14734,7 @@ var CrawlOrchestrator = class {
|
|
|
14208
14734
|
let links;
|
|
14209
14735
|
let audit;
|
|
14210
14736
|
let cachedPage = false;
|
|
14737
|
+
let pageLedger;
|
|
14211
14738
|
if (reusable && cached.result !== null) {
|
|
14212
14739
|
result = cached.result;
|
|
14213
14740
|
links = linksOf(cached.result);
|
|
@@ -14216,12 +14743,29 @@ var CrawlOrchestrator = class {
|
|
|
14216
14743
|
} else {
|
|
14217
14744
|
const scrapeStartedAt = Date.now();
|
|
14218
14745
|
let outcome;
|
|
14746
|
+
const pagesRead = listsPages(runningTask) ? await this.store.listPagesRead(runningTask.id, item.canonicalUrl) : [];
|
|
14747
|
+
pageLedger = spend?.child(spec.budget.maxCostPerPageUsd ?? null);
|
|
14748
|
+
const pageSpend = pageLedger === void 0 ? {} : { spend: pageLedger };
|
|
14749
|
+
const pageScope = !listsPages(runningTask) ? { ...scope, ...pageSpend } : {
|
|
14750
|
+
...scope,
|
|
14751
|
+
...pageSpend,
|
|
14752
|
+
onListPage: (page) => {
|
|
14753
|
+
void this.store.putPageRead(runningTask.id, item.canonicalUrl, page).catch(() => {
|
|
14754
|
+
});
|
|
14755
|
+
},
|
|
14756
|
+
...pagesRead.length === 0 ? {} : { listResume: { pages: pagesRead } }
|
|
14757
|
+
};
|
|
14219
14758
|
try {
|
|
14220
|
-
outcome = await raceWithSignal(this.atom.scrape(item.url,
|
|
14759
|
+
outcome = await raceWithSignal(this.atom.scrape(item.url, pageScope), scope.signal);
|
|
14221
14760
|
} catch (error) {
|
|
14222
14761
|
if (stopped())
|
|
14223
14762
|
throw error;
|
|
14224
14763
|
outcome = { result: scrapeErrorResult(item.url, error, Date.now() - scrapeStartedAt, this.scrapeErrorCostUsd), links: [] };
|
|
14764
|
+
const paid = paidCallsOfError(error);
|
|
14765
|
+
if (paid !== null)
|
|
14766
|
+
outcome = { ...outcome, result: { ...outcome.result, trace: [...outcome.result.trace, paid] } };
|
|
14767
|
+
if (pageLedger !== void 0)
|
|
14768
|
+
outcome = { ...outcome, result: { ...outcome.result, usage: { ...outcome.result.usage, externalCostChargedUsd: pageLedger.settledUsd } } };
|
|
14225
14769
|
}
|
|
14226
14770
|
result = outcome.result;
|
|
14227
14771
|
links = outcome.links.length > 0 ? outcome.links : linksOf(outcome.result);
|
|
@@ -14269,6 +14813,8 @@ var CrawlOrchestrator = class {
|
|
|
14269
14813
|
pagesInFlight--;
|
|
14270
14814
|
const step = { id: this.newId(), taskId: runningTask.id, attemptId: runningAttempt.id, url: item.url, canonicalUrl: item.canonicalUrl, depth: item.depth, status: stepStatusFromResult(result.status), lane: result.lane, contentHash: result.evidence.rawBodySha256, cached: cachedPage, result, audit, createdAt: at, updatedAt: at };
|
|
14271
14815
|
await this.store.putStep(step);
|
|
14816
|
+
if (listsPages(runningTask) && !stoppedAtCheck(result))
|
|
14817
|
+
await this.store.clearPagesRead(runningTask.id, item.canonicalUrl);
|
|
14272
14818
|
taskUrls.add(item.canonicalUrl);
|
|
14273
14819
|
if (reserved) {
|
|
14274
14820
|
newPagesReserved--;
|
|
@@ -14279,6 +14825,10 @@ var CrawlOrchestrator = class {
|
|
|
14279
14825
|
else
|
|
14280
14826
|
pagesFetched += 1;
|
|
14281
14827
|
if (!cachedPage) {
|
|
14828
|
+
const tried = audit?.summary.attempts ?? [{ result }];
|
|
14829
|
+
const reported2 = result.usage.externalCostChargedUsd ?? tried.reduce((sum, { result: attempt2 }) => sum + (attempt2.usage.externalCostUsd ?? 0), 0);
|
|
14830
|
+
chargedUsd += Math.max(reported2, pageLedger?.settledUsd ?? 0);
|
|
14831
|
+
unboundedCost ||= tried.some(({ result: attempt2 }) => attempt2.usage.externalCostUsd === null && attempt2.usage.externalCostChargedUsd === void 0);
|
|
14282
14832
|
const meter = audit?.summary;
|
|
14283
14833
|
if (meter !== void 0) {
|
|
14284
14834
|
costUsd += meter.externalCost.knownSubtotal;
|
|
@@ -14491,6 +15041,12 @@ var CrawlOrchestrator = class {
|
|
|
14491
15041
|
return { ...record3, sources: loaded.sources, files: loaded.files, listed: loaded.urls.length, enqueued: offered.enqueued, truncated: loaded.truncated };
|
|
14492
15042
|
}
|
|
14493
15043
|
};
|
|
15044
|
+
function stoppedAtCheck(result) {
|
|
15045
|
+
return result.status === "blocked" && (result.actions?.lists ?? []).some((list2) => list2.stoppedBy === "challenge");
|
|
15046
|
+
}
|
|
15047
|
+
function listsPages(task) {
|
|
15048
|
+
return (task.batch?.actions ?? []).some((action) => action.type === "paginate");
|
|
15049
|
+
}
|
|
14494
15050
|
function seedBatchUrls(frontier, urls, completed) {
|
|
14495
15051
|
for (const url of urls) {
|
|
14496
15052
|
const canonical = canonicalizeUrl(url);
|
|
@@ -14580,13 +15136,20 @@ function absoluteHref(href, base) {
|
|
|
14580
15136
|
return href;
|
|
14581
15137
|
}
|
|
14582
15138
|
}
|
|
14583
|
-
function budgetHit(budget2, spent) {
|
|
15139
|
+
function budgetHit(budget2, spent, spend) {
|
|
14584
15140
|
if (budget2.maxWallMs !== null && spent.wallMs >= budget2.maxWallMs)
|
|
14585
15141
|
return "time";
|
|
14586
|
-
if (budget2.maxCostUsd !== null &&
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
15142
|
+
if (budget2.maxCostUsd !== null && spend !== void 0) {
|
|
15143
|
+
if (spent.unboundedCost)
|
|
15144
|
+
return "cost_unknown";
|
|
15145
|
+
if (Math.max(spent.chargedUsd, spend.settledUsd) + spend.reservedUsd >= budget2.maxCostUsd)
|
|
15146
|
+
return "cost";
|
|
15147
|
+
} else {
|
|
15148
|
+
if (budget2.maxCostUsd !== null && spent.costUnknown)
|
|
15149
|
+
return "cost_unknown";
|
|
15150
|
+
if (budget2.maxCostUsd !== null && spent.costUsd >= budget2.maxCostUsd)
|
|
15151
|
+
return "cost";
|
|
15152
|
+
}
|
|
14590
15153
|
if (budget2.maxTokens !== null && spent.tokensUnknown)
|
|
14591
15154
|
return "tokens_unknown";
|
|
14592
15155
|
if (budget2.maxTokens !== null && spent.tokens >= budget2.maxTokens)
|
|
@@ -14663,7 +15226,8 @@ function duplicateResult(url, { html: _html, rawHtml: _rawHtml, images: _images,
|
|
|
14663
15226
|
requestCount: prior.usage.requestCount,
|
|
14664
15227
|
attemptCount: prior.usage.attemptCount,
|
|
14665
15228
|
browserMs: prior.usage.browserMs,
|
|
14666
|
-
externalCostUsd: prior.usage.externalCostUsd
|
|
15229
|
+
externalCostUsd: prior.usage.externalCostUsd,
|
|
15230
|
+
...prior.usage.externalCostChargedUsd === void 0 ? {} : { externalCostChargedUsd: prior.usage.externalCostChargedUsd }
|
|
14667
15231
|
},
|
|
14668
15232
|
trace: [
|
|
14669
15233
|
...prior.trace,
|
|
@@ -16262,8 +16826,8 @@ var IdempotencyStore = class _IdempotencyStore {
|
|
|
16262
16826
|
this.db.close();
|
|
16263
16827
|
}
|
|
16264
16828
|
};
|
|
16265
|
-
function requestFingerprint(
|
|
16266
|
-
return createHash4("sha256").update(canonicalJson(
|
|
16829
|
+
function requestFingerprint(request8) {
|
|
16830
|
+
return createHash4("sha256").update(canonicalJson(request8)).digest("hex");
|
|
16267
16831
|
}
|
|
16268
16832
|
function canonicalJson(value) {
|
|
16269
16833
|
if (Array.isArray(value))
|
|
@@ -16286,7 +16850,7 @@ init_dist();
|
|
|
16286
16850
|
init_dist3();
|
|
16287
16851
|
init_dist2();
|
|
16288
16852
|
var EVERY_HOP_LANES = /* @__PURE__ */ new Set(["http"]);
|
|
16289
|
-
function toEvidenceRecord(result,
|
|
16853
|
+
function toEvidenceRecord(result, request8, output, options = {}) {
|
|
16290
16854
|
const { evidence } = result;
|
|
16291
16855
|
const httpStatus = typeof evidence.httpStatus === "number" && evidence.httpStatus >= 100 && evidence.httpStatus <= 599 ? evidence.httpStatus : null;
|
|
16292
16856
|
const requested = httpStatus !== null || result.usage.requestCount > 0;
|
|
@@ -16321,7 +16885,7 @@ function toEvidenceRecord(result, request7, output, options = {}) {
|
|
|
16321
16885
|
proxy: evidence.envProxy ?? null,
|
|
16322
16886
|
identity: {
|
|
16323
16887
|
userAgent,
|
|
16324
|
-
mode: result.compliance?.mode ?? identityEvent(result.trace)?.mode ??
|
|
16888
|
+
mode: result.compliance?.mode ?? identityEvent(result.trace)?.mode ?? request8.mode,
|
|
16325
16889
|
contact: userAgent === null ? null : declaredContact(userAgent),
|
|
16326
16890
|
device: requested ? declaredDevice(result) : null,
|
|
16327
16891
|
requestHeaders: requested ? sentCustomHeaders(result) : null
|
|
@@ -16330,16 +16894,40 @@ function toEvidenceRecord(result, request7, output, options = {}) {
|
|
|
16330
16894
|
access: evidenceAccess(result)
|
|
16331
16895
|
};
|
|
16332
16896
|
}
|
|
16897
|
+
var READ_STATUSES = /* @__PURE__ */ new Set(["success", "partial", "empty_verified"]);
|
|
16333
16898
|
function evidenceAccess(result) {
|
|
16334
16899
|
const event = (name) => result.trace.find((e) => e.event === name)?.detail;
|
|
16335
16900
|
const text2 = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
16336
16901
|
const hit = event("cache_hit");
|
|
16337
16902
|
const stored = hit?.externalCostUsd;
|
|
16338
16903
|
const externalCostUsd = hit === void 0 ? result.usage.externalCostUsd ?? null : typeof stored === "number" ? stored : null;
|
|
16339
|
-
const
|
|
16904
|
+
const read2 = READ_STATUSES.has(result.status);
|
|
16905
|
+
const own = (r) => r === "http" || r === "http_compat" || r === "browser" || r === "enhanced_browser" || r === "authed_browser";
|
|
16906
|
+
const laneEvent = (name) => [...result.trace].reverse().find((e) => e.event === name && sameLaneFamily(e.lane, result.lane))?.detail;
|
|
16907
|
+
const egress = (r) => {
|
|
16908
|
+
if (!own(r))
|
|
16909
|
+
return null;
|
|
16910
|
+
const proxy = laneEvent("egress_proxy");
|
|
16911
|
+
if (proxy === void 0)
|
|
16912
|
+
return typeof result.evidence.httpStatus === "number" ? { proxy: null, source: "direct", switchedFrom: null, exit: null } : null;
|
|
16913
|
+
const moved = [...result.trace].reverse().find((e) => e.event === "egress_switched")?.detail;
|
|
16914
|
+
const source = proxy.source === "environment" ? "environment" : "pool";
|
|
16915
|
+
const seen = source === "pool" ? [...result.trace].reverse().find((e) => e.event === "egress_exit" && e.detail?.proxy === proxy.proxy)?.detail : void 0;
|
|
16916
|
+
const ip = text2(seen?.ip);
|
|
16917
|
+
const observedAt = text2(seen?.observedAt);
|
|
16918
|
+
const exit = ip === null || observedAt === null ? null : { ip, country: text2(seen?.country), observedAt };
|
|
16919
|
+
return { proxy: text2(proxy.proxy), source, switchedFrom: text2(moved?.from), exit };
|
|
16920
|
+
};
|
|
16921
|
+
const session = (r) => {
|
|
16922
|
+
const id = own(r) ? text2(laneEvent("session_cookies")?.session) : null;
|
|
16923
|
+
return id === null ? null : { id };
|
|
16924
|
+
};
|
|
16925
|
+
const paid = paidCalls(result.trace);
|
|
16926
|
+
const route = (r, executor, executorVersion = null, profile = null, completion = "unattended") => ({ route: r, executor, executorVersion, profile, externalCostUsd, completion: read2 && r !== null ? completion : null, egress: egress(r), session: session(r), paidCalls: paid.calls, grant: paid.grant });
|
|
16340
16927
|
const laneRan = result.trace.some((e) => e.event === "identity_sent" || e.event === "identity_declared" || e.event === "provider_selected");
|
|
16341
16928
|
if (!laneRan)
|
|
16342
16929
|
return route(null, null);
|
|
16930
|
+
const userBrowser = (detail) => route("user_browser", text2(detail.browser), null, null, detail.sawGate != null && detail.act != null ? "handed_to_person" : "user_browser");
|
|
16343
16931
|
switch (result.lane) {
|
|
16344
16932
|
case "http": {
|
|
16345
16933
|
const transport = event("transport");
|
|
@@ -16349,17 +16937,44 @@ function evidenceAccess(result) {
|
|
|
16349
16937
|
case "browser_proxy": {
|
|
16350
16938
|
const engine = event("browser_engine");
|
|
16351
16939
|
if (event("session_attached") !== void 0)
|
|
16352
|
-
return route("authed_browser", "playwright");
|
|
16940
|
+
return route("authed_browser", "playwright", null, null, "authorized_session");
|
|
16353
16941
|
return engine !== void 0 && engine.engine === "patchright" ? route("enhanced_browser", "patchright", text2(engine.version)) : route("browser", "playwright");
|
|
16354
16942
|
}
|
|
16355
16943
|
case "browser_local_authed": {
|
|
16356
|
-
const
|
|
16357
|
-
return
|
|
16944
|
+
const handed = event("user_browser_read");
|
|
16945
|
+
return handed === void 0 ? route("authed_browser", "playwright", null, null, "authorized_session") : route("user_browser", text2(handed.browser), null, null, "handed_to_person");
|
|
16946
|
+
}
|
|
16947
|
+
case "my_browser": {
|
|
16948
|
+
const opened = event("user_browser_read");
|
|
16949
|
+
return opened === void 0 ? route(null, null) : userBrowser(opened);
|
|
16358
16950
|
}
|
|
16359
16951
|
case "provider":
|
|
16360
16952
|
return route("vendor", text2(event("provider_selected")?.provider));
|
|
16361
16953
|
}
|
|
16362
16954
|
}
|
|
16955
|
+
function paidCalls(trace) {
|
|
16956
|
+
const usd2 = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
16957
|
+
const text2 = (value) => typeof value === "string" && value !== "";
|
|
16958
|
+
const calls = [];
|
|
16959
|
+
let grant = null;
|
|
16960
|
+
for (const event of trace) {
|
|
16961
|
+
if (event.event !== "paid_calls")
|
|
16962
|
+
continue;
|
|
16963
|
+
for (const call of Array.isArray(event.detail?.calls) ? event.detail.calls : []) {
|
|
16964
|
+
const { provider, rung, capabilities, ceilingUsd, chargedUsd, reportedCostUsd, outcome, reason: reason2, answer } = call;
|
|
16965
|
+
if (!text2(provider) || !text2(rung) || !Array.isArray(capabilities) || !capabilities.every(text2) || !usd2(ceilingUsd) || !usd2(chargedUsd))
|
|
16966
|
+
continue;
|
|
16967
|
+
if (!(reportedCostUsd === null || usd2(reportedCostUsd)) || !(outcome === null || RESULT_STATUS.includes(outcome)) || !(reason2 === null || text2(reason2)) || typeof answer !== "boolean")
|
|
16968
|
+
continue;
|
|
16969
|
+
calls.push({ provider, rung, capabilities, ceilingUsd, chargedUsd, reportedCostUsd, outcome, reason: reason2, answer });
|
|
16970
|
+
}
|
|
16971
|
+
const named = event.detail?.grant;
|
|
16972
|
+
if (named != null && text2(named.tier) && (named.sha256 === null || typeof named.sha256 === "string" && /^[0-9a-f]{64}$/.test(named.sha256)) && (named.attestedAt === null || text2(named.attestedAt))) {
|
|
16973
|
+
grant = { sha256: named.sha256, tier: named.tier, attestedAt: named.attestedAt };
|
|
16974
|
+
}
|
|
16975
|
+
}
|
|
16976
|
+
return calls.length === 0 ? { calls: null, grant: null } : { calls, grant };
|
|
16977
|
+
}
|
|
16363
16978
|
function pageActions(result) {
|
|
16364
16979
|
if (result.actions === void 0)
|
|
16365
16980
|
return null;
|
|
@@ -16562,7 +17177,7 @@ function untriedAudit(wallMs) {
|
|
|
16562
17177
|
// packages/runtime/dist/map.js
|
|
16563
17178
|
init_dist();
|
|
16564
17179
|
init_dist2();
|
|
16565
|
-
var
|
|
17180
|
+
var READ_STATUSES2 = /* @__PURE__ */ new Set(["success", "empty_verified"]);
|
|
16566
17181
|
var MapRunner = class {
|
|
16567
17182
|
sources;
|
|
16568
17183
|
constructor(sources) {
|
|
@@ -16918,7 +17533,7 @@ ${title ?? ""}`.toLowerCase();
|
|
|
16918
17533
|
}
|
|
16919
17534
|
}
|
|
16920
17535
|
await preferHttps();
|
|
16921
|
-
const startFailed = startPage !== null && !
|
|
17536
|
+
const startFailed = startPage !== null && !READ_STATUSES2.has(startPage.status);
|
|
16922
17537
|
const unreadable = sitemap?.files.filter((file) => file.kind === "unreadable" || file.kind === "refused") ?? [];
|
|
16923
17538
|
const noneListed = sitemapMode === "only" && sitemapStarted && sitemap.error === null && unreadable.length === 0 && !sitemap.files.some((file) => file.kind === "urlset" || file.kind === "index");
|
|
16924
17539
|
const sitemapFailed = sitemap !== null && (sitemap.error !== null || sitemap.truncated === "files" || unreadable.length > 0 || noneListed);
|
|
@@ -16989,7 +17604,7 @@ function nonEmpty(value) {
|
|
|
16989
17604
|
import { createHash as createHash13 } from "crypto";
|
|
16990
17605
|
import { existsSync as existsSync3, mkdirSync as mkdirSync8, readdirSync, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
|
|
16991
17606
|
import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "fs/promises";
|
|
16992
|
-
import { isIP as
|
|
17607
|
+
import { isIP as isIP5 } from "net";
|
|
16993
17608
|
import { homedir as homedir2 } from "os";
|
|
16994
17609
|
import { join as join10 } from "path";
|
|
16995
17610
|
|
|
@@ -18729,7 +19344,7 @@ init_errorPage();
|
|
|
18729
19344
|
init_dist();
|
|
18730
19345
|
init_dist3();
|
|
18731
19346
|
init_dist2();
|
|
18732
|
-
import { ProxyAgent as ProxyAgent2, request as
|
|
19347
|
+
import { ProxyAgent as ProxyAgent2, request as request6 } from "undici";
|
|
18733
19348
|
|
|
18734
19349
|
// packages/bench/dist/contentEncoding.js
|
|
18735
19350
|
import { promisify } from "util";
|
|
@@ -18837,6 +19452,9 @@ var EgressRoute = class {
|
|
|
18837
19452
|
}
|
|
18838
19453
|
};
|
|
18839
19454
|
|
|
19455
|
+
// packages/bench/dist/compatBenefitHosts.js
|
|
19456
|
+
var COMPAT_BENEFIT_HOSTS = ["fred.stlouisfed.org", "www.idealo.de", "www.investing.com", "www.ironmountain.com", "www.wayfair.com"];
|
|
19457
|
+
|
|
18840
19458
|
// packages/bench/dist/compatTransport.js
|
|
18841
19459
|
init_dist();
|
|
18842
19460
|
init_egress();
|
|
@@ -19064,7 +19682,12 @@ var CompatTransport = class {
|
|
|
19064
19682
|
}
|
|
19065
19683
|
};
|
|
19066
19684
|
function compatHostsChoice(env, grant, hosted) {
|
|
19067
|
-
const
|
|
19685
|
+
const granted = (grant?.capabilities ?? []).includes("compatible_transport");
|
|
19686
|
+
if (env.W2L_COMPAT_HOSTS === void 0 || env.W2L_COMPAT_HOSTS.trim() === "")
|
|
19687
|
+
return granted && !hosted ? [...COMPAT_BENEFIT_HOSTS].sort() : [];
|
|
19688
|
+
if (env.W2L_COMPAT_HOSTS.trim().toLowerCase() === "none")
|
|
19689
|
+
return [];
|
|
19690
|
+
const raw = env.W2L_COMPAT_HOSTS.split(",").map((value) => value.trim().toLowerCase().replace(/\.$/, "")).filter(Boolean);
|
|
19068
19691
|
if (raw.length === 0)
|
|
19069
19692
|
return [];
|
|
19070
19693
|
const invalid = raw.filter((host) => {
|
|
@@ -19078,7 +19701,7 @@ function compatHostsChoice(env, grant, hosted) {
|
|
|
19078
19701
|
throw new Error(`W2L_COMPAT_HOSTS takes host names, such as example.com: not ${invalid.join(", ")}`);
|
|
19079
19702
|
if (hosted)
|
|
19080
19703
|
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 (!
|
|
19704
|
+
if (!granted) {
|
|
19082
19705
|
throw new Error("W2L_COMPAT_HOSTS needs an access grant that names compatible_transport (ADR 0005; --access-grant or W2L_ACCESS_GRANT)");
|
|
19083
19706
|
}
|
|
19084
19707
|
return [...new Set(raw)].sort();
|
|
@@ -19443,7 +20066,7 @@ var ResilientHttpSubject = class {
|
|
|
19443
20066
|
if (wire !== void 0 && customNames.length > 0 && !sameOrigin2)
|
|
19444
20067
|
wire.onWithheld(url, customNames);
|
|
19445
20068
|
const cookie = cookies === void 0 ? "" : await cookies.header(url);
|
|
19446
|
-
const response = await
|
|
20069
|
+
const response = await request6(url, {
|
|
19447
20070
|
dispatcher: this.dispatcherFor(url, routes),
|
|
19448
20071
|
method: "GET",
|
|
19449
20072
|
headersTimeout: init.headersTimeoutMs,
|
|
@@ -19518,7 +20141,7 @@ var ResilientHttpSubject = class {
|
|
|
19518
20141
|
return this.route.viaOperatorProxy(url);
|
|
19519
20142
|
}
|
|
19520
20143
|
/** `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) {
|
|
20144
|
+
async fetch(url, deadlineMs, signal, validators = {}, onRetryAfter, options = {}, onRobotsOverride, cookieSession, onTrace) {
|
|
19522
20145
|
if (this.localPreviewRobotsException && !isLocalPreviewProxyTarget(url))
|
|
19523
20146
|
throw new Error("Local platform exception is limited to fixed platform hosts");
|
|
19524
20147
|
const scope = createExecutionScope({ signal, deadlineAt: deadlineMs, onRetryAfter, ...cookieSession === void 0 ? {} : { cookieSession } });
|
|
@@ -19532,7 +20155,7 @@ var ResilientHttpSubject = class {
|
|
|
19532
20155
|
try {
|
|
19533
20156
|
permit = await this.scheduler.acquire(origin, scope.signal);
|
|
19534
20157
|
throwIfExecutionStopped(scope);
|
|
19535
|
-
return markDeadline(await this.fetchWithinBudget(url, { ...scope, onRobotsOverride }, validators, monotonicStart, permit.queueMs, permit.cooldownWaitMs, options, relaxed, permit.limitedByConcurrency ? permit.concurrencyWaitMs : void 0));
|
|
20158
|
+
return markDeadline(await this.fetchWithinBudget(url, { ...scope, onRobotsOverride, onTrace }, validators, monotonicStart, permit.queueMs, permit.cooldownWaitMs, options, relaxed, permit.limitedByConcurrency ? permit.concurrencyWaitMs : void 0));
|
|
19536
20159
|
} catch (error) {
|
|
19537
20160
|
if (!scope.signal.aborted && (deadlineMs === void 0 || Date.now() < deadlineMs))
|
|
19538
20161
|
throw error;
|
|
@@ -19595,7 +20218,7 @@ var ResilientHttpSubject = class {
|
|
|
19595
20218
|
}
|
|
19596
20219
|
};
|
|
19597
20220
|
};
|
|
19598
|
-
const trace =
|
|
20221
|
+
const trace = observedTrace(execution.onTrace);
|
|
19599
20222
|
const prepared = this.preparedFor(url, options);
|
|
19600
20223
|
const honest = recordHttpIdentity(prepared, trace, 0);
|
|
19601
20224
|
if (!honest) {
|
|
@@ -20156,6 +20779,27 @@ function bodyReadFailure(error) {
|
|
|
20156
20779
|
return "connection_error";
|
|
20157
20780
|
return null;
|
|
20158
20781
|
}
|
|
20782
|
+
function observedTrace(onTrace) {
|
|
20783
|
+
const trace = [];
|
|
20784
|
+
if (onTrace === void 0)
|
|
20785
|
+
return trace;
|
|
20786
|
+
Object.defineProperty(trace, "push", {
|
|
20787
|
+
configurable: true,
|
|
20788
|
+
writable: true,
|
|
20789
|
+
enumerable: false,
|
|
20790
|
+
value: (...events) => {
|
|
20791
|
+
const length = Array.prototype.push.apply(trace, events);
|
|
20792
|
+
for (const event of events) {
|
|
20793
|
+
try {
|
|
20794
|
+
onTrace(event);
|
|
20795
|
+
} catch {
|
|
20796
|
+
}
|
|
20797
|
+
}
|
|
20798
|
+
return length;
|
|
20799
|
+
}
|
|
20800
|
+
});
|
|
20801
|
+
return trace;
|
|
20802
|
+
}
|
|
20159
20803
|
function discard(body) {
|
|
20160
20804
|
body.on("error", () => {
|
|
20161
20805
|
});
|
|
@@ -20190,6 +20834,34 @@ var SessionCookies = class {
|
|
|
20190
20834
|
}
|
|
20191
20835
|
};
|
|
20192
20836
|
|
|
20837
|
+
// packages/bench/dist/subjects/listMerge.js
|
|
20838
|
+
init_dist3();
|
|
20839
|
+
function mergeListPages(pages, list2) {
|
|
20840
|
+
if (pages.length === 0)
|
|
20841
|
+
return null;
|
|
20842
|
+
const records = [];
|
|
20843
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20844
|
+
let page = 0;
|
|
20845
|
+
let cut2 = false;
|
|
20846
|
+
const { spec, detected } = resolveListSpec(pages[0].html, list2);
|
|
20847
|
+
for (const scrape of spec === null ? [] : pages) {
|
|
20848
|
+
const budget2 = { records: MAX_LIST_RECORDS - records.length, chars: MAX_LIST_VALUE_CHARS - records.reduce((sum, record3) => sum + Object.values(record3.values).reduce((n, value) => n + (value?.length ?? 0), 0), 0) };
|
|
20849
|
+
const read2 = extractListRecords(scrape.html, scrape.url, spec, page + 1, budget2);
|
|
20850
|
+
const key = read2.itemText ?? JSON.stringify(read2.map((record3) => record3.values));
|
|
20851
|
+
if (read2.length > 0 && seen.has(key))
|
|
20852
|
+
continue;
|
|
20853
|
+
seen.add(key);
|
|
20854
|
+
page++;
|
|
20855
|
+
records.push(...read2);
|
|
20856
|
+
if (read2.cut === true) {
|
|
20857
|
+
cut2 = true;
|
|
20858
|
+
break;
|
|
20859
|
+
}
|
|
20860
|
+
}
|
|
20861
|
+
const valued = spec !== null && records.some((record3) => record3.missing.length < spec.fields.length);
|
|
20862
|
+
return { list: listExtraction(spec, records, page, cut2, detected), spec, valued };
|
|
20863
|
+
}
|
|
20864
|
+
|
|
20193
20865
|
// packages/bench/dist/subjects/browserLocal.js
|
|
20194
20866
|
init_dist();
|
|
20195
20867
|
init_dist3();
|
|
@@ -20259,8 +20931,8 @@ init_dist();
|
|
|
20259
20931
|
init_dist2();
|
|
20260
20932
|
import { createHash as createHash7 } from "crypto";
|
|
20261
20933
|
var SCREENSHOT_TIMEOUT_MS = 3e4;
|
|
20262
|
-
function screenshotViewport(
|
|
20263
|
-
const asked =
|
|
20934
|
+
function screenshotViewport(request8, identity, fingerprint) {
|
|
20935
|
+
const asked = request8?.viewport;
|
|
20264
20936
|
if (asked === void 0)
|
|
20265
20937
|
return { viewport: fingerprint.viewport, issues: [] };
|
|
20266
20938
|
const issues = identityBundleIssues({ ...identityBundleFrom(identity, fingerprint), viewport: asked });
|
|
@@ -20293,9 +20965,9 @@ function imageSize(bytes) {
|
|
|
20293
20965
|
}
|
|
20294
20966
|
return null;
|
|
20295
20967
|
}
|
|
20296
|
-
async function captureScreenshot(page,
|
|
20297
|
-
const fullPage =
|
|
20298
|
-
const quality =
|
|
20968
|
+
async function captureScreenshot(page, request8, viewport, deviceScaleFactor, execution, trace, at, refusal = null) {
|
|
20969
|
+
const fullPage = request8.fullPage === true;
|
|
20970
|
+
const quality = request8.quality ?? null;
|
|
20299
20971
|
const contentType = quality === null ? "image/png" : "image/jpeg";
|
|
20300
20972
|
const failed = (error) => {
|
|
20301
20973
|
trace.push({ at: at(), lane: "browser_local", event: "screenshot_failed", detail: { fullPage, viewport, contentType, quality, error } });
|
|
@@ -20584,11 +21256,12 @@ async function pageState(ctx, itemSelector) {
|
|
|
20584
21256
|
const changing = JSON.stringify(first2) !== JSON.stringify(second);
|
|
20585
21257
|
if (second.items !== null) {
|
|
20586
21258
|
const items = hash3(second.items.map((item, i) => `${item.refs}${steady(first2.items?.[i]?.text ?? "", item.text)}`).join("\0"));
|
|
20587
|
-
|
|
21259
|
+
const itemRefs = second.items.some((item) => item.refs !== "") ? hash3(second.items.map((item) => item.refs).join("\0")) : null;
|
|
21260
|
+
return { html: html2, url, state: `${withoutHash(url)}\0${items}`, items, itemRefs, changing };
|
|
20588
21261
|
}
|
|
20589
21262
|
const stillThere = new Set(first2.refs);
|
|
20590
21263
|
const page = hash3(`${steady(first2.text, second.text)}${second.refs.filter((ref) => stillThere.has(ref)).join(" ")}`);
|
|
20591
|
-
return { html: html2, url, state: `${withoutHash(url)}\0${page}`, items: null, changing };
|
|
21264
|
+
return { html: html2, url, state: `${withoutHash(url)}\0${page}`, items: null, itemRefs: null, changing };
|
|
20592
21265
|
}
|
|
20593
21266
|
var STEADY_READ_WAIT_MS = 3e3;
|
|
20594
21267
|
async function steadyPageState(ctx, itemSelector) {
|
|
@@ -20742,41 +21415,110 @@ async function paginate(action, ctx, index, result, guard) {
|
|
|
20742
21415
|
let itemsRead = action.itemSelector === void 0 ? null : 0;
|
|
20743
21416
|
let items = null;
|
|
20744
21417
|
let stoppedBy;
|
|
21418
|
+
let challenge;
|
|
21419
|
+
const resumed = (ctx.execution.listResume?.pages ?? []).filter((read2) => read2.step === index).sort((a, b) => a.page - b.page);
|
|
21420
|
+
const resumedUrls = new Set(resumed.map((read2) => withoutHash(read2.url)));
|
|
21421
|
+
const addressed = resumed.length >= 2 && resumedUrls.size === resumed.length;
|
|
21422
|
+
const shownAgain = resumed.map(() => false);
|
|
21423
|
+
let replayed = 0;
|
|
21424
|
+
let fresh = 0;
|
|
21425
|
+
for (const read2 of resumed) {
|
|
21426
|
+
result.scrapes.push({ url: read2.url, html: read2.html, step: index });
|
|
21427
|
+
pages++;
|
|
21428
|
+
if (itemsRead !== null)
|
|
21429
|
+
itemsRead = read2.count === null ? itemsRead : itemsRead + read2.count;
|
|
21430
|
+
}
|
|
21431
|
+
if (resumed.length > 0)
|
|
21432
|
+
ctx.trace.push({ at: ctx.at(), lane: "browser_local", event: "list_resumed", detail: { step: index, pages: resumed.length, from: resumed[resumed.length - 1].url, addressed } });
|
|
21433
|
+
const refsCounts = /* @__PURE__ */ new Map();
|
|
21434
|
+
for (const page of resumed)
|
|
21435
|
+
if (page.itemRefs !== null)
|
|
21436
|
+
refsCounts.set(page.itemRefs, (refsCounts.get(page.itemRefs) ?? 0) + 1);
|
|
21437
|
+
const knownBy = { content: 0, links: 0, address: 0 };
|
|
21438
|
+
const kept = (read2) => {
|
|
21439
|
+
const left = (match) => resumed.findIndex((page, i) => !shownAgain[i] && match(page));
|
|
21440
|
+
let hit = left((page) => read2.state === page.state || read2.items !== null && read2.items === page.items);
|
|
21441
|
+
if (hit !== -1) {
|
|
21442
|
+
knownBy.content++;
|
|
21443
|
+
return hit;
|
|
21444
|
+
}
|
|
21445
|
+
if (read2.itemRefs !== null && refsCounts.get(read2.itemRefs) === 1)
|
|
21446
|
+
hit = left((page) => page.itemRefs === read2.itemRefs);
|
|
21447
|
+
if (hit !== -1) {
|
|
21448
|
+
knownBy.links++;
|
|
21449
|
+
return hit;
|
|
21450
|
+
}
|
|
21451
|
+
if (addressed)
|
|
21452
|
+
hit = left((page) => withoutHash(page.url) === withoutHash(read2.url));
|
|
21453
|
+
if (hit !== -1)
|
|
21454
|
+
knownBy.address++;
|
|
21455
|
+
return hit;
|
|
21456
|
+
};
|
|
21457
|
+
const tell = (read2) => {
|
|
21458
|
+
try {
|
|
21459
|
+
ctx.execution.onListPage?.({ step: index, page: pages, ...read2 });
|
|
21460
|
+
} catch {
|
|
21461
|
+
}
|
|
21462
|
+
};
|
|
20745
21463
|
try {
|
|
20746
21464
|
for (; ; ) {
|
|
20747
|
-
let
|
|
21465
|
+
let read2 = await steadyPageState(ctx, action.itemSelector);
|
|
21466
|
+
let { html: html2, url, state, items: listed } = read2;
|
|
20748
21467
|
const unchanged = () => state === lastState || listed !== null && listed === lastListed && ctx.loadedDocuments().length === loadsAtClick;
|
|
20749
21468
|
if (unchanged()) {
|
|
20750
21469
|
const until = comeBackUntil(ctx);
|
|
20751
21470
|
while (unchanged() && Date.now() < until) {
|
|
20752
21471
|
await abortableSleep(250, ctx.execution.signal);
|
|
20753
|
-
|
|
21472
|
+
read2 = await steadyPageState(ctx, action.itemSelector);
|
|
21473
|
+
({ html: html2, url, state, items: listed } = read2);
|
|
20754
21474
|
}
|
|
20755
21475
|
}
|
|
20756
21476
|
lastState = state;
|
|
20757
21477
|
lastListed = listed;
|
|
20758
|
-
|
|
20759
|
-
|
|
21478
|
+
const onPage = action.itemSelector === void 0 ? null : await bounded(ctx, ctx.page.locator(action.itemSelector).count());
|
|
21479
|
+
const gate = classifyGate({ status: 200, header: () => null, body: html2, contentful: true });
|
|
21480
|
+
if (gate !== null) {
|
|
21481
|
+
challenge = { page: pages + 1, url, reason: gate.reason, signals: gate.signals };
|
|
21482
|
+
ctx.trace.push({ at: ctx.at(), lane: "browser_local", event: "list_challenge", detail: { step: index, page: pages + 1, url, reason: gate.reason, signals: [...gate.signals] } });
|
|
21483
|
+
stoppedBy = "challenge";
|
|
20760
21484
|
break;
|
|
20761
21485
|
}
|
|
20762
|
-
|
|
20763
|
-
if (
|
|
20764
|
-
|
|
20765
|
-
|
|
20766
|
-
break;
|
|
20767
|
-
}
|
|
20768
|
-
} else {
|
|
21486
|
+
const held = kept(read2);
|
|
21487
|
+
if (held !== -1) {
|
|
21488
|
+
shownAgain[held] = true;
|
|
21489
|
+
replayed++;
|
|
20769
21490
|
alreadyRead = 0;
|
|
21491
|
+
seenStates.add(state);
|
|
20770
21492
|
if (listed !== null)
|
|
20771
21493
|
seenItems.add(listed);
|
|
20772
|
-
|
|
20773
|
-
|
|
20774
|
-
if (
|
|
20775
|
-
|
|
20776
|
-
|
|
21494
|
+
items = onPage;
|
|
21495
|
+
} else {
|
|
21496
|
+
if (seenStates.has(state)) {
|
|
21497
|
+
stoppedBy = "repeat";
|
|
21498
|
+
break;
|
|
21499
|
+
}
|
|
21500
|
+
seenStates.add(state);
|
|
21501
|
+
if (listed !== null && seenItems.has(listed)) {
|
|
21502
|
+
if (++alreadyRead >= 2) {
|
|
21503
|
+
stoppedBy = "repeat";
|
|
21504
|
+
break;
|
|
21505
|
+
}
|
|
21506
|
+
} else {
|
|
21507
|
+
alreadyRead = 0;
|
|
21508
|
+
if (listed !== null)
|
|
21509
|
+
seenItems.add(listed);
|
|
21510
|
+
result.scrapes.push({ url, html: html2, step: index });
|
|
21511
|
+
pages++;
|
|
21512
|
+
if (onPage !== null) {
|
|
21513
|
+
items = onPage;
|
|
21514
|
+
itemsRead = (itemsRead ?? 0) + onPage;
|
|
21515
|
+
}
|
|
21516
|
+
fresh++;
|
|
21517
|
+
if (onPage !== 0)
|
|
21518
|
+
tell({ url, html: html2, state, items: listed, itemRefs: read2.itemRefs, count: action.itemSelector === void 0 ? null : items });
|
|
20777
21519
|
}
|
|
20778
21520
|
}
|
|
20779
|
-
if (pages >= max) {
|
|
21521
|
+
if (pages >= max && (replayed >= resumed.length || fresh > 0)) {
|
|
20780
21522
|
stoppedBy = "max";
|
|
20781
21523
|
break;
|
|
20782
21524
|
}
|
|
@@ -20798,7 +21540,14 @@ async function paginate(action, ctx, index, result, guard) {
|
|
|
20798
21540
|
throw error;
|
|
20799
21541
|
stoppedBy = "deadline";
|
|
20800
21542
|
}
|
|
20801
|
-
|
|
21543
|
+
if (resumed.length > 0) {
|
|
21544
|
+
const event = ctx.trace.find((item) => item.event === "list_resumed" && item.detail?.step === index);
|
|
21545
|
+
if (event?.detail !== void 0) {
|
|
21546
|
+
event.detail.replayed = replayed;
|
|
21547
|
+
event.detail.knownBy = knownBy;
|
|
21548
|
+
}
|
|
21549
|
+
}
|
|
21550
|
+
return { index, type: "paginate", stoppedBy, rounds: pages, items, itemsRead, ...resumed.length === 0 ? {} : { resumed: resumed.length }, ...challenge === void 0 ? {} : { challenge } };
|
|
20802
21551
|
}
|
|
20803
21552
|
async function bounded(ctx, operation) {
|
|
20804
21553
|
const left = timeLeft(ctx);
|
|
@@ -20881,49 +21630,8 @@ function withoutHash(url) {
|
|
|
20881
21630
|
return at === -1 ? url : url.slice(0, at);
|
|
20882
21631
|
}
|
|
20883
21632
|
|
|
20884
|
-
// packages/bench/dist/
|
|
20885
|
-
|
|
20886
|
-
const maxMs = options.maxMs ?? 1500;
|
|
20887
|
-
const minMs = Math.min(options.minMs ?? 500, maxMs);
|
|
20888
|
-
const sampleMs = options.sampleMs ?? 100;
|
|
20889
|
-
const deadline = Date.now() + maxMs;
|
|
20890
|
-
let observedSince = Date.now();
|
|
20891
|
-
let previous = "";
|
|
20892
|
-
let stableRounds = 0;
|
|
20893
|
-
while (Date.now() < deadline) {
|
|
20894
|
-
let snapshot;
|
|
20895
|
-
try {
|
|
20896
|
-
snapshot = await page.evaluate(`(() => {
|
|
20897
|
-
const root = document.documentElement
|
|
20898
|
-
const body = document.body
|
|
20899
|
-
return JSON.stringify({
|
|
20900
|
-
size: root?.outerHTML.length ?? 0,
|
|
20901
|
-
text: body?.innerText ?? '',
|
|
20902
|
-
})
|
|
20903
|
-
})()`);
|
|
20904
|
-
} catch (error) {
|
|
20905
|
-
if (!isNavigationError(error))
|
|
20906
|
-
throw error;
|
|
20907
|
-
observedSince = Date.now();
|
|
20908
|
-
previous = "";
|
|
20909
|
-
stableRounds = 0;
|
|
20910
|
-
await page.waitForTimeout(Math.min(sampleMs, Math.max(1, deadline - Date.now())));
|
|
20911
|
-
continue;
|
|
20912
|
-
}
|
|
20913
|
-
const current = typeof snapshot === "string" ? snapshot : JSON.stringify(snapshot);
|
|
20914
|
-
if (current === previous)
|
|
20915
|
-
stableRounds++;
|
|
20916
|
-
else
|
|
20917
|
-
stableRounds = 0;
|
|
20918
|
-
previous = current;
|
|
20919
|
-
if (Date.now() - observedSince >= minMs && stableRounds >= 2)
|
|
20920
|
-
return;
|
|
20921
|
-
await page.waitForTimeout(Math.min(sampleMs, Math.max(1, deadline - Date.now())));
|
|
20922
|
-
}
|
|
20923
|
-
}
|
|
20924
|
-
function isNavigationError(error) {
|
|
20925
|
-
return error instanceof Error && /Execution context was destroyed|because of a navigation|page is navigating and changing the content|Cannot find context with specified id/i.test(error.message);
|
|
20926
|
-
}
|
|
21633
|
+
// packages/bench/dist/subjects/browserLocal.js
|
|
21634
|
+
init_browserSettle();
|
|
20927
21635
|
|
|
20928
21636
|
// packages/bench/dist/browserLayout.js
|
|
20929
21637
|
init_dist3();
|
|
@@ -21137,11 +21845,11 @@ var MainFrameDocuments = class {
|
|
|
21137
21845
|
return { chain: hops.length > 1 ? hops : [], complete };
|
|
21138
21846
|
}
|
|
21139
21847
|
onResponse(response) {
|
|
21140
|
-
const
|
|
21141
|
-
if (!
|
|
21848
|
+
const request8 = response.request();
|
|
21849
|
+
if (!request8.isNavigationRequest())
|
|
21142
21850
|
return;
|
|
21143
21851
|
try {
|
|
21144
|
-
if (
|
|
21852
|
+
if (request8.frame() !== this.page.mainFrame())
|
|
21145
21853
|
return;
|
|
21146
21854
|
} catch {
|
|
21147
21855
|
return;
|
|
@@ -21175,8 +21883,8 @@ var MainFrameDocuments = class {
|
|
|
21175
21883
|
};
|
|
21176
21884
|
function requestedUrls(response) {
|
|
21177
21885
|
const urls = [];
|
|
21178
|
-
for (let
|
|
21179
|
-
urls.unshift(
|
|
21886
|
+
for (let request8 = response.request(); request8 !== null; request8 = request8.redirectedFrom())
|
|
21887
|
+
urls.unshift(request8.url());
|
|
21180
21888
|
return urls;
|
|
21181
21889
|
}
|
|
21182
21890
|
function sameDocument(a, b) {
|
|
@@ -21329,9 +22037,9 @@ var CustomHeaderGate = class {
|
|
|
21329
22037
|
return [...reported2.filter(({ name }) => !this.names.has(name)), ...this.entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
21330
22038
|
}
|
|
21331
22039
|
};
|
|
21332
|
-
function sentHeadersOf(gate,
|
|
21333
|
-
const reported2 = asSentHeaders(
|
|
21334
|
-
return gate === null ||
|
|
22040
|
+
function sentHeadersOf(gate, request8) {
|
|
22041
|
+
const reported2 = asSentHeaders(request8?.headers() ?? {});
|
|
22042
|
+
return gate === null || request8 === null ? reported2 : gate.sent(request8.url(), reported2);
|
|
21335
22043
|
}
|
|
21336
22044
|
var CAPTURE_RESERVE_MS = 1e3;
|
|
21337
22045
|
var CAPTURE_ATTEMPTS = 3;
|
|
@@ -21445,8 +22153,8 @@ var BrowserLocalSubject = class {
|
|
|
21445
22153
|
return this.chain.toLedger();
|
|
21446
22154
|
}
|
|
21447
22155
|
/** `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 } });
|
|
22156
|
+
async fetch(url, deadlineMs, signal, onRetryAfter, options = {}, onRobotsOverride, cookieSession, listing) {
|
|
22157
|
+
const scope = createExecutionScope({ signal, deadlineAt: deadlineMs, onRetryAfter, ...cookieSession === void 0 ? {} : { cookieSession }, ...listing?.onListPage === void 0 ? {} : { onListPage: listing.onListPage }, ...listing?.listResume === void 0 ? {} : { listResume: listing.listResume } });
|
|
21450
22158
|
const start = Date.now();
|
|
21451
22159
|
const monotonicStart = performance.now();
|
|
21452
22160
|
let queueMs = 0;
|
|
@@ -21458,7 +22166,7 @@ var BrowserLocalSubject = class {
|
|
|
21458
22166
|
const finish2 = (result) => {
|
|
21459
22167
|
const totalMs = Math.max(0, performance.now() - monotonicStart);
|
|
21460
22168
|
const lead = [...robots.overrideWarning === null ? [] : [robots.overrideWarning], ...tlsWarning === null ? [] : [tlsWarning]];
|
|
21461
|
-
const done = withListCaveat(withActions(result, ran.actions, options.list));
|
|
22169
|
+
const done = withStillLoadingWarning(withListCaveat(withActions(result, ran.actions, options.list)));
|
|
21462
22170
|
return {
|
|
21463
22171
|
...done,
|
|
21464
22172
|
...lead.length === 0 ? {} : { warnings: [...lead, ...done.warnings ?? []] },
|
|
@@ -21700,11 +22408,11 @@ var BrowserLocalSubject = class {
|
|
|
21700
22408
|
const hasCustomHeaders = Object.keys(customHeaders).length > 0;
|
|
21701
22409
|
if (allowedHosts !== null) {
|
|
21702
22410
|
requestRoute = async (route) => {
|
|
21703
|
-
const
|
|
21704
|
-
const target =
|
|
22411
|
+
const request8 = route.request();
|
|
22412
|
+
const target = request8.url();
|
|
21705
22413
|
let allowed = false;
|
|
21706
22414
|
try {
|
|
21707
|
-
allowed = hostedBrowserRequestAllowed(target,
|
|
22415
|
+
allowed = hostedBrowserRequestAllowed(target, request8.resourceType(), allowedHosts);
|
|
21708
22416
|
if (allowed)
|
|
21709
22417
|
await assertSafeUrl(target, this.networkPolicy);
|
|
21710
22418
|
} catch {
|
|
@@ -21825,9 +22533,9 @@ var BrowserLocalSubject = class {
|
|
|
21825
22533
|
const documents = new MainFrameDocuments(page);
|
|
21826
22534
|
const shownPage = page;
|
|
21827
22535
|
let settledLoads = 0;
|
|
21828
|
-
const settle = async (maxMs) => {
|
|
22536
|
+
const settle = async (maxMs, loadingMaxMs = maxMs) => {
|
|
21829
22537
|
settledLoads = documents.loads;
|
|
21830
|
-
await raceWithSignal(waitForRenderedStability(shownPage, { maxMs }), signal);
|
|
22538
|
+
return await raceWithSignal(waitForRenderedStability(shownPage, { maxMs, loadingMaxMs }), signal);
|
|
21831
22539
|
};
|
|
21832
22540
|
let previousRequestAtMs = null;
|
|
21833
22541
|
let observedDelayMs = null;
|
|
@@ -21896,7 +22604,12 @@ var BrowserLocalSubject = class {
|
|
|
21896
22604
|
}
|
|
21897
22605
|
continue;
|
|
21898
22606
|
}
|
|
21899
|
-
|
|
22607
|
+
const callerWaits = (options.waitFor ?? 0) > 0 || (options.actions?.length ?? 0) > 0;
|
|
22608
|
+
const usual = remainingTimeout(execution, 1500);
|
|
22609
|
+
const settled2 = await settle(usual, callerWaits ? usual : execution.deadlineAt === void 0 ? LOADING_WAIT_MAX_MS : Math.max(1, Math.min(LOADING_WAIT_MAX_MS, execution.deadlineAt - CAPTURE_RESERVE_MS - Date.now())));
|
|
22610
|
+
const waited = callerWaits ? null : loadingWaitEvent(settled2, Date.now() - start, "browser_local");
|
|
22611
|
+
if (waited !== null)
|
|
22612
|
+
trace.push(waited);
|
|
21900
22613
|
throwIfExecutionStopped(execution);
|
|
21901
22614
|
if (status2 === 200 && variantFollowups === 0 && requestedAmazonAsin !== null) {
|
|
21902
22615
|
const variant = await raceWithSignal(page.evaluate((asin) => ({
|
|
@@ -21957,22 +22670,22 @@ var BrowserLocalSubject = class {
|
|
|
21957
22670
|
if (options.actions !== void 0 && options.actions.length > 0) {
|
|
21958
22671
|
const own = page;
|
|
21959
22672
|
actionsRoute = async (route) => {
|
|
21960
|
-
const
|
|
21961
|
-
if (!
|
|
22673
|
+
const request8 = route.request();
|
|
22674
|
+
if (!request8.isNavigationRequest())
|
|
21962
22675
|
return route.fallback();
|
|
21963
22676
|
let frame = null;
|
|
21964
22677
|
try {
|
|
21965
|
-
frame =
|
|
22678
|
+
frame = request8.frame();
|
|
21966
22679
|
} catch {
|
|
21967
22680
|
frame = null;
|
|
21968
22681
|
}
|
|
21969
22682
|
if (frame !== null && frame !== frame.page().mainFrame())
|
|
21970
22683
|
return route.fallback();
|
|
21971
|
-
const reason2 = await this.refuseNavigation(
|
|
22684
|
+
const reason2 = await this.refuseNavigation(request8.url(), identity, execution, relaxedRoutes, overriddenUrl).catch((error) => `it could not be checked (${error instanceof Error ? error.message.slice(0, 120) : String(error)})`);
|
|
21972
22685
|
if (reason2 === null)
|
|
21973
22686
|
return route.fallback();
|
|
21974
|
-
refusedNavigations.push({ url:
|
|
21975
|
-
trace.push({ at: Date.now() - start, lane: "browser_local", event: "navigation_refused", detail: { url:
|
|
22687
|
+
refusedNavigations.push({ url: request8.url(), reason: reason2 });
|
|
22688
|
+
trace.push({ at: Date.now() - start, lane: "browser_local", event: "navigation_refused", detail: { url: request8.url(), reason: reason2 } });
|
|
21976
22689
|
return route.fulfill({ status: 204, body: "" });
|
|
21977
22690
|
};
|
|
21978
22691
|
await context.route(routeMatch, actionsRoute);
|
|
@@ -22645,49 +23358,48 @@ var BrowserLocalSubject = class {
|
|
|
22645
23358
|
function withActions(result, ran, list2) {
|
|
22646
23359
|
if (ran === void 0)
|
|
22647
23360
|
return result;
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
const
|
|
22652
|
-
const
|
|
22653
|
-
|
|
22654
|
-
let cut2 = false;
|
|
22655
|
-
const { spec, detected } = resolveListSpec(pages[0].html, list2);
|
|
22656
|
-
for (const scrape of spec === null ? [] : pages) {
|
|
22657
|
-
const budget2 = { records: MAX_LIST_RECORDS - records.length, chars: MAX_LIST_VALUE_CHARS - records.reduce((sum, record3) => sum + Object.values(record3.values).reduce((n, value) => n + (value?.length ?? 0), 0), 0) };
|
|
22658
|
-
const read2 = extractListRecords(scrape.html, scrape.url, spec, page + 1, budget2);
|
|
22659
|
-
const key = read2.itemText ?? JSON.stringify(read2.map((record3) => record3.values));
|
|
22660
|
-
if (read2.length > 0 && seen.has(key))
|
|
23361
|
+
if (result.status === "blocked" && result.blockReason !== null) {
|
|
23362
|
+
const reason2 = result.blockReason;
|
|
23363
|
+
const signals = result.trace.find((event) => event.event === "gate_detected")?.detail?.signals ?? [];
|
|
23364
|
+
const last = ran.result.scrapes.at(-1);
|
|
23365
|
+
for (const run of ran.result.lists) {
|
|
23366
|
+
if (run.type !== "paginate" || run.stoppedBy === "challenge" || last === void 0 || last.step !== run.index || run.items !== 0)
|
|
22661
23367
|
continue;
|
|
22662
|
-
|
|
22663
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
22666
|
-
|
|
22667
|
-
|
|
22668
|
-
}
|
|
23368
|
+
ran.result.scrapes.pop();
|
|
23369
|
+
run.rounds--;
|
|
23370
|
+
run.items = null;
|
|
23371
|
+
run.stoppedBy = "challenge";
|
|
23372
|
+
run.challenge = { page: run.rounds + 1, url: last.url, reason: reason2, signals };
|
|
23373
|
+
result = { ...result, trace: [...result.trace, { at: result.usage.wallMs, lane: "browser_local", event: "list_challenge", detail: { step: run.index, page: run.rounds + 1, url: last.url, reason: reason2, signals, by: "page_verdict" } }] };
|
|
22669
23374
|
}
|
|
22670
|
-
|
|
22671
|
-
|
|
22672
|
-
|
|
23375
|
+
}
|
|
23376
|
+
const paginated = list2 === void 0 ? [] : [...ran.result.lists.filter((run) => run.type === "paginate").map((run) => run.index), ...ran.result.failed?.type === "paginate" ? [ran.result.failed.index] : []];
|
|
23377
|
+
const pages = ran.result.scrapes.filter((scrape) => scrape.step !== void 0 && paginated.includes(scrape.step));
|
|
23378
|
+
if (pages.length > 0) {
|
|
23379
|
+
const merged = mergeListPages(pages, list2);
|
|
23380
|
+
const rescued = merged.valued && result.status === "failed" && result.failureReason === "empty_unverified";
|
|
22673
23381
|
result = {
|
|
22674
23382
|
...result,
|
|
22675
|
-
list: merged,
|
|
23383
|
+
list: merged.list,
|
|
22676
23384
|
...rescued ? { status: "success", failureReason: null } : {},
|
|
22677
|
-
trace: [...result.trace, { at: result.usage.wallMs, lane: "browser_local", event: "list_extracted", detail: { records: merged.records.length, incomplete: merged.incomplete, pages: merged.pages, truncated: merged.truncated } }]
|
|
23385
|
+
trace: [...result.trace, { at: result.usage.wallMs, lane: "browser_local", event: "list_extracted", detail: { records: merged.list.records.length, incomplete: merged.list.incomplete, pages: merged.list.pages, truncated: merged.list.truncated } }]
|
|
22678
23386
|
};
|
|
22679
23387
|
}
|
|
22680
23388
|
const failed = ran.result.failed !== void 0 && CONTENTFUL_STATUS.has(result.status);
|
|
22681
|
-
const short = ran.result.lists.filter((list3) => list3.stoppedBy === "max" || list3.stoppedBy === "deadline");
|
|
23389
|
+
const short = ran.result.lists.filter((list3) => list3.stoppedBy === "max" || list3.stoppedBy === "deadline" || list3.stoppedBy === "challenge");
|
|
23390
|
+
const why = (list3) => list3.stoppedBy === "max" ? `its limit of ${list3.rounds} ${list3.type === "paginate" ? "pages" : "rounds"}` : list3.stoppedBy === "challenge" ? `a check the site put up at page ${list3.challenge?.page ?? list3.rounds + 1} (${list3.challenge?.reason ?? "unknown"})` : "the deadline";
|
|
22682
23391
|
const warnings = short.length === 0 ? result.warnings : [...result.warnings ?? [], {
|
|
22683
23392
|
code: "list_not_exhausted",
|
|
22684
|
-
message: `The list ${short.map((list3) => `of step ${list3.index} (${list3.type})`).join(" and ")} stopped before its end (${short.map(
|
|
23393
|
+
message: `The list ${short.map((list3) => `of step ${list3.index} (${list3.type})`).join(" and ")} stopped before its end (${short.map(why).join("; ")}): more items may follow.`
|
|
22685
23394
|
}];
|
|
23395
|
+
const checked = ran.result.lists.find((list3) => list3.challenge !== void 0)?.challenge;
|
|
23396
|
+
const blocked = checked !== void 0 && result.status !== "blocked";
|
|
22686
23397
|
return {
|
|
22687
23398
|
...result,
|
|
22688
23399
|
...warnings === void 0 ? {} : { warnings },
|
|
22689
23400
|
actions: ran.result,
|
|
22690
23401
|
...failed ? { status: "failed", failureReason: "action_failed", blockReason: null } : {},
|
|
23402
|
+
...blocked ? { status: "blocked", failureReason: null, blockReason: checked.reason, budgetExceeded: null } : {},
|
|
22691
23403
|
evidence: { ...result.evidence, artifacts: [...result.evidence.artifacts, ...ran.artifacts] }
|
|
22692
23404
|
};
|
|
22693
23405
|
}
|
|
@@ -22925,6 +23637,18 @@ function summarize(channelsTried, attempts) {
|
|
|
22925
23637
|
artifacts: attempts.flatMap((item) => item.result.evidence.artifacts)
|
|
22926
23638
|
};
|
|
22927
23639
|
}
|
|
23640
|
+
function strongerRungMayAnswer(result, channelId) {
|
|
23641
|
+
if (result.status !== "failed")
|
|
23642
|
+
return null;
|
|
23643
|
+
const status = result.evidence.httpStatus;
|
|
23644
|
+
if (result.failureReason === "http_error" && (status === 403 || status === 405))
|
|
23645
|
+
return `http_${status}`;
|
|
23646
|
+
if (result.failureReason === "empty_unverified" && !HTTP_CHANNELS.has(channelId))
|
|
23647
|
+
return "empty_unverified";
|
|
23648
|
+
if (result.failureReason === "connection_error" && HTTP_CHANNELS.has(channelId))
|
|
23649
|
+
return "connection_error";
|
|
23650
|
+
return null;
|
|
23651
|
+
}
|
|
22928
23652
|
function resultRequestsEscalation(result) {
|
|
22929
23653
|
return result.escalations.some((e) => e.improved === null) || qualityEscalationEvent(result) !== null;
|
|
22930
23654
|
}
|
|
@@ -23040,7 +23764,7 @@ var LadderRunner = class {
|
|
|
23040
23764
|
*/
|
|
23041
23765
|
async run(url, session, execution = {}, options = {}) {
|
|
23042
23766
|
const scope = createExecutionScope(execution);
|
|
23043
|
-
const progress = { startedAt: performance.now(), channelsTried: [], ladderTrace: [], attempts: [], robotsOverrides: [] };
|
|
23767
|
+
const progress = { startedAt: performance.now(), channelsTried: [], ladderTrace: [], attempts: [], robotsOverrides: [], charged: null, paidCalls: [], grant: null, ledger: null, steppedPast: null };
|
|
23044
23768
|
for (const filtered of this.options.channelsFiltered ?? []) {
|
|
23045
23769
|
progress.ladderTrace.push({ at: 0, event: "ladder_channels_filtered", channel: "\u2014", detail: { reason: filtered.reason, dropped: [...filtered.dropped] } });
|
|
23046
23770
|
}
|
|
@@ -23049,11 +23773,11 @@ var LadderRunner = class {
|
|
|
23049
23773
|
execution.onRobotsOverride?.(applied);
|
|
23050
23774
|
} };
|
|
23051
23775
|
try {
|
|
23052
|
-
return withRunCost(carryRobotsOverride(await this.runWithinBudget(url, session, rungs, options, progress), progress.robotsOverrides));
|
|
23776
|
+
return withRunCost(carryRobotsOverride(await this.runWithinBudget(url, session, rungs, options, progress), progress.robotsOverrides), progress);
|
|
23053
23777
|
} catch (error) {
|
|
23054
23778
|
if (!deadlineReached(scope))
|
|
23055
|
-
throw error;
|
|
23056
|
-
return withRunCost(carryRobotsOverride(deadlineOutcome(url, progress, null), progress.robotsOverrides));
|
|
23779
|
+
throw progress.paidCalls.length === 0 ? error : carryPaidCalls(error, paidCallsEvent(progress, null, Math.max(0, performance.now() - progress.startedAt), "provider"));
|
|
23780
|
+
return withRunCost(carryRobotsOverride(deadlineOutcome(url, progress, null), progress.robotsOverrides), progress);
|
|
23057
23781
|
} finally {
|
|
23058
23782
|
scope.dispose();
|
|
23059
23783
|
}
|
|
@@ -23073,13 +23797,19 @@ var LadderRunner = class {
|
|
|
23073
23797
|
};
|
|
23074
23798
|
};
|
|
23075
23799
|
const failedAnswer = (result) => {
|
|
23076
|
-
if (result.status !== "failed" || result.markdown !== null
|
|
23800
|
+
if (result.status !== "failed" || result.markdown !== null)
|
|
23077
23801
|
return result;
|
|
23078
|
-
const
|
|
23079
|
-
|
|
23802
|
+
const notImproved = (kept2) => ({ ...kept2, escalations: kept2.escalations.map((e) => e.improved === null ? { ...e, improved: false } : e) });
|
|
23803
|
+
const kept = classifyFetchFailure(result) === null ? noMainContentEvidence(attempts) : null;
|
|
23804
|
+
if (kept !== null && kept.result !== result) {
|
|
23805
|
+
ladderTrace.push({ at: result.usage.wallMs, event: "ladder_evidence_kept", channel: kept.channel, detail: { kept: kept.channel, failed: channelsTried.at(-1) ?? null, reason: result.failureReason } });
|
|
23806
|
+
return notImproved(kept.result);
|
|
23807
|
+
}
|
|
23808
|
+
const stepped = progress.steppedPast;
|
|
23809
|
+
if (stepped === null || stepped.result === result || result.evidence.httpStatus != null && result.failureReason !== "provider_error")
|
|
23080
23810
|
return result;
|
|
23081
|
-
ladderTrace.push({ at: result.usage.wallMs, event: "ladder_evidence_kept", channel:
|
|
23082
|
-
return
|
|
23811
|
+
ladderTrace.push({ at: result.usage.wallMs, event: "ladder_evidence_kept", channel: stepped.channel, detail: { kept: stepped.channel, failed: channelsTried.at(-1) ?? null, reason: result.failureReason ?? result.blockReason } });
|
|
23812
|
+
return notImproved(stepped.result);
|
|
23083
23813
|
};
|
|
23084
23814
|
const sessionsPermitted = this.policy.mode === "authed";
|
|
23085
23815
|
let effectiveSession = null;
|
|
@@ -23148,9 +23878,11 @@ var LadderRunner = class {
|
|
|
23148
23878
|
});
|
|
23149
23879
|
return finish2(identityBlock, false);
|
|
23150
23880
|
}
|
|
23151
|
-
channelsTried.push(channel.id);
|
|
23152
23881
|
const startedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
23153
|
-
const
|
|
23882
|
+
const fetched = await this.paidFetch(channel, channel.id, url, effectiveSession, execution, options, progress, () => channelsTried.push(channel.id));
|
|
23883
|
+
if (fetched === null)
|
|
23884
|
+
continue;
|
|
23885
|
+
const result = fetched;
|
|
23154
23886
|
attempts.push(attemptOf(channel, channel.id, result, attempts.length + 1, startedAt2));
|
|
23155
23887
|
if (result.usage.deadlineExceeded === true || result.failureReason === "timeout" && deadlineReached(execution)) {
|
|
23156
23888
|
return deadlineOutcome(url, progress, result);
|
|
@@ -23258,10 +23990,13 @@ var LadderRunner = class {
|
|
|
23258
23990
|
});
|
|
23259
23991
|
continue;
|
|
23260
23992
|
}
|
|
23261
|
-
return await this.attemptHandoff(url, result, channelsTried, channel, effectiveSession, ladderTrace, best, attempts, execution, options);
|
|
23993
|
+
return await this.attemptHandoff(url, result, channelsTried, channel, effectiveSession, ladderTrace, best, attempts, execution, options, progress);
|
|
23262
23994
|
}
|
|
23263
23995
|
const cls = classifyFetchFailure(result);
|
|
23264
23996
|
const subjectAsked = resultRequestsEscalation(result);
|
|
23997
|
+
const stronger = cls === null && !subjectAsked && best === null && channel !== sessionFirst && channel.id !== "authed_session" ? strongerRungMayAnswer(result, channel.id) : null;
|
|
23998
|
+
if (stronger !== null && (result.evidence.httpStatus != null || result.markdown !== null))
|
|
23999
|
+
progress.steppedPast = { channel: channel.id, result };
|
|
23265
24000
|
ladderTrace.push({
|
|
23266
24001
|
at: result.usage.wallMs,
|
|
23267
24002
|
event: "ladder_step",
|
|
@@ -23270,10 +24005,10 @@ var LadderRunner = class {
|
|
|
23270
24005
|
vendorId: channel.vendorId ?? null,
|
|
23271
24006
|
status: result.status,
|
|
23272
24007
|
failureClass: cls,
|
|
23273
|
-
escalate: subjectAsked ? "subject_escalations" :
|
|
24008
|
+
escalate: subjectAsked ? "subject_escalations" : stronger
|
|
23274
24009
|
}
|
|
23275
24010
|
});
|
|
23276
|
-
if (cls === null && !subjectAsked) {
|
|
24011
|
+
if (cls === null && !subjectAsked && stronger === null) {
|
|
23277
24012
|
if (best !== null)
|
|
23278
24013
|
break;
|
|
23279
24014
|
return finish2(withLowContentYield(failedAnswer(result), channelsTried), false);
|
|
@@ -23324,7 +24059,50 @@ var LadderRunner = class {
|
|
|
23324
24059
|
};
|
|
23325
24060
|
await this.history?.record(safeHost(url), vendorId, outcome);
|
|
23326
24061
|
}
|
|
23327
|
-
|
|
24062
|
+
/**
|
|
24063
|
+
* One rung's fetch. A provider is called only at a known price ceiling, reserved from the run's ledger first and settled
|
|
24064
|
+
* after it, at the price it reported or at the ceiling, a call that threw or was cut included (ROADMAP PA item 4). Null
|
|
24065
|
+
* when it was not called, with the reason in the trace; `onCall` hears of the call just before it goes out.
|
|
24066
|
+
*/
|
|
24067
|
+
async paidFetch(channel, label, url, session, execution, options, progress, onCall) {
|
|
24068
|
+
const { ladderTrace } = progress;
|
|
24069
|
+
let reservation = null;
|
|
24070
|
+
const ceiling = channel.vendorId === void 0 ? void 0 : channel.priceCeilingUsd ?? null;
|
|
24071
|
+
if (ceiling === null) {
|
|
24072
|
+
ladderTrace.push({ at: 0, event: "ladder_channel_skipped", channel: label, detail: { vendorId: channel.vendorId ?? null, reason: "no price ceiling: the access grant names no tariff for this provider, and a provider whose cost has no upper bound is not called" } });
|
|
24073
|
+
return null;
|
|
24074
|
+
}
|
|
24075
|
+
if (ceiling !== void 0) {
|
|
24076
|
+
const ledger = execution.spend ?? (progress.ledger ??= createSpendLedger(null));
|
|
24077
|
+
reservation = ledger.reserve(ceiling);
|
|
24078
|
+
if (reservation === null) {
|
|
24079
|
+
ladderTrace.push({ at: 0, event: "ladder_channel_skipped", channel: label, detail: { vendorId: channel.vendorId ?? null, reason: "budget: its price ceiling does not fit what the run (or this request) has left", ceilingUsd: ceiling, settledUsd: ledger.settledUsd, reservedUsd: ledger.reservedUsd, capUsd: ledger.capUsd } });
|
|
24080
|
+
return null;
|
|
24081
|
+
}
|
|
24082
|
+
}
|
|
24083
|
+
onCall();
|
|
24084
|
+
let result;
|
|
24085
|
+
try {
|
|
24086
|
+
result = await raceWithSignal(channel.fetch(url, session, execution, options), execution.signal);
|
|
24087
|
+
} catch (error) {
|
|
24088
|
+
if (reservation !== null && ceiling !== void 0) {
|
|
24089
|
+
const charged2 = reservation.settle(null);
|
|
24090
|
+
const call2 = paidCall(progress, channel, label, ceiling, charged2, null, null);
|
|
24091
|
+
ladderTrace.push({ at: Math.max(0, performance.now() - progress.startedAt), event: "spend_settled", channel: label, detail: { vendorId: channel.vendorId ?? null, ceilingUsd: ceiling, chargedUsd: charged2, basis: "ceiling", ended: "without_an_answer", call: call2 } });
|
|
24092
|
+
}
|
|
24093
|
+
throw error;
|
|
24094
|
+
}
|
|
24095
|
+
if (reservation === null || ceiling === void 0)
|
|
24096
|
+
return result;
|
|
24097
|
+
const charged = reservation.settle(result.usage.externalCostUsd);
|
|
24098
|
+
const call = paidCall(progress, channel, label, ceiling, charged, result.usage.externalCostUsd, result);
|
|
24099
|
+
return {
|
|
24100
|
+
...result,
|
|
24101
|
+
usage: { ...result.usage, externalCostChargedUsd: (result.usage.externalCostChargedUsd ?? 0) + charged },
|
|
24102
|
+
trace: [...result.trace, { at: result.usage.wallMs, lane: result.lane, event: "spend_settled", detail: { vendorId: channel.vendorId ?? null, ceilingUsd: ceiling, chargedUsd: charged, basis: result.usage.externalCostUsd === null ? "ceiling" : "reported", call } }]
|
|
24103
|
+
};
|
|
24104
|
+
}
|
|
24105
|
+
async attemptHandoff(url, result, channelsTried, channel, session, ladderTrace, best, attempts, execution, options, progress) {
|
|
23328
24106
|
ladderTrace.push({
|
|
23329
24107
|
at: result.usage.wallMs,
|
|
23330
24108
|
event: "ladder_step",
|
|
@@ -23388,7 +24166,14 @@ var LadderRunner = class {
|
|
|
23388
24166
|
};
|
|
23389
24167
|
}
|
|
23390
24168
|
const retryStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23391
|
-
const
|
|
24169
|
+
const paid = await this.paidFetch(channel, `${channel.id}(retry)`, url, snapshot, execution, options, progress, () => {
|
|
24170
|
+
});
|
|
24171
|
+
if (paid === null) {
|
|
24172
|
+
const tried2 = [...channelsTried, `${channel.id}(retry)`];
|
|
24173
|
+
ladderTrace.push({ at: result.usage.wallMs, event: "ladder_handoff_retry_failed", channel: `${channel.id}(retry)`, detail: { status: null, reason: "not called: no price ceiling, or one the budget cannot hold" } });
|
|
24174
|
+
return { result: sanitizeResult(best ?? result), channelsTried: tried2, handoffRequested: false, ladderTrace, summary: summarize(tried2, attempts) };
|
|
24175
|
+
}
|
|
24176
|
+
const retry = paid;
|
|
23392
24177
|
attempts.push(attemptOf(channel, `${channel.id}(retry)`, retry, attempts.length + 1, retryStartedAt));
|
|
23393
24178
|
ladderTrace.push({
|
|
23394
24179
|
at: retry.usage.wallMs,
|
|
@@ -23498,17 +24283,45 @@ function deadlineOutcome(url, progress, returned) {
|
|
|
23498
24283
|
result = { ...best.result, status: "partial", failureReason: null, blockReason: null, budgetExceeded: null, usage: { ...best.result.usage, deadlineExceeded: true }, trace: [...best.result.trace, event] };
|
|
23499
24284
|
} else {
|
|
23500
24285
|
const evidence = noMainContentEvidence(attempts);
|
|
23501
|
-
const
|
|
23502
|
-
const
|
|
23503
|
-
|
|
24286
|
+
const stepped = evidence === null && (returned === null || returned.evidence.httpStatus == null) ? progress.steppedPast : null;
|
|
24287
|
+
const base = evidence?.result ?? stepped?.result ?? (returned?.status === "failed" ? returned : deadlineFailure(url, laneOf(interrupted), at));
|
|
24288
|
+
const kept = evidence?.channel ?? stepped?.channel ?? null;
|
|
24289
|
+
const event = { at, lane: base.lane, event: "deadline_exceeded", detail: kept === null ? detail : { ...detail, evidence: kept } };
|
|
24290
|
+
result = { ...base, status: "failed", failureReason: "timeout", blockReason: null, budgetExceeded: null, markdown: evidence?.result.markdown ?? stepped?.result.markdown ?? null, usage: { ...base.usage, contentTokens: null, deadlineExceeded: true }, trace: [...base.trace, event] };
|
|
23504
24291
|
}
|
|
23505
24292
|
const summary = summarize(channelsTried, attempts);
|
|
23506
24293
|
const providerUnmeasured = interrupted !== null && attempts.length < channelsTried.length && laneOf(interrupted) === "provider";
|
|
23507
24294
|
const costed = providerUnmeasured ? { ...summary, externalCostUsd: null, externalCost: { ...summary.externalCost, unknown: true } } : summary;
|
|
23508
24295
|
return { result, channelsTried, handoffRequested: false, ladderTrace, summary: { ...costed, totalMs: at } };
|
|
23509
24296
|
}
|
|
23510
|
-
function
|
|
23511
|
-
|
|
24297
|
+
function paidCall(progress, channel, rung, ceilingUsd, chargedUsd, reportedCostUsd, result) {
|
|
24298
|
+
progress.charged = (progress.charged ?? 0) + chargedUsd;
|
|
24299
|
+
progress.grant ??= channel.grant ?? null;
|
|
24300
|
+
const call = progress.paidCalls.length + 1;
|
|
24301
|
+
progress.paidCalls.push({
|
|
24302
|
+
provider: channel.vendorId ?? channel.id,
|
|
24303
|
+
rung,
|
|
24304
|
+
capabilities: [...channel.grantCapabilities ?? []],
|
|
24305
|
+
ceilingUsd,
|
|
24306
|
+
chargedUsd,
|
|
24307
|
+
reportedCostUsd,
|
|
24308
|
+
outcome: result?.status ?? null,
|
|
24309
|
+
reason: result === null ? null : result.failureReason ?? result.blockReason ?? result.budgetExceeded ?? null,
|
|
24310
|
+
call
|
|
24311
|
+
});
|
|
24312
|
+
return call;
|
|
24313
|
+
}
|
|
24314
|
+
function withRunCost(run, progress) {
|
|
24315
|
+
const { externalCostChargedUsd: _attempt, ...usage2 } = run.result.usage;
|
|
24316
|
+
const result = { ...run.result, usage: { ...usage2, externalCostUsd: run.summary.externalCostUsd, ...progress.charged === null ? {} : { externalCostChargedUsd: progress.charged } } };
|
|
24317
|
+
if (progress.paidCalls.length === 0)
|
|
24318
|
+
return { ...run, result };
|
|
24319
|
+
const own = result.trace.find((event) => event.event === "spend_settled")?.detail?.call;
|
|
24320
|
+
return { ...run, result: { ...result, trace: [...result.trace, paidCallsEvent(progress, own, result.usage.wallMs, result.lane)] } };
|
|
24321
|
+
}
|
|
24322
|
+
function paidCallsEvent(progress, answer, at, lane) {
|
|
24323
|
+
const calls = progress.paidCalls.map(({ call, ...paid }) => ({ ...paid, answer: call === answer }));
|
|
24324
|
+
return { at, lane, event: "paid_calls", detail: { calls, grant: progress.grant } };
|
|
23512
24325
|
}
|
|
23513
24326
|
function noMainContentEvidence(attempts) {
|
|
23514
24327
|
for (let i = attempts.length - 1; i >= 0; i--) {
|
|
@@ -23690,6 +24503,7 @@ init_provider2();
|
|
|
23690
24503
|
|
|
23691
24504
|
// packages/bench/dist/vendors/cdp.js
|
|
23692
24505
|
init_dist2();
|
|
24506
|
+
init_browserSettle();
|
|
23693
24507
|
var playwrightConnector = async (wsUrl, deadlineMs, signal) => {
|
|
23694
24508
|
throwIfExecutionStopped({ signal, deadlineAt: deadlineMs });
|
|
23695
24509
|
const { chromium: chromium4 } = await import("playwright");
|
|
@@ -23764,8 +24578,9 @@ async function navigateOnce(browser, url, deadlineMs, signal) {
|
|
|
23764
24578
|
throwIfExecutionStopped(scope);
|
|
23765
24579
|
const timeout = navigationTimeout(deadlineMs, Date.now());
|
|
23766
24580
|
const response = await raceWithSignal(page.goto(url, { waitUntil: "domcontentloaded", timeout }), scope.signal);
|
|
23767
|
-
await raceWithSignal(waitForRenderedStability(page, {
|
|
23768
|
-
maxMs: deadlineMs === void 0 ? 1500 : Math.min(1500, Math.max(1, deadlineMs - Date.now()))
|
|
24581
|
+
const settle = await raceWithSignal(waitForRenderedStability(page, {
|
|
24582
|
+
maxMs: deadlineMs === void 0 ? 1500 : Math.min(1500, Math.max(1, deadlineMs - Date.now())),
|
|
24583
|
+
loadingMaxMs: deadlineMs === void 0 ? LOADING_WAIT_MAX_MS : Math.min(LOADING_WAIT_MAX_MS, Math.max(1, deadlineMs - 1e3 - Date.now()))
|
|
23769
24584
|
}), scope.signal);
|
|
23770
24585
|
throwIfExecutionStopped(scope);
|
|
23771
24586
|
const headers = {};
|
|
@@ -23792,7 +24607,8 @@ async function navigateOnce(browser, url, deadlineMs, signal) {
|
|
|
23792
24607
|
finalUrl: page.url(),
|
|
23793
24608
|
headers,
|
|
23794
24609
|
sentUserAgent,
|
|
23795
|
-
sentClientHints
|
|
24610
|
+
sentClientHints,
|
|
24611
|
+
settle
|
|
23796
24612
|
};
|
|
23797
24613
|
} finally {
|
|
23798
24614
|
scope.signal.removeEventListener("abort", abort);
|
|
@@ -23906,6 +24722,7 @@ var CdpVendorTransport = class {
|
|
|
23906
24722
|
// subject treats that as unobserved, never as agreement.
|
|
23907
24723
|
sentUserAgent: res.sentUserAgent,
|
|
23908
24724
|
sentClientHints: res.sentClientHints,
|
|
24725
|
+
...res.settle === void 0 ? {} : { settle: res.settle },
|
|
23909
24726
|
declaredUserAgent: declared3,
|
|
23910
24727
|
// Neither vendor states a per-request price in its API response, and
|
|
23911
24728
|
// an estimate in this field would read as a measurement.
|
|
@@ -24006,6 +24823,7 @@ async function connectVendor(ops, connector = playwrightConnector, resume, deadl
|
|
|
24006
24823
|
|
|
24007
24824
|
// packages/bench/dist/vendors/browserbase.js
|
|
24008
24825
|
init_dist2();
|
|
24826
|
+
var BROWSERBASE_MIN_SESSION_MS = 6e4;
|
|
24009
24827
|
var BROWSERBASE_CAPABILITIES = [
|
|
24010
24828
|
{ capability: "headless_browser", vendorDefaultOn: true, enableKey: null },
|
|
24011
24829
|
{ capability: "datacenter_proxy", vendorDefaultOn: true, enableKey: null },
|
|
@@ -24015,12 +24833,13 @@ var BROWSERBASE_CAPABILITIES = [
|
|
|
24015
24833
|
{ capability: "captcha_solving", vendorDefaultOn: true, enableKey: "vendor_captcha_solving" },
|
|
24016
24834
|
{ capability: "fingerprint_spoofing", vendorDefaultOn: false, enableKey: "vendor_stealth" }
|
|
24017
24835
|
];
|
|
24018
|
-
function browserbaseSessionBody(decision, projectId, resume) {
|
|
24836
|
+
function browserbaseSessionBody(decision, projectId, resume, sessionTimeoutMs) {
|
|
24019
24837
|
const persistEnabled = decision.enabled.some((c) => c.capability === "session_persistence");
|
|
24020
24838
|
const solveEnabled = decision.enabled.some((c) => c.capability === "captcha_solving");
|
|
24021
24839
|
const stealthEnabled = decision.enabled.some((c) => c.capability === "fingerprint_spoofing");
|
|
24022
24840
|
return {
|
|
24023
24841
|
...projectId === void 0 ? {} : { projectId },
|
|
24842
|
+
...sessionTimeoutMs === void 0 ? {} : { timeout: Math.ceil(Math.max(sessionTimeoutMs, BROWSERBASE_MIN_SESSION_MS) / 1e3), proxies: false, keepAlive: false },
|
|
24024
24843
|
browserSettings: {
|
|
24025
24844
|
// Browserbase defaults solveCaptchas to true, so the off state is an
|
|
24026
24845
|
// explicit false, never an omission.
|
|
@@ -24073,7 +24892,7 @@ function browserbaseOps(config, api = fetchVendorApi, policy = {}) {
|
|
|
24073
24892
|
return { browserbaseContextId: id };
|
|
24074
24893
|
},
|
|
24075
24894
|
async createSession(resume, deadlineMs, signal) {
|
|
24076
|
-
const body = browserbaseSessionBody(decision, config.projectId, resume ?? null);
|
|
24895
|
+
const body = browserbaseSessionBody(decision, config.projectId, resume ?? null, config.sessionTimeoutMs);
|
|
24077
24896
|
const res = await api({
|
|
24078
24897
|
method: "POST",
|
|
24079
24898
|
url: `${base}/v1/sessions`,
|
|
@@ -24123,6 +24942,7 @@ function browserbaseOps(config, api = fetchVendorApi, policy = {}) {
|
|
|
24123
24942
|
|
|
24124
24943
|
// packages/bench/dist/vendors/steel.js
|
|
24125
24944
|
init_dist2();
|
|
24945
|
+
var STEEL_MIN_SESSION_MS = 15e3;
|
|
24126
24946
|
var STEEL_CAPABILITIES = [
|
|
24127
24947
|
{ capability: "headless_browser", vendorDefaultOn: true, enableKey: null },
|
|
24128
24948
|
{ capability: "datacenter_proxy", vendorDefaultOn: true, enableKey: null },
|
|
@@ -24133,11 +24953,12 @@ var STEEL_CAPABILITIES = [
|
|
|
24133
24953
|
{ capability: "captcha_solving", vendorDefaultOn: false, enableKey: "vendor_captcha_solving" },
|
|
24134
24954
|
{ capability: "fingerprint_spoofing", vendorDefaultOn: true, enableKey: "vendor_stealth" }
|
|
24135
24955
|
];
|
|
24136
|
-
function steelSessionBody(decision, resume) {
|
|
24956
|
+
function steelSessionBody(decision, resume, sessionTimeoutMs) {
|
|
24137
24957
|
const persistEnabled = decision.enabled.some((c) => c.capability === "session_persistence");
|
|
24138
24958
|
const solveEnabled = decision.enabled.some((c) => c.capability === "captcha_solving");
|
|
24139
24959
|
const stealthEnabled = decision.enabled.some((c) => c.capability === "fingerprint_spoofing");
|
|
24140
24960
|
return {
|
|
24961
|
+
...sessionTimeoutMs === void 0 ? {} : { timeout: Math.max(sessionTimeoutMs, STEEL_MIN_SESSION_MS), useProxy: false },
|
|
24141
24962
|
// Steel injects a synthetic fingerprint BY DEFAULT. skipFingerprintInjection
|
|
24142
24963
|
// is the opt-out, sent unless the grant names `vendor_stealth` (ADR 0005);
|
|
24143
24964
|
// a body without it would buy the capability by omission. Solving follows
|
|
@@ -24177,7 +24998,7 @@ function steelOps(config, api = fetchVendorApi, policy = {}) {
|
|
|
24177
24998
|
return null;
|
|
24178
24999
|
},
|
|
24179
25000
|
async createSession(resume, deadlineMs, signal) {
|
|
24180
|
-
const body = steelSessionBody(decision, resume ?? null);
|
|
25001
|
+
const body = steelSessionBody(decision, resume ?? null, config.sessionTimeoutMs);
|
|
24181
25002
|
const res = await api({
|
|
24182
25003
|
method: "POST",
|
|
24183
25004
|
url: `${base}/v1/sessions`,
|
|
@@ -24250,7 +25071,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
24250
25071
|
{
|
|
24251
25072
|
id: "http",
|
|
24252
25073
|
identity: declared3,
|
|
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),
|
|
25074
|
+
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, execution?.onTrace),
|
|
24254
25075
|
close: async () => {
|
|
24255
25076
|
await http.teardown();
|
|
24256
25077
|
await opts.localSubjects?.http?.teardown?.();
|
|
@@ -24259,7 +25080,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
24259
25080
|
...compat === null ? [] : [{
|
|
24260
25081
|
id: "http_compat",
|
|
24261
25082
|
identity: identityBundleFrom(compatIdentity()),
|
|
24262
|
-
fetch: (url, _session, execution, options) => compat.fetch(url, execution?.deadlineAt, execution?.signal, {}, execution?.onRetryAfter, options, execution?.onRobotsOverride, execution?.cookieSession),
|
|
25083
|
+
fetch: (url, _session, execution, options) => compat.fetch(url, execution?.deadlineAt, execution?.signal, {}, execution?.onRetryAfter, options, execution?.onRobotsOverride, execution?.cookieSession, execution?.onTrace),
|
|
24263
25084
|
close: () => compat.teardown()
|
|
24264
25085
|
}],
|
|
24265
25086
|
{
|
|
@@ -24267,7 +25088,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
24267
25088
|
identity: declared3,
|
|
24268
25089
|
waitsFor: true,
|
|
24269
25090
|
// No session here, ever: the plain rung is the public browser.
|
|
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),
|
|
25091
|
+
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, execution === void 0 ? void 0 : { onListPage: execution.onListPage, listResume: execution.listResume }),
|
|
24271
25092
|
close: async () => {
|
|
24272
25093
|
await plainBrowser.teardown();
|
|
24273
25094
|
await opts.localSubjects?.browser_local?.teardown?.();
|
|
@@ -24327,7 +25148,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
24327
25148
|
};
|
|
24328
25149
|
return skip;
|
|
24329
25150
|
}
|
|
24330
|
-
return authedSubjectFor(session).fetch(url, execution?.deadlineAt, execution?.signal, execution?.onRetryAfter, options, execution?.onRobotsOverride);
|
|
25151
|
+
return authedSubjectFor(session).fetch(url, execution?.deadlineAt, execution?.signal, execution?.onRetryAfter, options, execution?.onRobotsOverride, void 0, execution === void 0 ? void 0 : { onListPage: execution.onListPage, listResume: execution.listResume });
|
|
24331
25152
|
},
|
|
24332
25153
|
close: async () => {
|
|
24333
25154
|
for (const subject of authedSubjects.values())
|
|
@@ -24335,7 +25156,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
24335
25156
|
}
|
|
24336
25157
|
});
|
|
24337
25158
|
}
|
|
24338
|
-
if (mode === "standard")
|
|
25159
|
+
if (mode === "standard" && opts.enhanced !== true)
|
|
24339
25160
|
return channels;
|
|
24340
25161
|
if (!(opts.vendorPolicy?.authorized ?? []).includes("vendor_remote_browser"))
|
|
24341
25162
|
return channels;
|
|
@@ -24345,7 +25166,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
24345
25166
|
const routedPolicy = opts.robotsFetcher === void 0 && opts.networkPolicy?.egressProxy ? opts.networkPolicy : null;
|
|
24346
25167
|
let providerRoutes = null;
|
|
24347
25168
|
const providerRobots = opts.robotsFetcher ?? (routedPolicy === null ? void 0 : robotsFetcherVia((url) => (providerRoutes ??= new EgressRoutes(routedPolicy)).dispatcherFor(url)));
|
|
24348
|
-
const vendorChannel = (vendorId, ops) => {
|
|
25169
|
+
const vendorChannel = (vendorId, ops, sessionFloorMs = 0) => {
|
|
24349
25170
|
let connected = null;
|
|
24350
25171
|
let pending = null;
|
|
24351
25172
|
let establishedResume = null;
|
|
@@ -24361,6 +25182,11 @@ function buildChannels(mode, opts = {}) {
|
|
|
24361
25182
|
if (establishedResume !== null)
|
|
24362
25183
|
pendingResume = establishedResume;
|
|
24363
25184
|
};
|
|
25185
|
+
const tariff = opts.vendorTariffs?.[vendorId] ?? null;
|
|
25186
|
+
const connectOwn = (execution) => {
|
|
25187
|
+
opts.onVendorConnect?.(vendorId);
|
|
25188
|
+
return connectVendor(ops, opts.vendorConnector, pendingResume ?? null, execution?.deadlineAt, execution?.signal, opts.vendorPolicy?.authorized ?? []);
|
|
25189
|
+
};
|
|
24364
25190
|
const ensureConnected = async (execution) => {
|
|
24365
25191
|
if (connected !== null)
|
|
24366
25192
|
return connected;
|
|
@@ -24376,7 +25202,7 @@ function buildChannels(mode, opts = {}) {
|
|
|
24376
25202
|
}
|
|
24377
25203
|
return await pending;
|
|
24378
25204
|
};
|
|
24379
|
-
|
|
25205
|
+
const raw = {
|
|
24380
25206
|
id: "provider",
|
|
24381
25207
|
vendorId,
|
|
24382
25208
|
identity: identityForRoute(mode, { resume: true }),
|
|
@@ -24434,13 +25260,19 @@ function buildChannels(mode, opts = {}) {
|
|
|
24434
25260
|
} else {
|
|
24435
25261
|
await preparePersistence(execution);
|
|
24436
25262
|
}
|
|
24437
|
-
const { declaration, transport } = await ensureConnected(execution);
|
|
24438
|
-
|
|
24439
|
-
|
|
25263
|
+
const { declaration, transport } = tariff !== null ? await connectOwn(execution) : await ensureConnected(execution);
|
|
25264
|
+
try {
|
|
25265
|
+
if (pendingResume !== null) {
|
|
25266
|
+
transport.useResumedSession(pendingResume);
|
|
25267
|
+
}
|
|
25268
|
+
const { ProviderSubject: ProviderSubject2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports));
|
|
25269
|
+
const subject = new ProviderSubject2(declaration, transport, mode, null, providerRobots);
|
|
25270
|
+
return await subject.fetch(url, execution?.deadlineAt, execution?.signal, execution?.onRetryAfter, options);
|
|
25271
|
+
} finally {
|
|
25272
|
+
if (tariff !== null)
|
|
25273
|
+
await transport.close().catch(() => {
|
|
25274
|
+
});
|
|
24440
25275
|
}
|
|
24441
|
-
const { ProviderSubject: ProviderSubject2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports));
|
|
24442
|
-
const subject = new ProviderSubject2(declaration, transport, mode, null, providerRobots);
|
|
24443
|
-
return subject.fetch(url, execution?.deadlineAt, execution?.signal, execution?.onRetryAfter, options);
|
|
24444
25276
|
},
|
|
24445
25277
|
close: async () => {
|
|
24446
25278
|
if (connected !== null) {
|
|
@@ -24452,14 +25284,28 @@ function buildChannels(mode, opts = {}) {
|
|
|
24452
25284
|
await providerRoutes?.close();
|
|
24453
25285
|
}
|
|
24454
25286
|
};
|
|
25287
|
+
const grantCapabilities = [.../* @__PURE__ */ new Set(["vendor_remote_browser", ...ops.decision.enabled.flatMap((c) => VENDOR_CAPABILITY_ACCESS[c.capability] ?? [])])];
|
|
25288
|
+
return {
|
|
25289
|
+
...raw,
|
|
25290
|
+
grantCapabilities,
|
|
25291
|
+
...opts.vendorGrant === void 0 ? {} : { grant: opts.vendorGrant },
|
|
25292
|
+
priceCeilingUsd: tariff === null ? null : tariffCeilingUsd(tariff, sessionFloorMs),
|
|
25293
|
+
fetch: async (url, session, execution, options) => {
|
|
25294
|
+
if (tariff === null || tariff.maxSessionMs === null)
|
|
25295
|
+
return raw.fetch(url, session, execution, options);
|
|
25296
|
+
return raw.fetch(url, session, { ...execution, deadlineAt: Math.min(execution?.deadlineAt ?? Number.POSITIVE_INFINITY, Date.now() + tariff.maxSessionMs) }, options);
|
|
25297
|
+
}
|
|
25298
|
+
};
|
|
24455
25299
|
};
|
|
24456
25300
|
if (bbKey !== "" || opts.vendorOps?.browserbase !== void 0) {
|
|
24457
|
-
const
|
|
24458
|
-
|
|
25301
|
+
const sessionTimeoutMs = opts.vendorTariffs?.browserbase?.maxSessionMs ?? null;
|
|
25302
|
+
const ops = opts.vendorOps?.browserbase ?? browserbaseOps({ apiKey: bbKey, ...sessionTimeoutMs === null ? {} : { sessionTimeoutMs } }, void 0, opts.vendorPolicy);
|
|
25303
|
+
channels.push(vendorChannel("browserbase", ops, BROWSERBASE_MIN_SESSION_MS));
|
|
24459
25304
|
}
|
|
24460
25305
|
if (steelKey !== "" || opts.vendorOps?.steel !== void 0) {
|
|
24461
|
-
const
|
|
24462
|
-
|
|
25306
|
+
const sessionTimeoutMs = opts.vendorTariffs?.steel?.maxSessionMs ?? null;
|
|
25307
|
+
const ops = opts.vendorOps?.steel ?? steelOps({ apiKey: steelKey, ...sessionTimeoutMs === null ? {} : { sessionTimeoutMs } }, void 0, opts.vendorPolicy);
|
|
25308
|
+
channels.push(vendorChannel("steel", ops, STEEL_MIN_SESSION_MS));
|
|
24463
25309
|
}
|
|
24464
25310
|
return channels;
|
|
24465
25311
|
}
|
|
@@ -24481,13 +25327,18 @@ init_dist();
|
|
|
24481
25327
|
init_dist3();
|
|
24482
25328
|
init_dist2();
|
|
24483
25329
|
init_errorPage();
|
|
24484
|
-
var LANE = "browser_local_authed";
|
|
24485
25330
|
function pageFromUserBrowser(read2, prior, options) {
|
|
25331
|
+
const LANE = prior === null ? "my_browser" : "browser_local_authed";
|
|
24486
25332
|
const { html: body, finalUrl, wallMs } = read2;
|
|
25333
|
+
const whole = read2.document ?? body;
|
|
24487
25334
|
const status = read2.status ?? 200;
|
|
24488
25335
|
const trace = [
|
|
24489
|
-
|
|
24490
|
-
|
|
25336
|
+
...prior === null ? [] : [
|
|
25337
|
+
{ at: 0, lane: LANE, event: "handoff_from", detail: { status: prior.status, blockReason: prior.blockReason, failureReason: prior.failureReason, lane: prior.lane, rawBodySha256: prior.evidence.rawBodySha256 } },
|
|
25338
|
+
...robotsOf(prior, LANE),
|
|
25339
|
+
// The stopped run's provider calls were paid for: they stay on the page's record, none of them the page the person read.
|
|
25340
|
+
...givenUpPaidCalls(prior.trace)
|
|
25341
|
+
],
|
|
24491
25342
|
{ at: 0, lane: LANE, event: "identity_sent", detail: { mode: "authed", headers: [], by: "user_browser" } },
|
|
24492
25343
|
{ at: 0, lane: LANE, event: "identity_unobserved", detail: { reason: `the person's own browser (${read2.browser}) sent the request; its headers were not seen` } },
|
|
24493
25344
|
{ at: wallMs, lane: LANE, event: "user_browser_read", detail: { browser: read2.browser, status: read2.status, sawGate: read2.sawGate, act: read2.act, waitedMs: wallMs } }
|
|
@@ -24504,14 +25355,14 @@ function pageFromUserBrowser(read2, prior, options) {
|
|
|
24504
25355
|
redirectChain: [],
|
|
24505
25356
|
redirectChainComplete: false,
|
|
24506
25357
|
contentType: read2.contentType,
|
|
24507
|
-
rawBodySha256: sha256Utf8(
|
|
25358
|
+
rawBodySha256: sha256Utf8(whole),
|
|
24508
25359
|
artifacts: [],
|
|
24509
25360
|
fetchedAt: read2.fetchedAt
|
|
24510
25361
|
},
|
|
24511
25362
|
usage: {
|
|
24512
25363
|
wallMs,
|
|
24513
25364
|
bytesWire: null,
|
|
24514
|
-
bytesDecompressed: Buffer.byteLength(
|
|
25365
|
+
bytesDecompressed: Buffer.byteLength(whole),
|
|
24515
25366
|
// W2L sent none: the person's browser made the requests, as many as it did.
|
|
24516
25367
|
requestCount: 0,
|
|
24517
25368
|
attemptCount: 0,
|
|
@@ -24521,7 +25372,7 @@ function pageFromUserBrowser(read2, prior, options) {
|
|
|
24521
25372
|
},
|
|
24522
25373
|
trace
|
|
24523
25374
|
};
|
|
24524
|
-
const gate = classifyGate({ status, header: () => null, body });
|
|
25375
|
+
const gate = classifyGate({ status, header: () => null, body: whole });
|
|
24525
25376
|
const errorPage = errorPageEvidence(status, read2.contentType, body, finalUrl, options);
|
|
24526
25377
|
const errorPageFields = { markdown: errorPage?.markdown ?? null, ...errorPage === null ? {} : { links: errorPage.links } };
|
|
24527
25378
|
const blocked = (verdict) => {
|
|
@@ -24540,7 +25391,8 @@ function pageFromUserBrowser(read2, prior, options) {
|
|
|
24540
25391
|
trace.push({ at: wallMs, lane: LANE, event: "extract", detail: { pageType: extracted.pageType, strategy: extracted.strategy, confidence: extracted.confidence, escalate: extracted.escalate, linkCount: links.length, ...options.onlyMainContent === false ? { onlyMainContent: false } : {}, ...tagOptions(options) } });
|
|
24541
25392
|
let wholePage = null;
|
|
24542
25393
|
let listPage = false;
|
|
24543
|
-
|
|
25394
|
+
const judged = whole === body || gate === null ? extracted : extractTf.extract(whole, { url: finalUrl, pruneSelectors: options.excludeTags, includeSelectors: options.includeTags, blockAds: options.blockAds });
|
|
25395
|
+
if ((judged.escalate || judged.lastResort === true) && gate !== null)
|
|
24544
25396
|
return blocked(gate);
|
|
24545
25397
|
if (extracted.escalate && !selectionAsked(options)) {
|
|
24546
25398
|
wholePage = wholePageMarkdown(body, finalUrl, options);
|
|
@@ -24549,7 +25401,7 @@ function pageFromUserBrowser(read2, prior, options) {
|
|
|
24549
25401
|
return { ...base, status: "failed", failureReason: "empty_unverified", blockReason: null, budgetExceeded: null, lane: LANE, escalations: [], markdown: wholePage, ...wholePage === null ? {} : { links } };
|
|
24550
25402
|
}
|
|
24551
25403
|
}
|
|
24552
|
-
const decisive = classifyGate({ status, header: () => null, body, contentful: true });
|
|
25404
|
+
const decisive = classifyGate({ status, header: () => null, body: whole, contentful: true });
|
|
24553
25405
|
if (decisive !== null)
|
|
24554
25406
|
return blocked(decisive);
|
|
24555
25407
|
const markdown = wholePageAsked(options) || listPage ? wholePage ?? htmlToMarkdown(body, { baseUrl: finalUrl, exclude: options.excludeTags, ...markdownOptions(options) }) : htmlToMarkdown(extracted.mainHtml, { baseUrl: extracted.baseUrl, ...markdownOptions(options) });
|
|
@@ -24579,11 +25431,30 @@ function pageFromUserBrowser(read2, prior, options) {
|
|
|
24579
25431
|
adapterValidation: extracted.adapterValidation,
|
|
24580
25432
|
labelledValues: extracted.labelledValues
|
|
24581
25433
|
},
|
|
24582
|
-
...htmlFormats(
|
|
25434
|
+
...htmlFormats(whole, body, extracted.mainHtml, options),
|
|
24583
25435
|
usage: { ...base.usage, contentTokens: estimateTokens(markdown) }
|
|
24584
25436
|
});
|
|
24585
25437
|
}
|
|
24586
|
-
function
|
|
25438
|
+
function unreadInUserBrowser(url, outcome, message2, wallMs) {
|
|
25439
|
+
return {
|
|
25440
|
+
requestedUrl: url,
|
|
25441
|
+
status: outcome.status,
|
|
25442
|
+
failureReason: outcome.status === "failed" ? outcome.failureReason : null,
|
|
25443
|
+
blockReason: outcome.status === "blocked" ? outcome.blockReason : null,
|
|
25444
|
+
budgetExceeded: null,
|
|
25445
|
+
lane: "my_browser",
|
|
25446
|
+
escalations: [],
|
|
25447
|
+
markdown: null,
|
|
25448
|
+
truncated: false,
|
|
25449
|
+
truncatedAt: null,
|
|
25450
|
+
compliance: null,
|
|
25451
|
+
evidence: { finalUrl: url, httpStatus: null, redirectChain: [], redirectChainComplete: false, contentType: null, rawBodySha256: null, artifacts: [], fetchedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
25452
|
+
usage: { wallMs, bytesWire: null, bytesDecompressed: 0, requestCount: 0, attemptCount: 0, contentTokens: null, browserMs: wallMs, externalCostUsd: 0 },
|
|
25453
|
+
trace: [{ at: wallMs, lane: "my_browser", event: "user_browser_unread", detail: { reason: message2 } }],
|
|
25454
|
+
warnings: [{ code: "my_browser_not_read", message: message2 }]
|
|
25455
|
+
};
|
|
25456
|
+
}
|
|
25457
|
+
function robotsOf(prior, LANE) {
|
|
24587
25458
|
const signed = prior.compliance?.robots;
|
|
24588
25459
|
if (signed !== void 0) {
|
|
24589
25460
|
return [
|
|
@@ -24837,7 +25708,7 @@ import { mkdir as mkdir7, writeFile as writeFile6 } from "fs/promises";
|
|
|
24837
25708
|
init_dist();
|
|
24838
25709
|
|
|
24839
25710
|
// packages/sdk/dist/version.js
|
|
24840
|
-
var SDK_VERSION = "0.3.
|
|
25711
|
+
var SDK_VERSION = "0.3.2";
|
|
24841
25712
|
|
|
24842
25713
|
// packages/sdk/dist/watcher.js
|
|
24843
25714
|
init_dist();
|
|
@@ -25047,7 +25918,7 @@ init_dist();
|
|
|
25047
25918
|
init_dist2();
|
|
25048
25919
|
import { createHash as createHash12 } from "crypto";
|
|
25049
25920
|
import { gunzipSync } from "zlib";
|
|
25050
|
-
import { request as
|
|
25921
|
+
import { request as request7 } from "undici";
|
|
25051
25922
|
init_egress();
|
|
25052
25923
|
init_robotsLookup();
|
|
25053
25924
|
var HEADERS_TIMEOUT_MS = 1e4;
|
|
@@ -25078,14 +25949,14 @@ var HttpSitemapSource = class {
|
|
|
25078
25949
|
this.ignoreRobotsTxt = options.ignoreRobotsTxt === true;
|
|
25079
25950
|
this.robots = options.robots ?? new RobotsOriginCache(this.policy, (url) => this.route.dispatcherFor(url));
|
|
25080
25951
|
}
|
|
25081
|
-
async load(
|
|
25952
|
+
async load(request8, context = {}) {
|
|
25082
25953
|
const scope = createExecutionScope(context);
|
|
25083
|
-
const soft =
|
|
25954
|
+
const soft = request8.softDeadlineAt === void 0 ? null : createExecutionScope({ signal: scope.signal, deadlineAt: request8.softDeadlineAt });
|
|
25084
25955
|
const work = soft ?? scope;
|
|
25085
|
-
const timedOut = () => soft !== null && !scope.signal.aborted && (soft.signal.aborted || Date.now() >=
|
|
25086
|
-
const details =
|
|
25956
|
+
const timedOut = () => soft !== null && !scope.signal.aborted && (soft.signal.aborted || Date.now() >= request8.softDeadlineAt);
|
|
25957
|
+
const details = request8.accept !== void 0 || request8.softDeadlineAt !== void 0;
|
|
25087
25958
|
try {
|
|
25088
|
-
const seed = new URL(
|
|
25959
|
+
const seed = new URL(request8.seedUrl);
|
|
25089
25960
|
const identity = this.identityFor(seed.hostname);
|
|
25090
25961
|
const result = { identity: { mode: this.mode, userAgent: identity.userAgent }, sources: [], files: [], urls: [], truncated: null };
|
|
25091
25962
|
let robots;
|
|
@@ -25102,7 +25973,7 @@ var HttpSitemapSource = class {
|
|
|
25102
25973
|
const queue = (declared3.length > 0 ? declared3 : [`${seed.origin}/sitemap.xml`]).map((url) => ({ url, depth: 0 }));
|
|
25103
25974
|
const seen = /* @__PURE__ */ new Set();
|
|
25104
25975
|
while (queue.length > 0) {
|
|
25105
|
-
if (result.files.length >=
|
|
25976
|
+
if (result.files.length >= request8.maxFiles) {
|
|
25106
25977
|
result.truncated = "files";
|
|
25107
25978
|
break;
|
|
25108
25979
|
}
|
|
@@ -25127,12 +25998,12 @@ var HttpSitemapSource = class {
|
|
|
25127
25998
|
continue;
|
|
25128
25999
|
}
|
|
25129
26000
|
let left = 0;
|
|
25130
|
-
const accept =
|
|
26001
|
+
const accept = request8.accept;
|
|
25131
26002
|
for (const [i, url] of file.locs.entries()) {
|
|
25132
26003
|
if (seen.has(url))
|
|
25133
26004
|
continue;
|
|
25134
26005
|
const entry3 = entryOf(url, next.url, file.details?.[i]);
|
|
25135
|
-
if (result.urls.length >=
|
|
26006
|
+
if (result.urls.length >= request8.maxUrls) {
|
|
25136
26007
|
if (accept === void 0) {
|
|
25137
26008
|
left++;
|
|
25138
26009
|
continue;
|
|
@@ -25147,7 +26018,7 @@ var HttpSitemapSource = class {
|
|
|
25147
26018
|
continue;
|
|
25148
26019
|
result.urls.push(entry3);
|
|
25149
26020
|
}
|
|
25150
|
-
if (result.urls.length >=
|
|
26021
|
+
if (result.urls.length >= request8.maxUrls && (left > 0 || queue.length > 0)) {
|
|
25151
26022
|
result.truncated = "urls";
|
|
25152
26023
|
break;
|
|
25153
26024
|
}
|
|
@@ -25273,7 +26144,7 @@ var HttpSitemapSource = class {
|
|
|
25273
26144
|
try {
|
|
25274
26145
|
await this.scheduler.beforeRequest(origin, scope.signal);
|
|
25275
26146
|
this.lastRequestAt.set(origin, performance.now());
|
|
25276
|
-
const response = await
|
|
26147
|
+
const response = await request7(current, {
|
|
25277
26148
|
dispatcher: this.route.dispatcherFor(current),
|
|
25278
26149
|
method: "GET",
|
|
25279
26150
|
// A copy per hop: undici's ProxyAgent writes `host` into the object it is given, so a reused one sends the first hop's Host to every later one.
|
|
@@ -25337,6 +26208,9 @@ function discard2(body) {
|
|
|
25337
26208
|
body.destroy();
|
|
25338
26209
|
}
|
|
25339
26210
|
|
|
26211
|
+
// packages/bench/dist/index.js
|
|
26212
|
+
init_browserSettle();
|
|
26213
|
+
|
|
25340
26214
|
// packages/api/dist/engine.js
|
|
25341
26215
|
init_dist3();
|
|
25342
26216
|
init_dist();
|
|
@@ -25346,15 +26220,21 @@ init_dist2();
|
|
|
25346
26220
|
init_dist();
|
|
25347
26221
|
import { connect as netConnect } from "net";
|
|
25348
26222
|
import { connect as tlsConnect } from "tls";
|
|
26223
|
+
import { isIP as isIP4 } from "net";
|
|
25349
26224
|
var EGRESS_COOLDOWN_MS = 10 * 6e4;
|
|
25350
26225
|
var MAX_EGRESS_SWITCHES = 2;
|
|
25351
26226
|
var EgressPool = class {
|
|
25352
26227
|
now;
|
|
26228
|
+
echoUrl;
|
|
26229
|
+
echo;
|
|
25353
26230
|
egresses;
|
|
25354
26231
|
cooldownUntil = /* @__PURE__ */ new Map();
|
|
26232
|
+
exits = /* @__PURE__ */ new Map();
|
|
25355
26233
|
next = 0;
|
|
25356
|
-
constructor(servers, policy, now = Date.now) {
|
|
26234
|
+
constructor(servers, policy, now = Date.now, echoUrl = null, echo = (server, url) => fetchThroughProxy(server, url)) {
|
|
25357
26235
|
this.now = now;
|
|
26236
|
+
this.echoUrl = echoUrl;
|
|
26237
|
+
this.echo = echo;
|
|
25358
26238
|
if (servers.length === 0)
|
|
25359
26239
|
throw new Error("an egress pool needs at least one proxy");
|
|
25360
26240
|
this.egresses = servers.map((server) => ({ id: server.endpoint, policy: withPoolProxy(policy, server), server }));
|
|
@@ -25386,14 +26266,51 @@ var EgressPool = class {
|
|
|
25386
26266
|
}
|
|
25387
26267
|
return [...pool].sort((a, b) => (this.cooldownUntil.get(a.id) ?? 0) - (this.cooldownUntil.get(b.id) ?? 0))[0];
|
|
25388
26268
|
}
|
|
25389
|
-
/** The egress failed its probe: it cools down. */
|
|
26269
|
+
/** The egress failed its probe: it cools down, and where it leaves from is asked again when it is next used. */
|
|
25390
26270
|
fail(id) {
|
|
25391
26271
|
this.cooldownUntil.set(id, this.now() + EGRESS_COOLDOWN_MS);
|
|
26272
|
+
this.exits.delete(id);
|
|
26273
|
+
}
|
|
26274
|
+
/**
|
|
26275
|
+
* Where this egress leaves from, as the echo URL saw it: asked once and kept for EGRESS_COOLDOWN_MS, since a
|
|
26276
|
+
* provider's exit may change. Null without an echo URL, and when the echo did not answer or named no address:
|
|
26277
|
+
* unknown stays unknown.
|
|
26278
|
+
*/
|
|
26279
|
+
exitOf(id) {
|
|
26280
|
+
const egress = this.byId(id);
|
|
26281
|
+
if (this.echoUrl === null || egress === void 0)
|
|
26282
|
+
return Promise.resolve(null);
|
|
26283
|
+
const known = this.exits.get(id);
|
|
26284
|
+
if (known !== void 0 && this.now() - known.at < EGRESS_COOLDOWN_MS)
|
|
26285
|
+
return known.exit;
|
|
26286
|
+
const echoUrl = this.echoUrl;
|
|
26287
|
+
const exit = this.echo(egress.server, echoUrl).then((answer) => answer.status >= 200 && answer.status < 300 ? parseEchoExit(answer.body, new Date(this.now()).toISOString()) : null).catch(() => null);
|
|
26288
|
+
this.exits.set(id, { at: this.now(), exit });
|
|
26289
|
+
return exit;
|
|
25392
26290
|
}
|
|
25393
26291
|
};
|
|
26292
|
+
function parseEchoExit(body, observedAt) {
|
|
26293
|
+
const text2 = body.trim();
|
|
26294
|
+
let ip = null;
|
|
26295
|
+
let country = null;
|
|
26296
|
+
try {
|
|
26297
|
+
const parsed = JSON.parse(text2);
|
|
26298
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
26299
|
+
const address = [parsed.ip, parsed.origin, parsed.query].find((value) => typeof value === "string");
|
|
26300
|
+
const last = typeof address === "string" ? address.split(",").map((part) => part.trim()).at(-1) ?? "" : "";
|
|
26301
|
+
ip = isIP4(last) === 0 ? null : last;
|
|
26302
|
+
const code = [parsed.country_code, parsed.countryCode, parsed.country].find((value) => typeof value === "string" && /^[A-Za-z]{2}$/.test(value));
|
|
26303
|
+
country = typeof code === "string" ? code.toUpperCase() : null;
|
|
26304
|
+
}
|
|
26305
|
+
} catch {
|
|
26306
|
+
ip = isIP4(text2) === 0 ? null : text2;
|
|
26307
|
+
}
|
|
26308
|
+
return ip === null ? null : { ip, country, observedAt };
|
|
26309
|
+
}
|
|
26310
|
+
var NETWORK_FAILURES = /* @__PURE__ */ new Set(["timeout", "dns_error", "connection_error", "tls_error"]);
|
|
25394
26311
|
function egressInDoubt(outcome) {
|
|
25395
26312
|
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);
|
|
26313
|
+
return outcome.result.status === "failed" && NETWORK_FAILURES.has(outcome.result.failureReason ?? "") && results.every((result) => (result.evidence.httpStatus ?? null) === null);
|
|
25397
26314
|
}
|
|
25398
26315
|
async function probeEgress(server, timeoutMs = 5e3) {
|
|
25399
26316
|
const url = new URL(server.url);
|
|
@@ -25447,7 +26364,7 @@ function fileHint(file) {
|
|
|
25447
26364
|
var QUALITY_EVENTS = /* @__PURE__ */ new Set(["quality_low_yield", "quality_client_rendered"]);
|
|
25448
26365
|
var GATES = /* @__PURE__ */ new Set(["cloudflare_challenge", "captcha", "bot_detected_generic"]);
|
|
25449
26366
|
var MAX_AGENT_HINTS = 5;
|
|
25450
|
-
var BROWSER_LANES = /* @__PURE__ */ new Set(["browser_local", "browser_local_authed", "browser_proxy"]);
|
|
26367
|
+
var BROWSER_LANES = /* @__PURE__ */ new Set(["browser_local", "browser_local_authed", "browser_proxy", "my_browser"]);
|
|
25451
26368
|
function httpLaneAskedForBrowser(result) {
|
|
25452
26369
|
return result.escalations.some((hop) => hop.improved === null) || result.trace.some((event) => QUALITY_EVENTS.has(event.event));
|
|
25453
26370
|
}
|
|
@@ -25605,7 +26522,7 @@ init_dist2();
|
|
|
25605
26522
|
var ChromeLoginError = class extends Error {
|
|
25606
26523
|
};
|
|
25607
26524
|
var CDP_COMMAND_TIMEOUT_MS = 3e4;
|
|
25608
|
-
var ENABLE_HINT = 'open chrome://inspect/#remote-debugging in Chrome (144 or later), turn on "Allow remote debugging for this browser instance", then run this again';
|
|
26525
|
+
var ENABLE_HINT = 'open chrome://inspect/#remote-debugging in Chrome (144 or later), turn on "Allow remote debugging for this browser instance", then run this again (while it is on, every page sees navigator.webdriver true: turn it off when you are done)';
|
|
25609
26526
|
function chromeUserDataDir(platform = process.platform, env = process.env, home = homedir()) {
|
|
25610
26527
|
if (platform === "darwin")
|
|
25611
26528
|
return join9(home, "Library", "Application Support", "Google", "Chrome");
|
|
@@ -25888,9 +26805,11 @@ var READ_TIMEOUT_MS = 1e4;
|
|
|
25888
26805
|
var WORLD = "w2l-handoff";
|
|
25889
26806
|
var HandoffNotThrough = class extends Error {
|
|
25890
26807
|
check;
|
|
25891
|
-
|
|
26808
|
+
kind;
|
|
26809
|
+
constructor(message2, check, kind = "timeout") {
|
|
25892
26810
|
super(message2);
|
|
25893
26811
|
this.check = check;
|
|
26812
|
+
this.kind = kind;
|
|
25894
26813
|
}
|
|
25895
26814
|
};
|
|
25896
26815
|
var STATE = `JSON.stringify({
|
|
@@ -25902,6 +26821,27 @@ var STATE = `JSON.stringify({
|
|
|
25902
26821
|
hidden: document.visibilityState === 'hidden',
|
|
25903
26822
|
field: (() => { const el = document.activeElement; if (!el) return null; if (el.isContentEditable) return 'edit:' + String(el.textContent).slice(0, 500); return ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName) ? el.tagName + ':' + String(el.value).slice(0, 500) : null })(),
|
|
25904
26823
|
})`;
|
|
26824
|
+
var SHOWN_HTML = `(() => {
|
|
26825
|
+
const root = document.documentElement
|
|
26826
|
+
if (!root) return JSON.stringify({ href: location.href, shown: '' })
|
|
26827
|
+
const copy = new DOMParser().parseFromString('', 'text/html').importNode(root, true)
|
|
26828
|
+
const originals = root.querySelectorAll('body *')
|
|
26829
|
+
const copies = copy.querySelectorAll('body *')
|
|
26830
|
+
const unshown = new Set(['SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT', 'LINK', 'META', 'TITLE', 'BASE'])
|
|
26831
|
+
const gone = []
|
|
26832
|
+
// In document order: what is inside an element already left out comes right after it, and goes with it.
|
|
26833
|
+
let last = null
|
|
26834
|
+
for (let i = 0; i < originals.length; i++) {
|
|
26835
|
+
const el = originals[i]
|
|
26836
|
+
if ((last !== null && last.contains(el)) || unshown.has(el.tagName) || el.closest('svg') !== null) continue
|
|
26837
|
+
const style = getComputedStyle(el)
|
|
26838
|
+
const hidden = style.display === 'none' || ((style.visibility === 'hidden' || style.visibility === 'collapse')
|
|
26839
|
+
&& !Array.from(el.querySelectorAll('*')).some((inner) => getComputedStyle(inner).visibility === 'visible'))
|
|
26840
|
+
if (hidden) { gone.push(copies[i]); last = el }
|
|
26841
|
+
}
|
|
26842
|
+
for (const el of gone) el.remove()
|
|
26843
|
+
return JSON.stringify({ href: location.href, shown: copy.outerHTML })
|
|
26844
|
+
})()`;
|
|
25905
26845
|
async function openUserChrome(options = {}, signal) {
|
|
25906
26846
|
const endpoint = await chromeEndpoint(options.userDataDir ?? chromeUserDataDir());
|
|
25907
26847
|
const connection = await (options.connect ?? connectCdp)(endpoint, options.approveTimeoutMs ?? 12e4, signal);
|
|
@@ -25918,17 +26858,251 @@ async function openUserChrome(options = {}, signal) {
|
|
|
25918
26858
|
}
|
|
25919
26859
|
return {
|
|
25920
26860
|
read: (url, readOptions = {}) => readPage(connection, browser, url, readOptions),
|
|
26861
|
+
readList: (url, readOptions) => readList(connection, browser, url, readOptions),
|
|
26862
|
+
allow: (scope, allowOptions = {}) => allowSites(connection, scope, allowOptions),
|
|
25921
26863
|
close: () => connection.close()
|
|
25922
26864
|
};
|
|
25923
26865
|
}
|
|
26866
|
+
function isAborted(signal) {
|
|
26867
|
+
return signal?.aborted === true;
|
|
26868
|
+
}
|
|
26869
|
+
var ANSWER = `document.documentElement.dataset.octocrawl || ''`;
|
|
26870
|
+
function scopePage(scope) {
|
|
26871
|
+
const escape = (text2) => text2.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
26872
|
+
const sites = scope.hosts.map((host) => `<li><code>${escape(host)}</code></li>`).join("");
|
|
26873
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Octocrawl asks to read pages</title>
|
|
26874
|
+
<style>body{font:15px/1.5 system-ui,sans-serif;max-width:40rem;margin:3rem auto;padding:0 1rem;color:#111}button{font:inherit;padding:.5rem 1rem;margin-right:.5rem}#state{margin-top:1rem;font-weight:600}</style></head>
|
|
26875
|
+
<body><h1>Octocrawl asks to read pages in this Chrome</h1>
|
|
26876
|
+
<p>Task: ${escape(scope.task)}</p><p>Sites it may read, signed in as you where you are:</p><ul>${sites}</ul>
|
|
26877
|
+
<p>It reads only pages on these sites, only while this page stays open. A page that shows a check waits for you.</p>
|
|
26878
|
+
<button id="allow">Allow reading these sites</button><button id="revoke">Revoke</button><p id="state"></p>
|
|
26879
|
+
<script>
|
|
26880
|
+
const root = document.documentElement, state = document.getElementById('state')
|
|
26881
|
+
document.getElementById('allow').onclick = () => { if (root.dataset.octocrawl !== 'revoked') { root.dataset.octocrawl = 'allowed'; state.textContent = 'Allowed. Close this page or click Revoke to stop.' } }
|
|
26882
|
+
document.getElementById('revoke').onclick = () => { root.dataset.octocrawl = 'revoked'; state.textContent = 'Revoked. Octocrawl reads nothing more here.' }
|
|
26883
|
+
</script></body></html>`;
|
|
26884
|
+
}
|
|
26885
|
+
async function allowSites(connection, scope, options) {
|
|
26886
|
+
const waitMs = options.waitMs ?? 6e5;
|
|
26887
|
+
const log = options.log ?? (() => void 0);
|
|
26888
|
+
const pollMs = options.pollMs ?? 500;
|
|
26889
|
+
const refused = (message2, kind) => new HandoffNotThrough(message2, null, kind);
|
|
26890
|
+
let targetId;
|
|
26891
|
+
try {
|
|
26892
|
+
targetId = (await connection.send("Target.createTarget", { url: "about:blank" })).targetId;
|
|
26893
|
+
} catch (error) {
|
|
26894
|
+
throw refused(`Octocrawl could not open its page in Chrome: ${error instanceof Error ? error.message : String(error)}`, "chrome");
|
|
26895
|
+
}
|
|
26896
|
+
const closeTab2 = () => connection.send("Target.closeTarget", { targetId }).then(() => void 0, () => void 0);
|
|
26897
|
+
const answerOf = async (sessionId, world) => {
|
|
26898
|
+
try {
|
|
26899
|
+
await connection.send("Target.getTargetInfo", { targetId }, void 0, READ_TIMEOUT_MS);
|
|
26900
|
+
const answer = await connection.send("Runtime.evaluate", { expression: ANSWER, returnByValue: true }, sessionId);
|
|
26901
|
+
const active = await connection.send("Runtime.evaluate", { expression: "navigator.userActivation.hasBeenActive", contextId: world, returnByValue: true }, sessionId);
|
|
26902
|
+
return { answer: String(answer.result?.value ?? ""), active: active.result?.value === true };
|
|
26903
|
+
} catch {
|
|
26904
|
+
return null;
|
|
26905
|
+
}
|
|
26906
|
+
};
|
|
26907
|
+
try {
|
|
26908
|
+
const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
|
|
26909
|
+
await connection.send("Target.activateTarget", { targetId }).catch(() => void 0);
|
|
26910
|
+
await connection.send("Runtime.evaluate", { expression: `document.open(); document.write(${JSON.stringify(scopePage(scope))}); document.close()` }, sessionId);
|
|
26911
|
+
const world = (await connection.send("Page.createIsolatedWorld", { frameId: targetId, worldName: WORLD }, sessionId)).executionContextId;
|
|
26912
|
+
log("shown", { targetId, hosts: scope.hosts.length });
|
|
26913
|
+
const wired = await connection.send("Runtime.evaluate", { expression: `typeof document.getElementById('allow')?.onclick === 'function' && typeof document.getElementById('revoke')?.onclick === 'function'`, returnByValue: true }, sessionId).then((r) => r.result?.value === true, () => false);
|
|
26914
|
+
log("ready", { buttons: wired });
|
|
26915
|
+
const started = Date.now();
|
|
26916
|
+
let last = { answer: "", active: false };
|
|
26917
|
+
const lastSeen = () => !wired ? "the page's buttons did not answer (its own script did not run), so no click could allow the sites" : last.answer === "allowed" ? "the page answered allowed, but no click of yours on it was seen (Chrome did not count one)" : last.active ? "you clicked on the page, but not on Allow reading these sites" : "the page was not clicked";
|
|
26918
|
+
const waited = waitMs % 6e4 === 0 ? `${waitMs / 6e4} minute${waitMs === 6e4 ? "" : "s"}` : `${Math.round(waitMs / 1e3)} s`;
|
|
26919
|
+
for (; ; ) {
|
|
26920
|
+
if (options.signal?.aborted === true) {
|
|
26921
|
+
log("refused", { reason: "cancelled", ...last });
|
|
26922
|
+
throw refused("the request was cancelled before you allowed the sites", "cancelled");
|
|
26923
|
+
}
|
|
26924
|
+
if (Date.now() - started >= waitMs) {
|
|
26925
|
+
log("refused", { reason: "timeout", ...last, buttons: wired });
|
|
26926
|
+
throw refused(`you did not allow the sites in Chrome within ${waited}: ${lastSeen()}`, "timeout");
|
|
26927
|
+
}
|
|
26928
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
26929
|
+
const seen = await answerOf(sessionId, world);
|
|
26930
|
+
if (seen === null) {
|
|
26931
|
+
log("refused", { reason: "closed", ...last });
|
|
26932
|
+
throw refused("you closed Octocrawl's page in Chrome before allowing the sites", "cancelled");
|
|
26933
|
+
}
|
|
26934
|
+
if (seen.answer !== last.answer || seen.active !== last.active)
|
|
26935
|
+
log("answer", { ...seen, afterMs: Date.now() - started });
|
|
26936
|
+
last = seen;
|
|
26937
|
+
if (seen.answer === "revoked") {
|
|
26938
|
+
log("refused", { reason: "revoked" });
|
|
26939
|
+
throw refused("you did not allow the sites: you clicked Revoke", "cancelled");
|
|
26940
|
+
}
|
|
26941
|
+
if (seen.answer === "allowed" && seen.active) {
|
|
26942
|
+
log("allowed", { afterMs: Date.now() - started });
|
|
26943
|
+
break;
|
|
26944
|
+
}
|
|
26945
|
+
}
|
|
26946
|
+
const revoked = new AbortController();
|
|
26947
|
+
let watching = true;
|
|
26948
|
+
void (async () => {
|
|
26949
|
+
while (watching && !revoked.signal.aborted) {
|
|
26950
|
+
await new Promise((resolve4) => setTimeout(resolve4, Math.max(pollMs, 500)));
|
|
26951
|
+
if (!watching)
|
|
26952
|
+
return;
|
|
26953
|
+
const seen = await answerOf(sessionId, world);
|
|
26954
|
+
if (seen === null || seen.answer === "revoked")
|
|
26955
|
+
revoked.abort(new Error("you revoked the sites in Chrome"));
|
|
26956
|
+
}
|
|
26957
|
+
})();
|
|
26958
|
+
return { hosts: [...scope.hosts], signal: revoked.signal, close: async () => {
|
|
26959
|
+
watching = false;
|
|
26960
|
+
await closeTab2();
|
|
26961
|
+
} };
|
|
26962
|
+
} catch (error) {
|
|
26963
|
+
await closeTab2();
|
|
26964
|
+
if (error instanceof HandoffNotThrough)
|
|
26965
|
+
throw error;
|
|
26966
|
+
throw refused(`Octocrawl could not ask you in Chrome: ${error instanceof Error ? error.message : String(error)}`, "chrome");
|
|
26967
|
+
}
|
|
26968
|
+
}
|
|
26969
|
+
async function closeTab(connection, tab) {
|
|
26970
|
+
for (const stop of tab.stops)
|
|
26971
|
+
stop();
|
|
26972
|
+
await connection.send("Target.closeTarget", { targetId: tab.targetId }).catch(() => void 0);
|
|
26973
|
+
}
|
|
25924
26974
|
async function readPage(connection, browser, url, options) {
|
|
26975
|
+
const { read: read2, tab } = await readOpen(connection, browser, url, options);
|
|
26976
|
+
await closeTab(connection, tab);
|
|
26977
|
+
return read2;
|
|
26978
|
+
}
|
|
26979
|
+
async function readList(connection, browser, url, options) {
|
|
26980
|
+
const { read: read2, tab } = await readOpen(connection, browser, url, options);
|
|
26981
|
+
try {
|
|
26982
|
+
return await followPages(connection, tab, url, read2, options);
|
|
26983
|
+
} finally {
|
|
26984
|
+
await closeTab(connection, tab);
|
|
26985
|
+
}
|
|
26986
|
+
}
|
|
26987
|
+
var FOLLOW = (nextSelector, itemSelector) => `JSON.stringify({
|
|
26988
|
+
href: location.href,
|
|
26989
|
+
ready: document.readyState,
|
|
26990
|
+
status: (performance.getEntriesByType('navigation')[0] || {}).responseStatus || null,
|
|
26991
|
+
html: document.documentElement ? document.documentElement.outerHTML : '',
|
|
26992
|
+
secret: Array.from(document.querySelectorAll('input[type=password], input[autocomplete="one-time-code"]')).some((el) => el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden'),
|
|
26993
|
+
key: ${itemSelector === void 0 ? `(document.body ? document.body.innerText : '')` : `Array.from(document.querySelectorAll(${JSON.stringify(itemSelector)})).map((el) => el.innerText).join('\\u0000')`},
|
|
26994
|
+
items: ${itemSelector === void 0 ? "null" : `document.querySelectorAll(${JSON.stringify(itemSelector)}).length`},
|
|
26995
|
+
next: (() => { const el = document.querySelector(${JSON.stringify(nextSelector)}); if (!el) return 'gone'; const style = getComputedStyle(el); if (el.getClientRects().length === 0 || style.visibility === 'hidden' || style.display === 'none') return 'hidden'; if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true' || el.closest('.disabled, [aria-disabled="true"]') !== null) return 'disabled'; return 'usable' })(),
|
|
26996
|
+
})`;
|
|
26997
|
+
var END_GRACE_MS = 5e3;
|
|
26998
|
+
async function followPages(connection, tab, url, first2, options) {
|
|
26999
|
+
const { follow } = options;
|
|
27000
|
+
const pollMs = options.pollMs ?? 500;
|
|
27001
|
+
const waitMs = options.waitMs ?? 6e5;
|
|
27002
|
+
const idleMs = follow.idleMs ?? 6e4;
|
|
27003
|
+
const max = follow.maxPages ?? Number.POSITIVE_INFINITY;
|
|
27004
|
+
const endGraceMs = follow.endGraceMs ?? END_GRACE_MS;
|
|
27005
|
+
const host = new URL(url).hostname;
|
|
27006
|
+
const started = Date.now();
|
|
27007
|
+
const script = FOLLOW(follow.nextSelector, follow.itemSelector);
|
|
27008
|
+
const isGone = (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/i.test(error.message);
|
|
27009
|
+
const look = async () => {
|
|
27010
|
+
const info = await connection.send("Target.getTargetInfo", { targetId: tab.targetId }, void 0, READ_TIMEOUT_MS);
|
|
27011
|
+
const answer = await connection.send("Runtime.evaluate", { expression: script, returnByValue: true }, tab.sessionId);
|
|
27012
|
+
if (typeof answer.result?.value !== "string")
|
|
27013
|
+
return null;
|
|
27014
|
+
const shown = JSON.parse(answer.result.value);
|
|
27015
|
+
if (typeof info.targetInfo?.url === "string")
|
|
27016
|
+
shown.href = info.targetInfo.url;
|
|
27017
|
+
return shown;
|
|
27018
|
+
};
|
|
27019
|
+
const pages = [{ url: first2.finalUrl, html: first2.html }];
|
|
27020
|
+
const keys = /* @__PURE__ */ new Set();
|
|
27021
|
+
let lastKey = null;
|
|
27022
|
+
let lastRead = null;
|
|
27023
|
+
let unusableSince = null;
|
|
27024
|
+
let lastNew = Date.now();
|
|
27025
|
+
let told = null;
|
|
27026
|
+
const done = (stoppedBy) => ({ first: first2, pages, stoppedBy });
|
|
27027
|
+
follow.onStart?.(url);
|
|
27028
|
+
try {
|
|
27029
|
+
const shown = await look();
|
|
27030
|
+
if (shown !== null) {
|
|
27031
|
+
keys.add(shown.key);
|
|
27032
|
+
lastKey = shown.key;
|
|
27033
|
+
lastRead = shown.key;
|
|
27034
|
+
pages[0].items = shown.href === first2.finalUrl ? shown.items ?? null : null;
|
|
27035
|
+
}
|
|
27036
|
+
} catch (error) {
|
|
27037
|
+
if (isGone(error))
|
|
27038
|
+
throw new HandoffNotThrough(`the tab for ${url} was closed, or Chrome quit, after ${pages.length} page(s) of the list were read`, null, "gone");
|
|
27039
|
+
}
|
|
27040
|
+
if (pages.length >= max)
|
|
27041
|
+
return done("max");
|
|
27042
|
+
while (Date.now() - started < waitMs) {
|
|
27043
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
27044
|
+
if (isAborted(options.signal))
|
|
27045
|
+
throw new HandoffNotThrough(`the reading of ${url} was cancelled after ${pages.length} page(s) of the list`, null, "cancelled");
|
|
27046
|
+
if (Date.now() - lastNew > idleMs)
|
|
27047
|
+
return done("deadline");
|
|
27048
|
+
let shown;
|
|
27049
|
+
try {
|
|
27050
|
+
shown = await look();
|
|
27051
|
+
} catch (error) {
|
|
27052
|
+
if (isGone(error))
|
|
27053
|
+
throw new HandoffNotThrough(`the tab for ${url} was closed, or Chrome quit, after ${pages.length} page(s) of the list were read`, null, "gone");
|
|
27054
|
+
continue;
|
|
27055
|
+
}
|
|
27056
|
+
if (shown === null || shown.ready !== "complete")
|
|
27057
|
+
continue;
|
|
27058
|
+
if (shown.key !== lastKey) {
|
|
27059
|
+
lastKey = shown.key;
|
|
27060
|
+
unusableSince = null;
|
|
27061
|
+
continue;
|
|
27062
|
+
}
|
|
27063
|
+
const readable = sameSite(shown.href, host) && !onLoginPath(shown.href, url) && !shown.secret && (shown.status === null || shown.status >= 200 && shown.status < 300);
|
|
27064
|
+
if (!readable) {
|
|
27065
|
+
unusableSince = null;
|
|
27066
|
+
continue;
|
|
27067
|
+
}
|
|
27068
|
+
if (!keys.has(shown.key) && shown.key !== "") {
|
|
27069
|
+
const gate = checksOf({ href: shown.href, ready: shown.ready, status: shown.status, html: shown.html, secret: shown.secret, field: null, hidden: false }, null, shown.status, options).gate;
|
|
27070
|
+
if (gate !== null) {
|
|
27071
|
+
lastNew = Date.now();
|
|
27072
|
+
if (told !== shown.href) {
|
|
27073
|
+
told = shown.href;
|
|
27074
|
+
options.onWaiting?.(shown.href, gate.reason);
|
|
27075
|
+
}
|
|
27076
|
+
continue;
|
|
27077
|
+
}
|
|
27078
|
+
keys.add(shown.key);
|
|
27079
|
+
lastRead = shown.key;
|
|
27080
|
+
pages.push({ url: shown.href, html: shown.html, items: shown.items ?? null });
|
|
27081
|
+
lastNew = Date.now();
|
|
27082
|
+
follow.onPage?.(shown.href, pages.length);
|
|
27083
|
+
if (pages.length >= max)
|
|
27084
|
+
return done("max");
|
|
27085
|
+
}
|
|
27086
|
+
if (shown.key === lastRead && shown.next !== "usable") {
|
|
27087
|
+
unusableSince ??= Date.now();
|
|
27088
|
+
if (Date.now() - unusableSince >= endGraceMs)
|
|
27089
|
+
return done("end");
|
|
27090
|
+
} else
|
|
27091
|
+
unusableSince = null;
|
|
27092
|
+
}
|
|
27093
|
+
return done("deadline");
|
|
27094
|
+
}
|
|
27095
|
+
async function readOpen(connection, browser, url, options) {
|
|
25925
27096
|
const waitMs = options.waitMs ?? 6e5;
|
|
25926
27097
|
const pollMs = options.pollMs ?? 500;
|
|
25927
27098
|
const host = new URL(url).hostname;
|
|
27099
|
+
const allowed = options.unattended === true ? new Set(options.allowedHosts ?? [new URL(url).host]) : null;
|
|
25928
27100
|
const started = Date.now();
|
|
25929
27101
|
let sawGate = null;
|
|
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;
|
|
25931
|
-
const ended = (error) => gone(error) ?? (error instanceof ChromeLoginError ? new HandoffNotThrough(`${url} was not read: ${error.message}`, sawGate) : error);
|
|
27102
|
+
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, "gone") : null;
|
|
27103
|
+
const ended = (error) => gone(error) ?? (error instanceof ChromeLoginError ? new HandoffNotThrough(`${url} was not read: ${error.message}`, sawGate, "chrome") : error);
|
|
27104
|
+
if (isAborted(options.signal))
|
|
27105
|
+
throw new HandoffNotThrough(`the read of ${url} was cancelled before Octocrawl read it`, null, "cancelled");
|
|
25932
27106
|
let targetId;
|
|
25933
27107
|
try {
|
|
25934
27108
|
targetId = (await connection.send("Target.createTarget", { url: "about:blank" })).targetId;
|
|
@@ -25936,10 +27110,29 @@ async function readPage(connection, browser, url, options) {
|
|
|
25936
27110
|
throw ended(error);
|
|
25937
27111
|
}
|
|
25938
27112
|
const stops = [];
|
|
27113
|
+
let kept = false;
|
|
25939
27114
|
try {
|
|
25940
27115
|
const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
|
|
25941
|
-
const heard = { document: null, act: null, ours: false, landings: /* @__PURE__ */ new Set(), documents: 0 };
|
|
27116
|
+
const heard = { document: null, act: null, ours: false, landings: /* @__PURE__ */ new Set(), documents: 0, committed: null, within: null };
|
|
25942
27117
|
if (connection.on !== void 0) {
|
|
27118
|
+
stops.push(connection.on("Page.frameNavigated", sessionId, (params) => {
|
|
27119
|
+
const frame = params.frame;
|
|
27120
|
+
if (frame?.id !== targetId || frame.parentId !== void 0)
|
|
27121
|
+
return;
|
|
27122
|
+
heard.committed = String(frame.url ?? "");
|
|
27123
|
+
heard.within = null;
|
|
27124
|
+
}));
|
|
27125
|
+
stops.push(connection.on("Page.navigatedWithinDocument", sessionId, (params) => {
|
|
27126
|
+
if (params.frameId !== targetId)
|
|
27127
|
+
return;
|
|
27128
|
+
const within = { url: String(params.url ?? ""), byPage: null };
|
|
27129
|
+
heard.within = within;
|
|
27130
|
+
void (async () => {
|
|
27131
|
+
const made = await connection.send("Page.createIsolatedWorld", { frameId: targetId, worldName: WORLD }, sessionId);
|
|
27132
|
+
const active = await connection.send("Runtime.evaluate", { expression: "navigator.userActivation.hasBeenActive", contextId: made.executionContextId, returnByValue: true }, sessionId);
|
|
27133
|
+
within.byPage = active.result?.value === false;
|
|
27134
|
+
})().catch(() => void 0);
|
|
27135
|
+
}));
|
|
25943
27136
|
stops.push(connection.on("Network.requestWillBeSent", sessionId, (params) => {
|
|
25944
27137
|
if (params.type === "Document" && params.frameId === targetId && params.hasUserGesture === true)
|
|
25945
27138
|
heard.act ??= "gesture_navigation";
|
|
@@ -25954,12 +27147,13 @@ async function readPage(connection, browser, url, options) {
|
|
|
25954
27147
|
heard.landings.add(pageOf(heard.document.url));
|
|
25955
27148
|
heard.ours = false;
|
|
25956
27149
|
}
|
|
25957
|
-
const
|
|
25958
|
-
const check = classifyGate({ status:
|
|
25959
|
-
if (check !== null || CHECK_STATUSES.has(
|
|
25960
|
-
sawGate ??= check?.reason ?? `http_${
|
|
27150
|
+
const seen2 = heard.document;
|
|
27151
|
+
const check = classifyGate({ status: seen2.status, header: (name) => seen2.headers[name.toLowerCase()] ?? null, body: "" });
|
|
27152
|
+
if (check !== null || CHECK_STATUSES.has(seen2.status))
|
|
27153
|
+
sawGate ??= check?.reason ?? `http_${seen2.status}`;
|
|
25961
27154
|
}));
|
|
25962
27155
|
await connection.send("Network.enable", {}, sessionId);
|
|
27156
|
+
await connection.send("Page.enable", {}, sessionId);
|
|
25963
27157
|
}
|
|
25964
27158
|
await connection.send("Target.activateTarget", { targetId }).catch(() => void 0);
|
|
25965
27159
|
let navigation = null;
|
|
@@ -25979,11 +27173,12 @@ async function readPage(connection, browser, url, options) {
|
|
|
25979
27173
|
let hiddenSince = null;
|
|
25980
27174
|
let toldHidden = false;
|
|
25981
27175
|
let clear = 0;
|
|
27176
|
+
let seen = { href: "", documents: 0 };
|
|
25982
27177
|
let last = null;
|
|
25983
27178
|
while (Date.now() - started < waitMs) {
|
|
25984
27179
|
await new Promise((resolve4) => setTimeout(resolve4, pollMs));
|
|
25985
27180
|
if (options.signal?.aborted === true)
|
|
25986
|
-
throw new HandoffNotThrough(`the
|
|
27181
|
+
throw new HandoffNotThrough(`the read of ${url} was cancelled before Octocrawl read it`, sawGate, "cancelled");
|
|
25987
27182
|
if (navigation !== null && gone(navigation) !== null)
|
|
25988
27183
|
throw gone(navigation);
|
|
25989
27184
|
let state;
|
|
@@ -25995,6 +27190,7 @@ async function readPage(connection, browser, url, options) {
|
|
|
25995
27190
|
continue;
|
|
25996
27191
|
}
|
|
25997
27192
|
state = JSON.parse(answer.result.value);
|
|
27193
|
+
seen = { href: state.href, documents: heard.documents };
|
|
25998
27194
|
if (typeof info.targetInfo?.url === "string")
|
|
25999
27195
|
state.href = info.targetInfo.url;
|
|
26000
27196
|
if (heard.act === null) {
|
|
@@ -26024,7 +27220,8 @@ async function readPage(connection, browser, url, options) {
|
|
|
26024
27220
|
hiddenSince = null;
|
|
26025
27221
|
toldHidden = false;
|
|
26026
27222
|
}
|
|
26027
|
-
const
|
|
27223
|
+
const inPlace = heard.committed !== null && heard.within !== null && heard.within.byPage === true && sameDocument2(heard.within.url, state.href) && (pageOf(heard.committed) === pageOf(url) || heard.landings.has(pageOf(heard.committed))) && rewrittenInPlace(heard.committed, state.href);
|
|
27224
|
+
const response = heard.document !== null && (sameDocument2(heard.document.url, state.href) || inPlace && sameDocument2(heard.document.url, heard.committed)) ? heard.document : null;
|
|
26028
27225
|
const status = response?.status ?? state.status;
|
|
26029
27226
|
last = { state, response };
|
|
26030
27227
|
const { full, decisive, gate } = checksOf(state, response, status, options);
|
|
@@ -26036,13 +27233,14 @@ async function readPage(connection, browser, url, options) {
|
|
|
26036
27233
|
}
|
|
26037
27234
|
const typing = state.field !== null && field3 !== void 0 && state.field !== field3;
|
|
26038
27235
|
field3 = state.field;
|
|
26039
|
-
const asked = pageOf(state.href) === pageOf(url) || heard.landings.has(pageOf(state.href));
|
|
27236
|
+
const asked = pageOf(state.href) === pageOf(url) || heard.landings.has(pageOf(state.href)) || inPlace;
|
|
26040
27237
|
const arrived = returns === 0 || connection.on === void 0 || heard.documents > documentsAtReturn;
|
|
26041
|
-
const
|
|
27238
|
+
const inScope = allowed === null || allowed.has(hostAndPort(state.href));
|
|
27239
|
+
const through = inScope && state.ready === "complete" && gate === null && (status === null || status >= 200 && status < 300) && sameSite(state.href, host) && !onLoginPath(state.href, url) && !state.secret && !typing && arrived;
|
|
26042
27240
|
clear = through ? clear + 1 : 0;
|
|
26043
27241
|
if (clear < CLEAR_READS)
|
|
26044
27242
|
continue;
|
|
26045
|
-
if (heard.act === null) {
|
|
27243
|
+
if (heard.act === null && options.unattended !== true) {
|
|
26046
27244
|
if (!confirming) {
|
|
26047
27245
|
confirming = true;
|
|
26048
27246
|
options.onConfirm?.(url);
|
|
@@ -26050,35 +27248,64 @@ async function readPage(connection, browser, url, options) {
|
|
|
26050
27248
|
continue;
|
|
26051
27249
|
}
|
|
26052
27250
|
if (!asked) {
|
|
26053
|
-
if (returns >= RETURNS)
|
|
26054
|
-
|
|
27251
|
+
if (returns >= RETURNS) {
|
|
27252
|
+
const where2 = sawGate !== null && heard.act !== null ? `after you got through, the tab stayed on ${pageOf(state.href)}` : `each time Octocrawl took the tab back to it, the site led it on to ${pageOf(state.href)}`;
|
|
27253
|
+
throw new HandoffNotThrough(`${url} was not read: ${where2}, not the page asked for`, sawGate, "elsewhere");
|
|
27254
|
+
}
|
|
26055
27255
|
returns++;
|
|
26056
27256
|
documentsAtReturn = heard.documents;
|
|
26057
27257
|
clear = 0;
|
|
26058
27258
|
open(true);
|
|
26059
27259
|
continue;
|
|
26060
27260
|
}
|
|
26061
|
-
|
|
27261
|
+
let html2 = state.html;
|
|
27262
|
+
let whole;
|
|
27263
|
+
if (options.shownOnly === true) {
|
|
27264
|
+
try {
|
|
27265
|
+
const shown = await connection.send("Runtime.evaluate", { expression: SHOWN_HTML, returnByValue: true }, sessionId);
|
|
27266
|
+
if (shown.exceptionDetails !== void 0 || typeof shown.result?.value !== "string") {
|
|
27267
|
+
clear = 0;
|
|
27268
|
+
continue;
|
|
27269
|
+
}
|
|
27270
|
+
const now = JSON.parse(shown.result.value);
|
|
27271
|
+
if (now.href !== seen.href || heard.documents !== seen.documents) {
|
|
27272
|
+
clear = 0;
|
|
27273
|
+
continue;
|
|
27274
|
+
}
|
|
27275
|
+
html2 = now.shown;
|
|
27276
|
+
whole = state.html;
|
|
27277
|
+
} catch (error) {
|
|
27278
|
+
const left = gone(error);
|
|
27279
|
+
if (left !== null)
|
|
27280
|
+
throw left;
|
|
27281
|
+
world = null;
|
|
27282
|
+
clear = 0;
|
|
27283
|
+
continue;
|
|
27284
|
+
}
|
|
27285
|
+
}
|
|
27286
|
+
kept = true;
|
|
27287
|
+
return { read: {
|
|
26062
27288
|
requestedUrl: url,
|
|
26063
27289
|
finalUrl: state.href,
|
|
26064
27290
|
status,
|
|
26065
27291
|
contentType: response?.headers["content-type"] ?? null,
|
|
26066
|
-
html:
|
|
27292
|
+
html: html2,
|
|
27293
|
+
...whole === void 0 ? {} : { document: whole },
|
|
26067
27294
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26068
27295
|
wallMs: Date.now() - started,
|
|
26069
27296
|
sawGate,
|
|
26070
27297
|
act: heard.act,
|
|
26071
27298
|
browser
|
|
26072
|
-
};
|
|
27299
|
+
}, tab: { targetId, sessionId, stops } };
|
|
26073
27300
|
}
|
|
26074
|
-
const
|
|
26075
|
-
|
|
27301
|
+
const holding = last === null ? null : stillGated(last, options);
|
|
27302
|
+
const where = last === null ? "it never loaded" : allowed !== null && !allowed.has(hostAndPort(last.state.href)) ? `it was on ${hostAndPort(last.state.href)}, which you did not allow (only ${[...allowed].join(", ")})` : !sameSite(last.state.href, host) ? `it was on ${safeHost2(last.state.href)}, not ${host}` : holding !== null ? `it still showed a check (${holding.reason}: ${holding.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";
|
|
27303
|
+
throw new HandoffNotThrough(`${url} was not through within ${Math.round(waitMs / 1e3)} s: ${where}`, holding?.reason ?? sawGate);
|
|
26076
27304
|
} catch (error) {
|
|
26077
27305
|
throw ended(error);
|
|
26078
27306
|
} finally {
|
|
26079
|
-
|
|
26080
|
-
|
|
26081
|
-
await connection.send("Target.closeTarget", { targetId }).catch(() => void 0);
|
|
27307
|
+
if (!kept)
|
|
27308
|
+
await closeTab(connection, { targetId, sessionId: "", stops });
|
|
26082
27309
|
}
|
|
26083
27310
|
}
|
|
26084
27311
|
function checksOf(state, response, status, options) {
|
|
@@ -26119,6 +27346,37 @@ function pageOf(href) {
|
|
|
26119
27346
|
function sameDocument2(a, b) {
|
|
26120
27347
|
return a.split("#")[0] === b.split("#")[0];
|
|
26121
27348
|
}
|
|
27349
|
+
function rewrittenInPlace(came, shown) {
|
|
27350
|
+
let a;
|
|
27351
|
+
let b;
|
|
27352
|
+
try {
|
|
27353
|
+
a = new URL(came);
|
|
27354
|
+
b = new URL(shown);
|
|
27355
|
+
} catch {
|
|
27356
|
+
return false;
|
|
27357
|
+
}
|
|
27358
|
+
if (a.origin !== b.origin || (a.pathname.replace(/\/+$/, "") || "/") !== (b.pathname.replace(/\/+$/, "") || "/"))
|
|
27359
|
+
return false;
|
|
27360
|
+
for (const name of new Set(a.searchParams.keys())) {
|
|
27361
|
+
if (!b.searchParams.has(name)) {
|
|
27362
|
+
if (a.searchParams.getAll(name).some((value) => /^\d+$/.test(value)))
|
|
27363
|
+
return false;
|
|
27364
|
+
continue;
|
|
27365
|
+
}
|
|
27366
|
+
const left = a.searchParams.getAll(name);
|
|
27367
|
+
const right = b.searchParams.getAll(name);
|
|
27368
|
+
if (left.length !== right.length || left.some((value, i) => value !== right[i]))
|
|
27369
|
+
return false;
|
|
27370
|
+
}
|
|
27371
|
+
return true;
|
|
27372
|
+
}
|
|
27373
|
+
function hostAndPort(href) {
|
|
27374
|
+
try {
|
|
27375
|
+
return new URL(href).host;
|
|
27376
|
+
} catch {
|
|
27377
|
+
return "another page";
|
|
27378
|
+
}
|
|
27379
|
+
}
|
|
26122
27380
|
function safeHost2(href) {
|
|
26123
27381
|
try {
|
|
26124
27382
|
return new URL(href).hostname;
|
|
@@ -27054,7 +28312,7 @@ function modelSchema(root) {
|
|
|
27054
28312
|
throw error;
|
|
27055
28313
|
}
|
|
27056
28314
|
}
|
|
27057
|
-
async function callModel(result, format,
|
|
28315
|
+
async function callModel(result, format, request8, deterministic, execution, config, repair) {
|
|
27058
28316
|
execution.signal?.throwIfAborted();
|
|
27059
28317
|
const fetchImpl = config.fetch ?? fetch;
|
|
27060
28318
|
const response = await fetchImpl(`${config.baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
|
|
@@ -27083,7 +28341,7 @@ async function callModel(result, format, request7, deterministic, execution, con
|
|
|
27083
28341
|
],
|
|
27084
28342
|
response_format: {
|
|
27085
28343
|
type: "json_schema",
|
|
27086
|
-
json_schema: { name: "w2l_extract", strict:
|
|
28344
|
+
json_schema: { name: "w2l_extract", strict: request8.strict, schema: request8.schema }
|
|
27087
28345
|
}
|
|
27088
28346
|
})
|
|
27089
28347
|
});
|
|
@@ -27211,7 +28469,7 @@ async function extractStructured(result, format, execution = {}, modelConfig = s
|
|
|
27211
28469
|
const base = withoutPaths(data, broken);
|
|
27212
28470
|
const baseEvidence = evidence.filter((item) => !broken.has(item.path));
|
|
27213
28471
|
const deterministic = withoutPaths(mapped, broken);
|
|
27214
|
-
const
|
|
28472
|
+
const request8 = modelSchema(format.schema);
|
|
27215
28473
|
let attempts = 0;
|
|
27216
28474
|
let inputTokens = 0;
|
|
27217
28475
|
let outputTokens = 0;
|
|
@@ -27223,13 +28481,13 @@ async function extractStructured(result, format, execution = {}, modelConfig = s
|
|
|
27223
28481
|
inputTokens: tokensKnown ? inputTokens : null,
|
|
27224
28482
|
outputTokens: tokensKnown ? outputTokens : null,
|
|
27225
28483
|
externalCostUsd: null,
|
|
27226
|
-
strict:
|
|
27227
|
-
...
|
|
28484
|
+
strict: request8.strict,
|
|
28485
|
+
...request8.reason === void 0 ? {} : { strictReason: request8.reason }
|
|
27228
28486
|
});
|
|
27229
28487
|
while (attempts < 2) {
|
|
27230
28488
|
attempts++;
|
|
27231
28489
|
try {
|
|
27232
|
-
const model = await callModel(result, format,
|
|
28490
|
+
const model = await callModel(result, format, request8, deterministic, execution, modelConfig, repair);
|
|
27233
28491
|
if (model.inputTokens === null || model.outputTokens === null)
|
|
27234
28492
|
tokensKnown = false;
|
|
27235
28493
|
else {
|
|
@@ -27361,7 +28619,7 @@ function scrapeEvidenceRecord(result, req, delivered) {
|
|
|
27361
28619
|
return toEvidenceRecord(result, { mode: defaultApiMode(req.mode) }, { markdown: delivered.markdown, ...delivered.json === void 0 ? {} : { json: delivered.json } });
|
|
27362
28620
|
}
|
|
27363
28621
|
var NO_PAGE_METADATA = { title: null, description: null, language: null, keywords: null, robots: null, favicon: null, canonicalUrl: null };
|
|
27364
|
-
var BROWSER_LANES2 = /* @__PURE__ */ new Set(["browser_local", "browser_local_authed", "browser_proxy"]);
|
|
28622
|
+
var BROWSER_LANES2 = /* @__PURE__ */ new Set(["browser_local", "browser_local_authed", "browser_proxy", "my_browser"]);
|
|
27365
28623
|
function proxyUsedOf(result) {
|
|
27366
28624
|
if (result.compliance?.access.egressOwner === "user")
|
|
27367
28625
|
return "user";
|
|
@@ -27528,6 +28786,50 @@ function writeTaskEgress(file, id) {
|
|
|
27528
28786
|
`, { mode: 384 });
|
|
27529
28787
|
}
|
|
27530
28788
|
var DEFAULT_WORKER_COUNT = 4;
|
|
28789
|
+
function listPagesRepeat(pages, itemSelector) {
|
|
28790
|
+
if (invalidSelector(itemSelector) !== null)
|
|
28791
|
+
return true;
|
|
28792
|
+
const merged = mergeListPages(pages.map(({ url, html: html2 }) => ({ url, html: html2 })), { type: "list", itemSelector });
|
|
28793
|
+
return merged === null || merged.spec === null || merged.list.pages !== pages.length || merged.list.truncated;
|
|
28794
|
+
}
|
|
28795
|
+
function continuedItemsRead(pages) {
|
|
28796
|
+
const at = (href) => {
|
|
28797
|
+
try {
|
|
28798
|
+
const parsed = new URL(href);
|
|
28799
|
+
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "") || "/"}${parsed.search}`;
|
|
28800
|
+
} catch {
|
|
28801
|
+
return href;
|
|
28802
|
+
}
|
|
28803
|
+
};
|
|
28804
|
+
const kept = pages.kept.map(at);
|
|
28805
|
+
const addressed = new Set(kept).size === kept.length && !kept.includes(at(pages.checkUrl)) && at(pages.checkUrl) !== at(pages.stepUrl) && pages.shown.every((page) => !kept.includes(at(page.url)));
|
|
28806
|
+
if (!addressed || typeof pages.keptItems !== "number")
|
|
28807
|
+
return null;
|
|
28808
|
+
let sum = pages.keptItems;
|
|
28809
|
+
for (const page of pages.shown) {
|
|
28810
|
+
if (typeof page.items !== "number")
|
|
28811
|
+
return null;
|
|
28812
|
+
sum += page.items;
|
|
28813
|
+
}
|
|
28814
|
+
return sum;
|
|
28815
|
+
}
|
|
28816
|
+
function settledBy(promise, context) {
|
|
28817
|
+
if (context.signal?.aborted || context.deadlineAt !== void 0 && context.deadlineAt <= Date.now())
|
|
28818
|
+
return Promise.resolve(null);
|
|
28819
|
+
if (context.signal === void 0 && context.deadlineAt === void 0)
|
|
28820
|
+
return promise;
|
|
28821
|
+
return new Promise((resolve4) => {
|
|
28822
|
+
const done = (value) => {
|
|
28823
|
+
clearTimeout(timer);
|
|
28824
|
+
context.signal?.removeEventListener("abort", aborted);
|
|
28825
|
+
resolve4(value);
|
|
28826
|
+
};
|
|
28827
|
+
const aborted = () => done(null);
|
|
28828
|
+
const timer = context.deadlineAt === void 0 ? void 0 : setTimeout(aborted, context.deadlineAt - Date.now());
|
|
28829
|
+
context.signal?.addEventListener("abort", aborted, { once: true });
|
|
28830
|
+
promise.then(done, () => done(null));
|
|
28831
|
+
});
|
|
28832
|
+
}
|
|
27531
28833
|
function fetchedPastRobots(result) {
|
|
27532
28834
|
return result.compliance?.robots.override !== void 0 || result.trace.some((event) => event.event === "robots_overridden");
|
|
27533
28835
|
}
|
|
@@ -27569,9 +28871,13 @@ function createApiEngine(options = {}) {
|
|
|
27569
28871
|
const compatTakes = (url, page) => compatHostListed(compatHosts, url) && compatOptionsRefusal(page) === null;
|
|
27570
28872
|
const grantedBudget = (budget2) => {
|
|
27571
28873
|
const cap = accessGrant?.budget.perRunUsd ?? null;
|
|
27572
|
-
|
|
27573
|
-
|
|
27574
|
-
|
|
28874
|
+
const perPage = accessGrant?.budget.perRequestUsd ?? null;
|
|
28875
|
+
return {
|
|
28876
|
+
...budget2,
|
|
28877
|
+
...cap === null ? {} : { maxCostUsd: budget2.maxCostUsd === null ? cap : Math.min(budget2.maxCostUsd, cap) },
|
|
28878
|
+
// One page's own cap within the run's (ROADMAP PA item 4).
|
|
28879
|
+
...perPage === null ? {} : { maxCostPerPageUsd: perPage }
|
|
28880
|
+
};
|
|
27575
28881
|
};
|
|
27576
28882
|
const basePolicy = options.networkPolicy ?? localNetworkPolicy();
|
|
27577
28883
|
const networkPolicy = {
|
|
@@ -27581,7 +28887,13 @@ function createApiEngine(options = {}) {
|
|
|
27581
28887
|
// The operator's file cap: the policy's own, else W2L_MAX_FILE_BYTES, else the default.
|
|
27582
28888
|
maxFileBytes: basePolicy.maxFileBytes ?? maxFileBytesFromEnv(process.env)
|
|
27583
28889
|
};
|
|
27584
|
-
const egressPool = egressProxyList.length === 0 ? null : new EgressPool(egressProxyList, networkPolicy);
|
|
28890
|
+
const egressPool = egressProxyList.length === 0 ? null : new EgressPool(egressProxyList, networkPolicy, Date.now, options.egressEchoUrl ?? null);
|
|
28891
|
+
const withEgressExit = async (result, egress, context = {}) => {
|
|
28892
|
+
if (egress === void 0 || egressPool === null || result.trace.some((event) => event.event === "cache_hit"))
|
|
28893
|
+
return result;
|
|
28894
|
+
const exit = await settledBy(egressPool.exitOf(egress.id), context);
|
|
28895
|
+
return exit === null ? result : { ...result, trace: [...result.trace, { at: 0, lane: result.lane, event: "egress_exit", detail: { proxy: egress.id, ...exit } }] };
|
|
28896
|
+
};
|
|
27585
28897
|
const fileStore = new FileStore(join10(taskRoot, "files"));
|
|
27586
28898
|
const pageCache = PageCache.open(taskRoot);
|
|
27587
28899
|
const cachePlanFor = (url, mode, page, formats, channels, robotsOverride, robotsSetAside2) => {
|
|
@@ -27701,6 +29013,11 @@ function createApiEngine(options = {}) {
|
|
|
27701
29013
|
if (hosted && req.actions !== void 0)
|
|
27702
29014
|
throw new RequestError("actions are not available in hosted mode: run Octocrawl locally to use them");
|
|
27703
29015
|
};
|
|
29016
|
+
const checkAccessChoice = (req) => {
|
|
29017
|
+
if (req.access !== "enhanced" || accessGrant?.tier === "enhanced")
|
|
29018
|
+
return;
|
|
29019
|
+
throw new RequestError("access enhanced: this server has no approved budget for enhanced access (an access grant of tier enhanced, --access-grant or W2L_ACCESS_GRANT, ADR 0005); ask with access standard, or my-browser on your own machine", "unsupported_parameter", { parameters: ["access"] }, [REFUSAL_HINTS.stealth]);
|
|
29020
|
+
};
|
|
27704
29021
|
const jobEvents = new JobEventHub();
|
|
27705
29022
|
const jobWebhooks = new JobWebhooks(deliveryStore, { hosted, allowHttpLoopback: options.webhookPolicy?.allowHttpLoopback ?? !hosted });
|
|
27706
29023
|
const logWebhookFailure = (taskId, stage, error) => {
|
|
@@ -27764,23 +29081,24 @@ function createApiEngine(options = {}) {
|
|
|
27764
29081
|
const activeScrapes = /* @__PURE__ */ new Set();
|
|
27765
29082
|
const crawlControllers = /* @__PURE__ */ new Map();
|
|
27766
29083
|
const runningCrawls = /* @__PURE__ */ new Map();
|
|
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 });
|
|
29084
|
+
const createChannels = options.channelsFor ?? ((mode, egress, enhanced) => {
|
|
29085
|
+
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 ?? [] }, vendorTariffs: accessGrant?.tariffs ?? {}, ...accessGrant === null ? {} : { vendorGrant: accessGrantRef(accessGrant) }, browserEngine: options.browserEngine ?? "playwright", compatTransport: compatHosts.length > 0, ...enhanced === true ? { enhanced } : {} });
|
|
27769
29086
|
return options.httpOnly ? channels.filter((channel) => HTTP_CHANNELS.has(channel.id)) : channels;
|
|
27770
29087
|
});
|
|
27771
29088
|
const channelsByMode = /* @__PURE__ */ new Map();
|
|
27772
29089
|
const historiesByMode = /* @__PURE__ */ new Map();
|
|
27773
|
-
const channelsFor = (mode, egress) => {
|
|
27774
|
-
const
|
|
29090
|
+
const channelsFor = (mode, egress, enhanced = false) => {
|
|
29091
|
+
const asEnhanced = enhanced && mode === "standard";
|
|
29092
|
+
const key = `${egress === void 0 ? mode : `${mode}|${egress.id}`}${asEnhanced ? "|enhanced" : ""}`;
|
|
27775
29093
|
const existing = channelsByMode.get(key);
|
|
27776
29094
|
if (existing !== void 0)
|
|
27777
29095
|
return existing;
|
|
27778
|
-
const channels = createChannels(mode, egress);
|
|
29096
|
+
const channels = asEnhanced ? createChannels(mode, egress, true) : createChannels(mode, egress);
|
|
27779
29097
|
channelsByMode.set(key, channels);
|
|
27780
29098
|
return channels;
|
|
27781
29099
|
};
|
|
27782
29100
|
const channelsForUrl = (mode, url, page = {}, formats = [], oneUrl = true, compat = true, egress) => {
|
|
27783
|
-
const channels = channelsFor(mode, egress);
|
|
29101
|
+
const channels = channelsFor(mode, egress, page.access === "enhanced");
|
|
27784
29102
|
const policy = options.channelPolicy?.(url) ?? "ladder";
|
|
27785
29103
|
let selected = policy === "ladder" ? channels : channels.filter((channel) => policy === "http_only" ? HTTP_CHANNELS.has(channel.id) : channel.id === "browser_local");
|
|
27786
29104
|
if (!compat || oneUrl && !compatHostListed(compatHosts, url))
|
|
@@ -27822,6 +29140,12 @@ function createApiEngine(options = {}) {
|
|
|
27822
29140
|
filtered.push({ reason: "fastMode", dropped });
|
|
27823
29141
|
selected = kept;
|
|
27824
29142
|
}
|
|
29143
|
+
if (page.access === "standard") {
|
|
29144
|
+
const dropped = selected.filter((channel) => channel.vendorId !== void 0).map(name);
|
|
29145
|
+
if (dropped.length > 0)
|
|
29146
|
+
filtered.push({ reason: "access standard", dropped });
|
|
29147
|
+
selected = selected.filter((channel) => channel.vendorId === void 0);
|
|
29148
|
+
}
|
|
27825
29149
|
const wire = ["headers", "mobile", "skipTlsVerification"].filter((option) => option === "headers" ? page.headers !== void 0 && Object.keys(page.headers).length > 0 : page[option] === true);
|
|
27826
29150
|
if (wire.length > 0) {
|
|
27827
29151
|
const dropped = selected.filter((channel) => channel.vendorId !== void 0).map(name);
|
|
@@ -27888,7 +29212,7 @@ function createApiEngine(options = {}) {
|
|
|
27888
29212
|
const completed = await store.countCompletedSteps(taskId);
|
|
27889
29213
|
const counts = await store.countSteps(taskId);
|
|
27890
29214
|
const webhook = jobWebhooks.status(task);
|
|
27891
|
-
const waitingForPerson = userChrome === null || !offersHandoff(task) ? void 0 : Object.entries(await store.countBlockReasons(taskId)).reduce((sum, [reason2, count]) => sum + (HANDOFF_REASONS[reason2] === void 0 ? 0 : count), 0);
|
|
29215
|
+
const waitingForPerson = userChrome === null || !offersHandoff(task) ? void 0 : listsPages2(task) ? (await stepsOf(store, taskId, "errors")).filter((step) => handoffNeededIn(task, step)).length : Object.entries(await store.countBlockReasons(taskId)).reduce((sum, [reason2, count]) => sum + (HANDOFF_REASONS[reason2] === void 0 ? 0 : count), 0);
|
|
27892
29216
|
return {
|
|
27893
29217
|
...report,
|
|
27894
29218
|
requested: task.batch.urls.length,
|
|
@@ -27898,9 +29222,10 @@ function createApiEngine(options = {}) {
|
|
|
27898
29222
|
succeeded: (counts.success ?? 0) + (counts.partial ?? 0) + (counts.empty_verified ?? 0),
|
|
27899
29223
|
failed: (counts.failed ?? 0) + (counts.blocked ?? 0) + (counts.cancelled ?? 0) + (counts.budget_exceeded ?? 0),
|
|
27900
29224
|
// The cap in force: the batch's own, never above this service's worker count.
|
|
27901
|
-
maxConcurrency: Math.min(task.batch.maxConcurrency ?? workerCount, workerCount),
|
|
29225
|
+
maxConcurrency: task.batch.lane === "my-browser" ? 1 : Math.min(task.batch.maxConcurrency ?? workerCount, workerCount),
|
|
27902
29226
|
...task.batch.invalidURLs === void 0 ? {} : { invalidURLs: task.batch.invalidURLs },
|
|
27903
29227
|
...waitingForPerson === void 0 ? {} : { waitingForPerson },
|
|
29228
|
+
...approvalsWaiting.has(taskId) ? { waitingForApproval: true } : {},
|
|
27904
29229
|
...webhook === void 0 ? {} : { webhook }
|
|
27905
29230
|
};
|
|
27906
29231
|
} finally {
|
|
@@ -27982,6 +29307,74 @@ function createApiEngine(options = {}) {
|
|
|
27982
29307
|
await store.close();
|
|
27983
29308
|
}
|
|
27984
29309
|
}
|
|
29310
|
+
function listCheckOf(result) {
|
|
29311
|
+
return (result.actions?.lists ?? []).find((run) => run.type === "paginate" && run.stoppedBy === "challenge" && run.challenge !== void 0) ?? null;
|
|
29312
|
+
}
|
|
29313
|
+
async function continueList(chrome, step, run, selection, fetchOpts, waitMs, hooks, signal) {
|
|
29314
|
+
const prior = step.result;
|
|
29315
|
+
const action = (selection.actions ?? []).find((item) => item.type === "paginate");
|
|
29316
|
+
if (action === void 0)
|
|
29317
|
+
return { reason: "the batch has no paginate step to go on with" };
|
|
29318
|
+
const kept = (prior.actions?.scrapes ?? []).filter((scrape) => scrape.step === run.index).map(({ url: url2, html: html2 }) => ({ url: url2, html: html2 }));
|
|
29319
|
+
const from = run.challenge.page;
|
|
29320
|
+
const url = run.challenge.url;
|
|
29321
|
+
try {
|
|
29322
|
+
const judged = { ...fetchOpts.includeTags === void 0 ? {} : { includeTags: fetchOpts.includeTags }, ...fetchOpts.excludeTags === void 0 ? {} : { excludeTags: fetchOpts.excludeTags }, ...fetchOpts.blockAds === void 0 ? {} : { blockAds: fetchOpts.blockAds } };
|
|
29323
|
+
const read2 = await chrome.readList(url, {
|
|
29324
|
+
...waitMs === void 0 ? {} : { waitMs },
|
|
29325
|
+
...hooks.onWaiting === void 0 ? {} : { onWaiting: hooks.onWaiting },
|
|
29326
|
+
...hooks.onConfirm === void 0 ? {} : { onConfirm: hooks.onConfirm },
|
|
29327
|
+
...hooks.onHidden === void 0 ? {} : { onHidden: hooks.onHidden },
|
|
29328
|
+
signal,
|
|
29329
|
+
...judged,
|
|
29330
|
+
follow: {
|
|
29331
|
+
nextSelector: action.nextSelector,
|
|
29332
|
+
...action.itemSelector === void 0 ? {} : { itemSelector: action.itemSelector },
|
|
29333
|
+
// A pager with no address of its own opens at the list's own page: the pages before the check are shown again on the way, and do not count against the limit.
|
|
29334
|
+
maxPages: Math.max(1, (action.maxPages ?? LIST_DEFAULTS.maxPages) - (new URL(url).pathname + new URL(url).search === new URL(step.url).pathname + new URL(step.url).search ? 0 : kept.length)),
|
|
29335
|
+
onStart: (pageUrl) => hooks.onContinue?.(pageUrl, kept.length + 1),
|
|
29336
|
+
onPage: (pageUrl, pages2) => hooks.onContinue?.(pageUrl, kept.length + pages2)
|
|
29337
|
+
}
|
|
29338
|
+
});
|
|
29339
|
+
if (read2.stoppedBy === "deadline" && read2.pages.length === 1)
|
|
29340
|
+
return { reason: `you got through the check at page ${from}, but did not page on in that tab in time: hand the batch over again to go on from there` };
|
|
29341
|
+
const last = read2.pages[read2.pages.length - 1];
|
|
29342
|
+
const { list: list2, ...withoutList } = fetchOpts;
|
|
29343
|
+
const base = pageFromUserBrowser({ ...read2.first, finalUrl: last.url, html: last.html }, prior, withoutList);
|
|
29344
|
+
const pages = [...kept, ...read2.pages];
|
|
29345
|
+
const merged = list2 === void 0 ? null : mergeListPages(pages, list2);
|
|
29346
|
+
const rounds = pages.length;
|
|
29347
|
+
const continued = { from, pages: read2.pages.length, by: "user_browser" };
|
|
29348
|
+
const itemsRead = action.itemSelector === void 0 || listPagesRepeat(pages, action.itemSelector) ? null : continuedItemsRead({ stepUrl: step.url, checkUrl: url, kept: kept.map((page) => page.url), keptItems: run.itemsRead, shown: read2.pages });
|
|
29349
|
+
const lists = (prior.actions?.lists ?? []).map((item) => item.index === run.index ? { ...item, stoppedBy: read2.stoppedBy, rounds, items: last.items ?? null, itemsRead, continued } : item);
|
|
29350
|
+
const actions = { ...prior.actions, scrapes: [...(prior.actions?.scrapes ?? []).filter((scrape) => scrape.step !== run.index), ...kept.map((page) => ({ ...page, step: run.index })), ...read2.pages.map(({ url: pageUrl, html: html2 }) => ({ url: pageUrl, html: html2, step: run.index, by: "user_browser" }))], lists };
|
|
29351
|
+
const valued = merged !== null && merged.valued;
|
|
29352
|
+
const short = read2.stoppedBy !== "end";
|
|
29353
|
+
const at = base.usage.wallMs;
|
|
29354
|
+
const result = {
|
|
29355
|
+
...base,
|
|
29356
|
+
...valued ? { status: "success", failureReason: null, blockReason: null } : {},
|
|
29357
|
+
actions,
|
|
29358
|
+
...merged === null ? {} : { list: merged.list },
|
|
29359
|
+
warnings: [
|
|
29360
|
+
...(base.warnings ?? []).filter((warning) => warning.code !== "list_not_exhausted"),
|
|
29361
|
+
...short ? [{ code: "list_not_exhausted", message: `The list of step ${run.index} (paginate) stopped before its end (${read2.stoppedBy === "max" ? `its limit of ${rounds} pages` : "the wait for you to page on in your Chrome ended"}): more items may follow.` }] : []
|
|
29362
|
+
],
|
|
29363
|
+
trace: [
|
|
29364
|
+
...base.trace,
|
|
29365
|
+
{ at, lane: base.lane, event: "list_continued", detail: { step: run.index, from, pages: read2.pages.length, stoppedBy: read2.stoppedBy, by: "user_browser", kept: kept.length, keptBy: prior.lane } },
|
|
29366
|
+
...merged === null ? [] : [{ at, lane: base.lane, event: "list_extracted", detail: { records: merged.list.records.length, incomplete: merged.list.incomplete, pages: merged.list.pages, truncated: merged.list.truncated } }]
|
|
29367
|
+
]
|
|
29368
|
+
};
|
|
29369
|
+
if (!CONTENTFUL_STATUS.has(result.status))
|
|
29370
|
+
return { reason: `the page Octocrawl read in Chrome was ${result.status} (${result.blockReason ?? result.failureReason ?? "no reason"}), not the list` };
|
|
29371
|
+
return { result };
|
|
29372
|
+
} catch (error) {
|
|
29373
|
+
if (!(error instanceof HandoffNotThrough))
|
|
29374
|
+
throw error;
|
|
29375
|
+
return { reason: error.message };
|
|
29376
|
+
}
|
|
29377
|
+
}
|
|
27985
29378
|
async function readThrough(chrome, url, prior, fetchOpts, waitMs, hooks, signal) {
|
|
27986
29379
|
try {
|
|
27987
29380
|
const judged = { ...fetchOpts.includeTags === void 0 ? {} : { includeTags: fetchOpts.includeTags }, ...fetchOpts.excludeTags === void 0 ? {} : { excludeTags: fetchOpts.excludeTags }, ...fetchOpts.blockAds === void 0 ? {} : { blockAds: fetchOpts.blockAds } };
|
|
@@ -28005,6 +29398,146 @@ function createApiEngine(options = {}) {
|
|
|
28005
29398
|
if (unread !== null)
|
|
28006
29399
|
throw new RequestError(`handoff: the request asks for ${unread}, which a page read in your own Chrome cannot give`, "unsupported_parameter", { parameters: ["handoff"] });
|
|
28007
29400
|
}
|
|
29401
|
+
function checkMyBrowser(req) {
|
|
29402
|
+
if (req.lane !== "my-browser")
|
|
29403
|
+
return;
|
|
29404
|
+
if ("urls" in req && req.maxConcurrency !== void 0 && req.maxConcurrency > 1)
|
|
29405
|
+
throw new RequestError("lane my-browser reads one page at a time, in your Chrome: maxConcurrency above 1 does not apply", "unsupported_parameter", { parameters: ["lane", "maxConcurrency"] });
|
|
29406
|
+
if (userChrome === null)
|
|
29407
|
+
throw new RequestError("lane my-browser: this server does not read pages in your Chrome; run Octocrawl on your own machine (octocrawl serve, the local MCP host, or the octocrawl CLI)", "unsupported_parameter", { parameters: ["lane"] });
|
|
29408
|
+
const unread = unreadByPerson(fetchOptions(req, req.formats));
|
|
29409
|
+
if (unread !== null)
|
|
29410
|
+
throw new RequestError(`lane my-browser: the request asks for ${unread}, which the my-browser lane does not give yet`, "unsupported_parameter", { parameters: ["lane"] });
|
|
29411
|
+
if (req.mode !== void 0 && req.mode !== "standard")
|
|
29412
|
+
throw new RequestError(`lane my-browser reads the page as you, in your Chrome: mode ${req.mode} does not apply`, "unsupported_parameter", { parameters: ["lane", "mode"] });
|
|
29413
|
+
if (req.lockdown === true)
|
|
29414
|
+
throw new RequestError("lane my-browser reads the page in your Chrome: lockdown answers from the cache alone, and a page read there is never cached", "unsupported_parameter", { parameters: ["lane", "lockdown"] });
|
|
29415
|
+
}
|
|
29416
|
+
async function myBrowserScrape(req, context, hooks) {
|
|
29417
|
+
if (handoffClosing.signal.aborted)
|
|
29418
|
+
throw new HandoffUnavailableError("Octocrawl is shutting down");
|
|
29419
|
+
const signal = AbortSignal.any([...context.signal === void 0 ? [] : [context.signal], ...hooks.signal === void 0 ? [] : [hooks.signal], shutdownController.signal, handoffClosing.signal]);
|
|
29420
|
+
const started = Date.now();
|
|
29421
|
+
let chrome;
|
|
29422
|
+
try {
|
|
29423
|
+
chrome = await openUserChrome(userChrome, signal);
|
|
29424
|
+
} catch (error) {
|
|
29425
|
+
throw new HandoffUnavailableError(error instanceof Error ? error.message : String(error));
|
|
29426
|
+
}
|
|
29427
|
+
try {
|
|
29428
|
+
const hosts = [new URL(req.url).host];
|
|
29429
|
+
hooks.onAllow?.(hosts);
|
|
29430
|
+
let allowed;
|
|
29431
|
+
try {
|
|
29432
|
+
allowed = await chrome.allow({ hosts, task: `scrape ${req.url}` }, { signal, log: approvalLog("scrape", null) });
|
|
29433
|
+
} catch (error) {
|
|
29434
|
+
throw new HandoffUnavailableError(error instanceof Error ? error.message : String(error));
|
|
29435
|
+
}
|
|
29436
|
+
try {
|
|
29437
|
+
return await readAllowed(chrome, allowed, req.url, fetchOptions(req, req.formats), req.handoff?.waitMs, hooks, signal, started);
|
|
29438
|
+
} finally {
|
|
29439
|
+
await allowed.close();
|
|
29440
|
+
}
|
|
29441
|
+
} finally {
|
|
29442
|
+
chrome.close();
|
|
29443
|
+
}
|
|
29444
|
+
}
|
|
29445
|
+
async function readAllowed(chrome, allowed, url, fetchOpts, waitMs, hooks, signal, started) {
|
|
29446
|
+
try {
|
|
29447
|
+
const read2 = await chrome.read(url, {
|
|
29448
|
+
unattended: true,
|
|
29449
|
+
allowedHosts: allowed.hosts,
|
|
29450
|
+
// What the person sees in their Chrome: not a panel the page keeps hidden (a sheet's shortcut list beside its grid).
|
|
29451
|
+
shownOnly: true,
|
|
29452
|
+
...waitMs === void 0 ? {} : { waitMs },
|
|
29453
|
+
...hooks.onWaiting === void 0 ? {} : { onWaiting: hooks.onWaiting },
|
|
29454
|
+
...hooks.onHidden === void 0 ? {} : { onHidden: hooks.onHidden },
|
|
29455
|
+
signal: AbortSignal.any([signal, allowed.signal]),
|
|
29456
|
+
...fetchOpts.includeTags === void 0 ? {} : { includeTags: fetchOpts.includeTags },
|
|
29457
|
+
...fetchOpts.excludeTags === void 0 ? {} : { excludeTags: fetchOpts.excludeTags },
|
|
29458
|
+
...fetchOpts.blockAds === void 0 ? {} : { blockAds: fetchOpts.blockAds }
|
|
29459
|
+
});
|
|
29460
|
+
return pageFromUserBrowser(read2, null, fetchOpts);
|
|
29461
|
+
} catch (error) {
|
|
29462
|
+
if (!(error instanceof HandoffNotThrough))
|
|
29463
|
+
throw error;
|
|
29464
|
+
const wallMs = Date.now() - started;
|
|
29465
|
+
if (allowed.signal.aborted)
|
|
29466
|
+
return unreadInUserBrowser(url, { status: "cancelled" }, `you revoked the sites in Chrome before ${url} was read`, wallMs);
|
|
29467
|
+
if (error.kind === "cancelled" || error.kind === "gone")
|
|
29468
|
+
return unreadInUserBrowser(url, { status: "cancelled" }, error.message, wallMs);
|
|
29469
|
+
if (error.kind === "chrome")
|
|
29470
|
+
return unreadInUserBrowser(url, { status: "failed", failureReason: "connection_error" }, error.message, wallMs);
|
|
29471
|
+
if (error.kind === "elsewhere")
|
|
29472
|
+
return unreadInUserBrowser(url, { status: "failed", failureReason: "redirect_limit" }, error.message, wallMs);
|
|
29473
|
+
const check = BLOCK_REASON.includes(error.check ?? "") ? error.check : null;
|
|
29474
|
+
return unreadInUserBrowser(url, check === null ? { status: "failed", failureReason: "timeout" } : { status: "blocked", blockReason: check }, error.message, wallMs);
|
|
29475
|
+
}
|
|
29476
|
+
}
|
|
29477
|
+
const approvalsWaiting = /* @__PURE__ */ new Set();
|
|
29478
|
+
const approvalLog = (kind, taskId) => (step, detail) => console.error(JSON.stringify({ component: "api", event: "my_browser_approval", kind, ...taskId === null ? {} : { taskId }, step, ...detail }));
|
|
29479
|
+
function myBrowserBatchReader(urls, taskId, fetchOpts) {
|
|
29480
|
+
const hosts = [...new Set(urls.map((url) => new URL(url).host))];
|
|
29481
|
+
let opened = null;
|
|
29482
|
+
const stop = new AbortController();
|
|
29483
|
+
const signal = AbortSignal.any([shutdownController.signal, handoffClosing.signal, stop.signal]);
|
|
29484
|
+
let reading = null;
|
|
29485
|
+
const open = () => opened ??= (async () => {
|
|
29486
|
+
if (userChrome === null)
|
|
29487
|
+
return { refused: "this server does not read pages in your Chrome: run Octocrawl on your own machine (octocrawl serve, the local MCP host, or the octocrawl CLI)", status: "connection_error" };
|
|
29488
|
+
let chrome;
|
|
29489
|
+
try {
|
|
29490
|
+
chrome = await openUserChrome(userChrome, signal);
|
|
29491
|
+
} catch (error) {
|
|
29492
|
+
return { refused: error instanceof Error ? error.message : String(error), status: "connection_error" };
|
|
29493
|
+
}
|
|
29494
|
+
approvalsWaiting.add(taskId);
|
|
29495
|
+
try {
|
|
29496
|
+
return { chrome, allowed: await chrome.allow({ hosts, task: `batch ${taskId}: ${urls.length} page${urls.length === 1 ? "" : "s"}` }, { signal, log: approvalLog("batch", taskId) }) };
|
|
29497
|
+
} catch (error) {
|
|
29498
|
+
chrome.close();
|
|
29499
|
+
return { refused: error instanceof Error ? error.message : String(error), status: "cancelled" };
|
|
29500
|
+
} finally {
|
|
29501
|
+
approvalsWaiting.delete(taskId);
|
|
29502
|
+
}
|
|
29503
|
+
})();
|
|
29504
|
+
const unread = (url, status, message2, started) => ({ result: unreadInUserBrowser(url, status === "cancelled" ? { status } : { status: "failed", failureReason: status }, message2, Date.now() - started), links: [] });
|
|
29505
|
+
return {
|
|
29506
|
+
async scrape(url, context) {
|
|
29507
|
+
const started = Date.now();
|
|
29508
|
+
const connection = await open();
|
|
29509
|
+
if ("refused" in connection)
|
|
29510
|
+
return unread(url, connection.status, connection.refused, started);
|
|
29511
|
+
if (connection.allowed.signal.aborted)
|
|
29512
|
+
return unread(url, "cancelled", `you revoked the sites in Chrome before ${url} was read`, started);
|
|
29513
|
+
if (!connection.allowed.hosts.includes(new URL(url).host))
|
|
29514
|
+
return unread(url, "cancelled", `${new URL(url).host} is not among the sites you allowed for this run of the batch`, started);
|
|
29515
|
+
if (signal.aborted || context?.signal?.aborted === true)
|
|
29516
|
+
return unread(url, "cancelled", `the batch stopped before ${url} was read`, started);
|
|
29517
|
+
const pending = readAllowed(connection.chrome, connection.allowed, url, fetchOpts, void 0, {}, AbortSignal.any([signal, ...context?.signal === void 0 ? [] : [context.signal]]), started);
|
|
29518
|
+
reading = pending;
|
|
29519
|
+
try {
|
|
29520
|
+
const read2 = await pending;
|
|
29521
|
+
return { result: read2, links: read2.links ?? [] };
|
|
29522
|
+
} finally {
|
|
29523
|
+
if (reading === pending)
|
|
29524
|
+
reading = null;
|
|
29525
|
+
}
|
|
29526
|
+
},
|
|
29527
|
+
async close() {
|
|
29528
|
+
stop.abort();
|
|
29529
|
+
if (opened === null)
|
|
29530
|
+
return;
|
|
29531
|
+
const connection = await opened;
|
|
29532
|
+
while (reading !== null)
|
|
29533
|
+
await reading.catch(() => void 0);
|
|
29534
|
+
if ("refused" in connection)
|
|
29535
|
+
return;
|
|
29536
|
+
await connection.allowed.close();
|
|
29537
|
+
connection.chrome.close();
|
|
29538
|
+
}
|
|
29539
|
+
};
|
|
29540
|
+
}
|
|
28008
29541
|
async function handOffScrape(req, prior, context, hooks) {
|
|
28009
29542
|
if (handoffClosing.signal.aborted)
|
|
28010
29543
|
return { reason: "Octocrawl is shutting down" };
|
|
@@ -28054,7 +29587,7 @@ function createApiEngine(options = {}) {
|
|
|
28054
29587
|
throw new CrawlStateError(`batch ${taskId} has a webhook: a page read in your own Chrome is read signed in as you, and is not sent to another address; its stopped items are not handed over`);
|
|
28055
29588
|
handoffs.add(taskId);
|
|
28056
29589
|
try {
|
|
28057
|
-
const waiting = (await stepsOf(store, taskId, "errors")).filter(
|
|
29590
|
+
const waiting = (await stepsOf(store, taskId, "errors")).filter((step) => handoffNeededIn(task, step));
|
|
28058
29591
|
const items = [];
|
|
28059
29592
|
if (waiting.length > 0) {
|
|
28060
29593
|
const selection = task.batch;
|
|
@@ -28074,7 +29607,8 @@ function createApiEngine(options = {}) {
|
|
|
28074
29607
|
items.push({ id: step.id, url: step.url, through: false, status: step.status, reason: "the handoff was cancelled before this page" });
|
|
28075
29608
|
continue;
|
|
28076
29609
|
}
|
|
28077
|
-
const
|
|
29610
|
+
const listRun = step.result?.actions === void 0 ? null : listCheckOf(step.result);
|
|
29611
|
+
const read2 = listRun === null ? await readThrough(chrome, step.url, step.result, fetchOpts, req.waitMs, hooks, signal) : await continueList(chrome, step, listRun, selection, fetchOpts, req.waitMs, hooks, signal);
|
|
28078
29612
|
if ("reason" in read2) {
|
|
28079
29613
|
items.push({ id: step.id, url: step.url, through: false, status: step.status, reason: read2.reason });
|
|
28080
29614
|
continue;
|
|
@@ -28094,6 +29628,8 @@ function createApiEngine(options = {}) {
|
|
|
28094
29628
|
const { audit: _stoppedAudit, ...kept } = step;
|
|
28095
29629
|
const replaced = { ...kept, status: stepStatusFromResult(stored.status), lane: stored.lane, contentHash: stored.evidence.rawBodySha256, cached: false, result: stored, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
28096
29630
|
await store.putStep(replaced);
|
|
29631
|
+
if (listRun !== null)
|
|
29632
|
+
await store.clearPagesRead(taskId, step.canonicalUrl);
|
|
28097
29633
|
items.push({ id: step.id, url: step.url, through: true, status: stored.status });
|
|
28098
29634
|
const page = compactPage(replaced, task);
|
|
28099
29635
|
if (webhookOf(task) !== void 0) {
|
|
@@ -28124,7 +29660,8 @@ function createApiEngine(options = {}) {
|
|
|
28124
29660
|
const stored = task.batch ?? task.crawl;
|
|
28125
29661
|
const selection = stored === void 0 || !hosted ? stored : { ...stored, skipTlsVerification: false };
|
|
28126
29662
|
const egressFile = join10(task.taskDir, EGRESS_FILE);
|
|
28127
|
-
const
|
|
29663
|
+
const myBrowser = task.batch?.lane === "my-browser" ? myBrowserBatchReader(task.batch.urls, task.id, fetchOptions(task.batch, task.batch.formats)) : null;
|
|
29664
|
+
const firstEgress = egressPool === null || mode === "authed" || myBrowser !== null ? void 0 : bindTaskEgress(egressPool, egressFile);
|
|
28128
29665
|
let switches = 0;
|
|
28129
29666
|
const lanes = [];
|
|
28130
29667
|
const routeOf = (egress) => {
|
|
@@ -28138,7 +29675,7 @@ function createApiEngine(options = {}) {
|
|
|
28138
29675
|
removeCookieSessions(task.taskDir);
|
|
28139
29676
|
rmSync(egressFile, { force: true });
|
|
28140
29677
|
};
|
|
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 });
|
|
29678
|
+
const runnerFor = (lane) => new LadderRunner(lane.channels, { mode, ...selection?.access === "enhanced" ? { enhanced: true } : {}, ...req.policyAllowlist.length ? { allowlistedDomains: req.policyAllowlist } : {} }, historyFor(mode), null, task.batch === void 0 ? null : sessionsFor(mode), { channelsFiltered: lane.filtered });
|
|
28142
29679
|
const recordedOverrideFor = !robotsSetAside || task.batch?.robotsOverrides === void 0 ? null : robotsOverrideLookup(task.batch.robotsOverrides);
|
|
28143
29680
|
const namedOverride = task.batch === void 0 ? ignoreRobotsOverride(task.crawl?.ignoreRobotsTxt) : namedUrlOverride;
|
|
28144
29681
|
const robotsOverrideFor = recordedOverrideFor === null && namedOverride === void 0 ? null : (url) => recordedOverrideFor?.(url) ?? namedOverride;
|
|
@@ -28156,6 +29693,7 @@ function createApiEngine(options = {}) {
|
|
|
28156
29693
|
lanes.push(current);
|
|
28157
29694
|
const closeLadders = async () => {
|
|
28158
29695
|
await Promise.all(lanes.map((lane) => lane.ladder.close()));
|
|
29696
|
+
await myBrowser?.close();
|
|
28159
29697
|
};
|
|
28160
29698
|
const pageAtom = selection === void 0 ? { scrape: (url, context) => {
|
|
28161
29699
|
const lane = current;
|
|
@@ -28163,15 +29701,16 @@ function createApiEngine(options = {}) {
|
|
|
28163
29701
|
}, close: closeLadders } : {
|
|
28164
29702
|
async scrape(url, context) {
|
|
28165
29703
|
const lane = current;
|
|
28166
|
-
const
|
|
29704
|
+
const pageDeadline = myBrowser !== null ? context?.deadlineAt ?? Infinity : Math.min(context?.deadlineAt ?? Infinity, Date.now() + (selection.timeout ?? DEFAULT_SCRAPE_TIMEOUT_MS));
|
|
29705
|
+
const page = createExecutionScope({ ...context, ...pageDeadline === Infinity ? {} : { deadlineAt: pageDeadline }, ...lane.cookieSession === void 0 ? {} : { cookieSession: lane.cookieSession } });
|
|
28167
29706
|
const formats = selection.formats ?? ["markdown"];
|
|
28168
29707
|
const wants = (name) => hasFormat(formats, name);
|
|
28169
29708
|
const custom = customJsonFormat(formats);
|
|
28170
29709
|
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;
|
|
29710
|
+
const plan = myBrowser !== null ? null : lane.cookieSession === void 0 || cachePlan?.lockdown === true ? cachePlan : null;
|
|
28172
29711
|
const answer = consultCache(plan, url, { mode, ...req.policyAllowlist.length ? { allowlistedDomains: req.policyAllowlist } : {} });
|
|
28173
29712
|
const { outcome, json: json2 } = await (async () => {
|
|
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" };
|
|
29713
|
+
const outcome2 = answer.kind === "fetch" ? await (myBrowser !== null ? myBrowser.scrape(url, context) : 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" };
|
|
28175
29714
|
const json3 = wants("json") ? await extractStructured(extractionInput(outcome2.result), custom, page, structuredModelConfigFromEnv()) : void 0;
|
|
28176
29715
|
return { outcome: outcome2, json: json3 };
|
|
28177
29716
|
})().finally(() => page.dispose());
|
|
@@ -28200,6 +29739,7 @@ function createApiEngine(options = {}) {
|
|
|
28200
29739
|
let used = current;
|
|
28201
29740
|
let outcome = await pageAtom.scrape(url, context);
|
|
28202
29741
|
const events = [];
|
|
29742
|
+
const carried = [];
|
|
28203
29743
|
while (egressInDoubt(outcome)) {
|
|
28204
29744
|
let reason2;
|
|
28205
29745
|
if (current === used) {
|
|
@@ -28222,10 +29762,12 @@ function createApiEngine(options = {}) {
|
|
|
28222
29762
|
reason2 = "moved_with_task";
|
|
28223
29763
|
}
|
|
28224
29764
|
events.push({ at: 0, lane: outcome.result.lane, event: "egress_switched", detail: { from: used.egress.id, to: current.egress.id, reason: reason2, switches } });
|
|
29765
|
+
carried.push(...givenUpPaidCalls(outcome.result.trace));
|
|
28225
29766
|
used = current;
|
|
28226
29767
|
outcome = await pageAtom.scrape(url, context);
|
|
28227
29768
|
}
|
|
28228
|
-
|
|
29769
|
+
const result = await withEgressExit(events.length === 0 ? outcome.result : { ...outcome.result, trace: [...carried, ...outcome.result.trace, ...events] }, used.egress, context);
|
|
29770
|
+
return result === outcome.result ? outcome : { ...outcome, result };
|
|
28229
29771
|
},
|
|
28230
29772
|
close: () => pageAtom.close()
|
|
28231
29773
|
};
|
|
@@ -28248,8 +29790,9 @@ function createApiEngine(options = {}) {
|
|
|
28248
29790
|
const orchestrator = new CrawlOrchestrator({
|
|
28249
29791
|
store,
|
|
28250
29792
|
atom,
|
|
28251
|
-
// A page whose scrape threw called a provider only if
|
|
28252
|
-
|
|
29793
|
+
// A page whose scrape threw called a provider only if the task's rungs have one (its mode and its access choice, as
|
|
29794
|
+
// its lane was built): otherwise its third-party cost is a known 0.
|
|
29795
|
+
scrapeErrorCostUsd: current.rungs.channels.some((channel) => channel.vendorId !== void 0) ? null : 0,
|
|
28253
29796
|
workerCount,
|
|
28254
29797
|
perHostConcurrency: Math.min(4, networkPolicy.perHostConcurrency),
|
|
28255
29798
|
perHostMinDelayMs: networkPolicy.perHostMinDelayMs,
|
|
@@ -28290,18 +29833,18 @@ function createApiEngine(options = {}) {
|
|
|
28290
29833
|
launchTask(pending, store, batchRunOptions(pending.batch, true));
|
|
28291
29834
|
return;
|
|
28292
29835
|
}
|
|
28293
|
-
await finishJob(task, store);
|
|
28294
|
-
inflight.delete(task.id);
|
|
28295
29836
|
const after = await store.getTask(task.id);
|
|
28296
29837
|
await endCookieSession(after !== null && isTerminalStatus(after.status));
|
|
29838
|
+
await finishJob(task, store);
|
|
29839
|
+
inflight.delete(task.id);
|
|
28297
29840
|
await store.close();
|
|
28298
29841
|
}).catch(async (error) => {
|
|
28299
29842
|
inflight.delete(task.id);
|
|
28300
29843
|
crawlControllers.delete(task.id);
|
|
28301
29844
|
runningCrawls.delete(task.id);
|
|
28302
29845
|
await markCrawlFailed(store, task.id);
|
|
28303
|
-
await finishJob(task, store, error instanceof Error ? error.message : String(error));
|
|
28304
29846
|
await endCookieSession(true);
|
|
29847
|
+
await finishJob(task, store, error instanceof Error ? error.message : String(error));
|
|
28305
29848
|
await store.close();
|
|
28306
29849
|
});
|
|
28307
29850
|
runningCrawls.set(task.id, orchestrator);
|
|
@@ -28333,6 +29876,8 @@ function createApiEngine(options = {}) {
|
|
|
28333
29876
|
}
|
|
28334
29877
|
async function runScrape(req, context, record3, hooks = {}) {
|
|
28335
29878
|
checkHandoff(req);
|
|
29879
|
+
checkAccessChoice(req);
|
|
29880
|
+
checkMyBrowser(req);
|
|
28336
29881
|
checkFileCap(req);
|
|
28337
29882
|
checkSelectors(req);
|
|
28338
29883
|
checkAttributeSelectors(req.formats);
|
|
@@ -28345,17 +29890,46 @@ function createApiEngine(options = {}) {
|
|
|
28345
29890
|
const deadlineAt = req.timeout === void 0 ? context.deadlineAt ?? Date.now() + DEFAULT_SCRAPE_TIMEOUT_MS : Math.min(context.deadlineAt ?? Infinity, Date.now() + req.timeout);
|
|
28346
29891
|
const scope = createExecutionScope({ ...context, signal: context.signal ? AbortSignal.any([context.signal, shutdownController.signal]) : shutdownController.signal, deadlineAt });
|
|
28347
29892
|
const mode = defaultApiMode(req.mode);
|
|
28348
|
-
const
|
|
29893
|
+
const scrapeEgress = mode === "authed" ? void 0 : egressPool?.pick();
|
|
29894
|
+
const rungs = channelsForUrl(mode, req.url, req, req.formats ?? [], true, true, scrapeEgress);
|
|
28349
29895
|
const policy = {
|
|
28350
29896
|
mode,
|
|
29897
|
+
// access enhanced permits the provider lane in mode standard too (the grant still decides whether one exists).
|
|
29898
|
+
...req.access === "enhanced" ? { enhanced: true } : {},
|
|
28351
29899
|
...req.allowlistedDomains !== void 0 && req.allowlistedDomains.length > 0 ? { allowlistedDomains: req.allowlistedDomains } : {}
|
|
28352
29900
|
};
|
|
28353
29901
|
const runner = new LadderRunner(rungs.channels, policy, historyFor(mode), null, sessionsFor(mode), { channelsFiltered: rungs.filtered });
|
|
28354
29902
|
const robotsOverride = req.robotsOverride ?? (record3 ? namedUrlOverride : void 0);
|
|
28355
29903
|
const plan = record3 ? cachePlanFor(req.url, mode, req, req.formats, rungs.channels, req.robotsOverride, robotsOverride !== void 0) : null;
|
|
29904
|
+
const deliver = async (full, shapeScope, fetched) => {
|
|
29905
|
+
const shaped = await prepareScrapeResponse(full, req, shapeScope, null, overallStart, scrapeId).finally(() => {
|
|
29906
|
+
if (shapeScope !== scope)
|
|
29907
|
+
shapeScope.dispose();
|
|
29908
|
+
});
|
|
29909
|
+
const response = fetched ? shaped : withoutFetchUsage(shaped);
|
|
29910
|
+
if (!record3)
|
|
29911
|
+
return response;
|
|
29912
|
+
try {
|
|
29913
|
+
await writeScrapeRecord(scrapeRecordOf(scrapeId, requestedAt, req, full, response));
|
|
29914
|
+
return response;
|
|
29915
|
+
} catch (error) {
|
|
29916
|
+
console.error(JSON.stringify({ component: "api", event: "scrape_record_unwritten", scrapeId, error: error instanceof Error ? error.message : String(error) }));
|
|
29917
|
+
if (!("trace" in response))
|
|
29918
|
+
return response;
|
|
29919
|
+
return { ...response, trace: [...response.trace, { at: Math.round(performance.now() - overallStart), lane: response.lane, event: "scrape_record_unwritten", detail: { scrapeId } }] };
|
|
29920
|
+
}
|
|
29921
|
+
};
|
|
28356
29922
|
const operation = (async () => {
|
|
29923
|
+
if (req.lane === "my-browser") {
|
|
29924
|
+
const read3 = await myBrowserScrape(req, context, hooks);
|
|
29925
|
+
const agentHints2 = agentHintsFor(req, { channelsTried: [read3.lane], result: read3 });
|
|
29926
|
+
const full2 = { ...read3, channelsTried: [read3.lane], ladderTrace: [], summary: summarize([read3.lane], [{ channel: read3.lane, result: read3 }]), ...agentHints2.length === 0 ? {} : { agentHints: agentHints2 } };
|
|
29927
|
+
return deliver(full2, createExecutionScope({ signal: AbortSignal.any([...context.signal === void 0 ? [] : [context.signal], shutdownController.signal]) }), true);
|
|
29928
|
+
}
|
|
29929
|
+
const scrapeCap = accessGrant?.budget.perRequestUsd ?? accessGrant?.budget.perRunUsd ?? null;
|
|
29930
|
+
const scrapeSpend = scrapeCap === null ? void 0 : createSpendLedger(scrapeCap);
|
|
28357
29931
|
const answer = consultCache(plan, req.url, policy);
|
|
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)) };
|
|
29932
|
+
const run = answer.kind === "fetch" ? await runner.run(req.url, void 0, scrapeSpend === void 0 ? scope : { ...scope, spend: scrapeSpend }, { ...fetchOptions(req, req.formats), ...robotsOverride === void 0 ? {} : { robotsOverride } }).then(async (fetched) => ({ ...fetched, result: afterFetch(plan, answer, await withEgressExit(fetched.result, scrapeEgress, scope)) })) : { result: answer.result, ...untriedAudit(Math.round(performance.now() - overallStart)) };
|
|
28359
29933
|
const handed = req.handoff !== void 0 && answer.kind === "fetch" && handoffResult(run.result) ? await handOffScrape(req, run.result, context, hooks) : null;
|
|
28360
29934
|
const read2 = handed !== null && "result" in handed ? handed.result : null;
|
|
28361
29935
|
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;
|
|
@@ -28364,29 +29938,14 @@ function createApiEngine(options = {}) {
|
|
|
28364
29938
|
const full = {
|
|
28365
29939
|
...result,
|
|
28366
29940
|
// 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 },
|
|
29941
|
+
usage: { ...result.usage, externalCostUsd: summary.externalCostUsd, ...run.result.usage.externalCostChargedUsd === void 0 ? {} : { externalCostChargedUsd: run.result.usage.externalCostChargedUsd } },
|
|
28368
29942
|
channelsTried: run.channelsTried,
|
|
28369
29943
|
ladderTrace: run.ladderTrace,
|
|
28370
29944
|
summary,
|
|
28371
29945
|
...agentHints.length === 0 ? {} : { agentHints }
|
|
28372
29946
|
};
|
|
28373
29947
|
const shapeScope = handed !== null && "result" in handed ? createExecutionScope({ signal: AbortSignal.any([...context.signal === void 0 ? [] : [context.signal], shutdownController.signal]) }) : scope;
|
|
28374
|
-
|
|
28375
|
-
if (shapeScope !== scope)
|
|
28376
|
-
shapeScope.dispose();
|
|
28377
|
-
});
|
|
28378
|
-
const response = answer.kind === "fetch" ? shaped : withoutFetchUsage(shaped);
|
|
28379
|
-
if (!record3)
|
|
28380
|
-
return response;
|
|
28381
|
-
try {
|
|
28382
|
-
await writeScrapeRecord(scrapeRecordOf(scrapeId, requestedAt, req, full, response));
|
|
28383
|
-
return response;
|
|
28384
|
-
} catch (error) {
|
|
28385
|
-
console.error(JSON.stringify({ component: "api", event: "scrape_record_unwritten", scrapeId, error: error instanceof Error ? error.message : String(error) }));
|
|
28386
|
-
if (!("trace" in response))
|
|
28387
|
-
return response;
|
|
28388
|
-
return { ...response, trace: [...response.trace, { at: Math.round(performance.now() - overallStart), lane: response.lane, event: "scrape_record_unwritten", detail: { scrapeId } }] };
|
|
28389
|
-
}
|
|
29948
|
+
return deliver(full, shapeScope, answer.kind === "fetch");
|
|
28390
29949
|
})();
|
|
28391
29950
|
activeScrapes.add(operation);
|
|
28392
29951
|
try {
|
|
@@ -28514,6 +30073,7 @@ function createApiEngine(options = {}) {
|
|
|
28514
30073
|
checkAttributeSelectors(req.formats);
|
|
28515
30074
|
checkListSelectors(req.formats);
|
|
28516
30075
|
checkHostedOptions(req);
|
|
30076
|
+
checkAccessChoice(req);
|
|
28517
30077
|
const webhookConfig = jobWebhooks.check(req.webhook);
|
|
28518
30078
|
if (defaultMaxPages !== null && req.maxPages != null && req.maxPages > defaultMaxPages) {
|
|
28519
30079
|
throw new RequestError(`maxPages must be at most ${defaultMaxPages} on this server`);
|
|
@@ -28546,6 +30106,7 @@ function createApiEngine(options = {}) {
|
|
|
28546
30106
|
maxTokens: null
|
|
28547
30107
|
},
|
|
28548
30108
|
crawl: {
|
|
30109
|
+
...req.access === void 0 ? {} : { access: req.access },
|
|
28549
30110
|
formats: req.formats ?? ["markdown"],
|
|
28550
30111
|
includeLinks: req.includeLinks === true,
|
|
28551
30112
|
includePaths: req.includePaths ?? [],
|
|
@@ -28607,6 +30168,8 @@ function createApiEngine(options = {}) {
|
|
|
28607
30168
|
checkListSelectors(req.formats);
|
|
28608
30169
|
checkRobotsOverride("robotsOverrides", req.robotsOverrides);
|
|
28609
30170
|
checkHostedOptions(req);
|
|
30171
|
+
checkAccessChoice(req);
|
|
30172
|
+
checkMyBrowser(req);
|
|
28610
30173
|
const webhookConfig = jobWebhooks.check(req.webhook);
|
|
28611
30174
|
batchStartInProgress = true;
|
|
28612
30175
|
try {
|
|
@@ -28634,7 +30197,9 @@ function createApiEngine(options = {}) {
|
|
|
28634
30197
|
...req.robotsOverrides === void 0 ? {} : { robotsOverrides: req.robotsOverrides },
|
|
28635
30198
|
...req.maxConcurrency === void 0 ? {} : { maxConcurrency: req.maxConcurrency },
|
|
28636
30199
|
...invalidURLs === void 0 ? {} : { invalidURLs },
|
|
28637
|
-
...webhookConfig === void 0 ? {} : { webhook: jobWebhooks.register(taskId, webhookConfig, req.webhookPayloadFormat) }
|
|
30200
|
+
...webhookConfig === void 0 ? {} : { webhook: jobWebhooks.register(taskId, webhookConfig, req.webhookPayloadFormat) },
|
|
30201
|
+
...req.lane === void 0 ? {} : { lane: req.lane },
|
|
30202
|
+
...req.access === void 0 ? {} : { access: req.access }
|
|
28638
30203
|
};
|
|
28639
30204
|
const task = {
|
|
28640
30205
|
id: taskId,
|
|
@@ -29126,11 +30691,11 @@ function attributionOf(req) {
|
|
|
29126
30691
|
return Object.keys(attribution).length === 0 ? {} : { attribution };
|
|
29127
30692
|
}
|
|
29128
30693
|
function scrapeRecordOf(scrapeId, requestedAt, req, run, response) {
|
|
29129
|
-
const { origin: _origin, integration: _integration, ...
|
|
30694
|
+
const { origin: _origin, integration: _integration, ...request8 } = req;
|
|
29130
30695
|
return {
|
|
29131
30696
|
scrapeId,
|
|
29132
30697
|
requestedAt,
|
|
29133
|
-
request: { ...
|
|
30698
|
+
request: { ...request8, ...req.headers === void 0 ? {} : { headers: Object.fromEntries(Object.keys(req.headers).map((name) => [name, name])) } },
|
|
29134
30699
|
...attributionOf(req).attribution,
|
|
29135
30700
|
status: response.status,
|
|
29136
30701
|
failureReason: response.failureReason,
|
|
@@ -29167,7 +30732,7 @@ function crawlOptionsStored(task) {
|
|
|
29167
30732
|
}
|
|
29168
30733
|
var BATCH_SCOPE = { regexOnFullURL: false, ignoreQueryParameters: false, deduplicateSimilarURLs: false, crawlEntireDomain: true, allowSubdomains: false, allowExternalLinks: false };
|
|
29169
30734
|
function batchRunOptions(batch, resume) {
|
|
29170
|
-
return { maxDepth: 0, allowlistedDomains: [], useCached: false, resume, scope: BATCH_SCOPE, sitemap: "skip", maxConcurrency: batch.maxConcurrency ?? null, policyAllowlist: [] };
|
|
30735
|
+
return { maxDepth: 0, allowlistedDomains: [], useCached: false, resume, scope: BATCH_SCOPE, sitemap: "skip", maxConcurrency: batch.lane === "my-browser" ? 1 : batch.maxConcurrency ?? null, policyAllowlist: [] };
|
|
29171
30736
|
}
|
|
29172
30737
|
async function appendedWithoutStep(taskId, store) {
|
|
29173
30738
|
const task = await store.getTask(taskId);
|
|
@@ -29217,11 +30782,23 @@ function crawlPolicyAllowlist(seedUrl, allowlistedDomains, scope) {
|
|
|
29217
30782
|
return [];
|
|
29218
30783
|
const host = new URL(seedUrl).hostname.toLowerCase();
|
|
29219
30784
|
const apex = host.startsWith("www.") ? host.slice(4) : host;
|
|
29220
|
-
const twin = host.startsWith("www.") ? apex : host.includes(".") &&
|
|
30785
|
+
const twin = host.startsWith("www.") ? apex : host.includes(".") && isIP5(host) === 0 ? `www.${host}` : null;
|
|
29221
30786
|
return [.../* @__PURE__ */ new Set([host, ...twin === null ? [] : [twin], ...allowlistedDomains, ...scope.allowSubdomains ? [`*.${apex}`] : []])];
|
|
29222
30787
|
}
|
|
29223
30788
|
function handoffUnread(task) {
|
|
29224
|
-
|
|
30789
|
+
if (task.batch === void 0)
|
|
30790
|
+
return null;
|
|
30791
|
+
const options = fetchOptions(task.batch, task.batch.formats);
|
|
30792
|
+
return listsPages2(task) ? unreadByPerson({ ...options, actions: void 0 }) : unreadByPerson(options);
|
|
30793
|
+
}
|
|
30794
|
+
function listsPages2(task) {
|
|
30795
|
+
const actions = task.batch?.actions ?? [];
|
|
30796
|
+
return actions.length === 1 && actions[0].type === "paginate" && actions[0].itemSelector !== void 0;
|
|
30797
|
+
}
|
|
30798
|
+
function handoffNeededIn(task, step) {
|
|
30799
|
+
if (!handoffNeeded(step))
|
|
30800
|
+
return false;
|
|
30801
|
+
return !listsPages2(task) || (step.result?.actions?.lists ?? []).some((run) => run.type === "paginate" && run.stoppedBy === "challenge" && run.challenge !== void 0);
|
|
29225
30802
|
}
|
|
29226
30803
|
function offersHandoff(task) {
|
|
29227
30804
|
return handoffUnread(task) === null && webhookOf(task) === void 0;
|
|
@@ -29292,7 +30869,7 @@ function compactPage(step, task) {
|
|
|
29292
30869
|
}
|
|
29293
30870
|
function toCrawlPage(step, includeLinks, task, handoffOffered = false) {
|
|
29294
30871
|
const result = step.result;
|
|
29295
|
-
const handoff = handoffOffered &&
|
|
30872
|
+
const handoff = handoffOffered && handoffNeededIn(task, step) ? handoffRequestOf(step) : null;
|
|
29296
30873
|
const mode = task.mode;
|
|
29297
30874
|
const agentHints = result === null ? [] : agentHintsFor({ fastMode: (task.batch ?? task.crawl)?.fastMode }, { channelsTried: step.audit?.channelsTried ?? [result.lane], result, ...step.audit === void 0 ? {} : { summary: step.audit.summary, ladderTrace: step.audit.ladderTrace } });
|
|
29298
30875
|
return {
|
|
@@ -30053,6 +31630,8 @@ function createApp(engine, options = {}) {
|
|
|
30053
31630
|
return fail(c, err.code, err.message, err.details, err.agentHints);
|
|
30054
31631
|
if (err instanceof CrawlStateError)
|
|
30055
31632
|
return fail(c, "conflict", err.message);
|
|
31633
|
+
if (err instanceof HandoffUnavailableError)
|
|
31634
|
+
return fail(c, "conflict", err.message);
|
|
30056
31635
|
if (err instanceof TaskNotFoundError)
|
|
30057
31636
|
return fail(c, "not_found", "not found");
|
|
30058
31637
|
if (err instanceof InvalidCursorError)
|
|
@@ -30082,6 +31661,22 @@ function readEgressProxies(env, grant, hosted) {
|
|
|
30082
31661
|
throw new Error("W2L_EGRESS_PROXIES needs an access grant that names egress_sessions (ADR 0005; --access-grant or W2L_ACCESS_GRANT)");
|
|
30083
31662
|
return proxies;
|
|
30084
31663
|
}
|
|
31664
|
+
function readEgressEchoUrl(env, egresses) {
|
|
31665
|
+
const raw = env.W2L_EGRESS_ECHO_URL?.trim() ?? "";
|
|
31666
|
+
if (raw === "")
|
|
31667
|
+
return null;
|
|
31668
|
+
if (egresses.length === 0)
|
|
31669
|
+
throw new Error("W2L_EGRESS_ECHO_URL asks each egress proxy where it leaves from: it needs W2L_EGRESS_PROXIES");
|
|
31670
|
+
let url;
|
|
31671
|
+
try {
|
|
31672
|
+
url = new URL(raw);
|
|
31673
|
+
} catch {
|
|
31674
|
+
throw new Error("W2L_EGRESS_ECHO_URL must be an http(s) URL");
|
|
31675
|
+
}
|
|
31676
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
31677
|
+
throw new Error("W2L_EGRESS_ECHO_URL must be an http(s) URL");
|
|
31678
|
+
return url.href;
|
|
31679
|
+
}
|
|
30085
31680
|
function readAccessGrant(argv, env, hosted) {
|
|
30086
31681
|
const source = (readFlag(argv, "--access-grant") ?? env["W2L_ACCESS_GRANT"] ?? "").trim();
|
|
30087
31682
|
if (source === "")
|
|
@@ -30100,7 +31695,7 @@ function readAccessGrant(argv, env, hosted) {
|
|
|
30100
31695
|
}
|
|
30101
31696
|
function accessGrantNotice(grant) {
|
|
30102
31697
|
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)}
|
|
31698
|
+
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)}${grant.attestation === null ? "" : `; accepted by ${grant.attestation.principal}`}`;
|
|
30104
31699
|
}
|
|
30105
31700
|
function jobStreamsEnabled(env) {
|
|
30106
31701
|
return (env["W2L_JOB_STREAMS"] ?? "").trim().toLowerCase() !== "off";
|
|
@@ -30117,8 +31712,9 @@ function parseListen(argv, env = process.env) {
|
|
|
30117
31712
|
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
31713
|
const compatHosts = compatHostsChoice(env, accessGrant, hosted);
|
|
30119
31714
|
const egressList = readEgressProxies(env, accessGrant, hosted);
|
|
30120
|
-
const
|
|
30121
|
-
const
|
|
31715
|
+
const egressEchoUrl = readEgressEchoUrl(env, egressList);
|
|
31716
|
+
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${egressEchoUrl === null ? "" : `; each is asked where it leaves from at ${new URL(egressEchoUrl).host}`}`];
|
|
31717
|
+
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${(env.W2L_COMPAT_HOSTS ?? "").trim() === "" ? " (the hosts its acceptance showed it helps, research/access/benefit-hosts.v1.json; W2L_COMPAT_HOSTS names others, none turns it off)" : ""}; a request with custom headers or mobile keeps the http rung`];
|
|
30122
31718
|
if (hosted) {
|
|
30123
31719
|
if (tokens.length === 0) {
|
|
30124
31720
|
throw new Error("hosted mode requires --token, W2L_API_TOKEN or W2L_API_TOKENS");
|
|
@@ -30142,7 +31738,8 @@ function parseListen(argv, env = process.env) {
|
|
|
30142
31738
|
accessGrant,
|
|
30143
31739
|
browserEngine,
|
|
30144
31740
|
compatHosts,
|
|
30145
|
-
egressProxies: egressList
|
|
31741
|
+
egressProxies: egressList,
|
|
31742
|
+
egressEchoUrl
|
|
30146
31743
|
};
|
|
30147
31744
|
}
|
|
30148
31745
|
const networkPolicy = withOperatorContact(withEnvironmentProxy(tunedPolicy(localNetworkPolicy(), env), env), env);
|
|
@@ -30168,7 +31765,8 @@ function parseListen(argv, env = process.env) {
|
|
|
30168
31765
|
accessGrant,
|
|
30169
31766
|
browserEngine,
|
|
30170
31767
|
compatHosts,
|
|
30171
|
-
egressProxies: egressList
|
|
31768
|
+
egressProxies: egressList,
|
|
31769
|
+
egressEchoUrl
|
|
30172
31770
|
};
|
|
30173
31771
|
}
|
|
30174
31772
|
function deliveryConfig(mode, env = process.env) {
|
|
@@ -30285,7 +31883,8 @@ async function runApiServer(argv, env = process.env, options = {}) {
|
|
|
30285
31883
|
accessGrant: listen.accessGrant,
|
|
30286
31884
|
browserEngine: listen.browserEngine,
|
|
30287
31885
|
compatHosts: listen.compatHosts,
|
|
30288
|
-
egressProxies: listen.egressProxies
|
|
31886
|
+
egressProxies: listen.egressProxies,
|
|
31887
|
+
egressEchoUrl: listen.egressEchoUrl
|
|
30289
31888
|
});
|
|
30290
31889
|
const app = createApp(engine, { loopbackOnly: servesLogins, tokens: listen.tokens, exposeInternalErrors: listen.mode === "local", jobStreams: listen.jobStreams, ...listen.rateLimit === void 0 ? {} : { rateLimit: listen.rateLimit } });
|
|
30291
31890
|
const deliveryStore = DeliveryStore.open(join11(taskRoot, "section-b-control.sqlite"));
|
|
@@ -30382,6 +31981,8 @@ var KINDS = {
|
|
|
30382
31981
|
maxConcurrency: "int",
|
|
30383
31982
|
limit: "int",
|
|
30384
31983
|
mode: "string",
|
|
31984
|
+
lane: "string",
|
|
31985
|
+
access: "string",
|
|
30385
31986
|
sitemap: "string",
|
|
30386
31987
|
idempotencyKey: "string",
|
|
30387
31988
|
appendToId: "string",
|
|
@@ -30556,8 +32157,8 @@ function usage(command) {
|
|
|
30556
32157
|
}
|
|
30557
32158
|
const synopsis = {
|
|
30558
32159
|
crawl: "usage: octocrawl crawl <url> [options] [--out <dir>] | octocrawl crawl --resume <taskId>",
|
|
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.",
|
|
32160
|
+
scrape: "usage: octocrawl scrape <url> [options] [--markdown] [--out <dir>] [--handoff] [--lane my-browser]\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.\n--lane my-browser: read the page in your own Chrome instead of fetching it: click Allow in Chrome, then\nAllow reading these sites in the page Octocrawl opens there (close it or click Revoke to stop).",
|
|
32161
|
+
batch: "usage: octocrawl batch <url>... [--urls-file <file>] [options] [--out <dir>] [--handoff] [--lane my-browser]\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. A batch with one paginate step (with itemSelector) is handed over too: the page its list\nstopped at opens, you get through the check and page on by clicking Next, and Octocrawl reads each page as it shows.\n--lane my-browser: read every page in your own Chrome, one at a time: click Allow in Chrome, then Allow reading\nthese sites once in the page Octocrawl opens there (close it or click Revoke to stop).",
|
|
30561
32162
|
map: "usage: octocrawl map <url> [options]"
|
|
30562
32163
|
};
|
|
30563
32164
|
const lines = optionKeys(command).map((key) => {
|
|
@@ -30674,7 +32275,7 @@ ${LOGIN_USAGE}`);
|
|
|
30674
32275
|
}
|
|
30675
32276
|
|
|
30676
32277
|
// packages/cli/src/run.ts
|
|
30677
|
-
var CLI_VERSION = "0.3.
|
|
32278
|
+
var CLI_VERSION = "0.3.2";
|
|
30678
32279
|
async function runCli(argv, io) {
|
|
30679
32280
|
const [name, ...rest] = argv;
|
|
30680
32281
|
if (name === void 0 || name === "--help" || name === "help") {
|
|
@@ -30724,7 +32325,8 @@ ${usage(null)}`);
|
|
|
30724
32325
|
accessGrant: listen.accessGrant,
|
|
30725
32326
|
browserEngine: listen.browserEngine,
|
|
30726
32327
|
compatHosts: listen.compatHosts,
|
|
30727
|
-
egressProxies: listen.egressProxies
|
|
32328
|
+
egressProxies: listen.egressProxies,
|
|
32329
|
+
egressEchoUrl: listen.egressEchoUrl
|
|
30728
32330
|
});
|
|
30729
32331
|
try {
|
|
30730
32332
|
return await runCommand(engine, command, line.urls, line.body, line.cli, io);
|
|
@@ -30790,6 +32392,7 @@ async function runCommand(engine, command, urls, body, cli, io) {
|
|
|
30790
32392
|
if (urls.length === 0) throw new UsageError("octocrawl batch takes URLs as arguments or --urls-file");
|
|
30791
32393
|
const accepted = await engine.startBatch(parseBatchStartRequest({ ...body, urls, origin: ORIGIN }));
|
|
30792
32394
|
io.stderr(`octocrawl batch: task ${accepted.taskId}${accepted.invalidURLs?.length ? `, ${accepted.invalidURLs.length} invalid URLs skipped` : ""}`);
|
|
32395
|
+
if (body.lane === "my-browser" || body.access === "my-browser") io.stderr("octocrawl batch: in your Chrome, click Allow, then Allow reading these sites in the page Octocrawl opened (it lists every site of the batch); close that page or click Revoke to stop");
|
|
30793
32396
|
return finish(engine, "batch", accepted.taskId, cli, io);
|
|
30794
32397
|
}
|
|
30795
32398
|
}
|
|
@@ -30822,6 +32425,8 @@ function handoffPrompts(command, io) {
|
|
|
30822
32425
|
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)`),
|
|
30823
32426
|
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
32427
|
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)`),
|
|
32428
|
+
onContinue: (url, pages) => io.stderr(`octocrawl ${command}: through: page on in that tab by clicking Next, Octocrawl reads each page as it shows (${pages} read so far); it stops when Next is gone, or after 60 s without a new page`),
|
|
32429
|
+
onAllow: (hosts) => io.stderr(`octocrawl ${command}: in your Chrome, click Allow, then Allow reading these sites in the page Octocrawl opened (${hosts.join(", ")}); close that page or click Revoke to stop. While remote debugging is on, sites see navigator.webdriver true: turn it off at chrome://inspect/#remote-debugging when you are done`),
|
|
30825
32430
|
...io.signal === void 0 ? {} : { signal: io.signal }
|
|
30826
32431
|
};
|
|
30827
32432
|
}
|