(apiUrl: string, timeoutMs = LOGIN_TIMEOUT_MS)
| 113 | }; |
| 114 | |
| 115 | async function waitForBrowserLogin(apiUrl: string, timeoutMs = LOGIN_TIMEOUT_MS): Promise<BrowserLoginResult> { |
| 116 | const cliState = randomBytes(16).toString("hex"); |
| 117 | |
| 118 | let settled = false; |
| 119 | let server: Server | null = null; |
| 120 | // Single timeout handle that's cleared in finish() and the outer |
| 121 | // finally — earlier versions had two setTimeouts and one of them |
| 122 | // wasn't cleared on success, keeping the Node event loop alive for |
| 123 | // the full timeout (2 min) after a successful login. Symptom was |
| 124 | // the terminal hanging after "Logged in to <host>" printed. |
| 125 | let timeoutHandle: ReturnType<typeof setTimeout> | undefined; |
| 126 | |
| 127 | try { |
| 128 | return await new Promise<BrowserLoginResult>((resolve, reject) => { |
| 129 | const finish = async (value: BrowserLoginResult | Error, isError: boolean) => { |
| 130 | if (settled) return; |
| 131 | settled = true; |
| 132 | if (timeoutHandle) clearTimeout(timeoutHandle); |
| 133 | if (server) { |
| 134 | await closeServer(server); |
| 135 | } |
| 136 | if (isError) reject(value); |
| 137 | else resolve(value as BrowserLoginResult); |
| 138 | }; |
| 139 | |
| 140 | timeoutHandle = setTimeout( |
| 141 | () => void finish(new Error("timed out waiting for browser login"), true), |
| 142 | timeoutMs, |
| 143 | ); |
| 144 | |
| 145 | server = createServer(async (req, res) => { |
| 146 | res.setHeader("Connection", "close"); |
| 147 | try { |
| 148 | if (req.method !== "POST") { |
| 149 | res.statusCode = 405; |
| 150 | res.setHeader("Content-Type", "text/html; charset=utf-8"); |
| 151 | res.end(renderCallbackPage("Return to your terminal", "Run e2a login again to finish connecting the CLI.")); |
| 152 | return; |
| 153 | } |
| 154 | |
| 155 | const body = await readRequestBody(req); |
| 156 | const params = new URLSearchParams(body); |
| 157 | const returnedState = params.get("cli_state") || ""; |
| 158 | const apiKey = params.get("api_key") || ""; |
| 159 | const agentEmail = params.get("agent_email") || ""; |
| 160 | const error = params.get("error") || ""; |
| 161 | |
| 162 | if (returnedState != cliState) { |
| 163 | res.statusCode = 400; |
| 164 | res.setHeader("Content-Type", "text/html; charset=utf-8"); |
| 165 | res.end(renderCallbackPage("e2a login failed", "The browser login state did not match this terminal session. Return to the terminal and try again.")); |
| 166 | await finish(new Error("browser login state mismatch"), true); |
| 167 | return; |
| 168 | } |
| 169 | |
| 170 | if (error) { |
| 171 | res.statusCode = 400; |
| 172 | res.setHeader("Content-Type", "text/html; charset=utf-8"); |
no test coverage detected