@@ -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);
|
||||
|
||||
32
packages/ai/test/openai-codex-oauth.test.ts
Normal file
32
packages/ai/test/openai-codex-oauth.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user