feat(FN-4709): add codex oauth fallback delivery

Implements Codex OAuth fallback delivery as the final step of FN-4709, with a corresponding changeset for the `@runfusion/fusion` package and test coverage for the updated usage tracking logic.

Fusion-Task-Id: FN-4709
This commit is contained in:
Fusion (runfusion.ai)
2026-05-15 22:12:22 -07:00
committed by gsxdsm
parent 8137920b11
commit bdda0e2da8
3 changed files with 222 additions and 13 deletions

View File

@@ -1,4 +1,15 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
const coreInteropMocks = vi.hoisted(() => ({
choosePreferredStoredCredential: vi.fn(),
readStoredCredentialsFromAuthFile: vi.fn(),
}));
vi.mock("@fusion/core", () => ({
choosePreferredStoredCredential: coreInteropMocks.choosePreferredStoredCredential,
readStoredCredentialsFromAuthFile: coreInteropMocks.readStoredCredentialsFromAuthFile,
}));
import {
fetchAllProviderUsage,
clearUsageCache,
@@ -59,6 +70,10 @@ describe("usage", () => {
mockExecFileSync.mockImplementation(() => {
throw new Error("File not found");
});
coreInteropMocks.choosePreferredStoredCredential.mockImplementation((...credentials: any[]) =>
credentials.findLast((credential) => credential !== undefined)
);
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({});
vi.stubEnv("HOME", "/home/testuser");
});
@@ -1776,6 +1791,148 @@ describe("usage", () => {
expect(codex).toBeUndefined();
});
it("falls back to Fusion openai-codex oauth when codex auth.json is missing", async () => {
mockReadFile.mockRejectedValue(new Error("File not found"));
coreInteropMocks.readStoredCredentialsFromAuthFile.mockImplementation((filePath: string) => {
if (filePath.includes(".fusion/agent/auth.json")) {
return {
"openai-codex": {
type: "oauth",
access: "fusion-access-token",
refresh: "fusion-refresh-token",
expires: Date.now() + 60_000,
},
};
}
return {};
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
expect(options.headers.authorization).toBe("Bearer fusion-access-token");
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"email":"fusion@example.com","plan_type":"pro"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex")!;
expect(codex.status).toBe("ok");
expect(codex.email).toBe("fusion@example.com");
});
it("prefers codex auth.json over Fusion openai-codex oauth", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".codex/auth.json")) {
return JSON.stringify({
tokens: {
access_token: "codex-cli-token",
id_token: "header.eyJlbWFpbCI6ImNsaUBleGFtcGxlLmNvbSJ9.signature",
},
});
}
return Promise.reject(new Error("File not found"));
});
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({
"openai-codex": {
type: "oauth",
access: "fusion-access-token",
refresh: "fusion-refresh-token",
expires: Date.now() + 60_000,
},
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
expect(options.headers.authorization).toBe("Bearer codex-cli-token");
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"email":"cli@example.com"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex")!;
expect(codex.status).toBe("ok");
});
it("returns no-auth when Fusion openai-codex oauth is expired", async () => {
mockReadFile.mockRejectedValue(new Error("File not found"));
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({
"openai-codex": {
type: "oauth",
access: "fusion-access-token",
refresh: "fusion-refresh-token",
expires: Date.now() - 60_000,
},
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex");
expect(codex).toBeUndefined();
expect(mockRequest).not.toHaveBeenCalled();
});
it("returns no-auth when Fusion openai-codex entry is non-oauth", async () => {
mockReadFile.mockRejectedValue(new Error("File not found"));
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({
"openai-codex": {
type: "api_key",
key: "not-a-bearer-token",
},
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex");
expect(codex).toBeUndefined();
expect(mockRequest).not.toHaveBeenCalled();
});
it("surfaces Fusion re-login guidance when Fusion-sourced token gets 401", async () => {
mockReadFile.mockRejectedValue(new Error("File not found"));
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({
"openai-codex": {
type: "oauth",
access: "fusion-access-token",
refresh: "fusion-refresh-token",
expires: Date.now() + 60_000,
},
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error":"unauthorized"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex")!;
expect(codex.status).toBe("error");
expect(codex.error).toContain("re-login from Fusion Settings");
});
it("parses usage data from API response", async () => {
const mockResponse = {
email: "test@example.com",

View File

@@ -3,6 +3,7 @@ import * as path from "node:path";
import { readFile } from "node:fs/promises";
import * as https from "node:https";
import * as child_process from "node:child_process";
import { choosePreferredStoredCredential, readStoredCredentialsFromAuthFile } from "@fusion/core";
import { getAuthFileCandidates } from "./auth-paths.js";
function getHomeDir(): string {
@@ -1114,15 +1115,13 @@ async function fetchClaudeUsage(authStorage?: AuthStorageLike): Promise<Provider
// ── Codex fetcher ──────────────────────────────────────────────────────────
async function fetchCodexUsage(): Promise<ProviderUsage> {
const usage: ProviderUsage = {
name: "Codex",
icon: "🟢",
status: "no-auth",
windows: [],
};
type CodexCredential = {
accessToken: string;
idToken?: string;
source: "codex-cli" | "fusion-auth";
};
// Load Codex auth
async function loadCodexCredential(): Promise<CodexCredential | null> {
const codexHome = process.env.CODEX_HOME || path.join(getHomeDir(), ".codex");
const authPath = path.join(codexHome, "auth.json");
@@ -1131,19 +1130,64 @@ async function fetchCodexUsage(): Promise<ProviderUsage> {
try {
auth = JSON.parse(await readFile(authPath, "utf-8"));
} catch {
auth = null;
}
const codexCliAccessToken = auth?.tokens?.access_token;
if (typeof codexCliAccessToken === "string" && codexCliAccessToken.length > 0) {
return {
accessToken: codexCliAccessToken,
idToken: typeof auth?.tokens?.id_token === "string" ? auth.tokens.id_token : undefined,
source: "codex-cli",
};
}
let preferredCredential: ReturnType<typeof choosePreferredStoredCredential>;
for (const candidatePath of getAuthFileCandidates()) {
const authEntries = readStoredCredentialsFromAuthFile(candidatePath);
preferredCredential = choosePreferredStoredCredential(preferredCredential, authEntries["openai-codex"]);
}
if (
preferredCredential?.type !== "oauth"
|| typeof preferredCredential.access !== "string"
|| preferredCredential.access.length === 0
|| typeof preferredCredential.expires !== "number"
|| !Number.isFinite(preferredCredential.expires)
|| preferredCredential.expires <= Date.now()
) {
return null;
}
return {
accessToken: preferredCredential.access,
source: "fusion-auth",
};
}
async function fetchCodexUsage(): Promise<ProviderUsage> {
const usage: ProviderUsage = {
name: "Codex",
icon: "🟢",
status: "no-auth",
windows: [],
};
const credential = await loadCodexCredential();
if (!credential) {
usage.error = "No Codex credentials — run 'codex' to login";
return usage;
}
const accessToken = auth?.tokens?.access_token;
const accessToken = credential.accessToken;
if (!accessToken) {
usage.error = "No Codex access token found";
return usage;
}
// Extract plan and email from id_token
if (auth?.tokens?.id_token) {
const claims = decodeJwtPayload(auth.tokens.id_token);
// Extract plan and email from id_token when sourced from Codex CLI auth
if (credential.source === "codex-cli" && credential.idToken) {
const claims = decodeJwtPayload(credential.idToken);
if (claims) {
usage.email = claims.email || null;
const openaiAuth = claims["https://api.openai.com/auth"];
@@ -1163,7 +1207,10 @@ async function fetchCodexUsage(): Promise<ProviderUsage> {
if (res.status === 401 || res.status === 403) {
usage.status = "error";
usage.error = "Auth expired — run 'codex' to re-login";
usage.error =
credential.source === "fusion-auth"
? "Auth expired — re-login from Fusion Settings or run 'codex'"
: "Auth expired — run 'codex' to re-login";
return usage;
}