scripts/verify-mcp-sign-in.ts
Rama main · réplica de lectura del repositorio original.
// Real MCP panel + server + fake OAuth provider, only in a disposable workspace.
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { launchVerificationServer, runControlOmb } from "./control-omb.ts";
import { ensureUiBrowser } from "./testing/control-omb-ui.ts";
import { fixtureApi, mountPreview, type MountedPreview } from "./testing/preview-fixture.ts";
import { startFakeOAuth } from "../server/testing/fake-oauth-server.ts";
import { startFakeHttpMcp } from "../server/testing/fake-http-mcp-server.ts";
const fixture = await launchVerificationServer();
let oauth: Awaited<ReturnType<typeof startFakeOAuth>> | undefined;
let mcp: Awaited<ReturnType<typeof startFakeHttpMcp>> | undefined;
let ui: MountedPreview | undefined;
let closeBrowser: (() => Promise<unknown>) | undefined;
let releaseToken!: () => void;
const tokenPending = new Promise<void>((resolve) => { releaseToken = resolve; });
try {
oauth = await startFakeOAuth({ beforeToken: () => tokenPending });
mcp = await startFakeHttpMcp({ acceptBearer: oauth.isValid, wwwAuthenticate: oauth.challenge });
const api = fixtureApi(fixture.info.url);
await runControlOmb(["doctor", "--url", fixture.info.url]);
await api("POST", "/api/mcp/servers", { name: "documents", url: mcp.url });
await api("POST", "/api/mcp/servers/documents/test");
const pairing = await api("POST", "/api/auth/pairing", { scopes: ["admin", "client"] });
let authorizationUrl = "";
ui = await mountPreview(fixture, {
entry: "/scripts/testing/mcp-sign-in-preview.tsx", route: "/__mcp-sign-in.html", title: "MCP sign-in fixture",
extraRoutes: [{
path: "/api/mcp/servers/documents/sign-in", method: "POST",
handler: async (req, res) => {
const response = await fetch(`${fixture.info.url}/api/mcp/servers/documents/sign-in`, {
method: "POST", headers: { cookie: req.headers.cookie ?? "", "x-forwarded-for": "192.0.2.10" },
});
const body = await response.json();
if (response.ok) {
authorizationUrl = body.auth.authorizationUrl;
// The in-process provider uses HTTP; the renderer only opens HTTPS.
// Stand in for its public login page, without weakening that check.
body.auth.authorizationUrl = "https://oauth.example.test/authorize";
}
res.writeHead(response.status, { "content-type": "application/json" }).end(JSON.stringify(body));
},
}],
});
const { binary, chrome } = await ensureUiBrowser(process.env);
const temp = join(fixture.info.dataDir, "browser-tmp");
mkdirSync(temp, { recursive: true });
const env = {
HOME: fixture.info.dataDir, USERPROFILE: fixture.info.dataDir, PATH: process.env.PATH,
TMPDIR: temp, TEMP: temp, TMP: temp, AGENT_BROWSER_SESSION: `mcp-oauth-${process.pid}`,
AGENT_BROWSER_HEADLESS: "1", AGENT_BROWSER_NO_WEBMCP: "1",
...(chrome ? { AGENT_BROWSER_EXECUTABLE_PATH: chrome } : {}),
};
const command = async (...args: string[]) => {
const { stdout } = await promisify(execFile)(binary, [...args, "--json"], { env, timeout: 30_000, maxBuffer: 4 * 1024 * 1024 });
const result = JSON.parse(stdout);
assert.equal(result.success, true, JSON.stringify(result.error));
return result.data;
};
closeBrowser = () => command("close");
const clickButton = async (name: string) => {
await command("wait", "--fn", `[...document.querySelectorAll('button')].some(b => b.textContent.trim() === ${JSON.stringify(name)} && !b.disabled)`);
await command("find", "role", "button", "click", "--name", name, "--exact");
};
const evaluate = async (js: string) => (await command("eval", js)).result;
await command("open", ui.previewUrl);
assert.equal(await evaluate(`(async () => (await fetch('/api/auth/pair', {method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({code:${JSON.stringify(pairing.code)},cookie:true,label:'MCP sign-in fixture'})})).status)()`), 200);
await command("reload");
await command("wait", "--fn", "[...document.querySelectorAll('button')].some(b => b.textContent.trim() === 'Sign in')");
// Simulate a blocked popup; the explicit reopen and paste UI must still work.
await evaluate("window.open = () => null");
const evidence = resolve(process.env.OMB_UI_EVIDENCE_DIR ?? ".omb-scratch/mcp-oauth-evidence");
mkdirSync(evidence, { recursive: true });
await command("screenshot", join(evidence, "before.png"), "--full");
await clickButton("Sign in");
// A remote browser with no https address to return to: the paste box is shown up front.
await command("wait", "--fn", "document.body.textContent.includes('After you approve, your browser shows a page that can') && document.getElementById('mcp-callback-documents') !== null");
await command("screenshot", join(evidence, "waiting.png"), "--full");
await command("fill", "#mcp-callback-documents", "http://127.0.0.1:1/wrong?code=wrong&state=wrong");
await clickButton("Complete sign-in");
await command("wait", "--fn", "document.querySelector('[role=alert]') !== null");
assert.equal(oauth.counts.token, 0);
const approved = await fetch(authorizationUrl, { redirect: "manual" });
const callbackUrl = approved.headers.get("location")!;
await command("fill", "#mcp-callback-documents", callbackUrl);
await clickButton("Complete sign-in");
await command("wait", "--fn", "document.querySelector('button[aria-busy=true] .animate-spin') !== null");
assert.equal(await evaluate("document.querySelector('button[aria-busy=true]').disabled"), true);
assert.equal(await evaluate("document.getElementById('mcp-callback-documents').value"), callbackUrl);
await command("screenshot", join(evidence, "authenticating.png"), "--full");
// Cancel while the completion request is held, then retry before that old
// request settles. The new form must not inherit the old busy state.
await clickButton("Cancel");
await clickButton("Sign in");
await command("wait", "--fn", "document.body.textContent.includes('After you approve, your browser shows a page that can') && document.getElementById('mcp-callback-documents') !== null");
const retried = await fetch(authorizationUrl, { redirect: "manual" });
await command("fill", "#mcp-callback-documents", retried.headers.get("location")!);
await clickButton("Complete sign-in");
await command("wait", "--fn", "document.querySelector('button[aria-busy=true] .animate-spin') !== null");
releaseToken();
await command("wait", "--fn", "document.body.textContent.includes('Signed in') && !document.getElementById('mcp-callback-documents')");
assert.equal(oauth.counts.token, 2);
await clickButton("Test");
await command("wait", "--fn", "document.body.textContent.includes('Connected: 1 tool')");
await command("screenshot", join(evidence, "signed-in.png"), "--full");
// Revoke the initiating browser while another attempt waits.
await clickButton("Sign out");
await clickButton("Test");
await command("wait", "--fn", "[...document.querySelectorAll('button')].some(b => b.textContent.trim() === 'Sign in')");
await clickButton("Sign in");
await command("wait", "--fn", "document.body.textContent.includes('After you approve, your browser shows a page that can') && document.getElementById('mcp-callback-documents') !== null");
const pending = await fetch(authorizationUrl, { redirect: "manual" });
assert.equal(await evaluate("fetch('/api/auth/logout', {method:'POST',headers:{'content-type':'application/json'}}).then(r=>r.status)"), 200);
await assert.rejects(fetch(pending.headers.get("location")!));
console.log(JSON.stringify({ ok: true, evidence, tested: ["remote admin start", "blocked popup", "invalid URL retry", "pending spinner and preserved input", "cancel and retry during pending completion", "paste completion", "MCP tools", "logout cancellation"] }));
} finally {
releaseToken();
await closeBrowser?.().catch(() => {});
await ui?.close();
await fixture.close();
await mcp?.close();
await oauth?.close();
}