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