mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 20:03:05 +00:00
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:
parent
1c31d91c83
commit
bf022d2581
2 changed files with 145 additions and 141 deletions
145
packages/coding-agent/src/core/auth-storage.ts
Normal file
145
packages/coding-agent/src/core/auth-storage.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue