#!/usr/bin/env node const assert = require("assert"); const cp = require("child_process"); const fs = require("fs"); 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"); const project = path.join(tmp, "project"); fs.rmSync(tmp, { recursive: true, force: true }); fs.mkdirSync(project, { 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 trace = ${JSON.stringify(trace)}; const loginUrl = new URL(process.argv[2]); const state = loginUrl.searchParams.get("state"); const nonce = loginUrl.searchParams.get("nonce"); const challenge = loginUrl.searchParams.get("code_challenge"); if (loginUrl.protocol !== "https:" || !state || !nonce || !challenge) { fs.appendFileSync(trace, "missing server-owned OIDC parameters\\n"); process.exit(1); } fs.appendFileSync(trace, "server-owned browser transaction\\n"); // Model a real browser/opener that remains alive after the CLI transaction. // Its descriptors must not keep the invoking CLI process open. setTimeout(() => process.exit(0), 5000); ` ); fs.chmodSync(opener, 0o755); return opener; } function startCoordinator() { return new Promise((resolve, reject) => { const requests = []; const server = net.createServer((socket) => { let buffered = ""; socket.on("data", (chunk) => { buffered += chunk.toString("utf8"); while (buffered.includes("\n")) { const newline = buffered.indexOf("\n"); const line = buffered.slice(0, newline); buffered = buffered.slice(newline + 1); const envelope = JSON.parse(line); requests.push(envelope); assert.strictEqual(envelope.type, "coordinator_request"); assert.strictEqual(envelope.protocol_version, 1); assert.strictEqual(envelope.authentication.kind, "none"); const request = envelope.payload; if (requests.length === 1) { assert.strictEqual(envelope.request_id, "cli-1"); assert.strictEqual(envelope.operation, "begin_oidc_browser_login"); assert.deepStrictEqual(request, { type: "begin_oidc_browser_login", requested_project: "project-smoke", }); socket.write( `${JSON.stringify({ type: "oidc_browser_login_started", transaction_id: "login-transaction", polling_secret: "opaque-polling-secret", authorization_url: "https://auth.michelpaulissen.com/application/o/authorize/?state=server-state&nonce=server-nonce&code_challenge=server-pkce&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fclusterflux.michelpaulissen.com%2Fauth%2Fcallback", expires_at_epoch_seconds: 1800000000, })}\n` ); continue; } assert.strictEqual(envelope.request_id, "cli-2"); assert.strictEqual(envelope.operation, "poll_oidc_browser_login"); assert.deepStrictEqual(request, { type: "poll_oidc_browser_login", transaction_id: "login-transaction", polling_secret: "opaque-polling-secret", }); socket.end( `${JSON.stringify({ type: "oidc_browser_session", session: { tenant: "tenant-smoke", project: "project-smoke", user: "user-smoke", cli_session_credential_kind: "CliDeviceSession", cli_session_secret: "scoped-cli-session-secret", expires_at_epoch_seconds: 1800000000, provider_tokens_sent_to_nodes: false, }, })}\n` ); server.close(); } }); }); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); resolve({ url: `${address.address}:${address.port}`, requests, close: () => new Promise((closeResolve) => { if (!server.listening) closeResolve(); else server.close(() => closeResolve()); }), }); }); }); } function runClusterflux(args, env) { return new Promise((resolve, reject) => { const child = cp.spawn( "cargo", [ "run", "-q", "--manifest-path", path.join(repo, "Cargo.toml"), "-p", "clusterflux-cli", "--bin", "clusterflux", "--", ...args, ], { cwd: project, env, stdio: ["ignore", "pipe", "pipe"] } ); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => (stdout += chunk.toString("utf8"))); child.stderr.on("data", (chunk) => (stderr += chunk.toString("utf8"))); child.once("error", reject); child.once("close", (code) => { if (code === 0) resolve(stdout); else reject(new Error(`clusterflux exited ${code}\n${stderr}\n${stdout}`)); }); }); } (async () => { const opener = writeOpener(); const coordinator = await startCoordinator(); try { const loginStarted = Date.now(); const report = JSON.parse( await runClusterflux( [ "login", "--browser", "--json", "--coordinator", coordinator.url, "--project-id", "project-smoke", ], { ...process.env, CLUSTERFLUX_BROWSER_OPEN_COMMAND: opener, CLUSTERFLUX_BROWSER_LOGIN_TIMEOUT_SECONDS: "5", } ) ); assert( Date.now() - loginStarted < 3000, "a long-lived browser opener must not keep CLI output pipes or login completion open" ); 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.local_cli_session_file_written, true); assert.strictEqual(report.boundary.provider_tokens_persisted_locally, false); 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, 2); assert.strictEqual(coordinator.requests.length, 2); const sessionFile = path.join(project, ".clusterflux", "session.json"); const sessionText = fs.readFileSync(sessionFile, "utf8"); const session = JSON.parse(sessionText); assert.strictEqual(session.kind, "human"); assert.strictEqual(session.coordinator, coordinator.url); assert.strictEqual(session.tenant, "tenant-smoke"); assert.strictEqual(session.project, "project-smoke"); assert.strictEqual(session.user, "user-smoke"); assert.strictEqual(session.cli_session_credential_kind, "CliDeviceSession"); assert.strictEqual(session.token_expiry_posture, "expires_at"); assert.strictEqual(session.expires_at, "1800000000"); assert.strictEqual(session.provider_tokens_exposed_to_cli, false); assert.strictEqual(session.provider_tokens_sent_to_nodes, false); assert.doesNotMatch( sessionText, /access_token|refresh_token|id_token|provider-secret|authorization_code|Bearer/ ); const authStatus = JSON.parse( await runClusterflux(["auth", "status", "--json"], process.env) ); assert.strictEqual(authStatus.active_coordinator, coordinator.url); assert.strictEqual(authStatus.principal, "user-smoke"); assert.strictEqual(authStatus.tenant, "tenant-smoke"); assert.strictEqual(authStatus.project, "project-smoke"); assert.strictEqual(authStatus.session.kind, "human"); assert.strictEqual(authStatus.session.source, "session_file"); assert.strictEqual(authStatus.session.provider_tokens_exposed_to_cli, false); assert.strictEqual(authStatus.session.provider_tokens_exposed_to_nodes, false); } finally { await coordinator.close(); } console.log("CLI browser login flow smoke passed"); })().catch((error) => { console.error(error.stack || error.message); process.exit(1); });