Files
rea/scripts/lib/browser-url-userinfo-e2e.mjs
T
ShuaixinHuang 6b0113a534 fix(browser): preserve URL paths when removing credentials (#4731)
* fix(browser): preserve URL paths when removing credentials

* test(browser): verify URL userinfo through real V8 captures
2026-10-17 23:23:37 +08:00

187 lines
6.2 KiB
JavaScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { artifactCliEvidence } from "./artifact-e2e.mjs";
import { closeVerifierResources } from "./verifier-cleanup.mjs";
/** Verify real V8 source labels, which retain spelling that navigation normalizes. */
export async function verifyBrowserUrlUserInfo(endpoint, origin) {
const rawUrl = `${origin.replace("://", "://user:pass@")}\\path@tail/../keep.js?x=1#frag`;
const expectedUrl = `${origin}\\path@tail/../keep.js?x=1#frag`;
const sources = [rawUrl, expectedUrl].map(
(url, index) =>
`globalThis.reaUserinfoUrlProbe${index} = () => 4731;\n//# sourceURL=${url}`,
);
const response = await fetch(`${endpoint}/json/new?about:blank`, {
method: "PUT",
});
assert.equal(
response.ok,
true,
"Browser refused the owned URL fixture target",
);
const target = await response.json();
assert.equal(typeof target.id, "string");
let connection;
let transport;
let client;
let verificationError;
try {
connection = await CdpConnection.connect(
target.webSocketDebuggerUrl,
"inspect_web_page",
);
await loadFixturePage(connection, `${origin}/app?userinfo-probe`);
const producerUrls = [];
connection.onEvent((event) => {
if (event.method === "Debugger.scriptParsed")
producerUrls.push(event.params.url);
});
await connection.send("Debugger.enable");
const browserUrl = await connection.send("Runtime.evaluate", {
expression: `new URL(${JSON.stringify(rawUrl)}).href`,
returnByValue: true,
});
assert.equal(browserUrl.exceptionDetails, undefined);
assert.equal(browserUrl.result.value, new URL(rawUrl).href);
assert.notEqual(browserUrl.result.value, rawUrl);
assert.equal(new URL(browserUrl.result.value).pathname, "/keep.js");
for (const source of sources) {
const evaluated = await connection.send("Runtime.evaluate", {
expression: source,
returnByValue: true,
});
assert.equal(evaluated.exceptionDetails, undefined);
}
assert.ok(
[rawUrl, expectedUrl].every((url) => producerUrls.includes(url)),
"Real V8 did not retain the exact sourceURL spelling on the CDP wire",
);
const assertScripts = (evidence) => {
const result = parseEvidence(evidence).normalized_result;
return sources.map((source) => {
const script = result.scripts.items.find(
(item) =>
item.source.included && item.source.artifact.text === source,
);
assert.ok(
script,
"Public capture omitted the retained V8 fixture script",
);
assert.equal(script.url, expectedUrl);
assert.equal(new URL(script.url).origin, origin);
assert.equal(
script.source.artifact.sha256,
createHash("sha256").update(source).digest("hex"),
);
return script.url;
});
};
const cliUrl = assertScripts(
await artifactCliEvidence("inspect-web-page", endpoint, [
target.id,
"--allowed-origins",
origin,
"--include-script-sources",
"--observation-ms",
"0",
]),
);
transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
env: { ...process.env, REA_LOG_LEVEL: "silent" },
stderr: "pipe",
});
client = new Client({ name: "browser-url-userinfo-e2e", version: "1" });
await client.connect(transport);
const mcp = await client.callTool({
name: "inspect_web_page",
arguments: {
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: target.id,
include_script_sources: true,
observation_ms: 0,
},
});
assert.notEqual(mcp.isError, true, JSON.stringify(mcp));
assert.deepEqual(assertScripts(mcp.structuredContent), cliUrl);
return {
cli: true,
stdio_mcp: true,
raw_v8_source_url: true,
credential_free_control: true,
original_source_digest: true,
browser_url_parser_normalizes_backslash_and_dot_segments: true,
};
} catch (cause) {
verificationError = cause;
throw cause;
} finally {
await closeVerifierResources(
[
async () => client?.close(),
async () => transport?.close(),
async () => connection?.close(),
async () => {
const closed = await fetch(`${endpoint}/json/close/${target.id}`);
assert.equal(
closed.ok,
true,
"Browser retained the owned URL fixture target",
);
},
],
verificationError,
);
}
}
async function loadFixturePage(connection, url) {
const loaded = new Set();
let loaderId;
let resolveLoaded;
let timer;
const load = new Promise((resolve) => {
resolveLoaded = resolve;
});
const unsubscribe = connection.onEvent((event) => {
if (event.method !== "Page.lifecycleEvent" || event.params.name !== "load")
return;
loaded.add(event.params.loaderId);
if (event.params.loaderId === loaderId) resolveLoaded();
});
try {
await connection.send("Page.enable");
await connection.send("Page.setLifecycleEventsEnabled", { enabled: true });
const navigation = await connection.send("Page.navigate", { url });
assert.equal(navigation.errorText, undefined);
assert.equal(typeof navigation.loaderId, "string");
loaderId = navigation.loaderId;
if (!loaded.has(loaderId))
await Promise.race([
load,
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error("Owned URL fixture did not finish loading")),
10_000,
);
}),
]);
assert.equal(
(await connection.send("Page.getFrameTree")).frameTree.frame.url,
url,
);
} finally {
clearTimeout(timer);
unsubscribe();
await connection.send("Page.setLifecycleEventsEnabled", { enabled: false });
}
}