fix(FN-1952): read legacy oauth auth
This commit is contained in:
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||
|
||||
function makeAuthStorage(credentials: Record<string, { type: string; key?: string }> = {}) {
|
||||
function makeAuthStorage(credentials: Record<string, { type: string; key?: string; access?: string; refresh?: string; expires?: number }> = {}) {
|
||||
return {
|
||||
reload: vi.fn(),
|
||||
getOAuthProviders: vi.fn(() => []),
|
||||
@@ -99,4 +99,26 @@ describe("wrapAuthStorageWithApiKeyProviders", () => {
|
||||
expect(await storage.getApiKey("openrouter")).toBe("legacy-key");
|
||||
expect(existsSync(missingLegacyAuth)).toBe(false);
|
||||
});
|
||||
|
||||
it("reads non-expired OAuth credentials from legacy auth JSON", async () => {
|
||||
const tempDir = join(tmpdir(), `fusion-provider-auth-oauth-${process.pid}-${Date.now()}`);
|
||||
const legacyAgentDir = join(tempDir, ".pi", "agent");
|
||||
const legacyAgentAuth = join(legacyAgentDir, "auth.json");
|
||||
mkdirSync(legacyAgentDir, { recursive: true });
|
||||
writeFileSync(
|
||||
legacyAgentAuth,
|
||||
JSON.stringify({
|
||||
"openai-codex": {
|
||||
type: "oauth",
|
||||
access: "legacy-access-token",
|
||||
refresh: "legacy-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const storage = createReadOnlyAuthFileStorage([legacyAgentAuth]);
|
||||
|
||||
expect(await storage.getApiKey("openai-codex")).toBe("legacy-access-token");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
AuthStorage,
|
||||
ModelRegistry,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
|
||||
import type { OAuthCredentials } from "@mariozechner/pi-ai/oauth";
|
||||
|
||||
export type LoginCallbacks = Parameters<AuthStorage["login"]>[1];
|
||||
|
||||
@@ -24,11 +26,20 @@ interface ReadFallbackAuthStorage {
|
||||
reload(): void;
|
||||
hasAuth(provider: string): boolean;
|
||||
getApiKey(providerId: string): Promise<string | undefined>;
|
||||
get(providerId: string): { type?: string; key?: string } | undefined;
|
||||
getAll(): Record<string, { type?: string; key?: string }>;
|
||||
get(providerId: string): StoredCredential | undefined;
|
||||
getAll(): Record<string, StoredCredential>;
|
||||
list(): string[];
|
||||
}
|
||||
|
||||
type StoredCredential = {
|
||||
type?: string;
|
||||
key?: string;
|
||||
access?: string;
|
||||
refresh?: string;
|
||||
expires?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const BUILT_IN_API_KEY_PROVIDERS: Array<{ id: string; name: string }> = [
|
||||
{ id: "kimi-coding", name: "Kimi" },
|
||||
{ id: "minimax", name: "Minimax" },
|
||||
@@ -171,17 +182,46 @@ export function mergeAuthStorageReads(
|
||||
}) as AuthStorage;
|
||||
}
|
||||
|
||||
function resolveStoredApiKey(key: string | undefined): string | undefined {
|
||||
if (!key) return undefined;
|
||||
return process.env[key] ?? key;
|
||||
}
|
||||
|
||||
function resolveOAuthApiKey(providerId: string, credential: StoredCredential): string | undefined {
|
||||
if (
|
||||
credential.type !== "oauth" ||
|
||||
typeof credential.access !== "string" ||
|
||||
typeof credential.refresh !== "string" ||
|
||||
typeof credential.expires !== "number" ||
|
||||
Date.now() >= credential.expires
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getOAuthProvider(providerId)?.getApiKey(credential as OAuthCredentials);
|
||||
}
|
||||
|
||||
function resolveStoredCredentialApiKey(providerId: string, credential: StoredCredential | undefined): string | undefined {
|
||||
if (credential?.type === "api_key") {
|
||||
return resolveStoredApiKey(credential.key);
|
||||
}
|
||||
if (credential?.type === "oauth") {
|
||||
return resolveOAuthApiKey(providerId, credential);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function createReadOnlyAuthFileStorage(authPaths: string[]): ReadFallbackAuthStorage {
|
||||
let credentials: Record<string, { type?: string; key?: string }> = {};
|
||||
let credentials: Record<string, StoredCredential> = {};
|
||||
|
||||
const reload = () => {
|
||||
const nextCredentials: Record<string, { type?: string; key?: string }> = {};
|
||||
const nextCredentials: Record<string, StoredCredential> = {};
|
||||
for (const authPath of authPaths) {
|
||||
if (!existsSync(authPath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as Record<string, { type?: string; key?: string }>;
|
||||
const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as Record<string, StoredCredential>;
|
||||
for (const [provider, credential] of Object.entries(parsed)) {
|
||||
nextCredentials[provider] ??= credential;
|
||||
}
|
||||
@@ -201,8 +241,7 @@ export function createReadOnlyAuthFileStorage(authPaths: string[]): ReadFallback
|
||||
getAll: () => ({ ...credentials }),
|
||||
list: () => Object.keys(credentials),
|
||||
getApiKey: async (provider) => {
|
||||
const credential = credentials[provider];
|
||||
return credential?.type === "api_key" ? credential.key : undefined;
|
||||
return resolveStoredCredentialApiKey(provider, credentials[provider]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user