smooth-operator-mcp 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -5
- package/README.md +5 -2
- package/dist/smooth-operator.mjs +122 -183
- package/dist/smooth-operator.mjs.map +2 -2
- package/docs/mcp-server.md +28 -24
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -40,16 +40,16 @@ SMOOTH_OPERATOR_ALLOW_EVAL=true
|
|
|
40
40
|
# these private roots by default.
|
|
41
41
|
# SMOOTH_OPERATOR_ALLOWED_FILE_ROOTS=~/.smooth-operator/files,~/.smooth-operator/downloads
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
# to
|
|
43
|
+
# Compatibility baseline. Browser identity and automation signals remain native;
|
|
44
|
+
# set false to skip the explicit-viewport compatibility script.
|
|
45
45
|
# SMOOTH_OPERATOR_STEALTH_ENABLED=true
|
|
46
46
|
# Stealth profile: balanced (default) | max (accepted compatibility names; same supported patch set)
|
|
47
47
|
# SMOOTH_OPERATOR_STEALTH_PROFILE=balanced
|
|
48
48
|
# Add opt-in GPU launch flags (does not make an identity/coherence guarantee)
|
|
49
49
|
# SMOOTH_OPERATOR_STEALTH_GPU=false
|
|
50
|
-
# Behavioral
|
|
51
|
-
#
|
|
52
|
-
# SMOOTH_OPERATOR_BEHAVIOR_ENABLED=
|
|
50
|
+
# Behavioral timing is off for the fastest deterministic path; set true only
|
|
51
|
+
# when a site or workflow explicitly needs it.
|
|
52
|
+
# SMOOTH_OPERATOR_BEHAVIOR_ENABLED=false
|
|
53
53
|
|
|
54
54
|
# HTTP is opt-in; use a strong token before enabling remote access
|
|
55
55
|
# SMOOTH_OPERATOR_TRANSPORT=http
|
package/README.md
CHANGED
|
@@ -44,9 +44,12 @@ Ask: *“Scrape pricing into a table”*, *“Fill this form with ~/resume.pdf
|
|
|
44
44
|
|
|
45
45
|
## How to use
|
|
46
46
|
|
|
47
|
-
Talk to your harness normally. It can call `browser_navigate` → `browser_snapshot` → `browser_click` and the rest of the MCP surface as needed. For a challenge, `browser_solve_challenge` returns fresh bounded visual/state evidence
|
|
47
|
+
Talk to your harness normally. It can call `browser_navigate` → `browser_snapshot` → `browser_click` and the rest of the MCP surface as needed. For a challenge, `browser_solve_challenge` returns fresh bounded visual/state evidence and an attempt budget; the connected AI keeps using ordinary browser actions and calls it again until the final classification is clear or the budget is exhausted. Human handoff remains available only as an explicit final option.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Browser identity remains native, page JavaScript is available by default, and
|
|
50
|
+
behavioral timing is off for fast deterministic input. Set the explicit
|
|
51
|
+
environment flags to change those choices. See `docs/STEALTH-GUIDE.md` for
|
|
52
|
+
details and responsible use.
|
|
50
53
|
|
|
51
54
|
For the fastest supported operation, keep behavioral timing off with
|
|
52
55
|
`SMOOTH_OPERATOR_BEHAVIOR_ENABLED=false`; the native browser remains bounded and
|
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.2";
|
|
401
401
|
}
|
|
402
402
|
});
|
|
403
403
|
|
|
@@ -1554,7 +1554,7 @@ function writeSummary(ui, harness, choices) {
|
|
|
1554
1554
|
["Blocked sites", choices.blockedDomains.length ? choices.blockedDomains.join(", ") : "none"],
|
|
1555
1555
|
["Page JavaScript", choices.allowEval ? "enabled" : "off"],
|
|
1556
1556
|
["Stealth baseline", "enabled (balanced)"],
|
|
1557
|
-
["Behavioral
|
|
1557
|
+
["Behavioral timing", "off (fast native input)"],
|
|
1558
1558
|
["Data directory", choices.dataDir]
|
|
1559
1559
|
]
|
|
1560
1560
|
]);
|
|
@@ -1629,7 +1629,7 @@ async function persistWizardConfig(rawChoices, homeDir) {
|
|
|
1629
1629
|
enabled: true,
|
|
1630
1630
|
profile: "balanced",
|
|
1631
1631
|
gpu: false,
|
|
1632
|
-
behaviorEnabled:
|
|
1632
|
+
behaviorEnabled: false
|
|
1633
1633
|
};
|
|
1634
1634
|
const config = { ...previous, browser: browserSection, security: securitySection, stealth: stealthSection };
|
|
1635
1635
|
delete config.captchaSolver;
|
|
@@ -1734,7 +1734,7 @@ import { realpathSync as realpathSync2 } from "node:fs";
|
|
|
1734
1734
|
import process4 from "node:process";
|
|
1735
1735
|
import { Readable } from "node:stream";
|
|
1736
1736
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1737
|
-
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
1737
|
+
import { createMcpHandler, isJsonContentType as sdkIsJsonContentType } from "@modelcontextprotocol/server";
|
|
1738
1738
|
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
1739
1739
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
1740
1740
|
|
|
@@ -2570,7 +2570,7 @@ function loadServerConfig(args = [], environment = env, homeDirectory = homedir(
|
|
|
2570
2570
|
enabled: stealthEnabled,
|
|
2571
2571
|
profile: environment.SMOOTH_OPERATOR_STEALTH_PROFILE ?? nestedStealth.profile ?? "balanced",
|
|
2572
2572
|
gpu: parseBoolean(environment.SMOOTH_OPERATOR_STEALTH_GPU, nestedStealth.gpu ?? false),
|
|
2573
|
-
behaviorEnabled: environment.SMOOTH_OPERATOR_BEHAVIOR_ENABLED === void 0 ? nestedStealth.behaviorEnabled ??
|
|
2573
|
+
behaviorEnabled: environment.SMOOTH_OPERATOR_BEHAVIOR_ENABLED === void 0 ? nestedStealth.behaviorEnabled ?? false : parseBoolean(environment.SMOOTH_OPERATOR_BEHAVIOR_ENABLED, stealthEnabled)
|
|
2574
2574
|
};
|
|
2575
2575
|
const config = {
|
|
2576
2576
|
transport: argValue("--transport") ?? environment.SMOOTH_OPERATOR_TRANSPORT ?? fileConfig.transport ?? "stdio",
|
|
@@ -2820,6 +2820,7 @@ var BrowserActionFieldsSchema = z2.object({
|
|
|
2820
2820
|
verify: z2.boolean().optional(),
|
|
2821
2821
|
durationMs: z2.number().int().min(0).max(3e4).optional(),
|
|
2822
2822
|
pollMs: z2.number().int().min(250).max(1e4).optional(),
|
|
2823
|
+
maxAttempts: z2.number().int().min(1).max(100).optional(),
|
|
2823
2824
|
optionValue: BoundedString(2e3).optional(),
|
|
2824
2825
|
optionValues: z2.array(BoundedString(2e3)).min(1).max(200).optional(),
|
|
2825
2826
|
cookieName: BoundedString(256).optional(),
|
|
@@ -3256,7 +3257,8 @@ var SolveChallengeRequestSchema = z2.object({
|
|
|
3256
3257
|
full: z2.boolean().optional(),
|
|
3257
3258
|
maxDimension: z2.number().int().min(1).max(2e4).optional(),
|
|
3258
3259
|
max_dim: z2.number().int().min(1).max(2e4).optional(),
|
|
3259
|
-
maxChars: z2.number().int().min(1e3).max(MCP_PAGE_TEXT_MAX_CHARS).optional()
|
|
3260
|
+
maxChars: z2.number().int().min(1e3).max(MCP_PAGE_TEXT_MAX_CHARS).optional(),
|
|
3261
|
+
maxAttempts: z2.number().int().min(1).max(100).optional()
|
|
3260
3262
|
}).strict().superRefine((input, context) => {
|
|
3261
3263
|
if (input.includeScreenshot !== void 0 && input.include_screenshot !== void 0) {
|
|
3262
3264
|
context.addIssue({ code: "custom", message: "Provide includeScreenshot or include_screenshot, not both." });
|
|
@@ -3419,6 +3421,23 @@ var MCP_OUTPUT_TRUNCATION_MARKER_BYTES = UTF8_ENCODER2.encode(MCP_OUTPUT_TRUNCAT
|
|
|
3419
3421
|
var MCP_ERROR_CODE_MAX_BYTES = 200;
|
|
3420
3422
|
var MCP_ERROR_MESSAGE_MAX_BYTES = 4e3;
|
|
3421
3423
|
var MCP_JSON_TEXT_CACHE = /* @__PURE__ */ new WeakMap();
|
|
3424
|
+
var MCP_OUTPUT_CONTRACT_ARRAY_KEYS = /* @__PURE__ */ new Set([
|
|
3425
|
+
"links",
|
|
3426
|
+
"results",
|
|
3427
|
+
"entries",
|
|
3428
|
+
"interactive",
|
|
3429
|
+
"nodes",
|
|
3430
|
+
"matches",
|
|
3431
|
+
"frames"
|
|
3432
|
+
]);
|
|
3433
|
+
var MCP_OUTPUT_ARRAY_BOUNDS = [
|
|
3434
|
+
["links", "linksTruncated"],
|
|
3435
|
+
["entries", "entriesTruncated"],
|
|
3436
|
+
["interactive", "interactiveTruncated"],
|
|
3437
|
+
["nodes", "nodesTruncated"],
|
|
3438
|
+
["matches", "matchesTruncated"],
|
|
3439
|
+
["frames", "framesTruncated"]
|
|
3440
|
+
];
|
|
3422
3441
|
var NetworkIdleSchema = z3.object({
|
|
3423
3442
|
timeoutMs: z3.number().int().min(100).max(12e4).optional(),
|
|
3424
3443
|
pageId: z3.string().trim().min(1).max(200).optional()
|
|
@@ -3552,28 +3571,14 @@ var BrowserExecCodeSchema = z3.string().trim().min(1).max(8e4).superRefine((code
|
|
|
3552
3571
|
context.addIssue({ code: "custom", message: "code must be a JSON array of validated browser actions." });
|
|
3553
3572
|
return;
|
|
3554
3573
|
}
|
|
3555
|
-
if (!
|
|
3556
|
-
context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of
|
|
3574
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > 100) {
|
|
3575
|
+
context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of at most 100 browser actions." });
|
|
3557
3576
|
}
|
|
3558
3577
|
});
|
|
3559
3578
|
var BrowserExecRequestSchema = z3.object({
|
|
3560
3579
|
code: BrowserExecCodeSchema,
|
|
3561
3580
|
confirmDestructive: z3.boolean().optional()
|
|
3562
|
-
}).strict()
|
|
3563
|
-
if (input.confirmDestructive) {
|
|
3564
|
-
return;
|
|
3565
|
-
}
|
|
3566
|
-
let parsed;
|
|
3567
|
-
try {
|
|
3568
|
-
parsed = JSON.parse(input.code);
|
|
3569
|
-
} catch {
|
|
3570
|
-
return;
|
|
3571
|
-
}
|
|
3572
|
-
const actions = BrowserActionPlanSchema.safeParse(parsed);
|
|
3573
|
-
if (actions.success && actions.data.some((action) => isDestructiveBatchAction(action.action))) {
|
|
3574
|
-
context.addIssue({ code: "custom", path: ["confirmDestructive"], message: "This action plan contains destructive actions. Set confirmDestructive=true to execute them." });
|
|
3575
|
-
}
|
|
3576
|
-
});
|
|
3581
|
+
}).strict();
|
|
3577
3582
|
var BrowserUseStateSchema = z3.object({
|
|
3578
3583
|
include_screenshot: z3.boolean().optional(),
|
|
3579
3584
|
fullPage: z3.boolean().optional(),
|
|
@@ -3613,7 +3618,7 @@ var BROWSER_MUTATING = { ...MUTATING, openWorldHint: true };
|
|
|
3613
3618
|
var BROWSER_DESTRUCTIVE = { ...DESTRUCTIVE, openWorldHint: true };
|
|
3614
3619
|
var MCP_INSTRUCTIONS = [
|
|
3615
3620
|
"Use browser_snapshot or browser_get_state before interacting so element refs/indexes and viewport coordinates are current.",
|
|
3616
|
-
"
|
|
3621
|
+
"Use an observe -> act -> verify loop: serialize dependent browser calls as one navigation or mutation between observations. Parallel calls are appropriate only for independent read-only observations; a parallel snapshot and action do not form a transaction.",
|
|
3617
3622
|
"Give each request a bounded timeout or cancellation signal. After a timeout or cancellation, inspect current state before retrying a mutation; cancellation is not proof that a mutation did not happen.",
|
|
3618
3623
|
"After navigation, tab switching, scrolling that changes lazy content, or any DOM-changing action, discard old refs and indexes and capture a fresh snapshot instead of silently falling back to coordinates, text, or a different selector.",
|
|
3619
3624
|
"Only report titles, URLs, snippets, and metadata that are explicitly present in the returned MCP fields. Absence of a field is evidence of absence: never invent titles, summaries, counts, or other metadata that the tools did not return.",
|
|
@@ -3623,8 +3628,7 @@ var MCP_INSTRUCTIONS = [
|
|
|
3623
3628
|
"Prefer stable refs, indexes, and selectors over coordinates; use coordinates only when the page cannot expose a reliable target.",
|
|
3624
3629
|
"For open shadow roots, Puppeteer pierce/ selectors may be used explicitly; closed shadow roots remain unavailable.",
|
|
3625
3630
|
"Use browser_batch for short validated sequences, but keep destructive actions separate when user confirmation is needed.",
|
|
3626
|
-
"
|
|
3627
|
-
"browser_solve_challenge is an internal connected-AI loop. It collects a fresh challenge classification plus bounded visual/state evidence, then the connected AI uses normal browser actions and calls it again to verify. Never claim a challenge is solved unless the final classification explicitly reports it absent; unknown or failed classification is not success.",
|
|
3631
|
+
"browser_solve_challenge is an internal connected-AI loop. Each call is one bounded verification cycle; present and exhausted classifications include fresh visual/state evidence and attemptsRemaining. The connected AI should keep using normal browser actions and call it again until the final classification explicitly reports the challenge absent or automation_exhausted. Never claim a challenge is solved from a present, unknown, or failed classification. Human handoff is only an explicit final option after exhaustion.",
|
|
3628
3632
|
"The server contains no LLM or agent planner; the MCP client is responsible for reasoning, retries, and task completion."
|
|
3629
3633
|
].join(" ");
|
|
3630
3634
|
function createMcpServer(runtime) {
|
|
@@ -3719,7 +3723,10 @@ function registerBrowserTools(server, runtime) {
|
|
|
3719
3723
|
inputSchema: BrowserUseExtractSchema,
|
|
3720
3724
|
annotations: BROWSER_READ_ONLY
|
|
3721
3725
|
},
|
|
3722
|
-
async (input, ctx) =>
|
|
3726
|
+
async (input, ctx) => {
|
|
3727
|
+
const { extract_links, ...fields } = input;
|
|
3728
|
+
return callTool(() => runtime.run({ action: "extract", ...fields, includeLinks: extract_links, maxChars: MCP_PAGE_TEXT_MAX_CHARS }, ctx.mcpReq.signal), runtime);
|
|
3729
|
+
}
|
|
3723
3730
|
);
|
|
3724
3731
|
registerAction(server, runtime, "browser_navigate", "Navigate the browser", "Open an HTTP(S) URL after domain and private-network policy validation. DNS is checked before navigation but the browser resolver is not pinned. Set includeSnapshot=true for one trailing snapshot.", NavigateRequestSchema, "navigate", (input) => {
|
|
3725
3732
|
const { new_tab, ...fields } = input;
|
|
@@ -3757,7 +3764,10 @@ function registerBrowserTools(server, runtime) {
|
|
|
3757
3764
|
{ title: "Read browser console log", description: "Enable, disable, read, clear, or read-and-clear the bounded console log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
|
|
3758
3765
|
async (input, ctx) => callTool(() => runtime.run({ action: consoleAction(input.operation), pageId: input.pageId }, ctx.mcpReq.signal), runtime)
|
|
3759
3766
|
);
|
|
3760
|
-
registerAction(server, runtime, "browser_find_text", "Find text", "Find and center the first matching text on the page.", PageQuerySchema, "find_text", (input) =>
|
|
3767
|
+
registerAction(server, runtime, "browser_find_text", "Find text", "Find and center the first matching text on the page.", PageQuerySchema, "find_text", (input) => {
|
|
3768
|
+
const { query, ...fields } = input;
|
|
3769
|
+
return { ...fields, text: query };
|
|
3770
|
+
});
|
|
3761
3771
|
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 }));
|
|
3762
3772
|
registerAction(server, runtime, "browser_upload", "Upload a file", "Upload a file from an allowed server file root into a file input.", UploadRequestSchema, "upload_file");
|
|
3763
3773
|
registerAction(server, runtime, "browser_screenshot", "Capture a screenshot", "Capture a bounded PNG or JPEG screenshot of the current page.", ScreenshotRequestSchema, "screenshot", (input) => {
|
|
@@ -3776,7 +3786,10 @@ function registerBrowserTools(server, runtime) {
|
|
|
3776
3786
|
registerAction(server, runtime, "browser_computed_style", "Read computed style", "Read a small safe subset of computed style for an element.", SelectorRequestSchema, "get_computed_style");
|
|
3777
3787
|
registerAction(server, runtime, "browser_page_info", "Read page information", "Read URL, title, viewport, and document dimensions.", EmptyInputSchema, "get_page_info");
|
|
3778
3788
|
registerAction(server, runtime, "browser_hover", "Hover an element", "Move the pointer over a CSS selector or snapshot ref.", TargetRequestSchema, "hover");
|
|
3779
|
-
registerAction(server, runtime, "browser_move", "Move the pointer", "Move the pointer to bounded top-level viewport coordinates without clicking. Use this to inspect hover-driven UI before choosing a click point.", MoveRequestSchema, "move", (input) =>
|
|
3789
|
+
registerAction(server, runtime, "browser_move", "Move the pointer", "Move the pointer to bounded top-level viewport coordinates without clicking. Use this to inspect hover-driven UI before choosing a click point.", MoveRequestSchema, "move", (input) => {
|
|
3790
|
+
const { coordinate_x, coordinate_y, ...fields } = input;
|
|
3791
|
+
return { ...fields, coordinateX: fields.coordinateX ?? coordinate_x, coordinateY: fields.coordinateY ?? coordinate_y };
|
|
3792
|
+
});
|
|
3780
3793
|
registerAction(server, runtime, "browser_press_and_hold", "Press and hold or drag", "Press a mouse button on an element for a bounded duration. Optional startCoordinateX/startCoordinateY and endCoordinateX/endCoordinateY drag with interpolated mouse events; path supplies a bounded explicit pointer path for drawing or selection gestures.", HoldRequestSchema, "press_and_hold");
|
|
3781
3794
|
registerAction(server, runtime, "browser_challenge", "Detect a web challenge", "Detect bounded challenge markers and return a fresh classification for the current page. A detected challenge is not evidence that it has been solved.", EmptyInputSchema, "detect_challenge");
|
|
3782
3795
|
registerAction(server, runtime, "browser_wait_for_human", "Wait for human takeover", "Optionally wait for a user to complete a visible challenge or sign-in step in the browser. The result includes a fresh final classification.", WaitForHumanRequestSchema, "wait_for_human");
|
|
@@ -3784,7 +3797,7 @@ function registerBrowserTools(server, runtime) {
|
|
|
3784
3797
|
"browser_solve_challenge",
|
|
3785
3798
|
{
|
|
3786
3799
|
title: "Solve a web challenge",
|
|
3787
|
-
description: "Run the internal connected-AI challenge loop
|
|
3800
|
+
description: "Run one cycle of the internal connected-AI challenge loop. Collect fresh bounded visual/state evidence, use normal browser actions, and call again until the challenge is explicitly absent or the bounded attempt budget is exhausted. No external solver or token injection is used.",
|
|
3788
3801
|
inputSchema: SolveChallengeRequestSchema,
|
|
3789
3802
|
annotations: BROWSER_READ_ONLY
|
|
3790
3803
|
},
|
|
@@ -3811,11 +3824,24 @@ function registerBrowserTools(server, runtime) {
|
|
|
3811
3824
|
"browser_exec",
|
|
3812
3825
|
{
|
|
3813
3826
|
title: "Execute a browser action program",
|
|
3814
|
-
description: "Browser-use CLI compatibility entry point. The code must be a JSON array of validated browser actions; arbitrary Python
|
|
3827
|
+
description: "Browser-use CLI compatibility entry point. The code must be a JSON array of validated browser actions; it is not a shell or arbitrary Python runner. Page JavaScript is limited to the explicit evaluate action and server policy.",
|
|
3815
3828
|
inputSchema: BrowserExecRequestSchema,
|
|
3816
3829
|
annotations: BROWSER_DESTRUCTIVE
|
|
3817
3830
|
},
|
|
3818
|
-
async (input, ctx) => callBatchTool(() =>
|
|
3831
|
+
async (input, ctx) => callBatchTool(() => {
|
|
3832
|
+
const actions = parseBrowserExecCode(input.code);
|
|
3833
|
+
if (!input.confirmDestructive) {
|
|
3834
|
+
const destructiveIndex = actions.findIndex((action) => isDestructiveBatchAction(action.action));
|
|
3835
|
+
if (destructiveIndex >= 0) {
|
|
3836
|
+
const action = actions[destructiveIndex];
|
|
3837
|
+
throw new AppError("DESTRUCTIVE_CONFIRMATION_REQUIRED", `Action '${action.action}' must be executed separately or with confirmDestructive=true.`, {
|
|
3838
|
+
retryable: true,
|
|
3839
|
+
details: { failedIndex: destructiveIndex, failedAction: action.action, hint: "Set confirmDestructive=true or run the action separately." }
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
return runtime.runBatch(actions, { confirmDestructive: input.confirmDestructive }, ctx.mcpReq.signal);
|
|
3844
|
+
}, runtime)
|
|
3819
3845
|
);
|
|
3820
3846
|
server.registerTool(
|
|
3821
3847
|
"browser_batch",
|
|
@@ -4138,7 +4164,7 @@ function truncateUtf82(value, maxBytes) {
|
|
|
4138
4164
|
while (low < high) {
|
|
4139
4165
|
const midpoint = Math.ceil((low + high) / 2);
|
|
4140
4166
|
const candidate = decoder.decode(bytes.slice(0, midpoint));
|
|
4141
|
-
if (UTF8_ENCODER2.encode(candidate).byteLength <=
|
|
4167
|
+
if (UTF8_ENCODER2.encode(candidate).byteLength <= boundedMaxBytes) {
|
|
4142
4168
|
low = midpoint;
|
|
4143
4169
|
} else {
|
|
4144
4170
|
high = midpoint - 1;
|
|
@@ -4259,9 +4285,8 @@ function boundMcpOutput(value, options = {}) {
|
|
|
4259
4285
|
capArray("nodes", MCP_OUTPUT_NODE_LIMIT, "nodesTruncated");
|
|
4260
4286
|
capArray("matches", MCP_OUTPUT_MATCH_LIMIT, "matchesTruncated");
|
|
4261
4287
|
capArray("frames", 20, "framesTruncated");
|
|
4262
|
-
const contractArrayKeys = /* @__PURE__ */ new Set(["links", "results", "entries", "interactive", "nodes", "matches", "frames"]);
|
|
4263
4288
|
for (const [key, item] of Object.entries(output)) {
|
|
4264
|
-
if (!
|
|
4289
|
+
if (!MCP_OUTPUT_CONTRACT_ARRAY_KEYS.has(key) && Array.isArray(item)) {
|
|
4265
4290
|
capArray(key, MCP_OUTPUT_ARRAY_ITEM_LIMIT, `${key}Truncated`);
|
|
4266
4291
|
}
|
|
4267
4292
|
}
|
|
@@ -4324,10 +4349,7 @@ function boundMcpOutput(value, options = {}) {
|
|
|
4324
4349
|
markOutputTruncated();
|
|
4325
4350
|
}
|
|
4326
4351
|
}
|
|
4327
|
-
const arrayBounds =
|
|
4328
|
-
if (!options.preserveBatchResults) {
|
|
4329
|
-
arrayBounds.push(["results", "resultsTruncated"]);
|
|
4330
|
-
}
|
|
4352
|
+
const arrayBounds = options.preserveBatchResults ? MCP_OUTPUT_ARRAY_BOUNDS : [...MCP_OUTPUT_ARRAY_BOUNDS, ["results", "resultsTruncated"]];
|
|
4331
4353
|
for (const [key, flag] of arrayBounds) {
|
|
4332
4354
|
while (jsonByteLength2(output) > MCP_OUTPUT_MAX_BYTES && Array.isArray(output[key]) && output[key].length > 1) {
|
|
4333
4355
|
const items = output[key];
|
|
@@ -4732,81 +4754,16 @@ function boundedList(values) {
|
|
|
4732
4754
|
}
|
|
4733
4755
|
|
|
4734
4756
|
// src/server/browser/stealth.ts
|
|
4735
|
-
var STEALTH_BASELINE_ARGS = [
|
|
4736
|
-
"--disable-blink-features=AutomationControlled"
|
|
4737
|
-
// hide navigator.webdriver at the C++ source
|
|
4738
|
-
];
|
|
4757
|
+
var STEALTH_BASELINE_ARGS = [];
|
|
4739
4758
|
function buildStealthInitScript(profile, options = {}) {
|
|
4740
4759
|
const { width, height } = profile.viewport;
|
|
4741
4760
|
const applyViewport = options.applyViewport === true;
|
|
4742
|
-
const
|
|
4743
|
-
//
|
|
4744
|
-
|
|
4745
|
-
return 'function ' + fnName + '() { [native code] }';
|
|
4746
|
-
}
|
|
4747
|
-
function patchToString(target, fnStr) {
|
|
4748
|
-
try {
|
|
4749
|
-
Object.defineProperty(target, 'toString', {
|
|
4750
|
-
configurable: true,
|
|
4751
|
-
writable: true,
|
|
4752
|
-
value: function toString() { return fnStr; }
|
|
4753
|
-
});
|
|
4754
|
-
} catch (e) {}
|
|
4755
|
-
}
|
|
4756
|
-
function stripProxyFromErrors(fn) {
|
|
4757
|
-
try {
|
|
4758
|
-
return new Proxy(fn, {
|
|
4759
|
-
apply: function applyTrap(target, thisArg, args) {
|
|
4760
|
-
try { return Reflect.apply(target, thisArg, args); }
|
|
4761
|
-
catch (e) { throw e; }
|
|
4762
|
-
}
|
|
4763
|
-
});
|
|
4764
|
-
} catch (e) { return fn; }
|
|
4765
|
-
}
|
|
4766
|
-
|
|
4767
|
-
// ---- supported runtime values (only explicit configuration is interpolated) ----
|
|
4761
|
+
const source = `
|
|
4762
|
+
// Only an explicitly configured viewport is applied. Browser identity and
|
|
4763
|
+
// native automation signals remain untouched.
|
|
4768
4764
|
var APPLY_VIEWPORT = ${String(applyViewport)};
|
|
4769
4765
|
var VIEWPORT = { width: ${width}, height: ${height} };`;
|
|
4770
|
-
const
|
|
4771
|
-
// 1. navigator.webdriver \u2014 belt-and-suspenders (the launch flag is primary).
|
|
4772
|
-
try {
|
|
4773
|
-
var navProto = Object.getPrototypeOf(navigator);
|
|
4774
|
-
if (navProto && Object.prototype.hasOwnProperty.call(navProto, 'webdriver')) {
|
|
4775
|
-
delete navProto.webdriver;
|
|
4776
|
-
}
|
|
4777
|
-
} catch (e) {}
|
|
4778
|
-
|
|
4779
|
-
// 2. navigator.permissions.query \u2014 resolve the "impossible combination".
|
|
4780
|
-
try {
|
|
4781
|
-
if (navigator.permissions && typeof navigator.permissions.query === 'function') {
|
|
4782
|
-
var perms = navigator.permissions;
|
|
4783
|
-
var originalQuery = perms.query.bind(perms);
|
|
4784
|
-
perms.query = function (query) {
|
|
4785
|
-
if (query && query.name === 'notifications' &&
|
|
4786
|
-
window.location && window.location.protocol !== 'https:') {
|
|
4787
|
-
try {
|
|
4788
|
-
if (typeof PermissionStatus !== 'undefined') {
|
|
4789
|
-
return Promise.resolve(new PermissionStatus({ state: 'denied' }));
|
|
4790
|
-
}
|
|
4791
|
-
} catch (e) {}
|
|
4792
|
-
}
|
|
4793
|
-
return originalQuery(query);
|
|
4794
|
-
};
|
|
4795
|
-
}
|
|
4796
|
-
} catch (e) {}
|
|
4797
|
-
|
|
4798
|
-
// 3. toString / Proxy trace hiding \u2014 the patched getters hide themselves.
|
|
4799
|
-
try {
|
|
4800
|
-
if (navigator.permissions && typeof navigator.permissions.query === 'function') {
|
|
4801
|
-
var hiddenQuery = stripProxyFromErrors(navigator.permissions.query);
|
|
4802
|
-
patchToString(hiddenQuery, makeNativeString('query'));
|
|
4803
|
-
navigator.permissions.query = hiddenQuery;
|
|
4804
|
-
}
|
|
4805
|
-
} catch (e) {}
|
|
4806
|
-
|
|
4807
|
-
// Coherence: screen dimensions track an explicitly configured launch
|
|
4808
|
-
// viewport (guarded, best-effort). Without that explicit input, leave the
|
|
4809
|
-
// browser's real dimensions untouched.
|
|
4766
|
+
const viewport = `
|
|
4810
4767
|
try {
|
|
4811
4768
|
if (APPLY_VIEWPORT && window && typeof window.innerWidth === 'number') {
|
|
4812
4769
|
Object.defineProperty(window, 'innerWidth', {
|
|
@@ -4818,8 +4775,8 @@ function buildStealthInitScript(profile, options = {}) {
|
|
|
4818
4775
|
}
|
|
4819
4776
|
} catch (e) {}`;
|
|
4820
4777
|
return `(function () {
|
|
4821
|
-
${
|
|
4822
|
-
${
|
|
4778
|
+
${source}
|
|
4779
|
+
${viewport}
|
|
4823
4780
|
})();
|
|
4824
4781
|
`;
|
|
4825
4782
|
}
|
|
@@ -4868,67 +4825,10 @@ function nativeBrowserLaunchArgs(options = {}) {
|
|
|
4868
4825
|
init_discovery();
|
|
4869
4826
|
|
|
4870
4827
|
// src/server/browser/fingerprints.ts
|
|
4871
|
-
var DEFAULT_PROFILE = "balanced";
|
|
4872
|
-
var DEFAULT_VERSION = 124;
|
|
4873
|
-
var DEFAULT_PLATFORM = "Windows";
|
|
4874
|
-
var DEFAULT_LANGUAGE = "en-US";
|
|
4875
|
-
var DEFAULT_TIME_ZONE = "America/New_York";
|
|
4876
4828
|
var DEFAULT_VIEWPORT_WIDTH = 1920;
|
|
4877
4829
|
var DEFAULT_VIEWPORT_HEIGHT = 1080;
|
|
4878
|
-
var DEFAULT_SEED = 0;
|
|
4879
|
-
var PLATFORM_SEGMENTS = {
|
|
4880
|
-
Windows: "Windows NT 10.0; Win64; x64",
|
|
4881
|
-
macOS: "Mac OS X 10_15_7",
|
|
4882
|
-
Linux: "X11; Linux x86_64"
|
|
4883
|
-
};
|
|
4884
|
-
var HARDWARE_CONCURRENCY_SET = [2, 4, 8, 16, 32];
|
|
4885
|
-
var DEVICEMEMORY_SET = [1, 2, 4, 8];
|
|
4886
|
-
var MAX_TOUCH_POINTS_SET = [0, 5, 10];
|
|
4887
4830
|
function buildFingerprintProfile(options = {}) {
|
|
4888
|
-
|
|
4889
|
-
const version = normalizeVersion(options.version);
|
|
4890
|
-
const platform3 = normalizePlatform(options.platform);
|
|
4891
|
-
const viewport = normalizeViewport(options.viewport);
|
|
4892
|
-
const language = options.language ?? DEFAULT_LANGUAGE;
|
|
4893
|
-
const seed = normalizeSeed(options.seed);
|
|
4894
|
-
const versionStr = String(version);
|
|
4895
|
-
const userAgent = `Mozilla/5.0 (${PLATFORM_SEGMENTS[platform3]}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionStr}.0.0.0 Safari/537.36`;
|
|
4896
|
-
const brands = [
|
|
4897
|
-
{ brand: "Chromium", version: versionStr },
|
|
4898
|
-
{ brand: "Google Chrome", version: versionStr },
|
|
4899
|
-
{ brand: "Not=A?Brand", version: "8" }
|
|
4900
|
-
];
|
|
4901
|
-
const fullVersionList = {
|
|
4902
|
-
Chromium: versionStr,
|
|
4903
|
-
"Google Chrome": versionStr
|
|
4904
|
-
};
|
|
4905
|
-
const profileWithLanguages = {
|
|
4906
|
-
version,
|
|
4907
|
-
userAgent,
|
|
4908
|
-
platform: platform3,
|
|
4909
|
-
mobile: false,
|
|
4910
|
-
brands,
|
|
4911
|
-
fullVersionList,
|
|
4912
|
-
languages: buildLanguages(language),
|
|
4913
|
-
acceptLanguage: `${language},en;q=0.9`,
|
|
4914
|
-
viewport
|
|
4915
|
-
};
|
|
4916
|
-
if (profile === "max") {
|
|
4917
|
-
return {
|
|
4918
|
-
...profileWithLanguages,
|
|
4919
|
-
hardwareConcurrency: pickFromValidSet(HARDWARE_CONCURRENCY_SET, seed),
|
|
4920
|
-
deviceMemory: pickFromValidSet(DEVICEMEMORY_SET, seed),
|
|
4921
|
-
maxTouchPoints: pickFromValidSet(MAX_TOUCH_POINTS_SET, seed),
|
|
4922
|
-
timeZone: options.timeZone ?? DEFAULT_TIME_ZONE
|
|
4923
|
-
};
|
|
4924
|
-
}
|
|
4925
|
-
return profileWithLanguages;
|
|
4926
|
-
}
|
|
4927
|
-
function normalizeVersion(version) {
|
|
4928
|
-
return typeof version === "number" && Number.isFinite(version) && version > 0 ? Math.floor(version) : DEFAULT_VERSION;
|
|
4929
|
-
}
|
|
4930
|
-
function normalizePlatform(platform3) {
|
|
4931
|
-
return platform3 && platform3 in PLATFORM_SEGMENTS ? platform3 : DEFAULT_PLATFORM;
|
|
4831
|
+
return { viewport: normalizeViewport(options.viewport) };
|
|
4932
4832
|
}
|
|
4933
4833
|
function normalizeViewport(viewport) {
|
|
4934
4834
|
if (viewport && Number.isFinite(viewport.width) && Number.isFinite(viewport.height) && viewport.width > 0 && viewport.height > 0) {
|
|
@@ -4936,16 +4836,6 @@ function normalizeViewport(viewport) {
|
|
|
4936
4836
|
}
|
|
4937
4837
|
return { width: DEFAULT_VIEWPORT_WIDTH, height: DEFAULT_VIEWPORT_HEIGHT };
|
|
4938
4838
|
}
|
|
4939
|
-
function normalizeSeed(seed) {
|
|
4940
|
-
return typeof seed === "number" && Number.isFinite(seed) ? seed : DEFAULT_SEED;
|
|
4941
|
-
}
|
|
4942
|
-
function buildLanguages(language) {
|
|
4943
|
-
const parts = language.split(/[-_]/);
|
|
4944
|
-
return parts.length > 1 ? [language, parts[0].toLowerCase()] : [language];
|
|
4945
|
-
}
|
|
4946
|
-
function pickFromValidSet(set, seed) {
|
|
4947
|
-
return set[Math.floor(Math.abs(seed)) % set.length];
|
|
4948
|
-
}
|
|
4949
4839
|
|
|
4950
4840
|
// src/server/browser/utils.ts
|
|
4951
4841
|
var SENSITIVE_URL_PART = /(access[_-]?token|api[_-]?key|auth|code|credential|jwt|nonce|otp|password|secret|session|sig(?:nature)?|token)/i;
|
|
@@ -5051,6 +4941,8 @@ var MAX_DOM_TRAVERSAL_NODES = 2e4;
|
|
|
5051
4941
|
var MAX_TEXT_SCAN_CHARS = 5e5;
|
|
5052
4942
|
var MAX_MARKUP_EVIDENCE_CHARS = 12e4;
|
|
5053
4943
|
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.";
|
|
4944
|
+
var CHALLENGE_DEFAULT_MAX_ATTEMPTS = 32;
|
|
4945
|
+
var CHALLENGE_MAX_ATTEMPTS = 100;
|
|
5054
4946
|
var MAX_DOWNLOAD_ENTRIES = 100;
|
|
5055
4947
|
var TARGET_GUARD_MAX_REQUEST_IDS = 128;
|
|
5056
4948
|
var CLICK_SETTLE_TIMEOUT_MS = 10;
|
|
@@ -8338,6 +8230,7 @@ var BrowserService = class {
|
|
|
8338
8230
|
this.clearTargetGuardNavigationError(state.page);
|
|
8339
8231
|
state.policyVerifiedUrls?.clear();
|
|
8340
8232
|
state.challengeStatus = void 0;
|
|
8233
|
+
state.challengeAttempts = 0;
|
|
8341
8234
|
} catch (error) {
|
|
8342
8235
|
this.logger.debug("Blocked navigation recovery could not restore a blank page", { pageId: state.id, error: String(error) });
|
|
8343
8236
|
}
|
|
@@ -8570,7 +8463,7 @@ var BrowserService = class {
|
|
|
8570
8463
|
state.stealthInjected = true;
|
|
8571
8464
|
} catch (error) {
|
|
8572
8465
|
throwIfAborted(signal);
|
|
8573
|
-
throw new AppError("STEALTH_INITIALIZATION_FAILED", "The
|
|
8466
|
+
throw new AppError("STEALTH_INITIALIZATION_FAILED", "The browser compatibility script could not be injected.", { retryable: true, cause: error });
|
|
8574
8467
|
}
|
|
8575
8468
|
}
|
|
8576
8469
|
await this.releaseTargetGuardForPage(state.page);
|
|
@@ -8751,6 +8644,7 @@ var BrowserService = class {
|
|
|
8751
8644
|
const mainFrame = state.page.mainFrame();
|
|
8752
8645
|
if (frame === mainFrame) {
|
|
8753
8646
|
state.challengeStatus = void 0;
|
|
8647
|
+
state.challengeAttempts = 0;
|
|
8754
8648
|
}
|
|
8755
8649
|
} catch (error) {
|
|
8756
8650
|
this.logger.debug("Browser frame navigation event was unavailable after page disposal", { pageId: state.id, error: String(error) });
|
|
@@ -10372,12 +10266,15 @@ var BrowserService = class {
|
|
|
10372
10266
|
async solveChallenge(state, action, signal) {
|
|
10373
10267
|
throwIfAborted(signal);
|
|
10374
10268
|
const previousChallengeStatus = state.challengeStatus;
|
|
10269
|
+
const requestedMaxAttempts = action.maxAttempts;
|
|
10270
|
+
const maxAttempts = Number.isFinite(requestedMaxAttempts) ? Math.min(CHALLENGE_MAX_ATTEMPTS, Math.max(1, Math.floor(requestedMaxAttempts))) : CHALLENGE_DEFAULT_MAX_ATTEMPTS;
|
|
10375
10271
|
const detection = await this.detectChallenge(state, signal);
|
|
10376
10272
|
if (isChallengeUnknown(detection)) {
|
|
10377
10273
|
state.challengeStatus = "unknown";
|
|
10378
10274
|
return {
|
|
10379
10275
|
solved: false,
|
|
10380
10276
|
verified: false,
|
|
10277
|
+
challengeState: "unknown",
|
|
10381
10278
|
resolution: "challenge_state_unverified",
|
|
10382
10279
|
verification: "unknown",
|
|
10383
10280
|
workflow: "verification_unavailable",
|
|
@@ -10389,18 +10286,41 @@ var BrowserService = class {
|
|
|
10389
10286
|
}
|
|
10390
10287
|
if (isChallengeAbsent(detection)) {
|
|
10391
10288
|
const cleared = previousChallengeStatus === "present";
|
|
10289
|
+
const attempts2 = state.challengeAttempts ?? 0;
|
|
10392
10290
|
state.challengeStatus = "absent";
|
|
10291
|
+
state.challengeAttempts = 0;
|
|
10393
10292
|
return {
|
|
10394
10293
|
solved: true,
|
|
10395
10294
|
verified: true,
|
|
10295
|
+
challengeState: "clear",
|
|
10396
10296
|
resolution: cleared ? "challenge_cleared" : "no_challenge",
|
|
10397
10297
|
verification: "verified",
|
|
10398
10298
|
workflow: "verified",
|
|
10399
10299
|
pageId: state.id,
|
|
10300
|
+
...attempts2 > 0 ? { attempts: attempts2 } : {},
|
|
10400
10301
|
classification: detection
|
|
10401
10302
|
};
|
|
10402
10303
|
}
|
|
10403
10304
|
state.challengeStatus = "present";
|
|
10305
|
+
const attempts = (state.challengeAttempts ?? 0) + 1;
|
|
10306
|
+
state.challengeAttempts = attempts;
|
|
10307
|
+
if (attempts > maxAttempts) {
|
|
10308
|
+
return {
|
|
10309
|
+
solved: false,
|
|
10310
|
+
verified: true,
|
|
10311
|
+
challengeState: "present",
|
|
10312
|
+
resolution: "automation_exhausted",
|
|
10313
|
+
verification: "challenge_present",
|
|
10314
|
+
workflow: "human_handoff_available",
|
|
10315
|
+
pageId: state.id,
|
|
10316
|
+
attempts: attempts - 1,
|
|
10317
|
+
maxAttempts,
|
|
10318
|
+
attemptsRemaining: 0,
|
|
10319
|
+
classification: detection,
|
|
10320
|
+
nextAction: "Automation attempts are exhausted. Human handoff is available if the site permits it.",
|
|
10321
|
+
guidance: "Automation attempts are exhausted. Human handoff is available if the site permits it."
|
|
10322
|
+
};
|
|
10323
|
+
}
|
|
10404
10324
|
const includeScreenshot = action.includeScreenshot ?? action.include_screenshot ?? true;
|
|
10405
10325
|
const requestedMaxDimension = action.maxDimension ?? action.max_dim;
|
|
10406
10326
|
const maxDimension = Number.isFinite(requestedMaxDimension) ? Math.min(1600, Math.max(100, Math.floor(requestedMaxDimension))) : 1600;
|
|
@@ -10430,6 +10350,9 @@ var BrowserService = class {
|
|
|
10430
10350
|
snapshotId: snapshot.snapshotId,
|
|
10431
10351
|
domRevision: snapshot.domRevision,
|
|
10432
10352
|
viewport: snapshot.viewport,
|
|
10353
|
+
attempts,
|
|
10354
|
+
maxAttempts,
|
|
10355
|
+
attemptsRemaining: Math.max(0, maxAttempts - attempts),
|
|
10433
10356
|
refs: stableRefs,
|
|
10434
10357
|
interactive: stableRefs,
|
|
10435
10358
|
classification: detection,
|
|
@@ -12166,7 +12089,7 @@ var ServerRuntime = class _ServerRuntime {
|
|
|
12166
12089
|
headedBrowser: true,
|
|
12167
12090
|
pageEvaluation: true,
|
|
12168
12091
|
stealth: true,
|
|
12169
|
-
behavioralTiming:
|
|
12092
|
+
behavioralTiming: false
|
|
12170
12093
|
},
|
|
12171
12094
|
features: {
|
|
12172
12095
|
localBrowserTools: "available",
|
|
@@ -12196,7 +12119,9 @@ var ServerRuntime = class _ServerRuntime {
|
|
|
12196
12119
|
classification: "bounded-evidence",
|
|
12197
12120
|
connectedAiLoop: true,
|
|
12198
12121
|
humanHandoff: true,
|
|
12199
|
-
successRequiresAbsentClassification: true
|
|
12122
|
+
successRequiresAbsentClassification: true,
|
|
12123
|
+
defaultMaxAttempts: 32,
|
|
12124
|
+
maxAttempts: 100
|
|
12200
12125
|
},
|
|
12201
12126
|
persistence: {
|
|
12202
12127
|
fileRootsConfigured: this.config.security.allowedFileRoots.length > 0,
|
|
@@ -12309,7 +12234,9 @@ async function reclaimStaleLock(lockPath) {
|
|
|
12309
12234
|
if (after.ino !== before.ino || after.dev !== before.dev) {
|
|
12310
12235
|
return false;
|
|
12311
12236
|
}
|
|
12312
|
-
|
|
12237
|
+
const stalePath = `${lockPath}.stale-${randomUUID2()}`;
|
|
12238
|
+
await rename2(lockPath, stalePath);
|
|
12239
|
+
await unlink2(stalePath).catch(() => void 0);
|
|
12313
12240
|
return true;
|
|
12314
12241
|
} catch {
|
|
12315
12242
|
return false;
|
|
@@ -12520,6 +12447,10 @@ var HTTP_NOT_FOUND_BODY = JSON.stringify({ error: "not_found" });
|
|
|
12520
12447
|
var HTTP_SHUTTING_DOWN_BODY = JSON.stringify({ error: "server_shutting_down" });
|
|
12521
12448
|
var HTTP_BUSY_BODY = JSON.stringify({ error: "server_busy" });
|
|
12522
12449
|
var HTTP_UNAUTHORIZED_BODY = JSON.stringify({ error: "unauthorized" });
|
|
12450
|
+
var HTTP_UNSUPPORTED_MEDIA_BODY = JSON.stringify({
|
|
12451
|
+
jsonrpc: "2.0",
|
|
12452
|
+
error: { code: -32e3, message: "Unsupported Media Type: Content-Type must be application/json" }
|
|
12453
|
+
});
|
|
12523
12454
|
async function main(args = process4.argv.slice(2)) {
|
|
12524
12455
|
if (args[0] === "install") {
|
|
12525
12456
|
const yes = args.includes("--yes") || args.includes("--no-interactive");
|
|
@@ -12867,6 +12798,14 @@ async function dispatchHttpRequest(request, response, nodeHandler, maxBodyBytes,
|
|
|
12867
12798
|
if (request.aborted) {
|
|
12868
12799
|
throw new AppError("HTTP_REQUEST_ABORTED", "The HTTP client disconnected before the request completed.", { status: 499, retryable: true });
|
|
12869
12800
|
}
|
|
12801
|
+
const contentType = request.headers["content-type"];
|
|
12802
|
+
if (request.method?.toUpperCase() === "POST" && (typeof contentType !== "string" || !sdkIsJsonContentType(contentType))) {
|
|
12803
|
+
response.setHeader("connection", "close");
|
|
12804
|
+
closeIncompleteRequestAfterResponse(request, response);
|
|
12805
|
+
response.writeHead(415, { "content-type": "application/json" });
|
|
12806
|
+
response.end(HTTP_UNSUPPORTED_MEDIA_BODY);
|
|
12807
|
+
return;
|
|
12808
|
+
}
|
|
12870
12809
|
const contentLength = Number(request.headers["content-length"] ?? 0);
|
|
12871
12810
|
if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
|
|
12872
12811
|
closeIncompleteRequestAfterResponse(request, response);
|