mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
* fix(browser): preserve URL paths when removing credentials * test(browser): verify URL userinfo through real V8 captures
187 lines
6.2 KiB
JavaScript
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 });
|
|
}
|
|
}
|