#!/usr/bin/env node const assert = require("assert"); const cp = require("child_process"); const fs = require("fs"); const http = require("http"); const net = require("net"); const path = require("path"); const repo = path.resolve(__dirname, ".."); const tmp = path.join(repo, "target", "acceptance", "tmp", "cli-browser-login-flow"); fs.mkdirSync(tmp, { recursive: true }); function writeOpener() { const opener = path.join(tmp, "browser-opener.js"); const trace = path.join(tmp, "browser-opener.log"); fs.writeFileSync( opener, `#!/usr/bin/env node const fs = require("fs"); const http = require("http"); const trace = ${JSON.stringify(trace)}; fs.appendFileSync(trace, "started " + process.argv.slice(2).join(" ") + "\\n"); const loginUrl = new URL(process.argv[2]); const state = loginUrl.searchParams.get("state"); const redirect = new URL(loginUrl.searchParams.get("redirect_uri")); if (!state || !redirect) { fs.appendFileSync(trace, "missing state or redirect\\n"); console.error("browser opener did not receive state and redirect_uri"); process.exit(1); } redirect.searchParams.set("code", "browser-smoke-code"); redirect.searchParams.set("state", state); fs.appendFileSync(trace, "callback " + redirect.toString() + "\\n"); http.get(redirect, (response) => { response.resume(); response.on("end", () => { fs.appendFileSync(trace, "status " + response.statusCode + "\\n"); process.exit(response.statusCode === 200 ? 0 : 1); }); }).on("error", (error) => { fs.appendFileSync(trace, "error " + (error.stack || error.message) + "\\n"); console.error(error.stack || error.message); process.exit(1); }); ` ); fs.chmodSync(opener, 0o755); return opener; } function listen(server, host = "127.0.0.1") { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, host, () => { server.off("error", reject); resolve(server.address()); }); }); } async function startCoordinator() { const requests = []; const server = net.createServer((socket) => { let buffered = ""; socket.on("data", (chunk) => { buffered += chunk.toString("utf8"); if (!buffered.includes("\n")) return; const [line] = buffered.split(/\r?\n/); const request = JSON.parse(line); requests.push(request); assert.strictEqual(request.type, "oidc_browser_login"); assert.strictEqual(request.authorization_code, "browser-smoke-code"); assert.strictEqual(request.issuer_url, "http://127.0.0.1:1"); assert.strictEqual(request.client_id, "disasmer-smoke"); assert.match(request.redirect_path, /^http:\/\/127\.0\.0\.1:45173\/callback$/); assert.match(request.state, /^sha256:[a-f0-9]{64}$/); socket.end( JSON.stringify({ type: "oidc_browser_session", session: { tenant: request.tenant, project: request.project, user: request.user, browser_credential_kind: "BrowserSession", cli_session_credential_kind: "CliDeviceSession", provider_tokens_sent_to_nodes: false, flow: { authorization_url: "http://127.0.0.1:1/application/o/authorize/", callback_path: request.redirect_path, state: request.state, }, oidc_token_exchange: { token_endpoint: "http://127.0.0.1:1/application/o/token/", token_type: "Bearer", received_access_token: true, received_id_token: true, retained_provider_tokens: false, }, }, }) + "\n" ); server.close(); }); }); const address = await listen(server); return { url: `${address.address}:${address.port}`, requests, close: () => new Promise((resolve) => server.close(() => resolve())), }; } function runDisasmer(args, env) { return new Promise((resolve, reject) => { const child = cp.spawn("cargo", args, { cwd: repo, env, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; const timeout = setTimeout(() => { child.kill(); reject(new Error(`disasmer command timed out\n${stderr}`)); }, 30000); child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); }); child.on("error", (error) => { clearTimeout(timeout); reject(error); }); child.on("close", (code, signal) => { clearTimeout(timeout); if (code !== 0) { reject( new Error( `disasmer command failed with ${signal || code}\nSTDERR:\n${stderr}\nSTDOUT:\n${stdout}` ) ); } else { resolve(stdout); } }); }); } (async () => { const opener = writeOpener(); const coordinator = await startCoordinator(); try { const stdout = await runDisasmer( [ "run", "-q", "-p", "disasmer-cli", "--bin", "disasmer", "--", "login", "--browser", "--json", "--coordinator", coordinator.url, "--oidc-issuer-url", "http://127.0.0.1:1", "--oidc-client-id", "disasmer-smoke", "--tenant", "tenant-smoke", "--project-id", "project-smoke", "--user", "user-smoke", ], { ...process.env, DISASMER_BROWSER_OPEN_COMMAND: opener, DISASMER_BROWSER_LOGIN_TIMEOUT_SECONDS: "5", } ); const report = JSON.parse(stdout); assert.strictEqual(report.plan.coordinator, coordinator.url); assert.strictEqual(report.boundary.cli_contacted_coordinator, true); assert.strictEqual(report.boundary.scoped_cli_session_received, true); assert.strictEqual(report.boundary.provider_tokens_exposed_to_cli, false); assert.strictEqual(report.boundary.provider_tokens_sent_to_nodes, false); assert.strictEqual(report.boundary.coordinator_session_requests, 1); assert.strictEqual(coordinator.requests.length, 1); } finally { if (coordinator.requests.length === 0) { await coordinator.close().catch(() => {}); } } console.log("CLI browser login flow smoke passed"); })().catch((error) => { console.error(error.stack || error.message); process.exit(1); });