smooth-operator-mcp 3.0.4 → 3.0.6
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/.env.example +3 -0
- package/README.md +16 -0
- package/dist/smooth-operator.mjs +1399 -110
- package/dist/smooth-operator.mjs.map +3 -3
- package/docs/harnesses.md +7 -0
- package/docs/mcp-server.md +56 -5
- package/package.json +8 -7
package/dist/smooth-operator.mjs
CHANGED
|
@@ -354,7 +354,7 @@ function truncateUtf8(value, maxBytes) {
|
|
|
354
354
|
while (low < high) {
|
|
355
355
|
const midpoint = Math.ceil((low + high) / 2);
|
|
356
356
|
const candidate = decoder.decode(bytes.slice(0, midpoint));
|
|
357
|
-
if (UTF8_ENCODER.encode(candidate).byteLength <=
|
|
357
|
+
if (UTF8_ENCODER.encode(candidate).byteLength <= boundedMaxBytes) {
|
|
358
358
|
low = midpoint;
|
|
359
359
|
} else {
|
|
360
360
|
high = midpoint - 1;
|
|
@@ -397,7 +397,7 @@ var SERVER_VERSION;
|
|
|
397
397
|
var init_version = __esm({
|
|
398
398
|
"src/server/version.ts"() {
|
|
399
399
|
"use strict";
|
|
400
|
-
SERVER_VERSION = "3.0.
|
|
400
|
+
SERVER_VERSION = "3.0.6";
|
|
401
401
|
}
|
|
402
402
|
});
|
|
403
403
|
|
|
@@ -406,17 +406,39 @@ var discovery_exports = {};
|
|
|
406
406
|
__export(discovery_exports, {
|
|
407
407
|
chromeExecutableSearchPaths: () => chromeExecutableSearchPaths,
|
|
408
408
|
findChromeExecutable: () => findChromeExecutable,
|
|
409
|
-
findChromiumExecutables: () => findChromiumExecutables
|
|
409
|
+
findChromiumExecutables: () => findChromiumExecutables,
|
|
410
|
+
isExecutableReady: () => isExecutableReady
|
|
410
411
|
});
|
|
411
412
|
import * as nodeFs from "node:fs";
|
|
412
413
|
import { homedir as homedir2 } from "node:os";
|
|
413
414
|
import { delimiter, join as join3, win32 } from "node:path";
|
|
414
415
|
import { env as env2 } from "node:process";
|
|
415
416
|
function findChromeExecutable(fs = nodeFs) {
|
|
416
|
-
return dedupeCandidates(chromeExecutableCandidates()).find((candidate) =>
|
|
417
|
+
return dedupeCandidates(chromeExecutableCandidates()).find((candidate) => isExecutableReady(candidate.path, fs)) ?? null;
|
|
417
418
|
}
|
|
418
419
|
function findChromiumExecutables(fs = nodeFs) {
|
|
419
|
-
return dedupeCandidates(chromeExecutableCandidates()).filter((candidate) =>
|
|
420
|
+
return dedupeCandidates(chromeExecutableCandidates()).filter((candidate) => isExecutableReady(candidate.path, fs));
|
|
421
|
+
}
|
|
422
|
+
function isExecutableReady(path, fs = nodeFs, platformName = process.platform) {
|
|
423
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const stats = fs.statSync(path);
|
|
428
|
+
if (!stats.isFile()) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
if (platformName === "win32") {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
if ((stats.mode & 73) === 0) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
fs.accessSync(path, nodeFs.constants.X_OK);
|
|
438
|
+
return true;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
420
442
|
}
|
|
421
443
|
function chromeExecutableSearchPaths() {
|
|
422
444
|
return dedupeCandidates(chromeExecutableCandidates()).map((candidate) => candidate.path);
|
|
@@ -510,7 +532,7 @@ var init_discovery = __esm({
|
|
|
510
532
|
});
|
|
511
533
|
|
|
512
534
|
// src/server/installer.ts
|
|
513
|
-
import { constants as
|
|
535
|
+
import { constants as constants3, accessSync, existsSync } from "node:fs";
|
|
514
536
|
import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, open as open3, rename as rename3, unlink as unlink3, writeFile } from "node:fs/promises";
|
|
515
537
|
import { execFile } from "node:child_process";
|
|
516
538
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -596,7 +618,7 @@ function resolveStableNodeExecutable() {
|
|
|
596
618
|
const candidates = platform2() === "darwin" ? ["/opt/homebrew/bin/node", "/usr/local/bin/node"] : ["/usr/local/bin/node", "/usr/bin/node"];
|
|
597
619
|
for (const candidate of candidates) {
|
|
598
620
|
try {
|
|
599
|
-
accessSync(candidate,
|
|
621
|
+
accessSync(candidate, constants3.X_OK);
|
|
600
622
|
return candidate;
|
|
601
623
|
} catch {
|
|
602
624
|
}
|
|
@@ -731,20 +753,20 @@ async function installJsonConfig(target, plannedPath, options, allowOpenCodeJson
|
|
|
731
753
|
}
|
|
732
754
|
}
|
|
733
755
|
async function readSecureConfigFile(path) {
|
|
734
|
-
const noFollow = typeof
|
|
756
|
+
const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
|
|
735
757
|
if (!noFollow) {
|
|
736
758
|
await rejectSymlink2(path, "configuration file");
|
|
737
759
|
}
|
|
738
760
|
let handle;
|
|
739
761
|
try {
|
|
740
|
-
handle = await open3(path,
|
|
762
|
+
handle = await open3(path, constants3.O_RDONLY | noFollow);
|
|
741
763
|
} catch (error) {
|
|
742
764
|
if (isMissingFile2(error)) {
|
|
743
765
|
return void 0;
|
|
744
766
|
}
|
|
745
767
|
if (noFollow && (isErrorCode2(error, "EINVAL") || isErrorCode2(error, "ENOTSUP") || isErrorCode2(error, "EOPNOTSUPP"))) {
|
|
746
768
|
await rejectSymlink2(path, "configuration file");
|
|
747
|
-
handle = await open3(path,
|
|
769
|
+
handle = await open3(path, constants3.O_RDONLY);
|
|
748
770
|
} else {
|
|
749
771
|
if (isErrorCode2(error, "ELOOP") || isErrorCode2(error, "EFTYPE")) {
|
|
750
772
|
throw new AppError("INSTALL_CONFIG_FAILED", `The configuration file '${path}' must not be a symbolic link.`);
|
|
@@ -1238,7 +1260,7 @@ __export(installer_wizard_exports, {
|
|
|
1238
1260
|
runWizard: () => runWizard
|
|
1239
1261
|
});
|
|
1240
1262
|
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join7, parse as parse4, resolve as resolve6, win32 as win323 } from "node:path";
|
|
1241
|
-
import { accessSync as accessSync2, constants as
|
|
1263
|
+
import { accessSync as accessSync2, constants as constants4, statSync } from "node:fs";
|
|
1242
1264
|
import { chmod as chmod3, lstat as lstat4, rename as rename4, unlink as unlink4, writeFile as writeFile2 } from "node:fs/promises";
|
|
1243
1265
|
import { homedir as homedir4 } from "node:os";
|
|
1244
1266
|
import { isIP as isIP3 } from "node:net";
|
|
@@ -1261,7 +1283,7 @@ function isExecutableFile(path) {
|
|
|
1261
1283
|
return false;
|
|
1262
1284
|
}
|
|
1263
1285
|
if (process.platform !== "win32") {
|
|
1264
|
-
accessSync2(path,
|
|
1286
|
+
accessSync2(path, constants4.X_OK);
|
|
1265
1287
|
}
|
|
1266
1288
|
return true;
|
|
1267
1289
|
} catch {
|
|
@@ -2294,6 +2316,7 @@ var SecurityPolicy = class _SecurityPolicy {
|
|
|
2294
2316
|
// src/server/config.ts
|
|
2295
2317
|
var TransportSchema = z.enum(["stdio", "http"]);
|
|
2296
2318
|
var BrowserModeSchema = z.enum(["disabled", "connect", "launch", "managed"]);
|
|
2319
|
+
var BrowserIdleTimeoutSchema = z.number().int().min(0).max(864e5);
|
|
2297
2320
|
var ConfigPathSchema = z.string().trim().min(1).max(4096);
|
|
2298
2321
|
var DomainPatternSchema = z.string().trim().min(1).max(253).refine(isValidDomainPattern2, "Domain patterns must be exact hostnames or *.-prefixed suffixes.");
|
|
2299
2322
|
var HostPatternSchema = z.string().trim().min(1).max(255).refine(isValidHostPattern, "Host allowlists must contain hostnames or bracketed IPv6 addresses without ports.");
|
|
@@ -2326,7 +2349,8 @@ var RawConfigSchema = z.object({
|
|
|
2326
2349
|
connectTimeoutMs: z.number().int().min(1e3).max(18e4).optional(),
|
|
2327
2350
|
cdpTimeoutMs: z.number().int().min(100).max(12e4).optional(),
|
|
2328
2351
|
maxScreenshotBytes: z.number().int().min(1e5).max(2e7).optional(),
|
|
2329
|
-
maxHtmlChars: z.number().int().min(1e3).max(5e5).optional()
|
|
2352
|
+
maxHtmlChars: z.number().int().min(1e3).max(5e5).optional(),
|
|
2353
|
+
idleTimeoutMs: BrowserIdleTimeoutSchema.optional()
|
|
2330
2354
|
}).strict().optional(),
|
|
2331
2355
|
security: z.object({
|
|
2332
2356
|
allowedDomains: ConfigList(DomainPatternSchema).optional(),
|
|
@@ -2647,6 +2671,9 @@ function validateConfig(config) {
|
|
|
2647
2671
|
if (config.browser.maxHtmlChars < 1e3 || config.browser.maxHtmlChars > 5e5) {
|
|
2648
2672
|
throw new AppError("CONFIG_INVALID", "Maximum HTML characters must be between 1000 and 500000.");
|
|
2649
2673
|
}
|
|
2674
|
+
if (!Number.isSafeInteger(config.browser.idleTimeoutMs) || config.browser.idleTimeoutMs < 0 || config.browser.idleTimeoutMs > 864e5) {
|
|
2675
|
+
throw new AppError("CONFIG_INVALID", "Browser idle timeout must be between 0ms and 86400000ms.");
|
|
2676
|
+
}
|
|
2650
2677
|
validateBrowserEndpoint(config.browser.url, ["http:", "https:"], "Browser DevTools URL");
|
|
2651
2678
|
validateBrowserEndpoint(config.browser.wsEndpoint, ["ws:", "wss:"], "Browser WebSocket endpoint");
|
|
2652
2679
|
if (config.stealth && config.stealth.profile !== "balanced" && config.stealth.profile !== "max") {
|
|
@@ -2729,7 +2756,8 @@ function loadServerConfig(args = [], environment = env, homeDirectory = homedir(
|
|
|
2729
2756
|
connectTimeoutMs: parseInteger(environment.SMOOTH_OPERATOR_BROWSER_CONNECT_TIMEOUT_MS, nestedBrowser.connectTimeoutMs ?? 3e4),
|
|
2730
2757
|
cdpTimeoutMs: parseInteger(environment.SMOOTH_OPERATOR_BROWSER_CDP_TIMEOUT_MS, nestedBrowser.cdpTimeoutMs ?? 3e4),
|
|
2731
2758
|
maxScreenshotBytes: parseInteger(environment.SMOOTH_OPERATOR_MAX_SCREENSHOT_BYTES, nestedBrowser.maxScreenshotBytes ?? 8e6),
|
|
2732
|
-
maxHtmlChars: parseInteger(environment.SMOOTH_OPERATOR_MAX_HTML_CHARS, nestedBrowser.maxHtmlChars ?? 2e5)
|
|
2759
|
+
maxHtmlChars: parseInteger(environment.SMOOTH_OPERATOR_MAX_HTML_CHARS, nestedBrowser.maxHtmlChars ?? 2e5),
|
|
2760
|
+
idleTimeoutMs: parseInteger(environment.SMOOTH_OPERATOR_BROWSER_IDLE_TIMEOUT_MS, nestedBrowser.idleTimeoutMs ?? 0)
|
|
2733
2761
|
},
|
|
2734
2762
|
security: {
|
|
2735
2763
|
allowedDomains: normalizeDomainList(parseList(environment.SMOOTH_OPERATOR_ALLOWED_DOMAINS, nestedSecurity.allowedDomains ?? [])),
|
|
@@ -2795,6 +2823,9 @@ var RESEARCH_QUERY_MAX_CHARS = 4e3;
|
|
|
2795
2823
|
var RESEARCH_MIN_CHARS = 500;
|
|
2796
2824
|
var RESEARCH_MAX_CHARS = 4e3;
|
|
2797
2825
|
var RESEARCH_MAX_RESULTS = 10;
|
|
2826
|
+
var RESOURCE_BLOCKING_TYPES = ["image", "stylesheet", "font", "media", "script"];
|
|
2827
|
+
var ResourceBlockingTypeSchema = z2.enum(RESOURCE_BLOCKING_TYPES);
|
|
2828
|
+
var ResourceBlockingOperationSchema = z2.enum(["get", "set", "clear"]);
|
|
2798
2829
|
var isHttpUrl = (value) => {
|
|
2799
2830
|
try {
|
|
2800
2831
|
const url = new URL(value);
|
|
@@ -2831,6 +2862,8 @@ var BrowserActionNames = [
|
|
|
2831
2862
|
"enable_network_log",
|
|
2832
2863
|
"disable_network_log",
|
|
2833
2864
|
"get_network_log",
|
|
2865
|
+
"search_network_log",
|
|
2866
|
+
"resource_blocking",
|
|
2834
2867
|
"clear_network_log",
|
|
2835
2868
|
"getclear_network_log",
|
|
2836
2869
|
// canonical action spelling of the read_and_clear operation
|
|
@@ -2851,6 +2884,7 @@ var BrowserActionNames = [
|
|
|
2851
2884
|
"page_next",
|
|
2852
2885
|
"search_page",
|
|
2853
2886
|
"find_elements",
|
|
2887
|
+
"inspect_element",
|
|
2854
2888
|
"list_interactive",
|
|
2855
2889
|
"list_frames",
|
|
2856
2890
|
"accessibility_snapshot",
|
|
@@ -2920,15 +2954,25 @@ var BrowserActionFieldsSchema = z2.object({
|
|
|
2920
2954
|
state: z2.enum(["visible", "hidden", "attached", "detached"]).optional(),
|
|
2921
2955
|
waitUntil: z2.enum(["load", "domcontentloaded", "networkidle0", "networkidle2"]).optional(),
|
|
2922
2956
|
filePath: BoundedString(4e3).optional(),
|
|
2957
|
+
filePaths: z2.array(BoundedString(4e3)).min(1).max(20).optional(),
|
|
2923
2958
|
outputPath: BoundedString(4e3).optional(),
|
|
2924
2959
|
code: z2.string().trim().min(1).max(4e4).optional(),
|
|
2925
2960
|
script: z2.string().trim().min(1).max(4e4).optional(),
|
|
2926
2961
|
expression: z2.string().trim().min(1).max(4e4).optional(),
|
|
2927
2962
|
query: BoundedString(4e3).optional(),
|
|
2963
|
+
requestId: BoundedString(256).optional(),
|
|
2964
|
+
method: BoundedString(32).optional(),
|
|
2965
|
+
status: z2.number().int().min(0).max(999).optional(),
|
|
2966
|
+
resourceType: BoundedString(64).optional(),
|
|
2967
|
+
operation: ResourceBlockingOperationSchema.optional(),
|
|
2968
|
+
resourceTypes: z2.array(ResourceBlockingTypeSchema).min(1).max(RESOURCE_BLOCKING_TYPES.length).optional(),
|
|
2969
|
+
limit: z2.number().int().min(1).max(200).optional(),
|
|
2928
2970
|
includeLinks: z2.boolean().optional(),
|
|
2929
2971
|
includeSnapshot: z2.boolean().optional(),
|
|
2930
2972
|
maxChars: z2.number().int().min(100).max(MCP_PAGE_TEXT_MAX_CHARS).optional(),
|
|
2931
2973
|
maxNodes: z2.number().int().min(1).max(2e3).optional(),
|
|
2974
|
+
maxDepth: z2.number().int().min(0).max(3).optional(),
|
|
2975
|
+
maxChildren: z2.number().int().min(1).max(100).optional(),
|
|
2932
2976
|
interestingOnly: z2.boolean().optional(),
|
|
2933
2977
|
maxBytes: z2.number().int().min(1e5).max(2e7).optional(),
|
|
2934
2978
|
format: z2.enum(["png", "jpeg"]).optional(),
|
|
@@ -2958,6 +3002,7 @@ var BrowserActionFieldsSchema = z2.object({
|
|
|
2958
3002
|
cookiePath: BoundedString(2e3).optional(),
|
|
2959
3003
|
cookieSecure: z2.boolean().optional(),
|
|
2960
3004
|
cookieHttpOnly: z2.boolean().optional(),
|
|
3005
|
+
cookieSameSite: z2.enum(["Strict", "Lax", "None"]).optional(),
|
|
2961
3006
|
storageArea: z2.enum(["local", "session"]).optional(),
|
|
2962
3007
|
storageKey: BoundedString(1e3).optional(),
|
|
2963
3008
|
storageValue: z2.string().max(2e4).optional(),
|
|
@@ -3050,13 +3095,54 @@ var BrowserActionSchema = BrowserActionFieldsSchema.extend({ action: ActionNameS
|
|
|
3050
3095
|
if (input.cookieValue !== void 0 && input.value !== void 0 && input.action === "set_cookie") {
|
|
3051
3096
|
context.addIssue({ code: "custom", message: "Provide cookieValue or value, not both." });
|
|
3052
3097
|
}
|
|
3098
|
+
if (input.cookieSameSite !== void 0 && input.action !== "set_cookie") {
|
|
3099
|
+
context.addIssue({ code: "custom", message: "cookieSameSite is only valid for set_cookie." });
|
|
3100
|
+
}
|
|
3053
3101
|
if (input.storageValue !== void 0 && input.value !== void 0 && input.action === "set_storage") {
|
|
3054
3102
|
context.addIssue({ code: "custom", message: "Provide storageValue or value, not both." });
|
|
3055
3103
|
}
|
|
3056
3104
|
if (input.outputPath !== void 0 && input.filePath !== void 0 && input.action === "save_as_pdf") {
|
|
3057
3105
|
context.addIssue({ code: "custom", message: "Provide outputPath or filePath, not both." });
|
|
3058
3106
|
}
|
|
3059
|
-
if (
|
|
3107
|
+
if (input.action === "upload_file") {
|
|
3108
|
+
const hasFilePath = input.filePath !== void 0;
|
|
3109
|
+
const hasFilePaths = input.filePaths !== void 0;
|
|
3110
|
+
if (hasFilePath && hasFilePaths) {
|
|
3111
|
+
context.addIssue({ code: "custom", message: "Provide filePath or filePaths, not both." });
|
|
3112
|
+
}
|
|
3113
|
+
if (!hasFilePath && !hasFilePaths) {
|
|
3114
|
+
context.addIssue({ code: "custom", message: "Upload requires filePath or filePaths." });
|
|
3115
|
+
}
|
|
3116
|
+
} else if (input.filePaths !== void 0) {
|
|
3117
|
+
context.addIssue({ code: "custom", message: "filePaths is only valid for upload_file." });
|
|
3118
|
+
}
|
|
3119
|
+
if (input.action === "resource_blocking") {
|
|
3120
|
+
if (input.operation === void 0) {
|
|
3121
|
+
context.addIssue({ code: "custom", message: "Resource blocking requires operation." });
|
|
3122
|
+
} else if (input.operation === "set") {
|
|
3123
|
+
if (input.resourceTypes === void 0) {
|
|
3124
|
+
context.addIssue({ code: "custom", message: "Resource blocking set requires resourceTypes." });
|
|
3125
|
+
} else if (new Set(input.resourceTypes).size !== input.resourceTypes.length) {
|
|
3126
|
+
context.addIssue({ code: "custom", message: "Resource blocking resourceTypes must be de-duplicated." });
|
|
3127
|
+
}
|
|
3128
|
+
} else if (input.resourceTypes !== void 0) {
|
|
3129
|
+
context.addIssue({ code: "custom", message: `Resource blocking ${input.operation} does not accept resourceTypes.` });
|
|
3130
|
+
}
|
|
3131
|
+
} else {
|
|
3132
|
+
if (input.operation !== void 0) {
|
|
3133
|
+
context.addIssue({ code: "custom", message: "operation is only valid for resource blocking." });
|
|
3134
|
+
}
|
|
3135
|
+
if (input.resourceTypes !== void 0) {
|
|
3136
|
+
context.addIssue({ code: "custom", message: "resourceTypes is only valid for resource blocking." });
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (input.action !== "inspect_element" && input.maxDepth !== void 0) {
|
|
3140
|
+
context.addIssue({ code: "custom", message: "maxDepth is only valid for inspect_element." });
|
|
3141
|
+
}
|
|
3142
|
+
if (input.action !== "inspect_element" && input.maxChildren !== void 0) {
|
|
3143
|
+
context.addIssue({ code: "custom", message: "maxChildren is only valid for inspect_element." });
|
|
3144
|
+
}
|
|
3145
|
+
if (["navigate", "get_cookies", "set_cookie", "delete_cookies"].includes(input.action) && input.url !== void 0 && !isHttpUrl(input.url)) {
|
|
3060
3146
|
context.addIssue({ code: "custom", message: "Navigation and cookie URLs must be absolute HTTP(S) URLs." });
|
|
3061
3147
|
}
|
|
3062
3148
|
if (input.action === "click") {
|
|
@@ -3113,6 +3199,7 @@ var BrowserActionSchema = BrowserActionFieldsSchema.extend({ action: ActionNameS
|
|
|
3113
3199
|
case "wait_for_element":
|
|
3114
3200
|
case "dropdown_options":
|
|
3115
3201
|
case "find_elements":
|
|
3202
|
+
case "inspect_element":
|
|
3116
3203
|
case "get_computed_style":
|
|
3117
3204
|
case "hover":
|
|
3118
3205
|
case "press_and_hold":
|
|
@@ -3127,7 +3214,7 @@ var BrowserActionSchema = BrowserActionFieldsSchema.extend({ action: ActionNameS
|
|
|
3127
3214
|
break;
|
|
3128
3215
|
case "upload_file":
|
|
3129
3216
|
requireOne([input.target, input.ref, input.selector, input.index], "Upload requires target, ref, selector, or index.");
|
|
3130
|
-
requireOne([input.filePath], "Upload requires filePath.");
|
|
3217
|
+
requireOne([input.filePath, input.filePaths], "Upload requires filePath or filePaths.");
|
|
3131
3218
|
break;
|
|
3132
3219
|
case "save_as_pdf":
|
|
3133
3220
|
requireOne([input.outputPath, input.filePath], "PDF export requires outputPath.");
|
|
@@ -3215,6 +3302,7 @@ function normalizeBrowserActionInput(value) {
|
|
|
3215
3302
|
moveActionField(output, "cookiePath", "path", issues);
|
|
3216
3303
|
moveActionField(output, "cookieSecure", "secure", issues);
|
|
3217
3304
|
moveActionField(output, "cookieHttpOnly", "httpOnly", issues);
|
|
3305
|
+
moveActionField(output, "cookieSameSite", "sameSite", issues);
|
|
3218
3306
|
} else if (rawAction === "storage") {
|
|
3219
3307
|
moveActionField(output, "storageArea", "area", issues);
|
|
3220
3308
|
moveActionField(output, "storageKey", "key", issues);
|
|
@@ -3373,6 +3461,21 @@ var TargetRequestSchema = TargetFormSchema.superRefine((input, context) => {
|
|
|
3373
3461
|
}
|
|
3374
3462
|
});
|
|
3375
3463
|
var SelectorRequestSchema = z2.object({ selector: BoundedString(2e3), ...PageInput }).strict();
|
|
3464
|
+
var InspectElementTargetFieldsSchema = z2.object({
|
|
3465
|
+
target: BoundedString(2e3).optional(),
|
|
3466
|
+
ref: z2.string().trim().min(1).max(200).regex(/^(?:ref:)?e[1-9]\d*$/, "ref must be an element reference such as e5.").optional(),
|
|
3467
|
+
selector: BoundedString(2e3).optional(),
|
|
3468
|
+
index: z2.number().int().min(0).max(1e3).optional(),
|
|
3469
|
+
maxDepth: z2.number().int().min(0).max(3).optional(),
|
|
3470
|
+
maxChildren: z2.number().int().min(1).max(100).optional(),
|
|
3471
|
+
...PageInput
|
|
3472
|
+
}).strict();
|
|
3473
|
+
var InspectElementRequestSchema = InspectElementTargetFieldsSchema.superRefine((input, context) => {
|
|
3474
|
+
const targetCount = [input.target, input.ref, input.selector, input.index].filter((value) => value !== void 0).length;
|
|
3475
|
+
if (targetCount !== 1) {
|
|
3476
|
+
context.addIssue({ code: "custom", message: "Provide exactly one of target, ref, selector, or index." });
|
|
3477
|
+
}
|
|
3478
|
+
});
|
|
3376
3479
|
var WaitRequestSchema = z2.object({ milliseconds: z2.number().int().min(0).max(12e4).default(500), ...PageInput }).strict();
|
|
3377
3480
|
var WaitForTextRequestSchema = z2.object({ text: BoundedString(2e4), timeoutMs: z2.number().int().min(100).max(12e4).optional(), ...PageInput }).strict();
|
|
3378
3481
|
var WaitForUrlRequestSchema = z2.object({ url: BoundedString(8e3), timeoutMs: z2.number().int().min(100).max(12e4).optional(), ...PageInput }).strict();
|
|
@@ -3420,7 +3523,16 @@ var ScreenshotRequestSchema = z2.object({ fullPage: z2.boolean().optional(), ful
|
|
|
3420
3523
|
}
|
|
3421
3524
|
});
|
|
3422
3525
|
var PdfRequestSchema = z2.object({ outputPath: BoundedString(4e3), ...PageInput }).strict();
|
|
3423
|
-
var UploadRequestSchema = z2.object({ selector: BoundedString(2e3), filePath: BoundedString(4e3), ...PageInput }).strict()
|
|
3526
|
+
var UploadRequestSchema = z2.object({ selector: BoundedString(2e3), filePath: BoundedString(4e3).optional(), filePaths: z2.array(BoundedString(4e3)).min(1).max(20).optional(), ...PageInput }).strict().superRefine((input, context) => {
|
|
3527
|
+
const hasFilePath = input.filePath !== void 0;
|
|
3528
|
+
const hasFilePaths = input.filePaths !== void 0;
|
|
3529
|
+
if (hasFilePath && hasFilePaths) {
|
|
3530
|
+
context.addIssue({ code: "custom", message: "Provide filePath or filePaths, not both." });
|
|
3531
|
+
}
|
|
3532
|
+
if (!hasFilePath && !hasFilePaths) {
|
|
3533
|
+
context.addIssue({ code: "custom", message: "Upload requires filePath or filePaths." });
|
|
3534
|
+
}
|
|
3535
|
+
});
|
|
3424
3536
|
var EvaluateRequestSchema = z2.object({
|
|
3425
3537
|
code: z2.string().trim().min(1).max(4e4).optional(),
|
|
3426
3538
|
expression: z2.string().trim().min(1).max(4e4).optional(),
|
|
@@ -3434,6 +3546,32 @@ var EvaluateRequestSchema = z2.object({
|
|
|
3434
3546
|
}
|
|
3435
3547
|
});
|
|
3436
3548
|
var NetworkLogRequestSchema = z2.object({ operation: z2.enum(["enable", "disable", "read", "clear", "read_and_clear"]), ...PageInput }).strict();
|
|
3549
|
+
var NetworkSearchRequestSchema = z2.object({
|
|
3550
|
+
query: BoundedString(4e3).optional(),
|
|
3551
|
+
requestId: BoundedString(256).optional(),
|
|
3552
|
+
url: BoundedString(8e3).optional(),
|
|
3553
|
+
method: BoundedString(32).optional(),
|
|
3554
|
+
status: z2.number().int().min(0).max(999).optional(),
|
|
3555
|
+
resourceType: BoundedString(64).optional(),
|
|
3556
|
+
offset: z2.number().int().min(0).max(1e6).optional(),
|
|
3557
|
+
limit: z2.number().int().min(1).max(200).optional(),
|
|
3558
|
+
pageId: BoundedString(200).optional()
|
|
3559
|
+
}).strict();
|
|
3560
|
+
var ResourceBlockingRequestSchema = z2.object({
|
|
3561
|
+
operation: ResourceBlockingOperationSchema,
|
|
3562
|
+
resourceTypes: z2.array(ResourceBlockingTypeSchema).min(1).max(RESOURCE_BLOCKING_TYPES.length).optional(),
|
|
3563
|
+
...PageInput
|
|
3564
|
+
}).strict().superRefine((input, context) => {
|
|
3565
|
+
if (input.operation === "set") {
|
|
3566
|
+
if (input.resourceTypes === void 0) {
|
|
3567
|
+
context.addIssue({ code: "custom", message: "Resource blocking set requires resourceTypes." });
|
|
3568
|
+
} else if (new Set(input.resourceTypes).size !== input.resourceTypes.length) {
|
|
3569
|
+
context.addIssue({ code: "custom", message: "Resource blocking resourceTypes must be de-duplicated." });
|
|
3570
|
+
}
|
|
3571
|
+
} else if (input.resourceTypes !== void 0) {
|
|
3572
|
+
context.addIssue({ code: "custom", message: `Resource blocking ${input.operation} does not accept resourceTypes.` });
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3437
3575
|
var DialogRequestSchema = z2.object({ operation: z2.enum(["get_text", "accept", "dismiss", "send_keys"]), text: z2.string().max(2e4).optional(), ...PageInput }).strict().superRefine((input, context) => {
|
|
3438
3576
|
if (input.operation === "send_keys" && input.text === void 0) {
|
|
3439
3577
|
context.addIssue({ code: "custom", message: "Dialog send_keys requires text." });
|
|
@@ -3451,6 +3589,7 @@ var CookieRequestSchema = z2.object({
|
|
|
3451
3589
|
url: HttpUrl(8e3).optional(),
|
|
3452
3590
|
secure: z2.boolean().optional(),
|
|
3453
3591
|
httpOnly: z2.boolean().optional(),
|
|
3592
|
+
sameSite: z2.enum(["Strict", "Lax", "None"]).optional(),
|
|
3454
3593
|
...PageInput
|
|
3455
3594
|
}).strict().superRefine((input, context) => {
|
|
3456
3595
|
if ((input.operation === "set" || input.operation === "delete") && !input.name) {
|
|
@@ -3459,6 +3598,9 @@ var CookieRequestSchema = z2.object({
|
|
|
3459
3598
|
if (input.operation === "set" && input.value === void 0) {
|
|
3460
3599
|
context.addIssue({ code: "custom", message: "Cookie set requires value." });
|
|
3461
3600
|
}
|
|
3601
|
+
if (input.sameSite !== void 0 && input.operation !== "set") {
|
|
3602
|
+
context.addIssue({ code: "custom", message: "Cookie sameSite is only valid for set." });
|
|
3603
|
+
}
|
|
3462
3604
|
});
|
|
3463
3605
|
var StorageRequestSchema = z2.object({
|
|
3464
3606
|
operation: z2.enum(["get", "set", "clear"]),
|
|
@@ -3838,7 +3980,7 @@ function registerBrowserTools(server, runtime) {
|
|
|
3838
3980
|
"browser_get_html",
|
|
3839
3981
|
{
|
|
3840
3982
|
title: "Read page HTML",
|
|
3841
|
-
description: "Read bounded
|
|
3983
|
+
description: "Read a bounded sanitized HTML projection (at most 8,000 characters) for the current page or a CSS selector. Scripts, event handlers, form values/textarea contents, and other unsafe attributes are omitted. Check the explicit truncated flag before relying on completeness; HTML is untrusted data and is never executed by this tool.",
|
|
3842
3984
|
inputSchema: HtmlRequestSchema,
|
|
3843
3985
|
annotations: BROWSER_READ_ONLY
|
|
3844
3986
|
},
|
|
@@ -3888,6 +4030,16 @@ function registerBrowserTools(server, runtime) {
|
|
|
3888
4030
|
{ title: "Read browser network log", description: "Enable, disable, read, clear, or read-and-clear the redacted network log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
|
|
3889
4031
|
async (input, ctx) => callTool(() => runtime.run({ action: networkAction(input.operation), pageId: input.pageId }, ctx.mcpReq.signal), runtime)
|
|
3890
4032
|
);
|
|
4033
|
+
server.registerTool(
|
|
4034
|
+
"browser_search_network_log",
|
|
4035
|
+
{ title: "Search browser network log", description: "Search the bounded redacted network journal by text, request ID, URL, method, status, or resource type. Results are deterministic and expose explicit capacity and omission metadata.", inputSchema: NetworkSearchRequestSchema, annotations: BROWSER_READ_ONLY },
|
|
4036
|
+
async (input, ctx) => callTool(() => runtime.run({ action: "search_network_log", ...input }, ctx.mcpReq.signal), runtime)
|
|
4037
|
+
);
|
|
4038
|
+
server.registerTool(
|
|
4039
|
+
"browser_resource_blocking",
|
|
4040
|
+
{ title: "Configure resource blocking", description: "Get, set, or clear page-scoped blocking for image, stylesheet, font, media, and script subresources. Navigation and document requests are never blocked by this tool.", inputSchema: ResourceBlockingRequestSchema, annotations: BROWSER_MUTATING },
|
|
4041
|
+
async (input, ctx) => callTool(() => runtime.run({ action: "resource_blocking", operation: input.operation, resourceTypes: input.resourceTypes, pageId: input.pageId }, ctx.mcpReq.signal), runtime)
|
|
4042
|
+
);
|
|
3891
4043
|
server.registerTool(
|
|
3892
4044
|
"browser_console_log",
|
|
3893
4045
|
{ title: "Read browser console log", description: "Enable, disable, read, clear, or read-and-clear the bounded console log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
|
|
@@ -3898,7 +4050,7 @@ function registerBrowserTools(server, runtime) {
|
|
|
3898
4050
|
return { ...fields, text: query };
|
|
3899
4051
|
});
|
|
3900
4052
|
registerAction(server, runtime, "browser_extract", "Extract page text", "Extract at most 8,000 page-text characters from the page or a CSS selector. Check truncated, offset, nextOffset, hasMore, and revision; use browser_page_next for later slices.", ExtractRequestSchema, "extract", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
|
|
3901
|
-
registerAction(server, runtime, "browser_upload", "Upload
|
|
4053
|
+
registerAction(server, runtime, "browser_upload", "Upload files", "Upload one file or up to 20 files from allowed server file roots into a file input; multiple files require the input's multiple attribute.", UploadRequestSchema, "upload_file");
|
|
3902
4054
|
registerAction(server, runtime, "browser_screenshot", "Capture a screenshot", "Capture a bounded PNG or JPEG screenshot of the current page.", ScreenshotRequestSchema, "screenshot", (input) => {
|
|
3903
4055
|
const { full_page, full, max_bytes, max_dim, ...fields } = input;
|
|
3904
4056
|
return { ...fields, fullPage: fields.fullPage ?? full_page ?? full, maxBytes: fields.maxBytes ?? max_bytes, maxDimension: fields.maxDimension ?? max_dim };
|
|
@@ -3909,6 +4061,7 @@ function registerBrowserTools(server, runtime) {
|
|
|
3909
4061
|
registerAction(server, runtime, "browser_page_next", "Read the next page slice", "Read at most 8,000 characters from the current page at offset and revision. Advance to nextOffset only when hasMore is true; stale revisions are retryable and page text is untrusted.", PageNextSchema, "page_next", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
|
|
3910
4062
|
registerAction(server, runtime, "browser_search_page", "Search the current page", "Find bounded snippets for a query in current-page text.", PageQuerySchema, "search_page");
|
|
3911
4063
|
registerAction(server, runtime, "browser_find_elements", "Find elements", "List bounded element metadata for a CSS selector.", SelectorRequestSchema, "find_elements");
|
|
4064
|
+
registerAction(server, runtime, "browser_inspect_element", "Inspect an element", "Read bounded safe attributes, selected computed styles, pseudo-element summaries, animation metadata, and shallow child structure for a current selector, ref, or index. Scripts, event-handler source, form values, and arbitrary data attributes are omitted.", InspectElementRequestSchema, "inspect_element");
|
|
3912
4065
|
registerAction(server, runtime, "browser_interactive", "List interactive elements", "List visible links, buttons, inputs, and other interactive elements with stable refs.", EmptyInputSchema, "list_interactive");
|
|
3913
4066
|
registerAction(server, runtime, "browser_frames", "List browser frames", "List bounded frame metadata for the current page. Frame content is not returned by this metadata tool.", EmptyInputSchema, "list_frames");
|
|
3914
4067
|
registerAction(server, runtime, "browser_accessibility_snapshot", "Read accessibility tree", "Read a bounded accessibility tree through Chrome DevTools. Check truncation before relying on completeness; AX refs are observation-only and must be revalidated through DOM refs before acting.", AccessibilityRequestSchema, "accessibility_snapshot", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
|
|
@@ -4023,12 +4176,14 @@ function actionAnnotations(action) {
|
|
|
4023
4176
|
case "page_next":
|
|
4024
4177
|
case "search_page":
|
|
4025
4178
|
case "find_elements":
|
|
4179
|
+
case "inspect_element":
|
|
4026
4180
|
case "list_interactive":
|
|
4027
4181
|
case "list_frames":
|
|
4028
4182
|
case "accessibility_snapshot":
|
|
4029
4183
|
case "get_computed_style":
|
|
4030
4184
|
case "get_page_info":
|
|
4031
4185
|
case "get_network_log":
|
|
4186
|
+
case "search_network_log":
|
|
4032
4187
|
case "get_console_log":
|
|
4033
4188
|
case "alert_get_text":
|
|
4034
4189
|
case "detect_challenge":
|
|
@@ -4077,7 +4232,8 @@ function cookieAction(input) {
|
|
|
4077
4232
|
cookiePath: input.path,
|
|
4078
4233
|
url: input.url,
|
|
4079
4234
|
cookieSecure: input.secure,
|
|
4080
|
-
cookieHttpOnly: input.httpOnly
|
|
4235
|
+
cookieHttpOnly: input.httpOnly,
|
|
4236
|
+
cookieSameSite: input.sameSite
|
|
4081
4237
|
};
|
|
4082
4238
|
}
|
|
4083
4239
|
function storageAction(input) {
|
|
@@ -4384,6 +4540,13 @@ function boundMcpOutput(value, options = {}) {
|
|
|
4384
4540
|
if (typeof output.warning !== "string") {
|
|
4385
4541
|
output.warning = "Some search results were omitted by the MCP output limit; use a narrower request or a paginated tool.";
|
|
4386
4542
|
}
|
|
4543
|
+
} else if (key === "entries") {
|
|
4544
|
+
output.hasMore = true;
|
|
4545
|
+
if (typeof output.returnedCount === "number" && Number.isFinite(output.returnedCount)) {
|
|
4546
|
+
output.returnedCount = Math.min(Math.max(0, Math.trunc(output.returnedCount)), limit);
|
|
4547
|
+
}
|
|
4548
|
+
const previousOmittedCount = typeof output.omittedCount === "number" && Number.isSafeInteger(output.omittedCount) ? Math.max(0, output.omittedCount) : 0;
|
|
4549
|
+
output.omittedCount = previousOmittedCount + omitted;
|
|
4387
4550
|
}
|
|
4388
4551
|
markOutputTruncated();
|
|
4389
4552
|
}
|
|
@@ -4683,11 +4846,31 @@ var DEFAULT_TYPE = {
|
|
|
4683
4846
|
rng: Math.random
|
|
4684
4847
|
};
|
|
4685
4848
|
function randomRange(min, max, rand = Math.random) {
|
|
4686
|
-
|
|
4849
|
+
const sample = rand();
|
|
4850
|
+
const boundedSample = Number.isFinite(sample) ? Math.min(1, Math.max(0, sample)) : 0;
|
|
4851
|
+
return min + boundedSample * (max - min);
|
|
4687
4852
|
}
|
|
4688
|
-
function sleep(ms) {
|
|
4689
|
-
|
|
4690
|
-
|
|
4853
|
+
function sleep(ms, signal) {
|
|
4854
|
+
if (signal?.aborted) {
|
|
4855
|
+
return Promise.reject(new Error("Operation aborted"));
|
|
4856
|
+
}
|
|
4857
|
+
return new Promise((resolve7, reject) => {
|
|
4858
|
+
let settled = false;
|
|
4859
|
+
const timer = setTimeout(() => finish(resolve7), Math.max(0, ms));
|
|
4860
|
+
const onAbort = () => finish(() => reject(new Error("Operation aborted")));
|
|
4861
|
+
const finish = (callback) => {
|
|
4862
|
+
if (settled) {
|
|
4863
|
+
return;
|
|
4864
|
+
}
|
|
4865
|
+
settled = true;
|
|
4866
|
+
clearTimeout(timer);
|
|
4867
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4868
|
+
callback();
|
|
4869
|
+
};
|
|
4870
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4871
|
+
if (signal?.aborted) {
|
|
4872
|
+
onAbort();
|
|
4873
|
+
}
|
|
4691
4874
|
});
|
|
4692
4875
|
}
|
|
4693
4876
|
async function humanMouseMove(page, x1, y1, x2, y2, durationMs = 80, options = {}) {
|
|
@@ -4706,15 +4889,18 @@ async function humanType(page, text, options = {}) {
|
|
|
4706
4889
|
const cfg = { ...DEFAULT_TYPE, ...options, rng };
|
|
4707
4890
|
const keyboard = page.keyboard;
|
|
4708
4891
|
for (const char of text) {
|
|
4892
|
+
if (cfg.signal?.aborted) {
|
|
4893
|
+
throw new Error("Operation aborted");
|
|
4894
|
+
}
|
|
4709
4895
|
if (char === " ") {
|
|
4710
4896
|
await keyboard.down("Space");
|
|
4711
4897
|
await keyboard.up("Space");
|
|
4712
4898
|
} else {
|
|
4713
4899
|
await keyboard.type(char);
|
|
4714
4900
|
}
|
|
4715
|
-
await sleep(randomRange(cfg.minDelayMs, cfg.maxDelayMs, cfg.rng));
|
|
4901
|
+
await sleep(randomRange(cfg.minDelayMs, cfg.maxDelayMs, cfg.rng), cfg.signal);
|
|
4716
4902
|
if (cfg.rng() < cfg.thinkPauseChance) {
|
|
4717
|
-
await sleep(randomRange(cfg.thinkPauseMinMs, cfg.thinkPauseMaxMs, cfg.rng));
|
|
4903
|
+
await sleep(randomRange(cfg.thinkPauseMinMs, cfg.thinkPauseMaxMs, cfg.rng), cfg.signal);
|
|
4718
4904
|
}
|
|
4719
4905
|
}
|
|
4720
4906
|
}
|
|
@@ -5055,6 +5241,294 @@ function globMatches(value, glob) {
|
|
|
5055
5241
|
return compiled.test(value);
|
|
5056
5242
|
}
|
|
5057
5243
|
|
|
5244
|
+
// src/server/browser/network.ts
|
|
5245
|
+
var DEFAULT_CAPACITY = 500;
|
|
5246
|
+
var MAX_CAPACITY = 1e4;
|
|
5247
|
+
var DEFAULT_MAX_PAGES = 128;
|
|
5248
|
+
var MAX_MAX_PAGES = 1024;
|
|
5249
|
+
var DEFAULT_LIMIT = 50;
|
|
5250
|
+
var MAX_LIMIT = 200;
|
|
5251
|
+
var MAX_PAGE_ID_CHARS = 200;
|
|
5252
|
+
var MAX_REQUEST_ID_CHARS = 256;
|
|
5253
|
+
var MAX_METHOD_CHARS = 32;
|
|
5254
|
+
var MAX_RESOURCE_TYPE_CHARS = 64;
|
|
5255
|
+
var NetworkJournal = class {
|
|
5256
|
+
capacity;
|
|
5257
|
+
maxPages;
|
|
5258
|
+
pages = /* @__PURE__ */ new Map();
|
|
5259
|
+
generatedRequestSequence = 0;
|
|
5260
|
+
evictedPageCount = 0;
|
|
5261
|
+
constructor(options = {}) {
|
|
5262
|
+
this.capacity = boundedPositiveInteger(options.capacity ?? DEFAULT_CAPACITY, MAX_CAPACITY, "capacity");
|
|
5263
|
+
this.maxPages = boundedPositiveInteger(options.maxPages ?? DEFAULT_MAX_PAGES, MAX_MAX_PAGES, "maxPages");
|
|
5264
|
+
}
|
|
5265
|
+
/** Record or update request metadata and return an immutable snapshot. */
|
|
5266
|
+
recordRequest(event) {
|
|
5267
|
+
const pageId = normalizeRequiredIdentifier(event?.pageId, "pageId", MAX_PAGE_ID_CHARS);
|
|
5268
|
+
const page = this.ensurePage(pageId);
|
|
5269
|
+
const requestId = this.resolveRequestId(pageId, event?.requestId);
|
|
5270
|
+
const existing = page.entries.get(requestId);
|
|
5271
|
+
const timestamp = normalizeTimestamp(event?.timestamp);
|
|
5272
|
+
const resourceType = normalizeOptionalText(event?.resourceType, MAX_RESOURCE_TYPE_CHARS) ?? existing?.entry.resourceType;
|
|
5273
|
+
const entry = {
|
|
5274
|
+
pageId,
|
|
5275
|
+
requestId,
|
|
5276
|
+
url: safeNetworkUrl(event?.url),
|
|
5277
|
+
method: normalizeMethod(event?.method),
|
|
5278
|
+
...resourceType ? { resourceType } : {},
|
|
5279
|
+
...existing?.entry.status !== void 0 ? { status: existing.entry.status } : {},
|
|
5280
|
+
requestTimestamp: existing?.entry.requestTimestamp ?? timestamp,
|
|
5281
|
+
...existing?.entry.responseTimestamp ? { responseTimestamp: existing.entry.responseTimestamp } : {}
|
|
5282
|
+
};
|
|
5283
|
+
page.entries.set(requestId, this.stored(entry));
|
|
5284
|
+
this.enforcePageCapacity(page);
|
|
5285
|
+
return cloneEntry(page.entries.get(requestId)?.entry ?? entry);
|
|
5286
|
+
}
|
|
5287
|
+
/** Record or update response metadata and correlate it to its request. */
|
|
5288
|
+
recordResponse(event) {
|
|
5289
|
+
const pageId = normalizeRequiredIdentifier(event?.pageId, "pageId", MAX_PAGE_ID_CHARS);
|
|
5290
|
+
const page = this.ensurePage(pageId);
|
|
5291
|
+
const requestId = normalizeRequiredIdentifier(event?.requestId, "requestId", MAX_REQUEST_ID_CHARS);
|
|
5292
|
+
const existing = page.entries.get(requestId);
|
|
5293
|
+
const timestamp = normalizeTimestamp(event?.timestamp);
|
|
5294
|
+
const entry = existing ? {
|
|
5295
|
+
...existing.entry,
|
|
5296
|
+
...event.url !== void 0 ? { url: safeNetworkUrl(event.url) } : {},
|
|
5297
|
+
...event.resourceType !== void 0 ? { resourceType: normalizeOptionalText(event.resourceType, MAX_RESOURCE_TYPE_CHARS) } : {},
|
|
5298
|
+
...isValidStatus(event.status) ? { status: event.status } : {},
|
|
5299
|
+
responseTimestamp: timestamp
|
|
5300
|
+
} : {
|
|
5301
|
+
pageId,
|
|
5302
|
+
requestId,
|
|
5303
|
+
url: event.url === void 0 ? "[URL_UNAVAILABLE]" : safeNetworkUrl(event.url),
|
|
5304
|
+
method: "UNKNOWN",
|
|
5305
|
+
...normalizeOptionalText(event.resourceType, MAX_RESOURCE_TYPE_CHARS) ? { resourceType: normalizeOptionalText(event.resourceType, MAX_RESOURCE_TYPE_CHARS) } : {},
|
|
5306
|
+
...isValidStatus(event.status) ? { status: event.status } : {},
|
|
5307
|
+
requestTimestamp: timestamp,
|
|
5308
|
+
responseTimestamp: timestamp
|
|
5309
|
+
};
|
|
5310
|
+
page.entries.set(requestId, this.stored(entry));
|
|
5311
|
+
this.enforcePageCapacity(page);
|
|
5312
|
+
return cloneEntry(page.entries.get(requestId)?.entry ?? entry);
|
|
5313
|
+
}
|
|
5314
|
+
/** Query retained records using deterministic metadata filters and paging. */
|
|
5315
|
+
query(query = {}) {
|
|
5316
|
+
const normalized = normalizeQuery(query);
|
|
5317
|
+
const selectedPages = normalized.pageId === void 0 ? [...this.pages.entries()] : [[normalized.pageId, this.pages.get(normalized.pageId)]];
|
|
5318
|
+
const retainedCount = selectedPages.reduce((total, [, page]) => total + (page?.entries.size ?? 0), 0);
|
|
5319
|
+
const evictedCount = selectedPages.reduce((total, [, page]) => total + (page?.evictedCount ?? 0), 0) + (normalized.pageId === void 0 ? this.evictedPageCount : 0);
|
|
5320
|
+
const capacityReached = selectedPages.some(([, page]) => (page?.entries.size ?? 0) >= this.capacity || (page?.evictedCount ?? 0) > 0);
|
|
5321
|
+
const matches = [];
|
|
5322
|
+
for (const [, page] of selectedPages) {
|
|
5323
|
+
if (!page) continue;
|
|
5324
|
+
for (const stored of page.entries.values()) {
|
|
5325
|
+
if (matchesFilter(stored.entry, normalized)) {
|
|
5326
|
+
matches.push(stored.entry);
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
const entries = matches.slice(normalized.offset, normalized.offset + normalized.limit).map(cloneEntry);
|
|
5331
|
+
return {
|
|
5332
|
+
entries,
|
|
5333
|
+
offset: normalized.offset,
|
|
5334
|
+
limit: normalized.limit,
|
|
5335
|
+
total: matches.length,
|
|
5336
|
+
returnedCount: entries.length,
|
|
5337
|
+
omittedCount: Math.max(0, matches.length - entries.length),
|
|
5338
|
+
hasMore: normalized.offset + entries.length < matches.length,
|
|
5339
|
+
retainedCount,
|
|
5340
|
+
capacity: this.capacity,
|
|
5341
|
+
evictedCount,
|
|
5342
|
+
capacityReached
|
|
5343
|
+
};
|
|
5344
|
+
}
|
|
5345
|
+
/** Search all safe metadata fields using one bounded case-insensitive scan. */
|
|
5346
|
+
search(searchText, options = {}) {
|
|
5347
|
+
const query = normalizeSearchText(searchText);
|
|
5348
|
+
if (!query) {
|
|
5349
|
+
throw new RangeError("searchText must be a non-empty string.");
|
|
5350
|
+
}
|
|
5351
|
+
const normalized = normalizeQuery(options);
|
|
5352
|
+
const selectedPages = normalized.pageId === void 0 ? [...this.pages.entries()] : [[normalized.pageId, this.pages.get(normalized.pageId)]];
|
|
5353
|
+
const matches = [];
|
|
5354
|
+
for (const [, page] of selectedPages) {
|
|
5355
|
+
if (!page) continue;
|
|
5356
|
+
for (const stored of page.entries.values()) {
|
|
5357
|
+
if (stored.searchText.includes(query) && matchesFilter(stored.entry, normalized)) {
|
|
5358
|
+
matches.push(stored.entry);
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
return this.pageFromMatches(matches, normalized, selectedPages);
|
|
5363
|
+
}
|
|
5364
|
+
/** Remove all records, or only records associated with one page. */
|
|
5365
|
+
clear(pageId) {
|
|
5366
|
+
if (pageId === void 0) {
|
|
5367
|
+
const clearedCount2 = [...this.pages.values()].reduce((total, page2) => total + page2.entries.size, 0);
|
|
5368
|
+
this.pages.clear();
|
|
5369
|
+
this.evictedPageCount = 0;
|
|
5370
|
+
return { clearedCount: clearedCount2, retainedCount: 0 };
|
|
5371
|
+
}
|
|
5372
|
+
const normalizedPageId = normalizeRequiredIdentifier(pageId, "pageId", MAX_PAGE_ID_CHARS);
|
|
5373
|
+
const page = this.pages.get(normalizedPageId);
|
|
5374
|
+
const clearedCount = page?.entries.size ?? 0;
|
|
5375
|
+
this.pages.delete(normalizedPageId);
|
|
5376
|
+
return { clearedCount, retainedCount: this.retainedCount() };
|
|
5377
|
+
}
|
|
5378
|
+
/** Return bounded journal counts without exposing records. */
|
|
5379
|
+
stats(pageId) {
|
|
5380
|
+
const result = this.query(pageId === void 0 ? {} : { pageId, limit: 1 });
|
|
5381
|
+
return {
|
|
5382
|
+
retainedCount: result.retainedCount,
|
|
5383
|
+
capacity: result.capacity,
|
|
5384
|
+
evictedCount: result.evictedCount,
|
|
5385
|
+
capacityReached: result.capacityReached
|
|
5386
|
+
};
|
|
5387
|
+
}
|
|
5388
|
+
pageFromMatches(matches, query, selectedPages) {
|
|
5389
|
+
const entries = matches.slice(query.offset, query.offset + query.limit).map(cloneEntry);
|
|
5390
|
+
const retainedCount = selectedPages.reduce((total, [, page]) => total + (page?.entries.size ?? 0), 0);
|
|
5391
|
+
const evictedCount = selectedPages.reduce((total, [, page]) => total + (page?.evictedCount ?? 0), 0) + (query.pageId === void 0 ? this.evictedPageCount : 0);
|
|
5392
|
+
return {
|
|
5393
|
+
entries,
|
|
5394
|
+
offset: query.offset,
|
|
5395
|
+
limit: query.limit,
|
|
5396
|
+
total: matches.length,
|
|
5397
|
+
returnedCount: entries.length,
|
|
5398
|
+
omittedCount: Math.max(0, matches.length - entries.length),
|
|
5399
|
+
hasMore: query.offset + entries.length < matches.length,
|
|
5400
|
+
retainedCount,
|
|
5401
|
+
capacity: this.capacity,
|
|
5402
|
+
evictedCount,
|
|
5403
|
+
capacityReached: selectedPages.some(([, page]) => (page?.entries.size ?? 0) >= this.capacity || (page?.evictedCount ?? 0) > 0)
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5406
|
+
ensurePage(pageId) {
|
|
5407
|
+
const existing = this.pages.get(pageId);
|
|
5408
|
+
if (existing) return existing;
|
|
5409
|
+
while (this.pages.size >= this.maxPages) {
|
|
5410
|
+
const oldestPageId = this.pages.keys().next().value;
|
|
5411
|
+
if (oldestPageId === void 0) break;
|
|
5412
|
+
const oldest = this.pages.get(oldestPageId);
|
|
5413
|
+
this.evictedPageCount += oldest?.entries.size ?? 0;
|
|
5414
|
+
this.pages.delete(oldestPageId);
|
|
5415
|
+
}
|
|
5416
|
+
const page = { entries: /* @__PURE__ */ new Map(), evictedCount: 0 };
|
|
5417
|
+
this.pages.set(pageId, page);
|
|
5418
|
+
return page;
|
|
5419
|
+
}
|
|
5420
|
+
resolveRequestId(pageId, rawRequestId) {
|
|
5421
|
+
const normalized = normalizeOptionalIdentifier(rawRequestId, MAX_REQUEST_ID_CHARS);
|
|
5422
|
+
if (normalized) return normalized;
|
|
5423
|
+
this.generatedRequestSequence += 1;
|
|
5424
|
+
return `${pageId}:request-${this.generatedRequestSequence}`.slice(0, MAX_REQUEST_ID_CHARS);
|
|
5425
|
+
}
|
|
5426
|
+
stored(entry) {
|
|
5427
|
+
const searchParts = [entry.pageId, entry.requestId, entry.url, entry.method, entry.resourceType ?? "", entry.status === void 0 ? "" : String(entry.status)];
|
|
5428
|
+
return { entry, searchText: searchParts.join(" ").toLocaleLowerCase("en-US") };
|
|
5429
|
+
}
|
|
5430
|
+
enforcePageCapacity(page) {
|
|
5431
|
+
while (page.entries.size > this.capacity) {
|
|
5432
|
+
const oldestRequestId = page.entries.keys().next().value;
|
|
5433
|
+
if (oldestRequestId === void 0) break;
|
|
5434
|
+
page.entries.delete(oldestRequestId);
|
|
5435
|
+
page.evictedCount += 1;
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
retainedCount() {
|
|
5439
|
+
return [...this.pages.values()].reduce((total, page) => total + page.entries.size, 0);
|
|
5440
|
+
}
|
|
5441
|
+
};
|
|
5442
|
+
function normalizeQuery(query) {
|
|
5443
|
+
if (query === null || typeof query !== "object") {
|
|
5444
|
+
throw new TypeError("query must be an object.");
|
|
5445
|
+
}
|
|
5446
|
+
const offset = boundedNonnegativeInteger(query.offset ?? 0, "offset");
|
|
5447
|
+
const limit = boundedPositiveInteger(query.limit ?? DEFAULT_LIMIT, MAX_LIMIT, "limit");
|
|
5448
|
+
return {
|
|
5449
|
+
...query.pageId === void 0 ? {} : { pageId: normalizeRequiredIdentifier(query.pageId, "pageId", MAX_PAGE_ID_CHARS) },
|
|
5450
|
+
...query.requestId === void 0 ? {} : { requestId: normalizeOptionalText(query.requestId, MAX_REQUEST_ID_CHARS) },
|
|
5451
|
+
...query.url === void 0 ? {} : { url: normalizeSearchText(query.url) },
|
|
5452
|
+
...query.method === void 0 ? {} : { method: normalizeMethod(query.method) },
|
|
5453
|
+
...query.status === void 0 ? {} : { status: normalizeStatus(query.status) },
|
|
5454
|
+
...query.resourceType === void 0 ? {} : { resourceType: normalizeOptionalText(query.resourceType, MAX_RESOURCE_TYPE_CHARS) },
|
|
5455
|
+
offset,
|
|
5456
|
+
limit
|
|
5457
|
+
};
|
|
5458
|
+
}
|
|
5459
|
+
function matchesFilter(entry, filter) {
|
|
5460
|
+
if (filter.pageId !== void 0 && entry.pageId !== filter.pageId) return false;
|
|
5461
|
+
if (filter.requestId !== void 0 && !entry.requestId.toLocaleLowerCase("en-US").includes(filter.requestId.toLocaleLowerCase("en-US"))) return false;
|
|
5462
|
+
if (filter.url !== void 0 && !entry.url.toLocaleLowerCase("en-US").includes(filter.url.toLocaleLowerCase("en-US"))) return false;
|
|
5463
|
+
if (filter.method !== void 0 && entry.method !== filter.method) return false;
|
|
5464
|
+
if (filter.status !== void 0 && entry.status !== filter.status) return false;
|
|
5465
|
+
if (filter.resourceType !== void 0 && entry.resourceType?.toLocaleLowerCase("en-US") !== filter.resourceType.toLocaleLowerCase("en-US")) return false;
|
|
5466
|
+
return true;
|
|
5467
|
+
}
|
|
5468
|
+
function cloneEntry(entry) {
|
|
5469
|
+
return { ...entry };
|
|
5470
|
+
}
|
|
5471
|
+
function safeNetworkUrl(rawUrl) {
|
|
5472
|
+
if (typeof rawUrl !== "string" || !rawUrl.trim()) return "[URL_UNAVAILABLE]";
|
|
5473
|
+
const trimmed = rawUrl.trim();
|
|
5474
|
+
let parsed;
|
|
5475
|
+
try {
|
|
5476
|
+
parsed = new URL(trimmed);
|
|
5477
|
+
} catch {
|
|
5478
|
+
return "[INVALID_URL]";
|
|
5479
|
+
}
|
|
5480
|
+
if (!["http:", "https:", "ws:", "wss:"].includes(parsed.protocol)) {
|
|
5481
|
+
return "[NON_HTTP_URL]";
|
|
5482
|
+
}
|
|
5483
|
+
return redactSecretPlaceholders(sanitizeUrl(trimmed));
|
|
5484
|
+
}
|
|
5485
|
+
function normalizeRequiredIdentifier(value, name, maxChars) {
|
|
5486
|
+
const normalized = normalizeOptionalIdentifier(value, maxChars);
|
|
5487
|
+
if (!normalized) throw new TypeError(`${name} must be a non-empty string or number.`);
|
|
5488
|
+
return normalized;
|
|
5489
|
+
}
|
|
5490
|
+
function normalizeOptionalIdentifier(value, maxChars) {
|
|
5491
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
5492
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) return void 0;
|
|
5493
|
+
return normalizeOptionalText(String(value), maxChars);
|
|
5494
|
+
}
|
|
5495
|
+
function normalizeOptionalText(value, maxChars) {
|
|
5496
|
+
if (typeof value !== "string") return void 0;
|
|
5497
|
+
const normalized = value.normalize("NFKC").replace(/[\u0000-\u001f\u007f\u200b-\u200d\u2060\ufeff]/g, "").trim();
|
|
5498
|
+
return normalized ? normalized.slice(0, maxChars) : void 0;
|
|
5499
|
+
}
|
|
5500
|
+
function normalizeMethod(value) {
|
|
5501
|
+
return (normalizeOptionalText(value, MAX_METHOD_CHARS) ?? "UNKNOWN").toUpperCase();
|
|
5502
|
+
}
|
|
5503
|
+
function normalizeSearchText(value) {
|
|
5504
|
+
const normalized = normalizeOptionalText(value, 512);
|
|
5505
|
+
if (!normalized) throw new TypeError("search and filter values must be non-empty strings.");
|
|
5506
|
+
return normalized.toLocaleLowerCase("en-US");
|
|
5507
|
+
}
|
|
5508
|
+
function normalizeTimestamp(value) {
|
|
5509
|
+
const date = value instanceof Date ? value : typeof value === "number" && Number.isFinite(value) ? new Date(value) : typeof value === "string" && value.trim() ? new Date(value) : /* @__PURE__ */ new Date();
|
|
5510
|
+
return Number.isNaN(date.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : date.toISOString();
|
|
5511
|
+
}
|
|
5512
|
+
function isValidStatus(value) {
|
|
5513
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 999;
|
|
5514
|
+
}
|
|
5515
|
+
function normalizeStatus(value) {
|
|
5516
|
+
if (!isValidStatus(value)) throw new TypeError("status must be an integer between 0 and 999.");
|
|
5517
|
+
return value;
|
|
5518
|
+
}
|
|
5519
|
+
function boundedPositiveInteger(value, maximum, name) {
|
|
5520
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
5521
|
+
throw new RangeError(`${name} must be an integer between 1 and ${maximum}.`);
|
|
5522
|
+
}
|
|
5523
|
+
return value;
|
|
5524
|
+
}
|
|
5525
|
+
function boundedNonnegativeInteger(value, name) {
|
|
5526
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
5527
|
+
throw new RangeError(`${name} must be a non-negative integer.`);
|
|
5528
|
+
}
|
|
5529
|
+
return value;
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5058
5532
|
// src/server/browser/service.ts
|
|
5059
5533
|
var puppeteerModulePromise;
|
|
5060
5534
|
function loadPuppeteer() {
|
|
@@ -5069,6 +5543,52 @@ var POPUP_POST_CLICK_SETTLE_TIMEOUT_MS = 300;
|
|
|
5069
5543
|
var MAX_DOM_TRAVERSAL_NODES = 2e4;
|
|
5070
5544
|
var MAX_TEXT_SCAN_CHARS = 5e5;
|
|
5071
5545
|
var MAX_MARKUP_EVIDENCE_CHARS = 12e4;
|
|
5546
|
+
var MAX_INSPECT_NODES = 512;
|
|
5547
|
+
var MAX_INSPECT_STRING_CHARS = 500;
|
|
5548
|
+
var SAFE_ELEMENT_ATTRIBUTE_NAMES = [
|
|
5549
|
+
"id",
|
|
5550
|
+
"class",
|
|
5551
|
+
"role",
|
|
5552
|
+
"type",
|
|
5553
|
+
"name",
|
|
5554
|
+
"placeholder",
|
|
5555
|
+
"title",
|
|
5556
|
+
"tabindex",
|
|
5557
|
+
"style",
|
|
5558
|
+
"fill",
|
|
5559
|
+
"stroke",
|
|
5560
|
+
"x",
|
|
5561
|
+
"y",
|
|
5562
|
+
"x1",
|
|
5563
|
+
"x2",
|
|
5564
|
+
"y1",
|
|
5565
|
+
"y2",
|
|
5566
|
+
"r",
|
|
5567
|
+
"cx",
|
|
5568
|
+
"cy",
|
|
5569
|
+
"width",
|
|
5570
|
+
"height",
|
|
5571
|
+
"points",
|
|
5572
|
+
"transform",
|
|
5573
|
+
"font-size"
|
|
5574
|
+
];
|
|
5575
|
+
var SAFE_ELEMENT_DATA_ATTRIBUTE_NAMES = [
|
|
5576
|
+
"data-color",
|
|
5577
|
+
"data-index",
|
|
5578
|
+
"data-sides",
|
|
5579
|
+
"data-result",
|
|
5580
|
+
"data-key",
|
|
5581
|
+
"data-type",
|
|
5582
|
+
"data-item",
|
|
5583
|
+
"data-id",
|
|
5584
|
+
"data-start",
|
|
5585
|
+
"data-end",
|
|
5586
|
+
"data-duration",
|
|
5587
|
+
"data-output",
|
|
5588
|
+
"data-value",
|
|
5589
|
+
"data-position",
|
|
5590
|
+
"data-price"
|
|
5591
|
+
];
|
|
5072
5592
|
var CHALLENGE_AI_GUIDANCE = "Use normal browser click, input, scroll, or key tools on the visible challenge controls, then call solve_challenge again to verify that the challenge is cleared.";
|
|
5073
5593
|
var CHALLENGE_DEFAULT_MAX_ATTEMPTS = 32;
|
|
5074
5594
|
var CHALLENGE_MAX_ATTEMPTS = 100;
|
|
@@ -5081,6 +5601,11 @@ var NAVIGATION_CLICK_SETTLE_TIMEOUT_MS = 50;
|
|
|
5081
5601
|
var NAVIGATION_CLICK_EVENT_TIMEOUT_MS = 250;
|
|
5082
5602
|
var NAVIGATION_CLICK_READY_TIMEOUT_MS = 250;
|
|
5083
5603
|
var SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS = 1e3;
|
|
5604
|
+
var MIN_IDLE_SWEEP_INTERVAL_MS = 250;
|
|
5605
|
+
var MAX_IDLE_SWEEP_INTERVAL_MS = 6e4;
|
|
5606
|
+
var MAX_DEVTOOLS_PROBE_RESPONSE_BYTES = 64 * 1024;
|
|
5607
|
+
var MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
5608
|
+
var MAX_UPLOAD_TOTAL_BYTES = 100 * 1024 * 1024;
|
|
5084
5609
|
var COMMON_KEY_ALIASES = {
|
|
5085
5610
|
ALT: "Alt",
|
|
5086
5611
|
ARROWDOWN: "ArrowDown",
|
|
@@ -5167,13 +5692,15 @@ var PARALLEL_READ_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
5167
5692
|
"page_next",
|
|
5168
5693
|
"search_page",
|
|
5169
5694
|
"find_elements",
|
|
5695
|
+
"inspect_element",
|
|
5170
5696
|
"list_frames",
|
|
5171
5697
|
"accessibility_snapshot",
|
|
5172
5698
|
"get_computed_style",
|
|
5173
5699
|
"get_page_info",
|
|
5174
5700
|
"get_cookies",
|
|
5175
5701
|
"get_storage",
|
|
5176
|
-
"list_downloads"
|
|
5702
|
+
"list_downloads",
|
|
5703
|
+
"search_network_log"
|
|
5177
5704
|
]);
|
|
5178
5705
|
var BrowserService = class {
|
|
5179
5706
|
constructor(config, policy, logger, dependencies = {}) {
|
|
@@ -5181,6 +5708,7 @@ var BrowserService = class {
|
|
|
5181
5708
|
this.policy = policy;
|
|
5182
5709
|
this.logger = logger;
|
|
5183
5710
|
this.dependencies = dependencies;
|
|
5711
|
+
this.startIdleSweep();
|
|
5184
5712
|
}
|
|
5185
5713
|
config;
|
|
5186
5714
|
policy;
|
|
@@ -5211,6 +5739,7 @@ var BrowserService = class {
|
|
|
5211
5739
|
currentPageId;
|
|
5212
5740
|
sessionGeneration = 0;
|
|
5213
5741
|
states = /* @__PURE__ */ new Map();
|
|
5742
|
+
networkJournal = new NetworkJournal();
|
|
5214
5743
|
configuredDownloadContexts = /* @__PURE__ */ new WeakSet();
|
|
5215
5744
|
// The download directory is process/session scoped, while page setup is
|
|
5216
5745
|
// page scoped. Share the mkdir promise across pages so opening a tab does
|
|
@@ -5218,9 +5747,11 @@ var BrowserService = class {
|
|
|
5218
5747
|
// so a later page can retry after a transient filesystem failure.
|
|
5219
5748
|
downloadDirectoryPromise;
|
|
5220
5749
|
ids = /* @__PURE__ */ new WeakMap();
|
|
5750
|
+
networkRequestIds = /* @__PURE__ */ new WeakMap();
|
|
5221
5751
|
targetGuardSessions = /* @__PURE__ */ new Map();
|
|
5222
5752
|
targetGuardNavigationErrors = /* @__PURE__ */ new Map();
|
|
5223
5753
|
unguardedTargetSessions = /* @__PURE__ */ new Set();
|
|
5754
|
+
handledTargetGuardSessions = /* @__PURE__ */ new Set();
|
|
5224
5755
|
pendingTargetGuardSessions = /* @__PURE__ */ new Map();
|
|
5225
5756
|
pendingTargetGuardInfos = /* @__PURE__ */ new Map();
|
|
5226
5757
|
targetGuardUnavailable = false;
|
|
@@ -5228,11 +5759,134 @@ var BrowserService = class {
|
|
|
5228
5759
|
targetGuardConnectionListener;
|
|
5229
5760
|
targetGuardRawConnectionListener;
|
|
5230
5761
|
targetGuardDetachedListener;
|
|
5762
|
+
targetGuardReadinessPromise;
|
|
5231
5763
|
targetGuardOriginalEmit;
|
|
5232
5764
|
targetGuardWrappedEmit;
|
|
5233
5765
|
operationTail = Promise.resolve();
|
|
5234
5766
|
queuedOperations = 0;
|
|
5235
5767
|
benchmarkCounters = process.env.SMOOTH_OPERATOR_BENCHMARK_COUNTERS === "true" ? { browserOperations: 0, pageLookups: 0, pageEnumerations: 0, pageEvaluations: 0, cdpCommands: 0 } : void 0;
|
|
5768
|
+
idleSweepTimer;
|
|
5769
|
+
idleSweepPromise;
|
|
5770
|
+
idleSweepStopped = false;
|
|
5771
|
+
pendingTargetPreparations = /* @__PURE__ */ new Set();
|
|
5772
|
+
startIdleSweep() {
|
|
5773
|
+
const idleTimeoutMs = this.config.browser.idleTimeoutMs;
|
|
5774
|
+
if (this.config.browser.mode === "disabled" || idleTimeoutMs <= 0) {
|
|
5775
|
+
return;
|
|
5776
|
+
}
|
|
5777
|
+
const intervalMs = Math.min(
|
|
5778
|
+
MAX_IDLE_SWEEP_INTERVAL_MS,
|
|
5779
|
+
Math.max(MIN_IDLE_SWEEP_INTERVAL_MS, Math.floor(idleTimeoutMs / 2))
|
|
5780
|
+
);
|
|
5781
|
+
const timer = setInterval(() => this.scheduleIdleSweep(), intervalMs);
|
|
5782
|
+
timer.unref?.();
|
|
5783
|
+
this.idleSweepTimer = timer;
|
|
5784
|
+
}
|
|
5785
|
+
stopIdleSweep() {
|
|
5786
|
+
this.idleSweepStopped = true;
|
|
5787
|
+
if (this.idleSweepTimer) {
|
|
5788
|
+
clearInterval(this.idleSweepTimer);
|
|
5789
|
+
this.idleSweepTimer = void 0;
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
scheduleIdleSweep() {
|
|
5793
|
+
if (this.idleSweepStopped || this.idleSweepPromise || !this.browser || this.browser.connected === false) {
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
if (this.queuedOperations > 0 || this.activeOperationControllers.size > 0) {
|
|
5797
|
+
return;
|
|
5798
|
+
}
|
|
5799
|
+
if (this.connectionPromise || this.connectionSettlementPromise || this.browserClosePromise || this.interruptedBrowserShutdown || this.recoveryPromise || this.failedBrowserShutdown || this.browserShutdownFailure || this.pendingTargetPreparations.size > 0) {
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
const observedActivityAt = this.lastActivityAt;
|
|
5803
|
+
const intervalMs = this.idleSweepIntervalMs();
|
|
5804
|
+
const sweep = this.withOperationLock(
|
|
5805
|
+
void 0,
|
|
5806
|
+
(signal) => this.sweepIdleBrowser(observedActivityAt, signal),
|
|
5807
|
+
intervalMs,
|
|
5808
|
+
intervalMs,
|
|
5809
|
+
"exclusive",
|
|
5810
|
+
false
|
|
5811
|
+
);
|
|
5812
|
+
this.idleSweepPromise = sweep;
|
|
5813
|
+
void sweep.then(
|
|
5814
|
+
() => {
|
|
5815
|
+
if (this.idleSweepPromise === sweep) {
|
|
5816
|
+
this.idleSweepPromise = void 0;
|
|
5817
|
+
}
|
|
5818
|
+
},
|
|
5819
|
+
(error) => {
|
|
5820
|
+
if (this.idleSweepPromise === sweep) {
|
|
5821
|
+
this.idleSweepPromise = void 0;
|
|
5822
|
+
}
|
|
5823
|
+
this.logger.debug("Idle browser sweep did not complete", { error: safeErrorDiagnostic(error) });
|
|
5824
|
+
}
|
|
5825
|
+
);
|
|
5826
|
+
}
|
|
5827
|
+
idleSweepIntervalMs() {
|
|
5828
|
+
const idleTimeoutMs = this.config.browser.idleTimeoutMs;
|
|
5829
|
+
return Math.min(
|
|
5830
|
+
MAX_IDLE_SWEEP_INTERVAL_MS,
|
|
5831
|
+
Math.max(MIN_IDLE_SWEEP_INTERVAL_MS, Math.floor(idleTimeoutMs / 2))
|
|
5832
|
+
);
|
|
5833
|
+
}
|
|
5834
|
+
async sweepIdleBrowser(observedActivityAt, signal) {
|
|
5835
|
+
throwIfAborted(signal);
|
|
5836
|
+
if (this.idleSweepStopped || this.shuttingDown || this.config.browser.idleTimeoutMs <= 0) {
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
5839
|
+
const browser = this.browser;
|
|
5840
|
+
if (!browser || browser.connected === false) {
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
if (this.queuedOperations !== 1 || this.activeOperationControllers.size !== 1) {
|
|
5844
|
+
return;
|
|
5845
|
+
}
|
|
5846
|
+
if (this.connectionPromise || this.connectionSettlementPromise || this.browserClosePromise || this.interruptedBrowserShutdown || this.recoveryPromise || this.failedBrowserShutdown || this.browserShutdownFailure || this.pendingTargetPreparations.size > 0) {
|
|
5847
|
+
return;
|
|
5848
|
+
}
|
|
5849
|
+
if ([...this.states.values()].some((state) => !state.disposed && state.dialogs.length > 0)) {
|
|
5850
|
+
return;
|
|
5851
|
+
}
|
|
5852
|
+
if (typeof browser.pages === "function") {
|
|
5853
|
+
const pagesResult = await this.enumerateIdlePages(browser);
|
|
5854
|
+
if (!pagesResult) {
|
|
5855
|
+
return;
|
|
5856
|
+
}
|
|
5857
|
+
const livePages = pagesResult.filter((page) => !isPageClosed(page));
|
|
5858
|
+
for (const page of livePages) {
|
|
5859
|
+
const state = [...this.states.values()].find((candidate) => candidate.page === page);
|
|
5860
|
+
if (!state || state.disposed || state.lifecycleGeneration !== this.lifecycleGeneration || state.configurationPromise || !state.navigationGuardInstalled) {
|
|
5861
|
+
return;
|
|
5862
|
+
}
|
|
5863
|
+
if (state.dialogs.length > 0) {
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
if (this.queuedOperations !== 1 || this.activeOperationControllers.size !== 1 || this.pendingTargetPreparations.size > 0) {
|
|
5869
|
+
return;
|
|
5870
|
+
}
|
|
5871
|
+
if (this.lastActivityAt !== observedActivityAt || Date.now() - this.lastActivityAt < this.config.browser.idleTimeoutMs) {
|
|
5872
|
+
return;
|
|
5873
|
+
}
|
|
5874
|
+
await this.closeBrowser();
|
|
5875
|
+
}
|
|
5876
|
+
async enumerateIdlePages(browser) {
|
|
5877
|
+
const pagesPromise = Promise.resolve().then(() => browser.pages());
|
|
5878
|
+
const result = await settleWithTimeout(
|
|
5879
|
+
pagesPromise.then(
|
|
5880
|
+
(pages) => ({ pages }),
|
|
5881
|
+
(error) => ({ error })
|
|
5882
|
+
),
|
|
5883
|
+
Math.min(this.idleSweepIntervalMs(), SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS)
|
|
5884
|
+
);
|
|
5885
|
+
if (!result || "error" in result || !Array.isArray(result.pages)) {
|
|
5886
|
+
return void 0;
|
|
5887
|
+
}
|
|
5888
|
+
return result.pages;
|
|
5889
|
+
}
|
|
5236
5890
|
async close() {
|
|
5237
5891
|
if (this.closePromise) {
|
|
5238
5892
|
return this.closePromise;
|
|
@@ -5253,6 +5907,7 @@ var BrowserService = class {
|
|
|
5253
5907
|
if (this.shuttingDown) {
|
|
5254
5908
|
return { closed: false, owned: false, succeeded: true };
|
|
5255
5909
|
}
|
|
5910
|
+
this.stopIdleSweep();
|
|
5256
5911
|
this.shuttingDown = true;
|
|
5257
5912
|
this.lifecycleGeneration += 1;
|
|
5258
5913
|
this.shutdownController.abort();
|
|
@@ -5263,6 +5918,7 @@ var BrowserService = class {
|
|
|
5263
5918
|
const lateConnectionSettled = await settlesWithinTimeout(this.connectionSettlementPromise, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS);
|
|
5264
5919
|
const interruptedShutdown = this.interruptedBrowserShutdown;
|
|
5265
5920
|
const interruptedSucceeded = interruptedShutdown ? await settleWithTimeout(interruptedShutdown, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS).catch(() => void 0) === true : true;
|
|
5921
|
+
await settleWithTimeout(this.idleSweepPromise, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS).catch(() => void 0);
|
|
5266
5922
|
const browserResult = await this.closeBrowser();
|
|
5267
5923
|
return { ...browserResult, succeeded: browserResult.succeeded && connectionSettled && lateConnectionSettled && interruptedSucceeded };
|
|
5268
5924
|
}
|
|
@@ -5274,21 +5930,25 @@ var BrowserService = class {
|
|
|
5274
5930
|
queuedOperations: this.queuedOperations,
|
|
5275
5931
|
currentPageId: this.currentPageId ?? null,
|
|
5276
5932
|
recoveryRequired: this.recoveryRequired,
|
|
5933
|
+
idleTimeoutMs: this.config.browser.idleTimeoutMs,
|
|
5277
5934
|
...this.benchmarkCounters ? { benchmarkCounters: { ...this.benchmarkCounters } } : {}
|
|
5278
5935
|
};
|
|
5279
5936
|
}
|
|
5280
5937
|
sessionSummary() {
|
|
5281
5938
|
const status = this.connectionStatus();
|
|
5282
|
-
return { session_id: this.sessionId, active: status.connected, owned: status.owned, trackedPages: status.trackedPages, queuedOperations: status.queuedOperations, currentPageId: status.currentPageId, recoveryRequired: this.recoveryRequired, lastActivityAt: new Date(this.lastActivityAt).toISOString() };
|
|
5939
|
+
return { session_id: this.sessionId, active: status.connected, owned: status.owned, trackedPages: status.trackedPages, queuedOperations: status.queuedOperations, currentPageId: status.currentPageId, recoveryRequired: this.recoveryRequired, idleTimeoutMs: this.config.browser.idleTimeoutMs, lastActivityAt: new Date(this.lastActivityAt).toISOString() };
|
|
5283
5940
|
}
|
|
5284
5941
|
async doctor() {
|
|
5285
5942
|
const discovered = this.config.browser.executablePath ? void 0 : findChromeExecutable();
|
|
5286
|
-
const
|
|
5943
|
+
const configuredExecutablePath = this.config.browser.executablePath;
|
|
5944
|
+
const executablePath = configuredExecutablePath ?? discovered?.path;
|
|
5945
|
+
const executable = configuredExecutablePath ? { source: "configured", ready: isExecutableReady(configuredExecutablePath) } : discovered ? { source: "discovered", ready: isExecutableReady(discovered.path), label: discovered.label, channel: discovered.channel } : { source: "missing", ready: false };
|
|
5287
5946
|
const endpoint = await this.probeManagedEndpoint();
|
|
5288
5947
|
const browser = endpoint.version?.Browser;
|
|
5289
5948
|
return {
|
|
5290
5949
|
mode: this.config.browser.mode,
|
|
5291
5950
|
executablePath: executablePath ?? null,
|
|
5951
|
+
executable,
|
|
5292
5952
|
...executablePath ? {} : { searchedPaths: chromeExecutableSearchPaths().slice(0, 128) },
|
|
5293
5953
|
userDataDir: this.config.browser.userDataDir ?? null,
|
|
5294
5954
|
endpoint: {
|
|
@@ -5377,18 +6037,7 @@ var BrowserService = class {
|
|
|
5377
6037
|
this.recoveryRequired = !succeeded2;
|
|
5378
6038
|
return { closed: false, owned: false, succeeded: succeeded2 };
|
|
5379
6039
|
}
|
|
5380
|
-
|
|
5381
|
-
if (owned) {
|
|
5382
|
-
await Promise.resolve().then(() => browser.close()).catch((error) => {
|
|
5383
|
-
succeeded = false;
|
|
5384
|
-
this.logger.warn("Browser close failed", { error: String(error) });
|
|
5385
|
-
});
|
|
5386
|
-
} else {
|
|
5387
|
-
await Promise.resolve().then(() => browser.disconnect()).catch((error) => {
|
|
5388
|
-
succeeded = false;
|
|
5389
|
-
this.logger.warn("Browser disconnect failed", { error: String(error) });
|
|
5390
|
-
});
|
|
5391
|
-
}
|
|
6040
|
+
const succeeded = await closeConnectedBrowser(browser, owned, this.logger);
|
|
5392
6041
|
if (!succeeded) {
|
|
5393
6042
|
this.browserShutdownFailure = true;
|
|
5394
6043
|
this.failedBrowserShutdown = { browser, owned };
|
|
@@ -6326,12 +6975,56 @@ var BrowserService = class {
|
|
|
6326
6975
|
return { enabled: false };
|
|
6327
6976
|
case "get_network_log":
|
|
6328
6977
|
return { entries: untrustedLogEntries(state.network.slice(-MAX_LOG_ENTRIES)) };
|
|
6978
|
+
case "search_network_log": {
|
|
6979
|
+
const result = action.query ? this.networkJournal.search(action.query, {
|
|
6980
|
+
pageId: state.id,
|
|
6981
|
+
requestId: action.requestId,
|
|
6982
|
+
url: action.url,
|
|
6983
|
+
method: action.method,
|
|
6984
|
+
status: action.status,
|
|
6985
|
+
resourceType: action.resourceType,
|
|
6986
|
+
offset: action.offset,
|
|
6987
|
+
limit: action.limit
|
|
6988
|
+
}) : this.networkJournal.query({
|
|
6989
|
+
pageId: state.id,
|
|
6990
|
+
requestId: action.requestId,
|
|
6991
|
+
url: action.url,
|
|
6992
|
+
method: action.method,
|
|
6993
|
+
status: action.status,
|
|
6994
|
+
resourceType: action.resourceType,
|
|
6995
|
+
offset: action.offset,
|
|
6996
|
+
limit: action.limit
|
|
6997
|
+
});
|
|
6998
|
+
return {
|
|
6999
|
+
...result,
|
|
7000
|
+
entries: result.entries.map((entry) => ({
|
|
7001
|
+
...entry,
|
|
7002
|
+
url: wrapUntrustedText("network_log_url", redactSecretPlaceholders(entry.url), 4096)
|
|
7003
|
+
}))
|
|
7004
|
+
};
|
|
7005
|
+
}
|
|
7006
|
+
case "resource_blocking": {
|
|
7007
|
+
const operation = requireField(action.operation, "operation");
|
|
7008
|
+
if (operation === "set") {
|
|
7009
|
+
const resourceTypes2 = action.resourceTypes ?? [];
|
|
7010
|
+
if (resourceTypes2.length === 0 || new Set(resourceTypes2).size !== resourceTypes2.length || resourceTypes2.some((resourceType) => !RESOURCE_BLOCKING_TYPES.includes(resourceType))) {
|
|
7011
|
+
throw new AppError("INVALID_ACTION", "Resource blocking set requires a non-empty de-duplicated list of supported resourceTypes.");
|
|
7012
|
+
}
|
|
7013
|
+
state.blockedResourceTypes = new Set(resourceTypes2);
|
|
7014
|
+
} else if (operation === "clear") {
|
|
7015
|
+
state.blockedResourceTypes.clear();
|
|
7016
|
+
}
|
|
7017
|
+
const resourceTypes = RESOURCE_BLOCKING_TYPES.filter((resourceType) => state.blockedResourceTypes.has(resourceType));
|
|
7018
|
+
return { pageId: state.id, operation, resourceTypes };
|
|
7019
|
+
}
|
|
6329
7020
|
case "clear_network_log":
|
|
6330
7021
|
state.network = [];
|
|
7022
|
+
this.networkJournal.clear(state.id);
|
|
6331
7023
|
return { cleared: true };
|
|
6332
7024
|
case "getclear_network_log": {
|
|
6333
7025
|
const entries = untrustedLogEntries(state.network.slice(-MAX_LOG_ENTRIES));
|
|
6334
7026
|
state.network = [];
|
|
7027
|
+
this.networkJournal.clear(state.id);
|
|
6335
7028
|
return { entries, cleared: true };
|
|
6336
7029
|
}
|
|
6337
7030
|
case "enable_console_log":
|
|
@@ -6888,27 +7581,53 @@ var BrowserService = class {
|
|
|
6888
7581
|
case "upload_file": {
|
|
6889
7582
|
throwIfAborted(signal);
|
|
6890
7583
|
const selector = await this.selectorFor(state, targetForAction(action, "selector"), action.frameId);
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
throwIfAborted(signal);
|
|
6894
|
-
let input;
|
|
7584
|
+
const stagedFiles = [];
|
|
7585
|
+
let input = null;
|
|
6895
7586
|
try {
|
|
7587
|
+
if (action.filePath !== void 0 && action.filePaths !== void 0) {
|
|
7588
|
+
throw new AppError("INVALID_ACTION", "Provide filePath or filePaths, not both.");
|
|
7589
|
+
}
|
|
7590
|
+
const rawPaths = action.filePaths ?? (action.filePath !== void 0 ? [action.filePath] : []);
|
|
7591
|
+
if (rawPaths.length === 0 || rawPaths.length > 20) {
|
|
7592
|
+
throw new AppError("INVALID_ACTION", "Upload requires one to 20 paths in filePath or filePaths.");
|
|
7593
|
+
}
|
|
7594
|
+
let totalBytes = 0;
|
|
7595
|
+
for (const rawPath of rawPaths) {
|
|
7596
|
+
throwIfAborted(signal);
|
|
7597
|
+
const staged = await this.stageUploadFile(rawPath, signal);
|
|
7598
|
+
stagedFiles.push(staged);
|
|
7599
|
+
totalBytes += staged.size;
|
|
7600
|
+
if (totalBytes > MAX_UPLOAD_TOTAL_BYTES) {
|
|
7601
|
+
throw new AppError("FILE_TOO_LARGE", "The combined upload sources exceed the 100 MiB size limit.");
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
throwIfAborted(signal);
|
|
6896
7605
|
input = await frame.$(selector);
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
await input.uploadFile(staged.path);
|
|
7606
|
+
if (!input) {
|
|
7607
|
+
throw new AppError("ELEMENT_NOT_FOUND", `No element matched '${selector}'.`);
|
|
7608
|
+
}
|
|
7609
|
+
if (stagedFiles.length > 1) {
|
|
7610
|
+
const supportsMultiple = typeof input.evaluate === "function" && await input.evaluate((element) => element instanceof HTMLInputElement && element.type === "file" && element.multiple);
|
|
7611
|
+
if (!supportsMultiple) {
|
|
7612
|
+
throw new AppError("MULTIPLE_FILES_UNSUPPORTED", "Multiple uploads require a file input with the multiple attribute.");
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7615
|
+
await input.uploadFile(...stagedFiles.map((staged) => staged.path));
|
|
6907
7616
|
throwIfAborted(signal);
|
|
6908
|
-
|
|
7617
|
+
const names = stagedFiles.map((staged) => wrapUntrustedText("uploaded_file_name", redactSecretPlaceholders(basename2(staged.displayName)), 512));
|
|
7618
|
+
const bytes = Math.min(totalBytes, Number.MAX_SAFE_INTEGER);
|
|
7619
|
+
if (names.length === 1) {
|
|
7620
|
+
return { uploaded: names[0], bytes };
|
|
7621
|
+
}
|
|
7622
|
+
return { uploaded: names, files: names, count: names.length, bytes };
|
|
6909
7623
|
} finally {
|
|
6910
|
-
|
|
6911
|
-
|
|
7624
|
+
try {
|
|
7625
|
+
await input?.dispose();
|
|
7626
|
+
} catch {
|
|
7627
|
+
}
|
|
7628
|
+
for (const staged of stagedFiles) {
|
|
7629
|
+
await unlinkIfPresent(staged.path).catch(() => void 0);
|
|
7630
|
+
}
|
|
6912
7631
|
}
|
|
6913
7632
|
}
|
|
6914
7633
|
case "screenshot": {
|
|
@@ -7104,8 +7823,282 @@ var BrowserService = class {
|
|
|
7104
7823
|
}, query, { maxNodes: MAX_DOM_TRAVERSAL_NODES, maxChars: MAX_TEXT_SCAN_CHARS });
|
|
7105
7824
|
return { query, matches: matches.matches.map((match) => wrapUntrustedText("page_match", redactSecretPlaceholders(match), 500)), totalMatches: matches.totalMatches, matchesTruncated: matches.totalMatches > matches.matches.length || matches.scanTruncated };
|
|
7106
7825
|
}
|
|
7826
|
+
case "inspect_element": {
|
|
7827
|
+
const selector = await this.selectorFor(state, targetForAction(action, "target"), action.frameId, frame);
|
|
7828
|
+
const maxDepth = Number.isSafeInteger(action.maxDepth) ? Math.max(0, Math.min(3, action.maxDepth)) : 1;
|
|
7829
|
+
const maxChildren = Number.isSafeInteger(action.maxChildren) ? Math.max(1, Math.min(100, action.maxChildren)) : 20;
|
|
7830
|
+
const inspected = await frame.$eval(selector, (element, options) => {
|
|
7831
|
+
const excludedTags = /* @__PURE__ */ new Set(["script", "style", "template", "noscript"]);
|
|
7832
|
+
const safeAttributes = new Set(options.safeAttributeNames);
|
|
7833
|
+
const safeDataAttributes = new Set(options.safeDataAttributeNames);
|
|
7834
|
+
const styleNames = [
|
|
7835
|
+
"display",
|
|
7836
|
+
"visibility",
|
|
7837
|
+
"position",
|
|
7838
|
+
"color",
|
|
7839
|
+
"backgroundColor",
|
|
7840
|
+
"width",
|
|
7841
|
+
"height",
|
|
7842
|
+
"zIndex",
|
|
7843
|
+
"fontFamily",
|
|
7844
|
+
"fontSize",
|
|
7845
|
+
"fontWeight",
|
|
7846
|
+
"lineHeight",
|
|
7847
|
+
"opacity",
|
|
7848
|
+
"transform"
|
|
7849
|
+
];
|
|
7850
|
+
const animationNames = [
|
|
7851
|
+
"animationName",
|
|
7852
|
+
"animationDuration",
|
|
7853
|
+
"animationTimingFunction",
|
|
7854
|
+
"animationDelay",
|
|
7855
|
+
"animationIterationCount",
|
|
7856
|
+
"animationDirection",
|
|
7857
|
+
"animationFillMode",
|
|
7858
|
+
"animationPlayState",
|
|
7859
|
+
"transitionProperty",
|
|
7860
|
+
"transitionDuration",
|
|
7861
|
+
"transitionTimingFunction",
|
|
7862
|
+
"transitionDelay",
|
|
7863
|
+
"transform"
|
|
7864
|
+
];
|
|
7865
|
+
let visitedNodes = 0;
|
|
7866
|
+
const boundedString = (value, limit = options.maxStringChars) => typeof value === "string" ? value.slice(0, limit) : "";
|
|
7867
|
+
const boundedRect = (target) => {
|
|
7868
|
+
const rect = target.getBoundingClientRect();
|
|
7869
|
+
const bound = (value, minimum = -1e7) => Number.isFinite(value) ? Math.max(minimum, Math.min(1e7, Math.round(value))) : 0;
|
|
7870
|
+
return { x: bound(rect.x), y: bound(rect.y), width: bound(rect.width, 0), height: bound(rect.height, 0) };
|
|
7871
|
+
};
|
|
7872
|
+
const collectAttributes = (target) => {
|
|
7873
|
+
const attributes = {};
|
|
7874
|
+
let omittedAttributes = 0;
|
|
7875
|
+
const attributeCount = target.attributes.length;
|
|
7876
|
+
const inspectedAttributes = Math.min(attributeCount, 40);
|
|
7877
|
+
for (let index = 0; index < inspectedAttributes; index += 1) {
|
|
7878
|
+
const attribute = target.attributes[index];
|
|
7879
|
+
if (!attribute) continue;
|
|
7880
|
+
const name = attribute.name.toLowerCase();
|
|
7881
|
+
const allowed = safeAttributes.has(name) || safeDataAttributes.has(name) || /^aria-[a-z0-9_-]+$/i.test(name);
|
|
7882
|
+
if (!allowed) {
|
|
7883
|
+
omittedAttributes += 1;
|
|
7884
|
+
continue;
|
|
7885
|
+
}
|
|
7886
|
+
const value = boundedString(attribute.value, 200);
|
|
7887
|
+
attributes[name.slice(0, 100)] = value;
|
|
7888
|
+
if (value.length < attribute.value.length) omittedAttributes += 1;
|
|
7889
|
+
}
|
|
7890
|
+
omittedAttributes += Math.max(0, attributeCount - inspectedAttributes);
|
|
7891
|
+
return { attributes, omittedAttributes };
|
|
7892
|
+
};
|
|
7893
|
+
const readSafeText = (target) => {
|
|
7894
|
+
const tag = target.tagName.toLowerCase();
|
|
7895
|
+
if (["input", "textarea", "select", "option"].includes(tag)) {
|
|
7896
|
+
return { text: "", truncated: false };
|
|
7897
|
+
}
|
|
7898
|
+
const stack = [target];
|
|
7899
|
+
let text = "";
|
|
7900
|
+
let truncated = false;
|
|
7901
|
+
let visited = 0;
|
|
7902
|
+
while (stack.length > 0) {
|
|
7903
|
+
const node = stack.pop();
|
|
7904
|
+
if (!node) break;
|
|
7905
|
+
visited += 1;
|
|
7906
|
+
if (visited > options.maxNodes) {
|
|
7907
|
+
truncated = true;
|
|
7908
|
+
break;
|
|
7909
|
+
}
|
|
7910
|
+
if (node.nodeType === 3) {
|
|
7911
|
+
const raw = node.nodeValue ?? "";
|
|
7912
|
+
const remaining = options.maxStringChars - text.length;
|
|
7913
|
+
if (remaining <= 0) {
|
|
7914
|
+
truncated = true;
|
|
7915
|
+
break;
|
|
7916
|
+
}
|
|
7917
|
+
text += raw.slice(0, remaining);
|
|
7918
|
+
if (raw.length > remaining) truncated = true;
|
|
7919
|
+
continue;
|
|
7920
|
+
}
|
|
7921
|
+
if (node.nodeType !== 1) continue;
|
|
7922
|
+
const childElement = node;
|
|
7923
|
+
if (excludedTags.has(childElement.tagName.toLowerCase())) continue;
|
|
7924
|
+
const children = childElement.childNodes;
|
|
7925
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
7926
|
+
const child = children[index];
|
|
7927
|
+
if (child) stack.push(child);
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
return { text: text.replace(/\s+/g, " ").trim().slice(0, options.maxStringChars), truncated };
|
|
7931
|
+
};
|
|
7932
|
+
const readStyle = (target, pseudo = "") => {
|
|
7933
|
+
let style;
|
|
7934
|
+
try {
|
|
7935
|
+
style = getComputedStyle(target, pseudo);
|
|
7936
|
+
} catch {
|
|
7937
|
+
return {};
|
|
7938
|
+
}
|
|
7939
|
+
const output = {};
|
|
7940
|
+
for (const name of styleNames) {
|
|
7941
|
+
output[name] = boundedString(style[name]);
|
|
7942
|
+
}
|
|
7943
|
+
return output;
|
|
7944
|
+
};
|
|
7945
|
+
const readPseudo = (target, pseudo) => {
|
|
7946
|
+
let style;
|
|
7947
|
+
try {
|
|
7948
|
+
style = getComputedStyle(target, pseudo);
|
|
7949
|
+
} catch {
|
|
7950
|
+
return { content: "", styles: {} };
|
|
7951
|
+
}
|
|
7952
|
+
const styles = {};
|
|
7953
|
+
for (const name of styleNames) {
|
|
7954
|
+
styles[name] = boundedString(style[name]);
|
|
7955
|
+
}
|
|
7956
|
+
return { content: boundedString(style.content), styles };
|
|
7957
|
+
};
|
|
7958
|
+
const readAnimations = (target) => {
|
|
7959
|
+
let style;
|
|
7960
|
+
try {
|
|
7961
|
+
style = getComputedStyle(target);
|
|
7962
|
+
} catch {
|
|
7963
|
+
return {};
|
|
7964
|
+
}
|
|
7965
|
+
const output = {};
|
|
7966
|
+
for (const name of animationNames) {
|
|
7967
|
+
output[name] = boundedString(style[name]);
|
|
7968
|
+
}
|
|
7969
|
+
return output;
|
|
7970
|
+
};
|
|
7971
|
+
const collectChildren = (parent, depth) => {
|
|
7972
|
+
const children = [];
|
|
7973
|
+
let childrenTruncated = false;
|
|
7974
|
+
let omittedChildren = 0;
|
|
7975
|
+
const childElements = Array.from(parent.children);
|
|
7976
|
+
if (depth >= options.maxDepth) {
|
|
7977
|
+
return {
|
|
7978
|
+
children,
|
|
7979
|
+
childrenTruncated: childElements.length > 0,
|
|
7980
|
+
omittedChildren: childElements.length
|
|
7981
|
+
};
|
|
7982
|
+
}
|
|
7983
|
+
for (let index = 0; index < childElements.length; index += 1) {
|
|
7984
|
+
const child = childElements[index];
|
|
7985
|
+
if (!child) continue;
|
|
7986
|
+
if (excludedTags.has(child.tagName.toLowerCase())) {
|
|
7987
|
+
omittedChildren += 1;
|
|
7988
|
+
continue;
|
|
7989
|
+
}
|
|
7990
|
+
if (children.length >= options.maxChildren) {
|
|
7991
|
+
childrenTruncated = true;
|
|
7992
|
+
omittedChildren += childElements.length - index;
|
|
7993
|
+
break;
|
|
7994
|
+
}
|
|
7995
|
+
if (visitedNodes >= options.maxNodes) {
|
|
7996
|
+
childrenTruncated = true;
|
|
7997
|
+
omittedChildren += childElements.length - index;
|
|
7998
|
+
break;
|
|
7999
|
+
}
|
|
8000
|
+
visitedNodes += 1;
|
|
8001
|
+
const childAttributes = collectAttributes(child);
|
|
8002
|
+
const childText = readSafeText(child);
|
|
8003
|
+
const nested = collectChildren(child, depth + 1);
|
|
8004
|
+
children.push({
|
|
8005
|
+
tag: child.tagName.toLowerCase(),
|
|
8006
|
+
rect: boundedRect(child),
|
|
8007
|
+
text: childText.text,
|
|
8008
|
+
textTruncated: childText.truncated,
|
|
8009
|
+
attributes: childAttributes.attributes,
|
|
8010
|
+
omittedAttributes: childAttributes.omittedAttributes,
|
|
8011
|
+
children: nested.children,
|
|
8012
|
+
childrenTruncated: nested.childrenTruncated,
|
|
8013
|
+
omittedChildren: nested.omittedChildren
|
|
8014
|
+
});
|
|
8015
|
+
if (nested.childrenTruncated) childrenTruncated = true;
|
|
8016
|
+
omittedChildren += nested.omittedChildren;
|
|
8017
|
+
}
|
|
8018
|
+
return { children, childrenTruncated, omittedChildren };
|
|
8019
|
+
};
|
|
8020
|
+
const rootAttributes = collectAttributes(element);
|
|
8021
|
+
const rootText = readSafeText(element);
|
|
8022
|
+
const childResult = collectChildren(element, 0);
|
|
8023
|
+
const contentOmitted = excludedTags.has(element.tagName.toLowerCase());
|
|
8024
|
+
return {
|
|
8025
|
+
tag: element.tagName.toLowerCase(),
|
|
8026
|
+
rect: boundedRect(element),
|
|
8027
|
+
text: rootText.text,
|
|
8028
|
+
textTruncated: rootText.truncated,
|
|
8029
|
+
attributes: rootAttributes.attributes,
|
|
8030
|
+
omittedAttributes: rootAttributes.omittedAttributes,
|
|
8031
|
+
computedStyles: readStyle(element),
|
|
8032
|
+
pseudoElements: {
|
|
8033
|
+
before: readPseudo(element, "::before"),
|
|
8034
|
+
after: readPseudo(element, "::after")
|
|
8035
|
+
},
|
|
8036
|
+
animations: readAnimations(element),
|
|
8037
|
+
children: childResult.children,
|
|
8038
|
+
childrenTruncated: childResult.childrenTruncated,
|
|
8039
|
+
omittedChildren: childResult.omittedChildren,
|
|
8040
|
+
contentOmitted,
|
|
8041
|
+
omittedContent: contentOmitted ? 1 : 0,
|
|
8042
|
+
truncated: contentOmitted || rootText.truncated || rootAttributes.omittedAttributes > 0 || childResult.childrenTruncated || childResult.omittedChildren > 0
|
|
8043
|
+
};
|
|
8044
|
+
}, {
|
|
8045
|
+
maxDepth,
|
|
8046
|
+
maxChildren,
|
|
8047
|
+
maxNodes: MAX_INSPECT_NODES,
|
|
8048
|
+
maxStringChars: MAX_INSPECT_STRING_CHARS,
|
|
8049
|
+
safeAttributeNames: [...SAFE_ELEMENT_ATTRIBUTE_NAMES],
|
|
8050
|
+
safeDataAttributeNames: [...SAFE_ELEMENT_DATA_ATTRIBUTE_NAMES]
|
|
8051
|
+
});
|
|
8052
|
+
const wrap = (value, kind, max = MAX_INSPECT_STRING_CHARS) => wrapUntrustedText(kind, redactSecretPlaceholders(value), max);
|
|
8053
|
+
const wrapAttributes = (attributes) => Object.fromEntries(
|
|
8054
|
+
// Attribute names are restricted to the fixed allowlist or the
|
|
8055
|
+
// bounded aria-* grammar in the page callback. Keep those names as
|
|
8056
|
+
// keys for ergonomic parity with find_elements; values are still
|
|
8057
|
+
// untrusted and wrapped below.
|
|
8058
|
+
Object.entries(attributes).map(([name, value]) => [name, wrap(value, "inspect_attribute", 200)])
|
|
8059
|
+
);
|
|
8060
|
+
const wrapChild = (child) => ({
|
|
8061
|
+
tag: wrap(child.tag, "inspect_child_tag", 100),
|
|
8062
|
+
rect: child.rect,
|
|
8063
|
+
text: wrap(child.text, "inspect_child_text"),
|
|
8064
|
+
textTruncated: child.textTruncated,
|
|
8065
|
+
attributes: wrapAttributes(child.attributes),
|
|
8066
|
+
omittedAttributes: child.omittedAttributes,
|
|
8067
|
+
children: child.children.map(wrapChild),
|
|
8068
|
+
childrenTruncated: child.childrenTruncated,
|
|
8069
|
+
omittedChildren: child.omittedChildren
|
|
8070
|
+
});
|
|
8071
|
+
const wrapStyleMap = (styles) => Object.fromEntries(
|
|
8072
|
+
Object.entries(styles).map(([name, value]) => [name, wrap(value, "inspect_style")])
|
|
8073
|
+
);
|
|
8074
|
+
const wrapPseudo = (pseudo) => ({
|
|
8075
|
+
content: wrap(pseudo.content, "inspect_pseudo_content"),
|
|
8076
|
+
styles: wrapStyleMap(pseudo.styles)
|
|
8077
|
+
});
|
|
8078
|
+
return {
|
|
8079
|
+
tag: wrap(inspected.tag, "inspect_tag", 100),
|
|
8080
|
+
selector: wrap(selector, "inspect_selector"),
|
|
8081
|
+
rect: inspected.rect,
|
|
8082
|
+
text: wrap(inspected.text, "inspect_text"),
|
|
8083
|
+
textTruncated: inspected.textTruncated,
|
|
8084
|
+
attributes: wrapAttributes(inspected.attributes),
|
|
8085
|
+
omittedAttributes: inspected.omittedAttributes,
|
|
8086
|
+
computedStyles: wrapStyleMap(inspected.computedStyles),
|
|
8087
|
+
pseudoElements: {
|
|
8088
|
+
before: wrapPseudo(inspected.pseudoElements.before),
|
|
8089
|
+
after: wrapPseudo(inspected.pseudoElements.after)
|
|
8090
|
+
},
|
|
8091
|
+
animations: wrapStyleMap(inspected.animations),
|
|
8092
|
+
children: inspected.children.map(wrapChild),
|
|
8093
|
+
childrenTruncated: inspected.childrenTruncated,
|
|
8094
|
+
omittedChildren: inspected.omittedChildren,
|
|
8095
|
+
contentOmitted: inspected.contentOmitted,
|
|
8096
|
+
omittedContent: inspected.omittedContent,
|
|
8097
|
+
truncated: inspected.truncated
|
|
8098
|
+
};
|
|
8099
|
+
}
|
|
7107
8100
|
case "find_elements": {
|
|
7108
|
-
let collectFindElements2 = function(matches, fallbackSelector) {
|
|
8101
|
+
let collectFindElements2 = function(matches, fallbackSelector, safeAttributeNames, safeDataAttributeNames) {
|
|
7109
8102
|
const boundedText = (root) => {
|
|
7110
8103
|
const maybeChildNodes = root.childNodes;
|
|
7111
8104
|
if (!maybeChildNodes) {
|
|
@@ -7179,8 +8172,8 @@ var BrowserService = class {
|
|
|
7179
8172
|
const boundedHeight = Number.isFinite(rect.height) ? Math.max(-1e7, Math.min(1e7, Math.round(rect.height))) : 0;
|
|
7180
8173
|
const attributes = {};
|
|
7181
8174
|
let omittedAttributes = 0;
|
|
7182
|
-
const safeAttributes =
|
|
7183
|
-
const safeDataAttributes =
|
|
8175
|
+
const safeAttributes = new Set(safeAttributeNames);
|
|
8176
|
+
const safeDataAttributes = new Set(safeDataAttributeNames);
|
|
7184
8177
|
const attributeCount = element.attributes.length;
|
|
7185
8178
|
const inspectedAttributes = Math.min(attributeCount, 40);
|
|
7186
8179
|
for (let index = 0; index < inspectedAttributes; index += 1) {
|
|
@@ -7209,7 +8202,7 @@ var BrowserService = class {
|
|
|
7209
8202
|
var collectFindElements = collectFindElements2;
|
|
7210
8203
|
const selector = targetForAction(action, "selector");
|
|
7211
8204
|
const safeSelector = await this.selectorFor(state, selector, action.frameId, frame);
|
|
7212
|
-
const elements = await frame.$$eval(safeSelector, collectFindElements2, safeSelector);
|
|
8205
|
+
const elements = await frame.$$eval(safeSelector, collectFindElements2, safeSelector, [...SAFE_ELEMENT_ATTRIBUTE_NAMES], [...SAFE_ELEMENT_DATA_ATTRIBUTE_NAMES]);
|
|
7213
8206
|
return elements.map((element) => ({
|
|
7214
8207
|
tag: element.tag,
|
|
7215
8208
|
selector: wrapUntrustedText("element_selector", redactSecretPlaceholders(element.selector), 500),
|
|
@@ -7386,7 +8379,8 @@ var BrowserService = class {
|
|
|
7386
8379
|
case "solve_challenge":
|
|
7387
8380
|
return this.solveChallenge(state, action, signal);
|
|
7388
8381
|
case "get_cookies": {
|
|
7389
|
-
const
|
|
8382
|
+
const scopedUrl = action.url ? await this.policy.assertNavigationAllowedAsync(action.url) : void 0;
|
|
8383
|
+
const cookies = scopedUrl ? await page.cookies(scopedUrl.toString()) : await page.cookies();
|
|
7390
8384
|
return cookies.slice(0, 200).map((cookie) => ({
|
|
7391
8385
|
name: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookie.name), 256),
|
|
7392
8386
|
domain: wrapUntrustedText("cookie_domain", redactSecretPlaceholders(cookie.domain), 512),
|
|
@@ -7404,15 +8398,16 @@ var BrowserService = class {
|
|
|
7404
8398
|
if (action.cookieDomain) {
|
|
7405
8399
|
await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
|
|
7406
8400
|
}
|
|
7407
|
-
await page.setCookie({ name: cookieName, value: action.cookieValue ?? action.value ?? "", url: url.toString(), domain: action.cookieDomain, path: action.cookiePath ?? "/", secure: action.cookieSecure, httpOnly: action.cookieHttpOnly });
|
|
8401
|
+
await page.setCookie({ name: cookieName, value: action.cookieValue ?? action.value ?? "", url: url.toString(), domain: action.cookieDomain, path: action.cookiePath ?? "/", secure: action.cookieSecure, httpOnly: action.cookieHttpOnly, sameSite: action.cookieSameSite });
|
|
7408
8402
|
return { set: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
|
|
7409
8403
|
}
|
|
7410
8404
|
case "delete_cookies": {
|
|
7411
8405
|
const cookieName = requireField(action.cookieName, "cookieName");
|
|
8406
|
+
const scopedUrl = action.url ? await this.policy.assertNavigationAllowedAsync(action.url) : void 0;
|
|
7412
8407
|
if (action.cookieDomain) {
|
|
7413
8408
|
await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
|
|
7414
8409
|
}
|
|
7415
|
-
await page.deleteCookie({ name: cookieName, domain: action.cookieDomain, path: action.cookiePath ?? "/" });
|
|
8410
|
+
await page.deleteCookie({ name: cookieName, ...scopedUrl ? { url: scopedUrl.toString() } : {}, domain: action.cookieDomain, path: action.cookiePath ?? "/" });
|
|
7416
8411
|
return { deleted: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
|
|
7417
8412
|
}
|
|
7418
8413
|
case "get_storage": {
|
|
@@ -7657,6 +8652,7 @@ var BrowserService = class {
|
|
|
7657
8652
|
if (!executablePath) {
|
|
7658
8653
|
throw new AppError("BROWSER_NOT_CONFIGURED", `Managed browser mode could not find Chrome. Checked: ${chromeExecutableSearchPaths().join(", ")}. Install Chrome or set SMOOTH_OPERATOR_BROWSER_EXECUTABLE.`);
|
|
7659
8654
|
}
|
|
8655
|
+
this.assertExecutableReady(executablePath);
|
|
7660
8656
|
connection = this.launch({
|
|
7661
8657
|
headless: this.config.browser.headless,
|
|
7662
8658
|
executablePath,
|
|
@@ -7674,6 +8670,7 @@ var BrowserService = class {
|
|
|
7674
8670
|
if (!this.config.browser.executablePath) {
|
|
7675
8671
|
throw new AppError("BROWSER_NOT_CONFIGURED", "Launch mode requires SMOOTH_OPERATOR_BROWSER_EXECUTABLE.");
|
|
7676
8672
|
}
|
|
8673
|
+
this.assertExecutableReady(this.config.browser.executablePath);
|
|
7677
8674
|
ownsBrowser = true;
|
|
7678
8675
|
connection = this.launch({
|
|
7679
8676
|
headless: this.config.browser.headless,
|
|
@@ -7701,6 +8698,13 @@ var BrowserService = class {
|
|
|
7701
8698
|
await closeConnectedBrowser(lateBrowser, ownsBrowser, this.logger);
|
|
7702
8699
|
throw new AppError("SERVER_CLOSING", "The browser runtime is shutting down.", { retryable: true });
|
|
7703
8700
|
}
|
|
8701
|
+
await this.installTargetGuard(browser);
|
|
8702
|
+
if (this.shuttingDown || generation !== this.lifecycleGeneration || this.shutdownController.signal.aborted) {
|
|
8703
|
+
const lateBrowser = browser;
|
|
8704
|
+
browser = void 0;
|
|
8705
|
+
await closeConnectedBrowser(lateBrowser, ownsBrowser, this.logger);
|
|
8706
|
+
throw new AppError("SERVER_CLOSING", "The browser runtime is shutting down.", { retryable: true });
|
|
8707
|
+
}
|
|
7704
8708
|
this.browser = browser;
|
|
7705
8709
|
this.ownsBrowser = ownsBrowser;
|
|
7706
8710
|
const connectedBrowser = browser;
|
|
@@ -7715,9 +8719,8 @@ var BrowserService = class {
|
|
|
7715
8719
|
this.lifecycleGeneration += 1;
|
|
7716
8720
|
this.retireAllStates();
|
|
7717
8721
|
});
|
|
7718
|
-
this.installTargetGuard(browser);
|
|
7719
8722
|
browser.on("targetcreated", (target) => {
|
|
7720
|
-
void this.
|
|
8723
|
+
void this.trackTargetPreparation(target);
|
|
7721
8724
|
});
|
|
7722
8725
|
return browser;
|
|
7723
8726
|
} catch (error) {
|
|
@@ -7763,6 +8766,17 @@ var BrowserService = class {
|
|
|
7763
8766
|
}
|
|
7764
8767
|
return (await loadPuppeteer()).launch(options);
|
|
7765
8768
|
}
|
|
8769
|
+
assertExecutableReady(executablePath) {
|
|
8770
|
+
let ready = false;
|
|
8771
|
+
try {
|
|
8772
|
+
ready = this.dependencies.isExecutableReady?.(executablePath) ?? isExecutableReady(executablePath);
|
|
8773
|
+
} catch {
|
|
8774
|
+
ready = false;
|
|
8775
|
+
}
|
|
8776
|
+
if (!ready) {
|
|
8777
|
+
throw new AppError("BROWSER_NOT_CONFIGURED", "Configured browser executable is not ready.", { retryable: true });
|
|
8778
|
+
}
|
|
8779
|
+
}
|
|
7766
8780
|
async connect(options) {
|
|
7767
8781
|
if (this.dependencies.connect) {
|
|
7768
8782
|
return this.dependencies.connect(options);
|
|
@@ -7779,7 +8793,9 @@ var BrowserService = class {
|
|
|
7779
8793
|
try {
|
|
7780
8794
|
const info = await lstat(activePortPath);
|
|
7781
8795
|
if (!info.isFile() || info.size > 4096) {
|
|
7782
|
-
this.logger.debug("Managed browser DevTools endpoint file is invalid", {
|
|
8796
|
+
this.logger.debug("Managed browser DevTools endpoint file is invalid", {
|
|
8797
|
+
endpointFile: { kind: "devtools-active-port", regular: info.isFile(), bounded: info.size <= 4096 }
|
|
8798
|
+
});
|
|
7783
8799
|
return { state: "stale-probe-failed" };
|
|
7784
8800
|
}
|
|
7785
8801
|
raw = await readFile(activePortPath, "utf8");
|
|
@@ -7787,19 +8803,24 @@ var BrowserService = class {
|
|
|
7787
8803
|
if (isMissingFile(error)) {
|
|
7788
8804
|
return { state: "no-file" };
|
|
7789
8805
|
}
|
|
7790
|
-
this.logger.debug("Managed browser DevTools endpoint file could not be read", {
|
|
8806
|
+
this.logger.debug("Managed browser DevTools endpoint file could not be read", {
|
|
8807
|
+
endpointFile: { kind: "devtools-active-port", available: false },
|
|
8808
|
+
error: safeErrorDiagnostic(error)
|
|
8809
|
+
});
|
|
7791
8810
|
return { state: "stale-probe-failed" };
|
|
7792
8811
|
}
|
|
7793
8812
|
const browserURL = parseDevToolsActivePort(raw);
|
|
7794
8813
|
if (!browserURL) {
|
|
7795
|
-
this.logger.debug("Managed browser DevTools endpoint file is malformed", {
|
|
8814
|
+
this.logger.debug("Managed browser DevTools endpoint file is malformed", {
|
|
8815
|
+
endpointFile: { kind: "devtools-active-port", available: true, valid: false }
|
|
8816
|
+
});
|
|
7796
8817
|
return { state: "stale-probe-failed" };
|
|
7797
8818
|
}
|
|
7798
8819
|
try {
|
|
7799
8820
|
const version = await (this.dependencies.probeEndpoint ?? probeDevToolsEndpoint)(browserURL, 2e3);
|
|
7800
8821
|
return { state: "live", browserURL, version };
|
|
7801
8822
|
} catch (error) {
|
|
7802
|
-
this.logger.debug("Managed browser DevTools endpoint probe failed", { browserURL, error:
|
|
8823
|
+
this.logger.debug("Managed browser DevTools endpoint probe failed", { browserURL, error: safeErrorDiagnostic(error) });
|
|
7803
8824
|
return { state: "stale-probe-failed" };
|
|
7804
8825
|
}
|
|
7805
8826
|
}
|
|
@@ -7943,14 +8964,18 @@ var BrowserService = class {
|
|
|
7943
8964
|
return matches[0]?.id;
|
|
7944
8965
|
}
|
|
7945
8966
|
/** Install a Fetch guard at the CDP boundary to policy-check new targets while paused. */
|
|
7946
|
-
installTargetGuard(browser) {
|
|
8967
|
+
async installTargetGuard(browser) {
|
|
7947
8968
|
if (this.targetGuardConnection) {
|
|
8969
|
+
if (this.targetGuardReadinessPromise) {
|
|
8970
|
+
await this.targetGuardReadinessPromise;
|
|
8971
|
+
}
|
|
7948
8972
|
return;
|
|
7949
8973
|
}
|
|
7950
8974
|
const connection = browser._connection;
|
|
7951
8975
|
if (!connection || typeof connection.on !== "function") {
|
|
7952
8976
|
this.targetGuardUnavailable = true;
|
|
7953
8977
|
this.logger.warn("Browser target guard is unavailable; popup actions will be blocked");
|
|
8978
|
+
this.targetGuardReadinessPromise = Promise.resolve();
|
|
7954
8979
|
return;
|
|
7955
8980
|
}
|
|
7956
8981
|
this.targetGuardUnavailable = false;
|
|
@@ -7958,6 +8983,7 @@ var BrowserService = class {
|
|
|
7958
8983
|
if (typeof targetConnection.isAutoAttached !== "function") {
|
|
7959
8984
|
this.targetGuardUnavailable = true;
|
|
7960
8985
|
this.logger.warn("Browser target guard is unavailable; attachment ownership cannot be determined");
|
|
8986
|
+
this.targetGuardReadinessPromise = Promise.resolve();
|
|
7961
8987
|
return;
|
|
7962
8988
|
}
|
|
7963
8989
|
const sessionListener = (value) => {
|
|
@@ -7971,6 +8997,10 @@ var BrowserService = class {
|
|
|
7971
8997
|
if (!event) {
|
|
7972
8998
|
return;
|
|
7973
8999
|
}
|
|
9000
|
+
if (this.handledTargetGuardSessions.has(event.sessionId)) {
|
|
9001
|
+
return;
|
|
9002
|
+
}
|
|
9003
|
+
this.handledTargetGuardSessions.add(event.sessionId);
|
|
7974
9004
|
const session = this.pendingTargetGuardSessions.get(event.sessionId) ?? getCdpSession(targetConnection, event.sessionId);
|
|
7975
9005
|
this.pendingTargetGuardSessions.delete(event.sessionId);
|
|
7976
9006
|
this.pendingTargetGuardInfos.delete(event.sessionId);
|
|
@@ -7989,6 +9019,16 @@ var BrowserService = class {
|
|
|
7989
9019
|
}
|
|
7990
9020
|
return;
|
|
7991
9021
|
}
|
|
9022
|
+
if (!this.targetGuardOriginalEmit || !this.targetGuardWrappedEmit) {
|
|
9023
|
+
this.targetGuardUnavailable = true;
|
|
9024
|
+
this.unguardedTargetSessions.add(event.sessionId);
|
|
9025
|
+
if (targetConnection.send) {
|
|
9026
|
+
void targetConnection.send("Target.closeTarget", { targetId: event.targetInfo.targetId }).catch(() => void 0);
|
|
9027
|
+
}
|
|
9028
|
+
void sendSessionCommand(session, "Page.close").catch(() => void 0);
|
|
9029
|
+
this.logger.warn("New browser target guard cannot safely gate debugger resume");
|
|
9030
|
+
return;
|
|
9031
|
+
}
|
|
7992
9032
|
if (!session) {
|
|
7993
9033
|
this.unguardedTargetSessions.add(event.sessionId);
|
|
7994
9034
|
if (targetConnection.send) {
|
|
@@ -7998,7 +9038,7 @@ var BrowserService = class {
|
|
|
7998
9038
|
return;
|
|
7999
9039
|
}
|
|
8000
9040
|
void this.guardTargetSession(session, event.targetInfo).catch((error) => {
|
|
8001
|
-
this.logger.warn("New browser target guard failed", { error:
|
|
9041
|
+
this.logger.warn("New browser target guard failed", { error: safeErrorDiagnostic(error) });
|
|
8002
9042
|
});
|
|
8003
9043
|
};
|
|
8004
9044
|
const detachedListener = (value) => {
|
|
@@ -8015,12 +9055,14 @@ var BrowserService = class {
|
|
|
8015
9055
|
}
|
|
8016
9056
|
this.pendingTargetGuardSessions.delete(value.sessionId);
|
|
8017
9057
|
this.pendingTargetGuardInfos.delete(value.sessionId);
|
|
9058
|
+
this.handledTargetGuardSessions.delete(value.sessionId);
|
|
8018
9059
|
};
|
|
8019
9060
|
targetConnection.on("sessionattached", sessionListener);
|
|
8020
9061
|
targetConnection.on("Target.attachedToTarget", rawListener);
|
|
8021
9062
|
targetConnection.on("Target.detachedFromTarget", detachedListener);
|
|
8022
9063
|
const originalEmit = targetConnection.emit;
|
|
8023
|
-
|
|
9064
|
+
let emitGuardInstalled = false;
|
|
9065
|
+
if (typeof originalEmit === "function") {
|
|
8024
9066
|
const wrappedEmit = (event, value) => {
|
|
8025
9067
|
if (event === "Target.attachedToTarget") {
|
|
8026
9068
|
rawListener(value);
|
|
@@ -8029,8 +9071,12 @@ var BrowserService = class {
|
|
|
8029
9071
|
};
|
|
8030
9072
|
try {
|
|
8031
9073
|
targetConnection.emit = wrappedEmit;
|
|
9074
|
+
if (targetConnection.emit !== wrappedEmit) {
|
|
9075
|
+
throw new Error("Connection emit wrapper was not installed.");
|
|
9076
|
+
}
|
|
8032
9077
|
this.targetGuardOriginalEmit = originalEmit;
|
|
8033
9078
|
this.targetGuardWrappedEmit = wrappedEmit;
|
|
9079
|
+
emitGuardInstalled = true;
|
|
8034
9080
|
} catch {
|
|
8035
9081
|
this.targetGuardOriginalEmit = void 0;
|
|
8036
9082
|
this.targetGuardWrappedEmit = void 0;
|
|
@@ -8040,12 +9086,33 @@ var BrowserService = class {
|
|
|
8040
9086
|
this.targetGuardConnectionListener = sessionListener;
|
|
8041
9087
|
this.targetGuardRawConnectionListener = rawListener;
|
|
8042
9088
|
this.targetGuardDetachedListener = detachedListener;
|
|
9089
|
+
if (!emitGuardInstalled) {
|
|
9090
|
+
this.targetGuardUnavailable = true;
|
|
9091
|
+
this.targetGuardReadinessPromise = Promise.resolve();
|
|
9092
|
+
this.logger.warn("Browser target guard is unavailable; debugger resume cannot be gated");
|
|
9093
|
+
return;
|
|
9094
|
+
}
|
|
8043
9095
|
if (targetConnection.send) {
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
9096
|
+
const readiness = Promise.resolve().then(() => targetConnection.send?.("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true })).then(
|
|
9097
|
+
() => {
|
|
9098
|
+
if (this.targetGuardConnection === targetConnection) {
|
|
9099
|
+
this.targetGuardUnavailable = false;
|
|
9100
|
+
}
|
|
9101
|
+
},
|
|
9102
|
+
(error) => {
|
|
9103
|
+
if (this.targetGuardConnection === targetConnection) {
|
|
9104
|
+
this.targetGuardUnavailable = true;
|
|
9105
|
+
this.logger.warn("Browser target auto-attachment could not be enabled", { error: safeErrorDiagnostic(error) });
|
|
9106
|
+
}
|
|
9107
|
+
}
|
|
9108
|
+
).then(() => void 0);
|
|
9109
|
+
this.targetGuardReadinessPromise = readiness;
|
|
9110
|
+
await readiness;
|
|
9111
|
+
return;
|
|
8048
9112
|
}
|
|
9113
|
+
this.targetGuardUnavailable = true;
|
|
9114
|
+
this.targetGuardReadinessPromise = Promise.resolve();
|
|
9115
|
+
this.logger.warn("Browser target guard is unavailable; auto-attachment cannot be enabled");
|
|
8049
9116
|
}
|
|
8050
9117
|
detachTargetGuard() {
|
|
8051
9118
|
const connection = this.targetGuardConnection;
|
|
@@ -8074,11 +9141,13 @@ var BrowserService = class {
|
|
|
8074
9141
|
this.targetGuardConnectionListener = void 0;
|
|
8075
9142
|
this.targetGuardRawConnectionListener = void 0;
|
|
8076
9143
|
this.targetGuardDetachedListener = void 0;
|
|
9144
|
+
this.targetGuardReadinessPromise = void 0;
|
|
8077
9145
|
this.targetGuardOriginalEmit = void 0;
|
|
8078
9146
|
this.targetGuardWrappedEmit = void 0;
|
|
8079
9147
|
this.targetGuardUnavailable = false;
|
|
8080
9148
|
this.pendingTargetGuardSessions.clear();
|
|
8081
9149
|
this.pendingTargetGuardInfos.clear();
|
|
9150
|
+
this.handledTargetGuardSessions.clear();
|
|
8082
9151
|
for (const guard of this.targetGuardSessions.values()) {
|
|
8083
9152
|
guard.released = true;
|
|
8084
9153
|
removeCdpListener(guard.session, "Fetch.requestPaused", guard.requestPausedListener);
|
|
@@ -8108,7 +9177,7 @@ var BrowserService = class {
|
|
|
8108
9177
|
};
|
|
8109
9178
|
guard.requestPausedListener = (event) => {
|
|
8110
9179
|
const pending = this.handleTargetGuardRequest(guard, event).catch((error) => {
|
|
8111
|
-
this.logger.debug("New target request guard callback failed", { error:
|
|
9180
|
+
this.logger.debug("New target request guard callback failed", { error: safeErrorDiagnostic(error) });
|
|
8112
9181
|
});
|
|
8113
9182
|
guard.pendingRequests.add(pending);
|
|
8114
9183
|
void pending.finally(() => guard.pendingRequests.delete(pending)).catch(() => void 0);
|
|
@@ -8168,7 +9237,7 @@ var BrowserService = class {
|
|
|
8168
9237
|
this.targetGuardSessions.delete(sessionId);
|
|
8169
9238
|
this.unguardedTargetSessions.add(sessionId);
|
|
8170
9239
|
await this.closeGuardedTarget(guard);
|
|
8171
|
-
this.logger.warn("New browser target could not be guarded", { error:
|
|
9240
|
+
this.logger.warn("New browser target could not be guarded", { error: safeErrorDiagnostic(error) });
|
|
8172
9241
|
throw error;
|
|
8173
9242
|
}
|
|
8174
9243
|
}
|
|
@@ -8225,7 +9294,7 @@ var BrowserService = class {
|
|
|
8225
9294
|
await guard.session.send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" });
|
|
8226
9295
|
}
|
|
8227
9296
|
} catch (error) {
|
|
8228
|
-
this.logger.debug("New target request could not be resolved", { error:
|
|
9297
|
+
this.logger.debug("New target request could not be resolved", { error: safeErrorDiagnostic(error) });
|
|
8229
9298
|
} finally {
|
|
8230
9299
|
guard.requestIds.delete(requestId);
|
|
8231
9300
|
}
|
|
@@ -8304,9 +9373,15 @@ var BrowserService = class {
|
|
|
8304
9373
|
throw error;
|
|
8305
9374
|
}
|
|
8306
9375
|
} catch (error) {
|
|
8307
|
-
this.logger.warn("New browser tab could not be prepared", { error:
|
|
9376
|
+
this.logger.warn("New browser tab could not be prepared", { error: safeErrorDiagnostic(error) });
|
|
8308
9377
|
}
|
|
8309
9378
|
}
|
|
9379
|
+
trackTargetPreparation(target) {
|
|
9380
|
+
const preparation = this.prepareTarget(target);
|
|
9381
|
+
this.pendingTargetPreparations.add(preparation);
|
|
9382
|
+
void preparation.finally(() => this.pendingTargetPreparations.delete(preparation)).catch(() => void 0);
|
|
9383
|
+
return preparation;
|
|
9384
|
+
}
|
|
8310
9385
|
isUnguardedTargetPage(page) {
|
|
8311
9386
|
const identity = pageTargetIdentity(page);
|
|
8312
9387
|
return Boolean(identity.sessionId && this.unguardedTargetSessions.has(identity.sessionId));
|
|
@@ -8361,7 +9436,7 @@ var BrowserService = class {
|
|
|
8361
9436
|
state.challengeStatus = void 0;
|
|
8362
9437
|
state.challengeAttempts = 0;
|
|
8363
9438
|
} catch (error) {
|
|
8364
|
-
this.logger.debug("Blocked navigation recovery could not restore a blank page", { pageId: state.id, error:
|
|
9439
|
+
this.logger.debug("Blocked navigation recovery could not restore a blank page", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
8365
9440
|
}
|
|
8366
9441
|
}
|
|
8367
9442
|
async disposePageState(state) {
|
|
@@ -8386,6 +9461,7 @@ var BrowserService = class {
|
|
|
8386
9461
|
state.snapshotInteractive = void 0;
|
|
8387
9462
|
state.snapshotId = void 0;
|
|
8388
9463
|
state.policyVerifiedUrls?.clear();
|
|
9464
|
+
this.networkJournal.clear(state.id);
|
|
8389
9465
|
state.dialogs.length = 0;
|
|
8390
9466
|
state.navigationError = void 0;
|
|
8391
9467
|
state.activeNavigationGeneration = void 0;
|
|
@@ -8558,7 +9634,7 @@ var BrowserService = class {
|
|
|
8558
9634
|
throwIfAborted(signal);
|
|
8559
9635
|
const classified = error instanceof AppError && error.code === "DOWNLOAD_CONFIGURATION_FAILED" ? error : new AppError("DOWNLOAD_CONFIGURATION_FAILED", "The browser download directory could not be configured. Retry after reconnecting the browser.", { retryable: true, cause: error });
|
|
8560
9636
|
state.downloadConfigurationError = classified;
|
|
8561
|
-
this.logger.warn("Browser download behavior could not be configured", { pageId: state.id, error:
|
|
9637
|
+
this.logger.warn("Browser download behavior could not be configured", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
8562
9638
|
}
|
|
8563
9639
|
}
|
|
8564
9640
|
this.assertStateLive(state);
|
|
@@ -8638,6 +9714,18 @@ var BrowserService = class {
|
|
|
8638
9714
|
state.policyVerifiedUrls?.clear();
|
|
8639
9715
|
}
|
|
8640
9716
|
requestUrl = request.url();
|
|
9717
|
+
const resourceType = typeof request.resourceType === "function" ? request.resourceType()?.toLowerCase() : void 0;
|
|
9718
|
+
if (!navigationRequest && resourceType && state.blockedResourceTypes.has(resourceType)) {
|
|
9719
|
+
let handled = true;
|
|
9720
|
+
try {
|
|
9721
|
+
handled = request.isInterceptResolutionHandled();
|
|
9722
|
+
} catch {
|
|
9723
|
+
}
|
|
9724
|
+
if (!handled) {
|
|
9725
|
+
await request.abort("blockedbyclient").catch(() => void 0);
|
|
9726
|
+
}
|
|
9727
|
+
return;
|
|
9728
|
+
}
|
|
8641
9729
|
if (/^about:blank(?:#.*)?$/i.test(requestUrl)) {
|
|
8642
9730
|
await request.continue();
|
|
8643
9731
|
return;
|
|
@@ -8681,6 +9769,35 @@ var BrowserService = class {
|
|
|
8681
9769
|
}
|
|
8682
9770
|
}
|
|
8683
9771
|
}
|
|
9772
|
+
networkRequestId(request) {
|
|
9773
|
+
const existing = this.networkRequestIds.get(request);
|
|
9774
|
+
if (existing) {
|
|
9775
|
+
return existing;
|
|
9776
|
+
}
|
|
9777
|
+
try {
|
|
9778
|
+
const raw = request.id;
|
|
9779
|
+
if (typeof raw === "string" || typeof raw === "number") {
|
|
9780
|
+
return String(raw);
|
|
9781
|
+
}
|
|
9782
|
+
} catch {
|
|
9783
|
+
}
|
|
9784
|
+
return void 0;
|
|
9785
|
+
}
|
|
9786
|
+
networkRequestIdForResponse(pageId, request, url, resourceType, timestamp) {
|
|
9787
|
+
const known = this.networkRequestId(request);
|
|
9788
|
+
if (known) {
|
|
9789
|
+
return known;
|
|
9790
|
+
}
|
|
9791
|
+
const recorded = this.networkJournal.recordRequest({
|
|
9792
|
+
pageId,
|
|
9793
|
+
url,
|
|
9794
|
+
method: "UNKNOWN",
|
|
9795
|
+
...resourceType ? { resourceType } : {},
|
|
9796
|
+
timestamp
|
|
9797
|
+
});
|
|
9798
|
+
this.networkRequestIds.set(request, recorded.requestId);
|
|
9799
|
+
return recorded.requestId;
|
|
9800
|
+
}
|
|
8684
9801
|
stateFor(page) {
|
|
8685
9802
|
const existingId = this.ids.get(page);
|
|
8686
9803
|
if (existingId) {
|
|
@@ -8690,7 +9807,7 @@ var BrowserService = class {
|
|
|
8690
9807
|
}
|
|
8691
9808
|
this.ids.delete(page);
|
|
8692
9809
|
}
|
|
8693
|
-
const state = { id: randomUUID(), page, lifecycleGeneration: this.lifecycleGeneration, disposed: false, refs: /* @__PURE__ */ new Map(), domRevision: 0, networkEnabled: false, consoleEnabled: false, network: [], console: [], dialogs: [], listenersInstalled: false, timeoutsConfigured: false, viewportConfigured: false, downloadConfigured: false, navigationGuardInstalled: false, stealthInjected: false, navigationGeneration: 0, policyVerifiedUrls: /* @__PURE__ */ new Set() };
|
|
9810
|
+
const state = { id: randomUUID(), page, lifecycleGeneration: this.lifecycleGeneration, disposed: false, refs: /* @__PURE__ */ new Map(), domRevision: 0, networkEnabled: false, consoleEnabled: false, network: [], console: [], dialogs: [], listenersInstalled: false, timeoutsConfigured: false, viewportConfigured: false, downloadConfigured: false, navigationGuardInstalled: false, stealthInjected: false, navigationGeneration: 0, policyVerifiedUrls: /* @__PURE__ */ new Set(), blockedResourceTypes: /* @__PURE__ */ new Set() };
|
|
8694
9811
|
this.ids.set(page, state.id);
|
|
8695
9812
|
this.states.set(state.id, state);
|
|
8696
9813
|
this.installListeners(state);
|
|
@@ -8706,10 +9823,23 @@ var BrowserService = class {
|
|
|
8706
9823
|
return;
|
|
8707
9824
|
}
|
|
8708
9825
|
try {
|
|
8709
|
-
|
|
9826
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
9827
|
+
const url = request.url();
|
|
9828
|
+
const method = request.method();
|
|
9829
|
+
const resourceType = request.resourceType?.();
|
|
9830
|
+
const recorded = this.networkJournal.recordRequest({
|
|
9831
|
+
pageId: state.id,
|
|
9832
|
+
requestId: this.networkRequestId(request),
|
|
9833
|
+
url,
|
|
9834
|
+
method,
|
|
9835
|
+
...typeof resourceType === "string" ? { resourceType } : {},
|
|
9836
|
+
timestamp
|
|
9837
|
+
});
|
|
9838
|
+
this.networkRequestIds.set(request, recorded.requestId);
|
|
9839
|
+
state.network.push({ timestamp, type: "request", url: sanitizeUrl(url), method });
|
|
8710
9840
|
trimLog(state.network);
|
|
8711
9841
|
} catch (error) {
|
|
8712
|
-
this.logger.debug("Browser request log entry was unavailable after page disposal", { pageId: state.id, error:
|
|
9842
|
+
this.logger.debug("Browser request log entry was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
8713
9843
|
}
|
|
8714
9844
|
};
|
|
8715
9845
|
state.networkRequestListener = networkRequestListener;
|
|
@@ -8723,11 +9853,24 @@ var BrowserService = class {
|
|
|
8723
9853
|
state.mainFrameStatus = response.status();
|
|
8724
9854
|
}
|
|
8725
9855
|
if (state.networkEnabled) {
|
|
8726
|
-
|
|
9856
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
9857
|
+
const request = response.request();
|
|
9858
|
+
const url = response.url();
|
|
9859
|
+
const resourceType = request.resourceType?.();
|
|
9860
|
+
const requestId = this.networkRequestIdForResponse(state.id, request, url, resourceType, timestamp);
|
|
9861
|
+
this.networkJournal.recordResponse({
|
|
9862
|
+
pageId: state.id,
|
|
9863
|
+
requestId,
|
|
9864
|
+
url,
|
|
9865
|
+
status: response.status(),
|
|
9866
|
+
...typeof resourceType === "string" ? { resourceType } : {},
|
|
9867
|
+
timestamp
|
|
9868
|
+
});
|
|
9869
|
+
state.network.push({ timestamp, type: "response", url: sanitizeUrl(url), status: response.status() });
|
|
8727
9870
|
trimLog(state.network);
|
|
8728
9871
|
}
|
|
8729
9872
|
} catch (error) {
|
|
8730
|
-
this.logger.debug("Browser response log entry was unavailable after page disposal", { pageId: state.id, error:
|
|
9873
|
+
this.logger.debug("Browser response log entry was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
8731
9874
|
}
|
|
8732
9875
|
};
|
|
8733
9876
|
state.networkResponseListener = networkResponseListener;
|
|
@@ -8740,7 +9883,7 @@ var BrowserService = class {
|
|
|
8740
9883
|
state.console.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type: "console", level: message.type(), text: message.text().slice(0, 2e3) });
|
|
8741
9884
|
trimLog(state.console);
|
|
8742
9885
|
} catch (error) {
|
|
8743
|
-
this.logger.debug("Browser console log entry was unavailable after page disposal", { pageId: state.id, error:
|
|
9886
|
+
this.logger.debug("Browser console log entry was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
8744
9887
|
}
|
|
8745
9888
|
};
|
|
8746
9889
|
state.consoleListener = consoleListener;
|
|
@@ -8755,7 +9898,7 @@ var BrowserService = class {
|
|
|
8755
9898
|
this.currentPageId = state.id;
|
|
8756
9899
|
this.logger.info("Browser dialog opened", { pageId: state.id, type });
|
|
8757
9900
|
} catch (error) {
|
|
8758
|
-
this.logger.debug("Browser dialog event was unavailable after page disposal", { pageId: state.id, error:
|
|
9901
|
+
this.logger.debug("Browser dialog event was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
8759
9902
|
}
|
|
8760
9903
|
};
|
|
8761
9904
|
state.dialogListener = dialogListener;
|
|
@@ -8776,7 +9919,7 @@ var BrowserService = class {
|
|
|
8776
9919
|
state.challengeAttempts = 0;
|
|
8777
9920
|
}
|
|
8778
9921
|
} catch (error) {
|
|
8779
|
-
this.logger.debug("Browser frame navigation event was unavailable after page disposal", { pageId: state.id, error:
|
|
9922
|
+
this.logger.debug("Browser frame navigation event was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
8780
9923
|
}
|
|
8781
9924
|
};
|
|
8782
9925
|
state.frameNavigatedListener = frameNavigatedListener;
|
|
@@ -9436,6 +10579,7 @@ var BrowserService = class {
|
|
|
9436
10579
|
try {
|
|
9437
10580
|
await humanMouseMove(state.page, 0, 0, centerX, centerY, 80);
|
|
9438
10581
|
} catch {
|
|
10582
|
+
throwIfAborted(signal);
|
|
9439
10583
|
}
|
|
9440
10584
|
}
|
|
9441
10585
|
async clickTarget(state, target, button, clickCount, signal, frame = state.page.mainFrame(), pointerType = "mouse") {
|
|
@@ -9800,7 +10944,7 @@ var BrowserService = class {
|
|
|
9800
10944
|
removeDialogListener?.();
|
|
9801
10945
|
if (openedDialog === null) {
|
|
9802
10946
|
void click.catch((error) => {
|
|
9803
|
-
this.logger.debug("Browser click completed after dialog resolution", { pageId: state.id, error:
|
|
10947
|
+
this.logger.debug("Browser click completed after dialog resolution", { pageId: state.id, error: safeErrorDiagnostic(error) });
|
|
9804
10948
|
});
|
|
9805
10949
|
throwIfAborted(signal);
|
|
9806
10950
|
return { navigated: false, urlChanged: false };
|
|
@@ -9949,7 +11093,7 @@ var BrowserService = class {
|
|
|
9949
11093
|
if (!nativeControlValueSet) {
|
|
9950
11094
|
throwIfAborted(signal);
|
|
9951
11095
|
if (this.stealthSettings().behaviorEnabled) {
|
|
9952
|
-
await humanType(state.page, text);
|
|
11096
|
+
await humanType(state.page, text, { signal });
|
|
9953
11097
|
} else {
|
|
9954
11098
|
await state.page.keyboard.type(text);
|
|
9955
11099
|
}
|
|
@@ -10545,6 +11689,9 @@ var BrowserService = class {
|
|
|
10545
11689
|
if (before.isSymbolicLink()) {
|
|
10546
11690
|
throw new AppError("FILE_PATH_BLOCKED", "The upload source must not be a symbolic link.");
|
|
10547
11691
|
}
|
|
11692
|
+
if (before.size > MAX_UPLOAD_BYTES) {
|
|
11693
|
+
throw new AppError("FILE_TOO_LARGE", "The upload source exceeds the 50 MiB size limit.");
|
|
11694
|
+
}
|
|
10548
11695
|
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
10549
11696
|
let sourceHandle;
|
|
10550
11697
|
let stagingPath;
|
|
@@ -10554,6 +11701,9 @@ var BrowserService = class {
|
|
|
10554
11701
|
if (!opened.isFile()) {
|
|
10555
11702
|
throw new AppError("FILE_PATH_BLOCKED", "The upload source must be a regular file.");
|
|
10556
11703
|
}
|
|
11704
|
+
if (opened.size > MAX_UPLOAD_BYTES) {
|
|
11705
|
+
throw new AppError("FILE_TOO_LARGE", "The upload source exceeds the 50 MiB size limit.");
|
|
11706
|
+
}
|
|
10557
11707
|
const after = await lstat(candidate);
|
|
10558
11708
|
if (after.isSymbolicLink() || !sameFileIdentity(opened, after)) {
|
|
10559
11709
|
throw new AppError("FILE_PATH_BLOCKED", "The upload source changed while it was being opened.", { retryable: true });
|
|
@@ -10563,10 +11713,15 @@ var BrowserService = class {
|
|
|
10563
11713
|
await mkdir(stagingDirectory, { recursive: true, mode: 448 });
|
|
10564
11714
|
stagingPath = join4(stagingDirectory, `.upload-${randomUUID()}`);
|
|
10565
11715
|
const stagingHandle = await open(stagingPath, "wx", 384);
|
|
11716
|
+
let copiedBytes = 0;
|
|
10566
11717
|
try {
|
|
10567
11718
|
for await (const chunk of sourceHandle.createReadStream({ autoClose: false })) {
|
|
10568
11719
|
throwIfAborted(signal);
|
|
10569
11720
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
11721
|
+
copiedBytes += buffer.byteLength;
|
|
11722
|
+
if (copiedBytes > MAX_UPLOAD_BYTES) {
|
|
11723
|
+
throw new AppError("FILE_TOO_LARGE", "The upload source exceeds the 50 MiB size limit.");
|
|
11724
|
+
}
|
|
10570
11725
|
let offset = 0;
|
|
10571
11726
|
while (offset < buffer.byteLength) {
|
|
10572
11727
|
const written = await stagingHandle.write(buffer, offset, buffer.byteLength - offset, null);
|
|
@@ -10581,7 +11736,7 @@ var BrowserService = class {
|
|
|
10581
11736
|
await stagingHandle.close().catch(() => void 0);
|
|
10582
11737
|
}
|
|
10583
11738
|
throwIfAborted(signal);
|
|
10584
|
-
return { path: stagingPath, displayName: basename2(candidate), size:
|
|
11739
|
+
return { path: stagingPath, displayName: basename2(candidate), size: copiedBytes };
|
|
10585
11740
|
} catch (error) {
|
|
10586
11741
|
if (stagingPath) {
|
|
10587
11742
|
await unlinkIfPresent(stagingPath);
|
|
@@ -10662,7 +11817,7 @@ var BrowserService = class {
|
|
|
10662
11817
|
}).catch(() => void 0);
|
|
10663
11818
|
return recovery;
|
|
10664
11819
|
}
|
|
10665
|
-
async withOperationLock(signal, operation, queueTimeoutMs = this.config.browser.actionTimeoutMs, operationTimeoutMs, mode = "exclusive") {
|
|
11820
|
+
async withOperationLock(signal, operation, queueTimeoutMs = this.config.browser.actionTimeoutMs, operationTimeoutMs, mode = "exclusive", touchActivity = true) {
|
|
10666
11821
|
if (this.queuedOperations >= MAX_QUEUED_OPERATIONS) {
|
|
10667
11822
|
throw new AppError("BROWSER_QUEUE_FULL", "The browser action queue is full; wait for an active operation to finish and retry.", { retryable: true, details: { hint: "Wait for the active browser operation to finish, then retry." } });
|
|
10668
11823
|
}
|
|
@@ -10743,7 +11898,9 @@ var BrowserService = class {
|
|
|
10743
11898
|
}
|
|
10744
11899
|
}
|
|
10745
11900
|
}, operationBudgetMs);
|
|
10746
|
-
|
|
11901
|
+
if (touchActivity) {
|
|
11902
|
+
this.lastActivityAt = Date.now();
|
|
11903
|
+
}
|
|
10747
11904
|
operationPromise = Promise.resolve().then(() => operation(operationSignal));
|
|
10748
11905
|
void operationPromise.catch(() => void 0);
|
|
10749
11906
|
if (abortRequested) {
|
|
@@ -10870,17 +12027,20 @@ function isBrowserConnectTimeout(error) {
|
|
|
10870
12027
|
return error instanceof AppError && error.code === "BROWSER_CONNECT_TIMEOUT";
|
|
10871
12028
|
}
|
|
10872
12029
|
async function closeConnectedBrowser(browser, owned, logger) {
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
12030
|
+
const closing = Promise.resolve().then(() => owned ? browser.close() : browser.disconnect());
|
|
12031
|
+
const succeeded = await settleWithTimeout(
|
|
12032
|
+
closing.then(
|
|
12033
|
+
() => true,
|
|
12034
|
+
(error) => {
|
|
12035
|
+
logger.warn(owned ? "Browser close failed" : "Browser disconnect failed", { error: safeErrorDiagnostic(error) });
|
|
12036
|
+
return false;
|
|
12037
|
+
}
|
|
12038
|
+
),
|
|
12039
|
+
SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS
|
|
12040
|
+
);
|
|
12041
|
+
if (succeeded === void 0) {
|
|
12042
|
+
logger.warn(owned ? "Browser close timed out" : "Browser disconnect timed out");
|
|
12043
|
+
return false;
|
|
10884
12044
|
}
|
|
10885
12045
|
return succeeded;
|
|
10886
12046
|
}
|
|
@@ -11450,7 +12610,16 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
|
|
|
11450
12610
|
if (!response.ok) {
|
|
11451
12611
|
throw new Error(`DevTools endpoint returned HTTP ${response.status}.`);
|
|
11452
12612
|
}
|
|
11453
|
-
const
|
|
12613
|
+
const declaredLength = response.headers.get("content-length");
|
|
12614
|
+
if (declaredLength !== null) {
|
|
12615
|
+
const parsedLength = Number(declaredLength);
|
|
12616
|
+
if (Number.isFinite(parsedLength) && parsedLength > MAX_DEVTOOLS_PROBE_RESPONSE_BYTES) {
|
|
12617
|
+
cancelDevToolsProbeBody(response);
|
|
12618
|
+
throw new Error("DevTools endpoint response exceeded the safety limit.");
|
|
12619
|
+
}
|
|
12620
|
+
}
|
|
12621
|
+
const body = await readBoundedDevToolsResponse(response, MAX_DEVTOOLS_PROBE_RESPONSE_BYTES);
|
|
12622
|
+
const value = JSON.parse(body);
|
|
11454
12623
|
if (!isRecordValue(value)) {
|
|
11455
12624
|
throw new Error("DevTools endpoint returned an invalid version payload.");
|
|
11456
12625
|
}
|
|
@@ -11463,6 +12632,39 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
|
|
|
11463
12632
|
clearTimeout(timer);
|
|
11464
12633
|
}
|
|
11465
12634
|
}
|
|
12635
|
+
async function readBoundedDevToolsResponse(response, maxBytes) {
|
|
12636
|
+
if (!response.body) {
|
|
12637
|
+
throw new Error("DevTools endpoint returned an empty response body.");
|
|
12638
|
+
}
|
|
12639
|
+
const reader = response.body.getReader();
|
|
12640
|
+
const chunks = [];
|
|
12641
|
+
let total = 0;
|
|
12642
|
+
try {
|
|
12643
|
+
while (true) {
|
|
12644
|
+
const next = await reader.read();
|
|
12645
|
+
if (next.done) {
|
|
12646
|
+
break;
|
|
12647
|
+
}
|
|
12648
|
+
const value = next.value;
|
|
12649
|
+
if (!(value instanceof Uint8Array) || value.byteLength > maxBytes - total) {
|
|
12650
|
+
void reader.cancel().catch(() => void 0);
|
|
12651
|
+
throw new Error("DevTools endpoint response exceeded the safety limit.");
|
|
12652
|
+
}
|
|
12653
|
+
const chunk = Buffer.from(value);
|
|
12654
|
+
total += chunk.byteLength;
|
|
12655
|
+
chunks.push(chunk);
|
|
12656
|
+
}
|
|
12657
|
+
} finally {
|
|
12658
|
+
reader.releaseLock();
|
|
12659
|
+
}
|
|
12660
|
+
return Buffer.concat(chunks, total).toString("utf8");
|
|
12661
|
+
}
|
|
12662
|
+
function cancelDevToolsProbeBody(response) {
|
|
12663
|
+
try {
|
|
12664
|
+
void response.body?.cancel().catch(() => void 0);
|
|
12665
|
+
} catch {
|
|
12666
|
+
}
|
|
12667
|
+
}
|
|
11466
12668
|
function boundedEndpointField(value) {
|
|
11467
12669
|
return typeof value === "string" ? value.slice(0, 4096) : void 0;
|
|
11468
12670
|
}
|
|
@@ -11574,6 +12776,8 @@ var MAX_RESULT_SNIPPET_CHARS = 4e3;
|
|
|
11574
12776
|
var MAX_ATTEMPTS = 3;
|
|
11575
12777
|
var RETRY_BASE_DELAY_MS = 250;
|
|
11576
12778
|
var RETRY_MAX_DELAY_MS = 2e3;
|
|
12779
|
+
var MAX_CONCURRENT_RESEARCH = 4;
|
|
12780
|
+
var MAX_RESEARCH_QUEUE = 16;
|
|
11577
12781
|
var ZERO_WIDTH_PATTERN2 = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
11578
12782
|
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
11579
12783
|
var ANTI_BOT_PATTERN = /(?:captcha|challenge|unusual\s+traffic|automated\s+(?:queries|requests)|access\s+denied|temporarily\s+blocked|too\s+many\s+requests)/i;
|
|
@@ -11582,6 +12786,70 @@ var RESULT_CLASS_ATTRIBUTE_PATTERN = /\bclass\s*=\s*(["'])([^"']*)\1/i;
|
|
|
11582
12786
|
var RESULT_HREF_ATTRIBUTE_PATTERN = /\bhref\s*=\s*(["'])([^"']*)\1/i;
|
|
11583
12787
|
var NEXT_RESULT_PATTERN = /<a\b[^>]*\bclass\s*=\s*(["'])[^"']*\bresult__a\b[^"']*\1/i;
|
|
11584
12788
|
var RESULT_SNIPPET_PATTERN = /\bclass\s*=\s*(["'])[^"']*\bresult__snippet\b[^"']*\1[^>]*>([\s\S]*?)<\/[^>]+>/i;
|
|
12789
|
+
var ResearchAdmission = class {
|
|
12790
|
+
active = 0;
|
|
12791
|
+
queue = [];
|
|
12792
|
+
acquire(signal, abortError = cancelledResearchError) {
|
|
12793
|
+
if (signal?.aborted) {
|
|
12794
|
+
return Promise.reject(abortError());
|
|
12795
|
+
}
|
|
12796
|
+
if (this.active < MAX_CONCURRENT_RESEARCH) {
|
|
12797
|
+
this.active += 1;
|
|
12798
|
+
return Promise.resolve(this.createRelease());
|
|
12799
|
+
}
|
|
12800
|
+
if (this.queue.length >= MAX_RESEARCH_QUEUE) {
|
|
12801
|
+
return Promise.reject(new AppError("RESEARCH_BUSY", "The research service is busy; retry later.", {
|
|
12802
|
+
retryable: true,
|
|
12803
|
+
status: 503,
|
|
12804
|
+
details: { classification: "overloaded" }
|
|
12805
|
+
}));
|
|
12806
|
+
}
|
|
12807
|
+
return new Promise((resolve7, reject) => {
|
|
12808
|
+
const waiter = { resolve: resolve7, reject, signal, abortError };
|
|
12809
|
+
const onAbort = () => {
|
|
12810
|
+
const index = this.queue.indexOf(waiter);
|
|
12811
|
+
if (index < 0) {
|
|
12812
|
+
return;
|
|
12813
|
+
}
|
|
12814
|
+
this.queue.splice(index, 1);
|
|
12815
|
+
signal?.removeEventListener("abort", onAbort);
|
|
12816
|
+
reject(abortError());
|
|
12817
|
+
};
|
|
12818
|
+
waiter.onAbort = onAbort;
|
|
12819
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
12820
|
+
this.queue.push(waiter);
|
|
12821
|
+
if (signal?.aborted) {
|
|
12822
|
+
onAbort();
|
|
12823
|
+
}
|
|
12824
|
+
});
|
|
12825
|
+
}
|
|
12826
|
+
createRelease() {
|
|
12827
|
+
let released = false;
|
|
12828
|
+
return () => {
|
|
12829
|
+
if (released) {
|
|
12830
|
+
return;
|
|
12831
|
+
}
|
|
12832
|
+
released = true;
|
|
12833
|
+
this.active -= 1;
|
|
12834
|
+
this.drain();
|
|
12835
|
+
};
|
|
12836
|
+
}
|
|
12837
|
+
drain() {
|
|
12838
|
+
while (this.active < MAX_CONCURRENT_RESEARCH && this.queue.length > 0) {
|
|
12839
|
+
const waiter = this.queue.shift();
|
|
12840
|
+
if (!waiter) {
|
|
12841
|
+
return;
|
|
12842
|
+
}
|
|
12843
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
12844
|
+
if (waiter.signal?.aborted) {
|
|
12845
|
+
waiter.reject(waiter.abortError());
|
|
12846
|
+
continue;
|
|
12847
|
+
}
|
|
12848
|
+
this.active += 1;
|
|
12849
|
+
waiter.resolve(this.createRelease());
|
|
12850
|
+
}
|
|
12851
|
+
}
|
|
12852
|
+
};
|
|
11585
12853
|
var ResearchService = class {
|
|
11586
12854
|
constructor(policy, logger) {
|
|
11587
12855
|
this.policy = policy;
|
|
@@ -11589,6 +12857,7 @@ var ResearchService = class {
|
|
|
11589
12857
|
}
|
|
11590
12858
|
policy;
|
|
11591
12859
|
logger;
|
|
12860
|
+
admission = new ResearchAdmission();
|
|
11592
12861
|
async research(query, options = {}, signal) {
|
|
11593
12862
|
if (typeof query !== "string") {
|
|
11594
12863
|
throw new AppError("RESEARCH_INVALID", "A non-empty research query is required.");
|
|
@@ -11627,7 +12896,17 @@ var ResearchService = class {
|
|
|
11627
12896
|
}, REQUEST_TIMEOUT_MS);
|
|
11628
12897
|
const abort = () => controller.abort();
|
|
11629
12898
|
signal?.addEventListener("abort", abort, { once: true });
|
|
12899
|
+
let release;
|
|
11630
12900
|
try {
|
|
12901
|
+
const abortError = () => signal?.aborted ? new AppError("CANCELLED", "The research request was cancelled.") : new AppError("RESEARCH_TIMEOUT", `The research request exceeded its ${REQUEST_TIMEOUT_MS / 1e3}-second timeout.`, {
|
|
12902
|
+
retryable: true,
|
|
12903
|
+
details: { classification: "timeout", timeoutMs: REQUEST_TIMEOUT_MS }
|
|
12904
|
+
});
|
|
12905
|
+
release = await this.admission.acquire(controller.signal, abortError);
|
|
12906
|
+
if (controller.signal.aborted) {
|
|
12907
|
+
controller.abort();
|
|
12908
|
+
throw abortError();
|
|
12909
|
+
}
|
|
11631
12910
|
const url = await awaitWithAbort2(
|
|
11632
12911
|
this.policy.assertNavigationAllowedAsync(`https://html.duckduckgo.com/html/?q=${encodedQuery}`),
|
|
11633
12912
|
controller.signal
|
|
@@ -11698,9 +12977,13 @@ var ResearchService = class {
|
|
|
11698
12977
|
} finally {
|
|
11699
12978
|
clearTimeout(timeout);
|
|
11700
12979
|
signal?.removeEventListener("abort", abort);
|
|
12980
|
+
release?.();
|
|
11701
12981
|
}
|
|
11702
12982
|
}
|
|
11703
12983
|
};
|
|
12984
|
+
function cancelledResearchError() {
|
|
12985
|
+
return new AppError("CANCELLED", "The research request was cancelled.");
|
|
12986
|
+
}
|
|
11704
12987
|
async function fetchWithRetry(url, signal) {
|
|
11705
12988
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
|
11706
12989
|
if (signal.aborted) {
|
|
@@ -12230,10 +13513,11 @@ var ServerRuntime = class _ServerRuntime {
|
|
|
12230
13513
|
mode: this.config.browser.mode,
|
|
12231
13514
|
configured: managedBrowser || !browserDisabled && (usesExecutable ? Boolean(this.config.browser.executablePath) : Boolean(this.config.browser.wsEndpoint || this.config.browser.url)),
|
|
12232
13515
|
connection: browserDisabled ? "disabled" : managedBrowser ? "managed" : usesExecutable ? "executable" : this.config.browser.wsEndpoint ? "websocket" : "devtools-http",
|
|
12233
|
-
runtime: browserDisabled ? { connected: false, owned: false, trackedPages: 0, queuedOperations: 0, currentPageId: null, recoveryRequired: false } : this.browser.connectionStatus(),
|
|
13516
|
+
runtime: browserDisabled ? { connected: false, owned: false, trackedPages: 0, queuedOperations: 0, currentPageId: null, recoveryRequired: false, idleTimeoutMs: this.config.browser.idleTimeoutMs } : this.browser.connectionStatus(),
|
|
12234
13517
|
actionTimeoutMs: this.config.browser.actionTimeoutMs,
|
|
12235
13518
|
connectTimeoutMs: this.config.browser.connectTimeoutMs,
|
|
12236
13519
|
cdpTimeoutMs: this.config.browser.cdpTimeoutMs,
|
|
13520
|
+
idleTimeoutMs: this.config.browser.idleTimeoutMs,
|
|
12237
13521
|
maxScreenshotBytes: this.config.browser.maxScreenshotBytes,
|
|
12238
13522
|
maxHtmlChars: this.config.browser.maxHtmlChars
|
|
12239
13523
|
},
|
|
@@ -12723,6 +14007,7 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12723
14007
|
response.on("error", (error) => runtime.logger.error("MCP HTTP response error", safeErrorDiagnostic(error)));
|
|
12724
14008
|
request.on("error", (error) => runtime.logger.error("MCP HTTP request error", safeErrorDiagnostic(error)));
|
|
12725
14009
|
if (!accepting) {
|
|
14010
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12726
14011
|
response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
|
|
12727
14012
|
response.end(HTTP_SHUTTING_DOWN_BODY);
|
|
12728
14013
|
return;
|
|
@@ -12735,11 +14020,13 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12735
14020
|
}
|
|
12736
14021
|
setCorsHeaders(request, response);
|
|
12737
14022
|
if (!requestPathMatches(request, config.http.path)) {
|
|
14023
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12738
14024
|
response.writeHead(404, { "content-type": "application/json" });
|
|
12739
14025
|
response.end(HTTP_NOT_FOUND_BODY);
|
|
12740
14026
|
return;
|
|
12741
14027
|
}
|
|
12742
14028
|
if (request.method === "OPTIONS") {
|
|
14029
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12743
14030
|
response.writeHead(204, {
|
|
12744
14031
|
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
12745
14032
|
"access-control-allow-headers": request.headers["access-control-request-headers"] ?? "authorization, content-type, accept, mcp-protocol-version, mcp-session-id, last-event-id",
|
|
@@ -12750,6 +14037,7 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12750
14037
|
return;
|
|
12751
14038
|
}
|
|
12752
14039
|
if (!authorized(request, expectedAuthDigest)) {
|
|
14040
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12753
14041
|
response.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" });
|
|
12754
14042
|
response.end(HTTP_UNAUTHORIZED_BODY);
|
|
12755
14043
|
return;
|
|
@@ -12757,6 +14045,7 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12757
14045
|
let streamPool = isPotentialHttpStream(request) ? activeHttpStreams : activeHttpRequests;
|
|
12758
14046
|
const poolLimit = streamPool === activeHttpStreams ? MAX_HTTP_STREAM_CONCURRENCY : MAX_HTTP_CONCURRENCY;
|
|
12759
14047
|
if (streamPool.size >= poolLimit) {
|
|
14048
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12760
14049
|
response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
|
|
12761
14050
|
response.end(HTTP_BUSY_BODY);
|
|
12762
14051
|
return;
|