fix(ai): stop Codex OAuth stderr writes

closes #4141
This commit is contained in:
Mario Zechner
2026-05-06 00:14:40 +02:00
parent 31f5c23271
commit 3029836894
2 changed files with 58 additions and 19 deletions

View File

@@ -30,7 +30,7 @@ const SCOPE = "openid profile email offline_access";
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
type TokenSuccess = { type: "success"; access: string; refresh: string; expires: number };
type TokenFailure = { type: "failed" };
type TokenFailure = { type: "failed"; message: string; status?: number };
type TokenResult = TokenSuccess | TokenFailure;
type JwtPayload = {
@@ -108,8 +108,11 @@ async function exchangeAuthorizationCode(
if (!response.ok) {
const text = await response.text().catch(() => "");
console.error("[openai-codex] code->token failed:", response.status, text);
return { type: "failed" };
return {
type: "failed",
status: response.status,
message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,
};
}
const json = (await response.json()) as {
@@ -119,8 +122,10 @@ async function exchangeAuthorizationCode(
};
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
console.error("[openai-codex] token response missing fields:", json);
return { type: "failed" };
return {
type: "failed",
message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,
};
}
return {
@@ -145,8 +150,11 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
if (!response.ok) {
const text = await response.text().catch(() => "");
console.error("[openai-codex] Token refresh failed:", response.status, text);
return { type: "failed" };
return {
type: "failed",
status: response.status,
message: `OpenAI Codex token refresh failed (${response.status}): ${text || response.statusText}`,
};
}
const json = (await response.json()) as {
@@ -156,8 +164,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
};
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
console.error("[openai-codex] Token refresh response missing fields:", json);
return { type: "failed" };
return {
type: "failed",
message: `OpenAI Codex token refresh response missing fields: ${JSON.stringify(json)}`,
};
}
return {
@@ -167,8 +177,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
expires: Date.now() + json.expires_in * 1000,
};
} catch (error) {
console.error("[openai-codex] Token refresh error:", error);
return { type: "failed" };
return {
type: "failed",
message: `OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
@@ -258,12 +270,7 @@ function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
waitForCode: () => waitForCodePromise,
});
})
.on("error", (err: NodeJS.ErrnoException) => {
console.error(
`[openai-codex] Failed to bind http://${CALLBACK_HOST}:1455 (`,
err.code,
") Falling back to manual paste.",
);
.on("error", (_err: NodeJS.ErrnoException) => {
settleWait?.(null);
resolve({
close: () => {
@@ -386,7 +393,7 @@ export async function loginOpenAICodex(options: {
const tokenResult = await exchangeAuthorizationCode(code, verifier);
if (tokenResult.type !== "success") {
throw new Error("Token exchange failed");
throw new Error(tokenResult.message);
}
const accountId = getAccountId(tokenResult.access);
@@ -411,7 +418,7 @@ export async function loginOpenAICodex(options: {
export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {
const result = await refreshAccessToken(refreshToken);
if (result.type !== "success") {
throw new Error("Failed to refresh OpenAI Codex token");
throw new Error(result.message);
}
const accountId = getAccountId(result.access);

View File

@@ -0,0 +1,32 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.js";
describe("OpenAI Codex OAuth", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("does not write token refresh failures to stderr", async () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
vi.stubGlobal(
"fetch",
vi.fn(async (): Promise<Response> => {
return new Response(
JSON.stringify({
error: {
message: "Could not validate your token. Please try signing in again.",
type: "invalid_request_error",
},
}),
{ status: 401, statusText: "Unauthorized", headers: { "Content-Type": "application/json" } },
);
}),
);
await expect(refreshOpenAICodexToken("invalid-refresh-token")).rejects.toThrow(
/OpenAI Codex token refresh failed \(401\).*Could not validate your token/,
);
expect(consoleError).not.toHaveBeenCalled();
});
});