smooth-operator-mcp 3.0.5 → 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 +1367 -102
- 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
|
@@ -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
|
}
|
|
@@ -5078,6 +5241,294 @@ function globMatches(value, glob) {
|
|
|
5078
5241
|
return compiled.test(value);
|
|
5079
5242
|
}
|
|
5080
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
|
+
|
|
5081
5532
|
// src/server/browser/service.ts
|
|
5082
5533
|
var puppeteerModulePromise;
|
|
5083
5534
|
function loadPuppeteer() {
|
|
@@ -5092,6 +5543,52 @@ var POPUP_POST_CLICK_SETTLE_TIMEOUT_MS = 300;
|
|
|
5092
5543
|
var MAX_DOM_TRAVERSAL_NODES = 2e4;
|
|
5093
5544
|
var MAX_TEXT_SCAN_CHARS = 5e5;
|
|
5094
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
|
+
];
|
|
5095
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.";
|
|
5096
5593
|
var CHALLENGE_DEFAULT_MAX_ATTEMPTS = 32;
|
|
5097
5594
|
var CHALLENGE_MAX_ATTEMPTS = 100;
|
|
@@ -5104,6 +5601,11 @@ var NAVIGATION_CLICK_SETTLE_TIMEOUT_MS = 50;
|
|
|
5104
5601
|
var NAVIGATION_CLICK_EVENT_TIMEOUT_MS = 250;
|
|
5105
5602
|
var NAVIGATION_CLICK_READY_TIMEOUT_MS = 250;
|
|
5106
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;
|
|
5107
5609
|
var COMMON_KEY_ALIASES = {
|
|
5108
5610
|
ALT: "Alt",
|
|
5109
5611
|
ARROWDOWN: "ArrowDown",
|
|
@@ -5190,13 +5692,15 @@ var PARALLEL_READ_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
5190
5692
|
"page_next",
|
|
5191
5693
|
"search_page",
|
|
5192
5694
|
"find_elements",
|
|
5695
|
+
"inspect_element",
|
|
5193
5696
|
"list_frames",
|
|
5194
5697
|
"accessibility_snapshot",
|
|
5195
5698
|
"get_computed_style",
|
|
5196
5699
|
"get_page_info",
|
|
5197
5700
|
"get_cookies",
|
|
5198
5701
|
"get_storage",
|
|
5199
|
-
"list_downloads"
|
|
5702
|
+
"list_downloads",
|
|
5703
|
+
"search_network_log"
|
|
5200
5704
|
]);
|
|
5201
5705
|
var BrowserService = class {
|
|
5202
5706
|
constructor(config, policy, logger, dependencies = {}) {
|
|
@@ -5204,6 +5708,7 @@ var BrowserService = class {
|
|
|
5204
5708
|
this.policy = policy;
|
|
5205
5709
|
this.logger = logger;
|
|
5206
5710
|
this.dependencies = dependencies;
|
|
5711
|
+
this.startIdleSweep();
|
|
5207
5712
|
}
|
|
5208
5713
|
config;
|
|
5209
5714
|
policy;
|
|
@@ -5234,6 +5739,7 @@ var BrowserService = class {
|
|
|
5234
5739
|
currentPageId;
|
|
5235
5740
|
sessionGeneration = 0;
|
|
5236
5741
|
states = /* @__PURE__ */ new Map();
|
|
5742
|
+
networkJournal = new NetworkJournal();
|
|
5237
5743
|
configuredDownloadContexts = /* @__PURE__ */ new WeakSet();
|
|
5238
5744
|
// The download directory is process/session scoped, while page setup is
|
|
5239
5745
|
// page scoped. Share the mkdir promise across pages so opening a tab does
|
|
@@ -5241,9 +5747,11 @@ var BrowserService = class {
|
|
|
5241
5747
|
// so a later page can retry after a transient filesystem failure.
|
|
5242
5748
|
downloadDirectoryPromise;
|
|
5243
5749
|
ids = /* @__PURE__ */ new WeakMap();
|
|
5750
|
+
networkRequestIds = /* @__PURE__ */ new WeakMap();
|
|
5244
5751
|
targetGuardSessions = /* @__PURE__ */ new Map();
|
|
5245
5752
|
targetGuardNavigationErrors = /* @__PURE__ */ new Map();
|
|
5246
5753
|
unguardedTargetSessions = /* @__PURE__ */ new Set();
|
|
5754
|
+
handledTargetGuardSessions = /* @__PURE__ */ new Set();
|
|
5247
5755
|
pendingTargetGuardSessions = /* @__PURE__ */ new Map();
|
|
5248
5756
|
pendingTargetGuardInfos = /* @__PURE__ */ new Map();
|
|
5249
5757
|
targetGuardUnavailable = false;
|
|
@@ -5251,11 +5759,134 @@ var BrowserService = class {
|
|
|
5251
5759
|
targetGuardConnectionListener;
|
|
5252
5760
|
targetGuardRawConnectionListener;
|
|
5253
5761
|
targetGuardDetachedListener;
|
|
5762
|
+
targetGuardReadinessPromise;
|
|
5254
5763
|
targetGuardOriginalEmit;
|
|
5255
5764
|
targetGuardWrappedEmit;
|
|
5256
5765
|
operationTail = Promise.resolve();
|
|
5257
5766
|
queuedOperations = 0;
|
|
5258
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
|
+
}
|
|
5259
5890
|
async close() {
|
|
5260
5891
|
if (this.closePromise) {
|
|
5261
5892
|
return this.closePromise;
|
|
@@ -5276,6 +5907,7 @@ var BrowserService = class {
|
|
|
5276
5907
|
if (this.shuttingDown) {
|
|
5277
5908
|
return { closed: false, owned: false, succeeded: true };
|
|
5278
5909
|
}
|
|
5910
|
+
this.stopIdleSweep();
|
|
5279
5911
|
this.shuttingDown = true;
|
|
5280
5912
|
this.lifecycleGeneration += 1;
|
|
5281
5913
|
this.shutdownController.abort();
|
|
@@ -5286,6 +5918,7 @@ var BrowserService = class {
|
|
|
5286
5918
|
const lateConnectionSettled = await settlesWithinTimeout(this.connectionSettlementPromise, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS);
|
|
5287
5919
|
const interruptedShutdown = this.interruptedBrowserShutdown;
|
|
5288
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);
|
|
5289
5922
|
const browserResult = await this.closeBrowser();
|
|
5290
5923
|
return { ...browserResult, succeeded: browserResult.succeeded && connectionSettled && lateConnectionSettled && interruptedSucceeded };
|
|
5291
5924
|
}
|
|
@@ -5297,21 +5930,25 @@ var BrowserService = class {
|
|
|
5297
5930
|
queuedOperations: this.queuedOperations,
|
|
5298
5931
|
currentPageId: this.currentPageId ?? null,
|
|
5299
5932
|
recoveryRequired: this.recoveryRequired,
|
|
5933
|
+
idleTimeoutMs: this.config.browser.idleTimeoutMs,
|
|
5300
5934
|
...this.benchmarkCounters ? { benchmarkCounters: { ...this.benchmarkCounters } } : {}
|
|
5301
5935
|
};
|
|
5302
5936
|
}
|
|
5303
5937
|
sessionSummary() {
|
|
5304
5938
|
const status = this.connectionStatus();
|
|
5305
|
-
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() };
|
|
5306
5940
|
}
|
|
5307
5941
|
async doctor() {
|
|
5308
5942
|
const discovered = this.config.browser.executablePath ? void 0 : findChromeExecutable();
|
|
5309
|
-
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 };
|
|
5310
5946
|
const endpoint = await this.probeManagedEndpoint();
|
|
5311
5947
|
const browser = endpoint.version?.Browser;
|
|
5312
5948
|
return {
|
|
5313
5949
|
mode: this.config.browser.mode,
|
|
5314
5950
|
executablePath: executablePath ?? null,
|
|
5951
|
+
executable,
|
|
5315
5952
|
...executablePath ? {} : { searchedPaths: chromeExecutableSearchPaths().slice(0, 128) },
|
|
5316
5953
|
userDataDir: this.config.browser.userDataDir ?? null,
|
|
5317
5954
|
endpoint: {
|
|
@@ -5400,18 +6037,7 @@ var BrowserService = class {
|
|
|
5400
6037
|
this.recoveryRequired = !succeeded2;
|
|
5401
6038
|
return { closed: false, owned: false, succeeded: succeeded2 };
|
|
5402
6039
|
}
|
|
5403
|
-
|
|
5404
|
-
if (owned) {
|
|
5405
|
-
await Promise.resolve().then(() => browser.close()).catch((error) => {
|
|
5406
|
-
succeeded = false;
|
|
5407
|
-
this.logger.warn("Browser close failed", { error: String(error) });
|
|
5408
|
-
});
|
|
5409
|
-
} else {
|
|
5410
|
-
await Promise.resolve().then(() => browser.disconnect()).catch((error) => {
|
|
5411
|
-
succeeded = false;
|
|
5412
|
-
this.logger.warn("Browser disconnect failed", { error: String(error) });
|
|
5413
|
-
});
|
|
5414
|
-
}
|
|
6040
|
+
const succeeded = await closeConnectedBrowser(browser, owned, this.logger);
|
|
5415
6041
|
if (!succeeded) {
|
|
5416
6042
|
this.browserShutdownFailure = true;
|
|
5417
6043
|
this.failedBrowserShutdown = { browser, owned };
|
|
@@ -6349,12 +6975,56 @@ var BrowserService = class {
|
|
|
6349
6975
|
return { enabled: false };
|
|
6350
6976
|
case "get_network_log":
|
|
6351
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
|
+
}
|
|
6352
7020
|
case "clear_network_log":
|
|
6353
7021
|
state.network = [];
|
|
7022
|
+
this.networkJournal.clear(state.id);
|
|
6354
7023
|
return { cleared: true };
|
|
6355
7024
|
case "getclear_network_log": {
|
|
6356
7025
|
const entries = untrustedLogEntries(state.network.slice(-MAX_LOG_ENTRIES));
|
|
6357
7026
|
state.network = [];
|
|
7027
|
+
this.networkJournal.clear(state.id);
|
|
6358
7028
|
return { entries, cleared: true };
|
|
6359
7029
|
}
|
|
6360
7030
|
case "enable_console_log":
|
|
@@ -6911,27 +7581,53 @@ var BrowserService = class {
|
|
|
6911
7581
|
case "upload_file": {
|
|
6912
7582
|
throwIfAborted(signal);
|
|
6913
7583
|
const selector = await this.selectorFor(state, targetForAction(action, "selector"), action.frameId);
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
throwIfAborted(signal);
|
|
6917
|
-
let input;
|
|
7584
|
+
const stagedFiles = [];
|
|
7585
|
+
let input = null;
|
|
6918
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);
|
|
6919
7605
|
input = await frame.$(selector);
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
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));
|
|
6930
7616
|
throwIfAborted(signal);
|
|
6931
|
-
|
|
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 };
|
|
6932
7623
|
} finally {
|
|
6933
|
-
|
|
6934
|
-
|
|
7624
|
+
try {
|
|
7625
|
+
await input?.dispose();
|
|
7626
|
+
} catch {
|
|
7627
|
+
}
|
|
7628
|
+
for (const staged of stagedFiles) {
|
|
7629
|
+
await unlinkIfPresent(staged.path).catch(() => void 0);
|
|
7630
|
+
}
|
|
6935
7631
|
}
|
|
6936
7632
|
}
|
|
6937
7633
|
case "screenshot": {
|
|
@@ -7127,8 +7823,282 @@ var BrowserService = class {
|
|
|
7127
7823
|
}, query, { maxNodes: MAX_DOM_TRAVERSAL_NODES, maxChars: MAX_TEXT_SCAN_CHARS });
|
|
7128
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 };
|
|
7129
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
|
+
}
|
|
7130
8100
|
case "find_elements": {
|
|
7131
|
-
let collectFindElements2 = function(matches, fallbackSelector) {
|
|
8101
|
+
let collectFindElements2 = function(matches, fallbackSelector, safeAttributeNames, safeDataAttributeNames) {
|
|
7132
8102
|
const boundedText = (root) => {
|
|
7133
8103
|
const maybeChildNodes = root.childNodes;
|
|
7134
8104
|
if (!maybeChildNodes) {
|
|
@@ -7202,8 +8172,8 @@ var BrowserService = class {
|
|
|
7202
8172
|
const boundedHeight = Number.isFinite(rect.height) ? Math.max(-1e7, Math.min(1e7, Math.round(rect.height))) : 0;
|
|
7203
8173
|
const attributes = {};
|
|
7204
8174
|
let omittedAttributes = 0;
|
|
7205
|
-
const safeAttributes =
|
|
7206
|
-
const safeDataAttributes =
|
|
8175
|
+
const safeAttributes = new Set(safeAttributeNames);
|
|
8176
|
+
const safeDataAttributes = new Set(safeDataAttributeNames);
|
|
7207
8177
|
const attributeCount = element.attributes.length;
|
|
7208
8178
|
const inspectedAttributes = Math.min(attributeCount, 40);
|
|
7209
8179
|
for (let index = 0; index < inspectedAttributes; index += 1) {
|
|
@@ -7232,7 +8202,7 @@ var BrowserService = class {
|
|
|
7232
8202
|
var collectFindElements = collectFindElements2;
|
|
7233
8203
|
const selector = targetForAction(action, "selector");
|
|
7234
8204
|
const safeSelector = await this.selectorFor(state, selector, action.frameId, frame);
|
|
7235
|
-
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]);
|
|
7236
8206
|
return elements.map((element) => ({
|
|
7237
8207
|
tag: element.tag,
|
|
7238
8208
|
selector: wrapUntrustedText("element_selector", redactSecretPlaceholders(element.selector), 500),
|
|
@@ -7409,7 +8379,8 @@ var BrowserService = class {
|
|
|
7409
8379
|
case "solve_challenge":
|
|
7410
8380
|
return this.solveChallenge(state, action, signal);
|
|
7411
8381
|
case "get_cookies": {
|
|
7412
|
-
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();
|
|
7413
8384
|
return cookies.slice(0, 200).map((cookie) => ({
|
|
7414
8385
|
name: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookie.name), 256),
|
|
7415
8386
|
domain: wrapUntrustedText("cookie_domain", redactSecretPlaceholders(cookie.domain), 512),
|
|
@@ -7427,15 +8398,16 @@ var BrowserService = class {
|
|
|
7427
8398
|
if (action.cookieDomain) {
|
|
7428
8399
|
await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
|
|
7429
8400
|
}
|
|
7430
|
-
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 });
|
|
7431
8402
|
return { set: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
|
|
7432
8403
|
}
|
|
7433
8404
|
case "delete_cookies": {
|
|
7434
8405
|
const cookieName = requireField(action.cookieName, "cookieName");
|
|
8406
|
+
const scopedUrl = action.url ? await this.policy.assertNavigationAllowedAsync(action.url) : void 0;
|
|
7435
8407
|
if (action.cookieDomain) {
|
|
7436
8408
|
await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
|
|
7437
8409
|
}
|
|
7438
|
-
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 ?? "/" });
|
|
7439
8411
|
return { deleted: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
|
|
7440
8412
|
}
|
|
7441
8413
|
case "get_storage": {
|
|
@@ -7680,6 +8652,7 @@ var BrowserService = class {
|
|
|
7680
8652
|
if (!executablePath) {
|
|
7681
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.`);
|
|
7682
8654
|
}
|
|
8655
|
+
this.assertExecutableReady(executablePath);
|
|
7683
8656
|
connection = this.launch({
|
|
7684
8657
|
headless: this.config.browser.headless,
|
|
7685
8658
|
executablePath,
|
|
@@ -7697,6 +8670,7 @@ var BrowserService = class {
|
|
|
7697
8670
|
if (!this.config.browser.executablePath) {
|
|
7698
8671
|
throw new AppError("BROWSER_NOT_CONFIGURED", "Launch mode requires SMOOTH_OPERATOR_BROWSER_EXECUTABLE.");
|
|
7699
8672
|
}
|
|
8673
|
+
this.assertExecutableReady(this.config.browser.executablePath);
|
|
7700
8674
|
ownsBrowser = true;
|
|
7701
8675
|
connection = this.launch({
|
|
7702
8676
|
headless: this.config.browser.headless,
|
|
@@ -7724,6 +8698,13 @@ var BrowserService = class {
|
|
|
7724
8698
|
await closeConnectedBrowser(lateBrowser, ownsBrowser, this.logger);
|
|
7725
8699
|
throw new AppError("SERVER_CLOSING", "The browser runtime is shutting down.", { retryable: true });
|
|
7726
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
|
+
}
|
|
7727
8708
|
this.browser = browser;
|
|
7728
8709
|
this.ownsBrowser = ownsBrowser;
|
|
7729
8710
|
const connectedBrowser = browser;
|
|
@@ -7738,9 +8719,8 @@ var BrowserService = class {
|
|
|
7738
8719
|
this.lifecycleGeneration += 1;
|
|
7739
8720
|
this.retireAllStates();
|
|
7740
8721
|
});
|
|
7741
|
-
this.installTargetGuard(browser);
|
|
7742
8722
|
browser.on("targetcreated", (target) => {
|
|
7743
|
-
void this.
|
|
8723
|
+
void this.trackTargetPreparation(target);
|
|
7744
8724
|
});
|
|
7745
8725
|
return browser;
|
|
7746
8726
|
} catch (error) {
|
|
@@ -7786,6 +8766,17 @@ var BrowserService = class {
|
|
|
7786
8766
|
}
|
|
7787
8767
|
return (await loadPuppeteer()).launch(options);
|
|
7788
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
|
+
}
|
|
7789
8780
|
async connect(options) {
|
|
7790
8781
|
if (this.dependencies.connect) {
|
|
7791
8782
|
return this.dependencies.connect(options);
|
|
@@ -7802,7 +8793,9 @@ var BrowserService = class {
|
|
|
7802
8793
|
try {
|
|
7803
8794
|
const info = await lstat(activePortPath);
|
|
7804
8795
|
if (!info.isFile() || info.size > 4096) {
|
|
7805
|
-
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
|
+
});
|
|
7806
8799
|
return { state: "stale-probe-failed" };
|
|
7807
8800
|
}
|
|
7808
8801
|
raw = await readFile(activePortPath, "utf8");
|
|
@@ -7810,19 +8803,24 @@ var BrowserService = class {
|
|
|
7810
8803
|
if (isMissingFile(error)) {
|
|
7811
8804
|
return { state: "no-file" };
|
|
7812
8805
|
}
|
|
7813
|
-
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
|
+
});
|
|
7814
8810
|
return { state: "stale-probe-failed" };
|
|
7815
8811
|
}
|
|
7816
8812
|
const browserURL = parseDevToolsActivePort(raw);
|
|
7817
8813
|
if (!browserURL) {
|
|
7818
|
-
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
|
+
});
|
|
7819
8817
|
return { state: "stale-probe-failed" };
|
|
7820
8818
|
}
|
|
7821
8819
|
try {
|
|
7822
8820
|
const version = await (this.dependencies.probeEndpoint ?? probeDevToolsEndpoint)(browserURL, 2e3);
|
|
7823
8821
|
return { state: "live", browserURL, version };
|
|
7824
8822
|
} catch (error) {
|
|
7825
|
-
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) });
|
|
7826
8824
|
return { state: "stale-probe-failed" };
|
|
7827
8825
|
}
|
|
7828
8826
|
}
|
|
@@ -7966,14 +8964,18 @@ var BrowserService = class {
|
|
|
7966
8964
|
return matches[0]?.id;
|
|
7967
8965
|
}
|
|
7968
8966
|
/** Install a Fetch guard at the CDP boundary to policy-check new targets while paused. */
|
|
7969
|
-
installTargetGuard(browser) {
|
|
8967
|
+
async installTargetGuard(browser) {
|
|
7970
8968
|
if (this.targetGuardConnection) {
|
|
8969
|
+
if (this.targetGuardReadinessPromise) {
|
|
8970
|
+
await this.targetGuardReadinessPromise;
|
|
8971
|
+
}
|
|
7971
8972
|
return;
|
|
7972
8973
|
}
|
|
7973
8974
|
const connection = browser._connection;
|
|
7974
8975
|
if (!connection || typeof connection.on !== "function") {
|
|
7975
8976
|
this.targetGuardUnavailable = true;
|
|
7976
8977
|
this.logger.warn("Browser target guard is unavailable; popup actions will be blocked");
|
|
8978
|
+
this.targetGuardReadinessPromise = Promise.resolve();
|
|
7977
8979
|
return;
|
|
7978
8980
|
}
|
|
7979
8981
|
this.targetGuardUnavailable = false;
|
|
@@ -7981,6 +8983,7 @@ var BrowserService = class {
|
|
|
7981
8983
|
if (typeof targetConnection.isAutoAttached !== "function") {
|
|
7982
8984
|
this.targetGuardUnavailable = true;
|
|
7983
8985
|
this.logger.warn("Browser target guard is unavailable; attachment ownership cannot be determined");
|
|
8986
|
+
this.targetGuardReadinessPromise = Promise.resolve();
|
|
7984
8987
|
return;
|
|
7985
8988
|
}
|
|
7986
8989
|
const sessionListener = (value) => {
|
|
@@ -7994,6 +8997,10 @@ var BrowserService = class {
|
|
|
7994
8997
|
if (!event) {
|
|
7995
8998
|
return;
|
|
7996
8999
|
}
|
|
9000
|
+
if (this.handledTargetGuardSessions.has(event.sessionId)) {
|
|
9001
|
+
return;
|
|
9002
|
+
}
|
|
9003
|
+
this.handledTargetGuardSessions.add(event.sessionId);
|
|
7997
9004
|
const session = this.pendingTargetGuardSessions.get(event.sessionId) ?? getCdpSession(targetConnection, event.sessionId);
|
|
7998
9005
|
this.pendingTargetGuardSessions.delete(event.sessionId);
|
|
7999
9006
|
this.pendingTargetGuardInfos.delete(event.sessionId);
|
|
@@ -8012,6 +9019,16 @@ var BrowserService = class {
|
|
|
8012
9019
|
}
|
|
8013
9020
|
return;
|
|
8014
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
|
+
}
|
|
8015
9032
|
if (!session) {
|
|
8016
9033
|
this.unguardedTargetSessions.add(event.sessionId);
|
|
8017
9034
|
if (targetConnection.send) {
|
|
@@ -8021,7 +9038,7 @@ var BrowserService = class {
|
|
|
8021
9038
|
return;
|
|
8022
9039
|
}
|
|
8023
9040
|
void this.guardTargetSession(session, event.targetInfo).catch((error) => {
|
|
8024
|
-
this.logger.warn("New browser target guard failed", { error:
|
|
9041
|
+
this.logger.warn("New browser target guard failed", { error: safeErrorDiagnostic(error) });
|
|
8025
9042
|
});
|
|
8026
9043
|
};
|
|
8027
9044
|
const detachedListener = (value) => {
|
|
@@ -8038,12 +9055,14 @@ var BrowserService = class {
|
|
|
8038
9055
|
}
|
|
8039
9056
|
this.pendingTargetGuardSessions.delete(value.sessionId);
|
|
8040
9057
|
this.pendingTargetGuardInfos.delete(value.sessionId);
|
|
9058
|
+
this.handledTargetGuardSessions.delete(value.sessionId);
|
|
8041
9059
|
};
|
|
8042
9060
|
targetConnection.on("sessionattached", sessionListener);
|
|
8043
9061
|
targetConnection.on("Target.attachedToTarget", rawListener);
|
|
8044
9062
|
targetConnection.on("Target.detachedFromTarget", detachedListener);
|
|
8045
9063
|
const originalEmit = targetConnection.emit;
|
|
8046
|
-
|
|
9064
|
+
let emitGuardInstalled = false;
|
|
9065
|
+
if (typeof originalEmit === "function") {
|
|
8047
9066
|
const wrappedEmit = (event, value) => {
|
|
8048
9067
|
if (event === "Target.attachedToTarget") {
|
|
8049
9068
|
rawListener(value);
|
|
@@ -8052,8 +9071,12 @@ var BrowserService = class {
|
|
|
8052
9071
|
};
|
|
8053
9072
|
try {
|
|
8054
9073
|
targetConnection.emit = wrappedEmit;
|
|
9074
|
+
if (targetConnection.emit !== wrappedEmit) {
|
|
9075
|
+
throw new Error("Connection emit wrapper was not installed.");
|
|
9076
|
+
}
|
|
8055
9077
|
this.targetGuardOriginalEmit = originalEmit;
|
|
8056
9078
|
this.targetGuardWrappedEmit = wrappedEmit;
|
|
9079
|
+
emitGuardInstalled = true;
|
|
8057
9080
|
} catch {
|
|
8058
9081
|
this.targetGuardOriginalEmit = void 0;
|
|
8059
9082
|
this.targetGuardWrappedEmit = void 0;
|
|
@@ -8063,12 +9086,33 @@ var BrowserService = class {
|
|
|
8063
9086
|
this.targetGuardConnectionListener = sessionListener;
|
|
8064
9087
|
this.targetGuardRawConnectionListener = rawListener;
|
|
8065
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
|
+
}
|
|
8066
9095
|
if (targetConnection.send) {
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
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;
|
|
8071
9112
|
}
|
|
9113
|
+
this.targetGuardUnavailable = true;
|
|
9114
|
+
this.targetGuardReadinessPromise = Promise.resolve();
|
|
9115
|
+
this.logger.warn("Browser target guard is unavailable; auto-attachment cannot be enabled");
|
|
8072
9116
|
}
|
|
8073
9117
|
detachTargetGuard() {
|
|
8074
9118
|
const connection = this.targetGuardConnection;
|
|
@@ -8097,11 +9141,13 @@ var BrowserService = class {
|
|
|
8097
9141
|
this.targetGuardConnectionListener = void 0;
|
|
8098
9142
|
this.targetGuardRawConnectionListener = void 0;
|
|
8099
9143
|
this.targetGuardDetachedListener = void 0;
|
|
9144
|
+
this.targetGuardReadinessPromise = void 0;
|
|
8100
9145
|
this.targetGuardOriginalEmit = void 0;
|
|
8101
9146
|
this.targetGuardWrappedEmit = void 0;
|
|
8102
9147
|
this.targetGuardUnavailable = false;
|
|
8103
9148
|
this.pendingTargetGuardSessions.clear();
|
|
8104
9149
|
this.pendingTargetGuardInfos.clear();
|
|
9150
|
+
this.handledTargetGuardSessions.clear();
|
|
8105
9151
|
for (const guard of this.targetGuardSessions.values()) {
|
|
8106
9152
|
guard.released = true;
|
|
8107
9153
|
removeCdpListener(guard.session, "Fetch.requestPaused", guard.requestPausedListener);
|
|
@@ -8131,7 +9177,7 @@ var BrowserService = class {
|
|
|
8131
9177
|
};
|
|
8132
9178
|
guard.requestPausedListener = (event) => {
|
|
8133
9179
|
const pending = this.handleTargetGuardRequest(guard, event).catch((error) => {
|
|
8134
|
-
this.logger.debug("New target request guard callback failed", { error:
|
|
9180
|
+
this.logger.debug("New target request guard callback failed", { error: safeErrorDiagnostic(error) });
|
|
8135
9181
|
});
|
|
8136
9182
|
guard.pendingRequests.add(pending);
|
|
8137
9183
|
void pending.finally(() => guard.pendingRequests.delete(pending)).catch(() => void 0);
|
|
@@ -8191,7 +9237,7 @@ var BrowserService = class {
|
|
|
8191
9237
|
this.targetGuardSessions.delete(sessionId);
|
|
8192
9238
|
this.unguardedTargetSessions.add(sessionId);
|
|
8193
9239
|
await this.closeGuardedTarget(guard);
|
|
8194
|
-
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) });
|
|
8195
9241
|
throw error;
|
|
8196
9242
|
}
|
|
8197
9243
|
}
|
|
@@ -8248,7 +9294,7 @@ var BrowserService = class {
|
|
|
8248
9294
|
await guard.session.send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" });
|
|
8249
9295
|
}
|
|
8250
9296
|
} catch (error) {
|
|
8251
|
-
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) });
|
|
8252
9298
|
} finally {
|
|
8253
9299
|
guard.requestIds.delete(requestId);
|
|
8254
9300
|
}
|
|
@@ -8327,9 +9373,15 @@ var BrowserService = class {
|
|
|
8327
9373
|
throw error;
|
|
8328
9374
|
}
|
|
8329
9375
|
} catch (error) {
|
|
8330
|
-
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) });
|
|
8331
9377
|
}
|
|
8332
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
|
+
}
|
|
8333
9385
|
isUnguardedTargetPage(page) {
|
|
8334
9386
|
const identity = pageTargetIdentity(page);
|
|
8335
9387
|
return Boolean(identity.sessionId && this.unguardedTargetSessions.has(identity.sessionId));
|
|
@@ -8384,7 +9436,7 @@ var BrowserService = class {
|
|
|
8384
9436
|
state.challengeStatus = void 0;
|
|
8385
9437
|
state.challengeAttempts = 0;
|
|
8386
9438
|
} catch (error) {
|
|
8387
|
-
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) });
|
|
8388
9440
|
}
|
|
8389
9441
|
}
|
|
8390
9442
|
async disposePageState(state) {
|
|
@@ -8409,6 +9461,7 @@ var BrowserService = class {
|
|
|
8409
9461
|
state.snapshotInteractive = void 0;
|
|
8410
9462
|
state.snapshotId = void 0;
|
|
8411
9463
|
state.policyVerifiedUrls?.clear();
|
|
9464
|
+
this.networkJournal.clear(state.id);
|
|
8412
9465
|
state.dialogs.length = 0;
|
|
8413
9466
|
state.navigationError = void 0;
|
|
8414
9467
|
state.activeNavigationGeneration = void 0;
|
|
@@ -8581,7 +9634,7 @@ var BrowserService = class {
|
|
|
8581
9634
|
throwIfAborted(signal);
|
|
8582
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 });
|
|
8583
9636
|
state.downloadConfigurationError = classified;
|
|
8584
|
-
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) });
|
|
8585
9638
|
}
|
|
8586
9639
|
}
|
|
8587
9640
|
this.assertStateLive(state);
|
|
@@ -8661,6 +9714,18 @@ var BrowserService = class {
|
|
|
8661
9714
|
state.policyVerifiedUrls?.clear();
|
|
8662
9715
|
}
|
|
8663
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
|
+
}
|
|
8664
9729
|
if (/^about:blank(?:#.*)?$/i.test(requestUrl)) {
|
|
8665
9730
|
await request.continue();
|
|
8666
9731
|
return;
|
|
@@ -8704,6 +9769,35 @@ var BrowserService = class {
|
|
|
8704
9769
|
}
|
|
8705
9770
|
}
|
|
8706
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
|
+
}
|
|
8707
9801
|
stateFor(page) {
|
|
8708
9802
|
const existingId = this.ids.get(page);
|
|
8709
9803
|
if (existingId) {
|
|
@@ -8713,7 +9807,7 @@ var BrowserService = class {
|
|
|
8713
9807
|
}
|
|
8714
9808
|
this.ids.delete(page);
|
|
8715
9809
|
}
|
|
8716
|
-
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() };
|
|
8717
9811
|
this.ids.set(page, state.id);
|
|
8718
9812
|
this.states.set(state.id, state);
|
|
8719
9813
|
this.installListeners(state);
|
|
@@ -8729,10 +9823,23 @@ var BrowserService = class {
|
|
|
8729
9823
|
return;
|
|
8730
9824
|
}
|
|
8731
9825
|
try {
|
|
8732
|
-
|
|
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 });
|
|
8733
9840
|
trimLog(state.network);
|
|
8734
9841
|
} catch (error) {
|
|
8735
|
-
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) });
|
|
8736
9843
|
}
|
|
8737
9844
|
};
|
|
8738
9845
|
state.networkRequestListener = networkRequestListener;
|
|
@@ -8746,11 +9853,24 @@ var BrowserService = class {
|
|
|
8746
9853
|
state.mainFrameStatus = response.status();
|
|
8747
9854
|
}
|
|
8748
9855
|
if (state.networkEnabled) {
|
|
8749
|
-
|
|
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() });
|
|
8750
9870
|
trimLog(state.network);
|
|
8751
9871
|
}
|
|
8752
9872
|
} catch (error) {
|
|
8753
|
-
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) });
|
|
8754
9874
|
}
|
|
8755
9875
|
};
|
|
8756
9876
|
state.networkResponseListener = networkResponseListener;
|
|
@@ -8763,7 +9883,7 @@ var BrowserService = class {
|
|
|
8763
9883
|
state.console.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type: "console", level: message.type(), text: message.text().slice(0, 2e3) });
|
|
8764
9884
|
trimLog(state.console);
|
|
8765
9885
|
} catch (error) {
|
|
8766
|
-
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) });
|
|
8767
9887
|
}
|
|
8768
9888
|
};
|
|
8769
9889
|
state.consoleListener = consoleListener;
|
|
@@ -8778,7 +9898,7 @@ var BrowserService = class {
|
|
|
8778
9898
|
this.currentPageId = state.id;
|
|
8779
9899
|
this.logger.info("Browser dialog opened", { pageId: state.id, type });
|
|
8780
9900
|
} catch (error) {
|
|
8781
|
-
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) });
|
|
8782
9902
|
}
|
|
8783
9903
|
};
|
|
8784
9904
|
state.dialogListener = dialogListener;
|
|
@@ -8799,7 +9919,7 @@ var BrowserService = class {
|
|
|
8799
9919
|
state.challengeAttempts = 0;
|
|
8800
9920
|
}
|
|
8801
9921
|
} catch (error) {
|
|
8802
|
-
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) });
|
|
8803
9923
|
}
|
|
8804
9924
|
};
|
|
8805
9925
|
state.frameNavigatedListener = frameNavigatedListener;
|
|
@@ -9824,7 +10944,7 @@ var BrowserService = class {
|
|
|
9824
10944
|
removeDialogListener?.();
|
|
9825
10945
|
if (openedDialog === null) {
|
|
9826
10946
|
void click.catch((error) => {
|
|
9827
|
-
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) });
|
|
9828
10948
|
});
|
|
9829
10949
|
throwIfAborted(signal);
|
|
9830
10950
|
return { navigated: false, urlChanged: false };
|
|
@@ -10569,6 +11689,9 @@ var BrowserService = class {
|
|
|
10569
11689
|
if (before.isSymbolicLink()) {
|
|
10570
11690
|
throw new AppError("FILE_PATH_BLOCKED", "The upload source must not be a symbolic link.");
|
|
10571
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
|
+
}
|
|
10572
11695
|
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
10573
11696
|
let sourceHandle;
|
|
10574
11697
|
let stagingPath;
|
|
@@ -10578,6 +11701,9 @@ var BrowserService = class {
|
|
|
10578
11701
|
if (!opened.isFile()) {
|
|
10579
11702
|
throw new AppError("FILE_PATH_BLOCKED", "The upload source must be a regular file.");
|
|
10580
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
|
+
}
|
|
10581
11707
|
const after = await lstat(candidate);
|
|
10582
11708
|
if (after.isSymbolicLink() || !sameFileIdentity(opened, after)) {
|
|
10583
11709
|
throw new AppError("FILE_PATH_BLOCKED", "The upload source changed while it was being opened.", { retryable: true });
|
|
@@ -10587,10 +11713,15 @@ var BrowserService = class {
|
|
|
10587
11713
|
await mkdir(stagingDirectory, { recursive: true, mode: 448 });
|
|
10588
11714
|
stagingPath = join4(stagingDirectory, `.upload-${randomUUID()}`);
|
|
10589
11715
|
const stagingHandle = await open(stagingPath, "wx", 384);
|
|
11716
|
+
let copiedBytes = 0;
|
|
10590
11717
|
try {
|
|
10591
11718
|
for await (const chunk of sourceHandle.createReadStream({ autoClose: false })) {
|
|
10592
11719
|
throwIfAborted(signal);
|
|
10593
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
|
+
}
|
|
10594
11725
|
let offset = 0;
|
|
10595
11726
|
while (offset < buffer.byteLength) {
|
|
10596
11727
|
const written = await stagingHandle.write(buffer, offset, buffer.byteLength - offset, null);
|
|
@@ -10605,7 +11736,7 @@ var BrowserService = class {
|
|
|
10605
11736
|
await stagingHandle.close().catch(() => void 0);
|
|
10606
11737
|
}
|
|
10607
11738
|
throwIfAborted(signal);
|
|
10608
|
-
return { path: stagingPath, displayName: basename2(candidate), size:
|
|
11739
|
+
return { path: stagingPath, displayName: basename2(candidate), size: copiedBytes };
|
|
10609
11740
|
} catch (error) {
|
|
10610
11741
|
if (stagingPath) {
|
|
10611
11742
|
await unlinkIfPresent(stagingPath);
|
|
@@ -10686,7 +11817,7 @@ var BrowserService = class {
|
|
|
10686
11817
|
}).catch(() => void 0);
|
|
10687
11818
|
return recovery;
|
|
10688
11819
|
}
|
|
10689
|
-
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) {
|
|
10690
11821
|
if (this.queuedOperations >= MAX_QUEUED_OPERATIONS) {
|
|
10691
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." } });
|
|
10692
11823
|
}
|
|
@@ -10767,7 +11898,9 @@ var BrowserService = class {
|
|
|
10767
11898
|
}
|
|
10768
11899
|
}
|
|
10769
11900
|
}, operationBudgetMs);
|
|
10770
|
-
|
|
11901
|
+
if (touchActivity) {
|
|
11902
|
+
this.lastActivityAt = Date.now();
|
|
11903
|
+
}
|
|
10771
11904
|
operationPromise = Promise.resolve().then(() => operation(operationSignal));
|
|
10772
11905
|
void operationPromise.catch(() => void 0);
|
|
10773
11906
|
if (abortRequested) {
|
|
@@ -10894,17 +12027,20 @@ function isBrowserConnectTimeout(error) {
|
|
|
10894
12027
|
return error instanceof AppError && error.code === "BROWSER_CONNECT_TIMEOUT";
|
|
10895
12028
|
}
|
|
10896
12029
|
async function closeConnectedBrowser(browser, owned, logger) {
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
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;
|
|
10908
12044
|
}
|
|
10909
12045
|
return succeeded;
|
|
10910
12046
|
}
|
|
@@ -11474,7 +12610,16 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
|
|
|
11474
12610
|
if (!response.ok) {
|
|
11475
12611
|
throw new Error(`DevTools endpoint returned HTTP ${response.status}.`);
|
|
11476
12612
|
}
|
|
11477
|
-
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);
|
|
11478
12623
|
if (!isRecordValue(value)) {
|
|
11479
12624
|
throw new Error("DevTools endpoint returned an invalid version payload.");
|
|
11480
12625
|
}
|
|
@@ -11487,6 +12632,39 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
|
|
|
11487
12632
|
clearTimeout(timer);
|
|
11488
12633
|
}
|
|
11489
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
|
+
}
|
|
11490
12668
|
function boundedEndpointField(value) {
|
|
11491
12669
|
return typeof value === "string" ? value.slice(0, 4096) : void 0;
|
|
11492
12670
|
}
|
|
@@ -11598,6 +12776,8 @@ var MAX_RESULT_SNIPPET_CHARS = 4e3;
|
|
|
11598
12776
|
var MAX_ATTEMPTS = 3;
|
|
11599
12777
|
var RETRY_BASE_DELAY_MS = 250;
|
|
11600
12778
|
var RETRY_MAX_DELAY_MS = 2e3;
|
|
12779
|
+
var MAX_CONCURRENT_RESEARCH = 4;
|
|
12780
|
+
var MAX_RESEARCH_QUEUE = 16;
|
|
11601
12781
|
var ZERO_WIDTH_PATTERN2 = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
11602
12782
|
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
11603
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;
|
|
@@ -11606,6 +12786,70 @@ var RESULT_CLASS_ATTRIBUTE_PATTERN = /\bclass\s*=\s*(["'])([^"']*)\1/i;
|
|
|
11606
12786
|
var RESULT_HREF_ATTRIBUTE_PATTERN = /\bhref\s*=\s*(["'])([^"']*)\1/i;
|
|
11607
12787
|
var NEXT_RESULT_PATTERN = /<a\b[^>]*\bclass\s*=\s*(["'])[^"']*\bresult__a\b[^"']*\1/i;
|
|
11608
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
|
+
};
|
|
11609
12853
|
var ResearchService = class {
|
|
11610
12854
|
constructor(policy, logger) {
|
|
11611
12855
|
this.policy = policy;
|
|
@@ -11613,6 +12857,7 @@ var ResearchService = class {
|
|
|
11613
12857
|
}
|
|
11614
12858
|
policy;
|
|
11615
12859
|
logger;
|
|
12860
|
+
admission = new ResearchAdmission();
|
|
11616
12861
|
async research(query, options = {}, signal) {
|
|
11617
12862
|
if (typeof query !== "string") {
|
|
11618
12863
|
throw new AppError("RESEARCH_INVALID", "A non-empty research query is required.");
|
|
@@ -11651,7 +12896,17 @@ var ResearchService = class {
|
|
|
11651
12896
|
}, REQUEST_TIMEOUT_MS);
|
|
11652
12897
|
const abort = () => controller.abort();
|
|
11653
12898
|
signal?.addEventListener("abort", abort, { once: true });
|
|
12899
|
+
let release;
|
|
11654
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
|
+
}
|
|
11655
12910
|
const url = await awaitWithAbort2(
|
|
11656
12911
|
this.policy.assertNavigationAllowedAsync(`https://html.duckduckgo.com/html/?q=${encodedQuery}`),
|
|
11657
12912
|
controller.signal
|
|
@@ -11722,9 +12977,13 @@ var ResearchService = class {
|
|
|
11722
12977
|
} finally {
|
|
11723
12978
|
clearTimeout(timeout);
|
|
11724
12979
|
signal?.removeEventListener("abort", abort);
|
|
12980
|
+
release?.();
|
|
11725
12981
|
}
|
|
11726
12982
|
}
|
|
11727
12983
|
};
|
|
12984
|
+
function cancelledResearchError() {
|
|
12985
|
+
return new AppError("CANCELLED", "The research request was cancelled.");
|
|
12986
|
+
}
|
|
11728
12987
|
async function fetchWithRetry(url, signal) {
|
|
11729
12988
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
|
11730
12989
|
if (signal.aborted) {
|
|
@@ -12254,10 +13513,11 @@ var ServerRuntime = class _ServerRuntime {
|
|
|
12254
13513
|
mode: this.config.browser.mode,
|
|
12255
13514
|
configured: managedBrowser || !browserDisabled && (usesExecutable ? Boolean(this.config.browser.executablePath) : Boolean(this.config.browser.wsEndpoint || this.config.browser.url)),
|
|
12256
13515
|
connection: browserDisabled ? "disabled" : managedBrowser ? "managed" : usesExecutable ? "executable" : this.config.browser.wsEndpoint ? "websocket" : "devtools-http",
|
|
12257
|
-
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(),
|
|
12258
13517
|
actionTimeoutMs: this.config.browser.actionTimeoutMs,
|
|
12259
13518
|
connectTimeoutMs: this.config.browser.connectTimeoutMs,
|
|
12260
13519
|
cdpTimeoutMs: this.config.browser.cdpTimeoutMs,
|
|
13520
|
+
idleTimeoutMs: this.config.browser.idleTimeoutMs,
|
|
12261
13521
|
maxScreenshotBytes: this.config.browser.maxScreenshotBytes,
|
|
12262
13522
|
maxHtmlChars: this.config.browser.maxHtmlChars
|
|
12263
13523
|
},
|
|
@@ -12747,6 +14007,7 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12747
14007
|
response.on("error", (error) => runtime.logger.error("MCP HTTP response error", safeErrorDiagnostic(error)));
|
|
12748
14008
|
request.on("error", (error) => runtime.logger.error("MCP HTTP request error", safeErrorDiagnostic(error)));
|
|
12749
14009
|
if (!accepting) {
|
|
14010
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12750
14011
|
response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
|
|
12751
14012
|
response.end(HTTP_SHUTTING_DOWN_BODY);
|
|
12752
14013
|
return;
|
|
@@ -12759,11 +14020,13 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12759
14020
|
}
|
|
12760
14021
|
setCorsHeaders(request, response);
|
|
12761
14022
|
if (!requestPathMatches(request, config.http.path)) {
|
|
14023
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12762
14024
|
response.writeHead(404, { "content-type": "application/json" });
|
|
12763
14025
|
response.end(HTTP_NOT_FOUND_BODY);
|
|
12764
14026
|
return;
|
|
12765
14027
|
}
|
|
12766
14028
|
if (request.method === "OPTIONS") {
|
|
14029
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12767
14030
|
response.writeHead(204, {
|
|
12768
14031
|
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
12769
14032
|
"access-control-allow-headers": request.headers["access-control-request-headers"] ?? "authorization, content-type, accept, mcp-protocol-version, mcp-session-id, last-event-id",
|
|
@@ -12774,6 +14037,7 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12774
14037
|
return;
|
|
12775
14038
|
}
|
|
12776
14039
|
if (!authorized(request, expectedAuthDigest)) {
|
|
14040
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12777
14041
|
response.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" });
|
|
12778
14042
|
response.end(HTTP_UNAUTHORIZED_BODY);
|
|
12779
14043
|
return;
|
|
@@ -12781,6 +14045,7 @@ async function serveHttp(runtime, shutdown) {
|
|
|
12781
14045
|
let streamPool = isPotentialHttpStream(request) ? activeHttpStreams : activeHttpRequests;
|
|
12782
14046
|
const poolLimit = streamPool === activeHttpStreams ? MAX_HTTP_STREAM_CONCURRENCY : MAX_HTTP_CONCURRENCY;
|
|
12783
14047
|
if (streamPool.size >= poolLimit) {
|
|
14048
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12784
14049
|
response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
|
|
12785
14050
|
response.end(HTTP_BUSY_BODY);
|
|
12786
14051
|
return;
|