()
| 119 | } |
| 120 | |
| 121 | async startDesktopFlow(): Promise<Result<{ flowId: string; authorizeUrl: string }, string>> { |
| 122 | const flowId = randomBase64Url(); |
| 123 | |
| 124 | const codeVerifier = randomBase64Url(); |
| 125 | const codeChallenge = sha256Base64Url(codeVerifier); |
| 126 | const redirectUri = CODEX_OAUTH_BROWSER_REDIRECT_URI; |
| 127 | |
| 128 | let loopback: Awaited<ReturnType<typeof startLoopbackServer>>; |
| 129 | try { |
| 130 | loopback = await startLoopbackServer({ |
| 131 | port: 1455, |
| 132 | host: "localhost", |
| 133 | callbackPath: "/auth/callback", |
| 134 | validateLoopback: true, |
| 135 | expectedState: flowId, |
| 136 | deferSuccessResponse: true, |
| 137 | }); |
| 138 | } catch (error) { |
| 139 | const message = getErrorMessage(error); |
| 140 | return Err(`Failed to start OAuth callback listener: ${message}`); |
| 141 | } |
| 142 | |
| 143 | const resultDeferred = createDeferred<Result<void, string>>(); |
| 144 | |
| 145 | this.desktopFlows.register(flowId, { |
| 146 | server: loopback.server, |
| 147 | resultDeferred, |
| 148 | // Keep server-side timeout tied to flow lifetime so abandoned flows |
| 149 | // (e.g. callers that never invoke waitForDesktopFlow) still self-clean. |
| 150 | timeoutHandle: setTimeout(() => { |
| 151 | void this.desktopFlows.finish(flowId, Err("Timed out waiting for OAuth callback")); |
| 152 | }, DEFAULT_DESKTOP_TIMEOUT_MS), |
| 153 | }); |
| 154 | |
| 155 | const authorizeUrl = buildCodexAuthorizeUrl({ |
| 156 | redirectUri, |
| 157 | state: flowId, |
| 158 | codeChallenge, |
| 159 | }); |
| 160 | |
| 161 | // Background task: wait for the loopback callback, exchange code for tokens, |
| 162 | // then finish the flow. Races against resultDeferred (which resolves on |
| 163 | // cancel/timeout) so the task exits cleanly if the flow is cancelled. |
| 164 | void (async () => { |
| 165 | const callbackResult = await Promise.race([ |
| 166 | loopback.result, |
| 167 | resultDeferred.promise.then(() => null), |
| 168 | ]); |
| 169 | |
| 170 | // null means the flow was finished externally (cancel/timeout). |
| 171 | if (!callbackResult) return; |
| 172 | |
| 173 | if (!callbackResult.success) { |
| 174 | await this.desktopFlows.finish(flowId, Err(callbackResult.error)); |
| 175 | return; |
| 176 | } |
| 177 | |
| 178 | const exchangeResult = await this.handleDesktopCallbackAndExchange({ |
nothing calls this directly
no test coverage detected