WIP: Add auth-storage.ts for credential management

- AuthStorage class for reading/writing auth.json
- Supports both api_key and oauth credential types
- getApiKey() priority: auth.json api_key > auth.json oauth > env var
This commit is contained in:
Mario Zechner 2025-12-25 01:18:29 +01:00
parent 1c31d91c83
commit bf022d2581
2 changed files with 145 additions and 141 deletions

View file

@ -0,0 +1,145 @@
/**
* Credential storage for API keys and OAuth tokens.
* Handles loading, saving, and refreshing credentials from auth.json.
*/
import { getApiKeyFromEnv, getOAuthApiKey, type OAuthCredentials, type OAuthProvider } from "@mariozechner/pi-ai";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname } from "path";
export type ApiKeyCredential = {
type: "api_key";
key: string;
};
export type OAuthCredential = {
type: "oauth";
} & OAuthCredentials;
export type AuthCredential = ApiKeyCredential | OAuthCredential;
export type AuthStorageData = Record<string, AuthCredential>;
/**
* Credential storage backed by a JSON file.
*/
export class AuthStorage {
private data: AuthStorageData = {};
constructor(private authPath: string) {
this.reload();
}
/**
* Reload credentials from disk.
*/
reload(): void {
if (!existsSync(this.authPath)) {
this.data = {};
return;
}
try {
this.data = JSON.parse(readFileSync(this.authPath, "utf-8"));
} catch {
this.data = {};
}
}
/**
* Save credentials to disk.
*/
private save(): void {
const dir = dirname(this.authPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true, mode: 0o700 });
}
writeFileSync(this.authPath, JSON.stringify(this.data, null, 2), "utf-8");
chmodSync(this.authPath, 0o600);
}
/**
* Get credential for a provider.
*/
get(provider: string): AuthCredential | null {
return this.data[provider] ?? null;
}
/**
* Set credential for a provider.
*/
set(provider: string, credential: AuthCredential): void {
this.data[provider] = credential;
this.save();
}
/**
* Remove credential for a provider.
*/
remove(provider: string): void {
delete this.data[provider];
this.save();
}
/**
* List all providers with credentials.
*/
list(): string[] {
return Object.keys(this.data);
}
/**
* Check if credentials exist for a provider.
*/
has(provider: string): boolean {
return provider in this.data;
}
/**
* Get all credentials (for passing to getOAuthApiKey).
*/
getAll(): AuthStorageData {
return { ...this.data };
}
/**
* Get API key for a provider.
* Priority:
* 1. API key from auth.json
* 2. OAuth token from auth.json (auto-refreshed)
* 3. Environment variable (via getApiKeyFromEnv)
*/
async getApiKey(provider: string): Promise<string | null> {
const cred = this.data[provider];
if (cred?.type === "api_key") {
return cred.key;
}
if (cred?.type === "oauth") {
// Build OAuthCredentials map (without type discriminator)
const oauthCreds: Record<string, OAuthCredentials> = {};
for (const [key, value] of Object.entries(this.data)) {
if (value.type === "oauth") {
const { type: _, ...rest } = value;
oauthCreds[key] = rest;
}
}
try {
const result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);
if (result) {
// Save refreshed credentials
this.data[provider] = { type: "oauth", ...result.newCredentials };
this.save();
return result.apiKey;
}
} catch {
// Token refresh failed, remove invalid credentials
this.remove(provider);
}
}
// Fall back to environment variable
return getApiKeyFromEnv(provider) ?? null;
}
}

View file

@ -1,141 +0,0 @@
/**
* OAuth management for coding-agent.
* Re-exports from @mariozechner/pi-ai and adds convenience wrappers.
*/
import {
getOAuthApiKey,
listOAuthProviders as listOAuthProvidersFromAi,
loadOAuthCredentials,
loginAnthropic,
loginAntigravity,
loginGeminiCli,
loginGitHubCopilot,
type OAuthCredentials,
type OAuthProvider,
type OAuthStorageBackend,
refreshToken as refreshTokenFromAi,
removeOAuthCredentials,
resetOAuthStorage,
saveOAuthCredentials,
setOAuthStorage,
} from "@mariozechner/pi-ai";
// Re-export types and functions
export type { OAuthCredentials, OAuthProvider, OAuthStorageBackend };
export { listOAuthProvidersFromAi as listOAuthProviders };
export {
getOAuthApiKey,
loadOAuthCredentials,
removeOAuthCredentials,
resetOAuthStorage,
saveOAuthCredentials,
setOAuthStorage,
};
// Types for OAuth flow
export interface OAuthAuthInfo {
url: string;
instructions?: string;
}
export interface OAuthPrompt {
message: string;
placeholder?: string;
}
export type OAuthProviderInfo = {
id: OAuthProvider;
name: string;
description: string;
available: boolean;
};
export function getOAuthProviders(): OAuthProviderInfo[] {
return [
{
id: "anthropic",
name: "Anthropic (Claude Pro/Max)",
description: "Use Claude with your Pro/Max subscription",
available: true,
},
{
id: "github-copilot",
name: "GitHub Copilot",
description: "Use models via GitHub Copilot subscription",
available: true,
},
{
id: "google-gemini-cli",
name: "Google Gemini CLI",
description: "Free Gemini 2.0/2.5 models via Google Cloud",
available: true,
},
{
id: "google-antigravity",
name: "Antigravity",
description: "Free Gemini 3, Claude, GPT-OSS via Google Cloud",
available: true,
},
];
}
/**
* Login with OAuth provider
*/
export async function login(
provider: OAuthProvider,
onAuth: (info: OAuthAuthInfo) => void,
onPrompt: (prompt: OAuthPrompt) => Promise<string>,
onProgress?: (message: string) => void,
): Promise<void> {
switch (provider) {
case "anthropic":
await loginAnthropic(
(url) => onAuth({ url }),
async () => onPrompt({ message: "Paste the authorization code below:" }),
);
break;
case "github-copilot": {
const creds = await loginGitHubCopilot({
onAuth: (url, instructions) => onAuth({ url, instructions }),
onPrompt,
onProgress,
});
saveOAuthCredentials("github-copilot", creds);
break;
}
case "google-gemini-cli": {
await loginGeminiCli((info) => onAuth({ url: info.url, instructions: info.instructions }), onProgress);
break;
}
case "google-antigravity": {
await loginAntigravity((info) => onAuth({ url: info.url, instructions: info.instructions }), onProgress);
break;
}
default:
throw new Error(`Unknown OAuth provider: ${provider}`);
}
}
/**
* Logout from OAuth provider
*/
export async function logout(provider: OAuthProvider): Promise<void> {
removeOAuthCredentials(provider);
}
/**
* Refresh OAuth token for provider.
* Delegates to the ai package implementation.
*/
export async function refreshToken(provider: OAuthProvider): Promise<string> {
return refreshTokenFromAi(provider);
}
/**
* Get OAuth token for provider (auto-refreshes if expired).
*/
export async function getOAuthToken(provider: OAuthProvider): Promise<string | null> {
return getOAuthApiKey(provider);
}