(input: {
code: string;
redirectUri: string;
codeVerifier: string;
})
| 422 | } |
| 423 | |
| 424 | private async exchangeCodeForTokens(input: { |
| 425 | code: string; |
| 426 | redirectUri: string; |
| 427 | codeVerifier: string; |
| 428 | }): Promise<Result<CodexOauthAuth, string>> { |
| 429 | try { |
| 430 | const response = await fetch(CODEX_OAUTH_TOKEN_URL, { |
| 431 | method: "POST", |
| 432 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, |
| 433 | body: buildCodexTokenExchangeBody({ |
| 434 | code: input.code, |
| 435 | redirectUri: input.redirectUri, |
| 436 | codeVerifier: input.codeVerifier, |
| 437 | }), |
| 438 | }); |
| 439 | |
| 440 | if (!response.ok) { |
| 441 | const errorText = await response.text().catch(() => ""); |
| 442 | const prefix = `Codex OAuth exchange failed (${response.status})`; |
| 443 | return Err(errorText ? `${prefix}: ${errorText}` : prefix); |
| 444 | } |
| 445 | |
| 446 | const json = (await response.json()) as unknown; |
| 447 | if (!isPlainObject(json)) { |
| 448 | return Err("Codex OAuth exchange returned an invalid JSON payload"); |
| 449 | } |
| 450 | |
| 451 | const accessToken = typeof json.access_token === "string" ? json.access_token : null; |
| 452 | const refreshToken = typeof json.refresh_token === "string" ? json.refresh_token : null; |
| 453 | const expiresIn = parseOptionalNumber(json.expires_in); |
| 454 | const idToken = typeof json.id_token === "string" ? json.id_token : undefined; |
| 455 | |
| 456 | if (!accessToken) { |
| 457 | return Err("Codex OAuth exchange response missing access_token"); |
| 458 | } |
| 459 | |
| 460 | if (!refreshToken) { |
| 461 | return Err("Codex OAuth exchange response missing refresh_token"); |
| 462 | } |
| 463 | |
| 464 | if (expiresIn === null) { |
| 465 | return Err("Codex OAuth exchange response missing expires_in"); |
| 466 | } |
| 467 | |
| 468 | const accountId = extractAccountIdFromTokens({ accessToken, idToken }) ?? undefined; |
| 469 | |
| 470 | return Ok({ |
| 471 | type: "oauth", |
| 472 | access: accessToken, |
| 473 | refresh: refreshToken, |
| 474 | expires: Date.now() + Math.max(0, Math.floor(expiresIn * 1000)), |
| 475 | accountId, |
| 476 | }); |
| 477 | } catch (error) { |
| 478 | const message = getErrorMessage(error); |
| 479 | return Err(`Codex OAuth exchange failed: ${message}`); |
| 480 | } |
| 481 | } |
no test coverage detected