mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-21 04:51:31 +00:00
refactor(providers): move auth status checks into provider runtimes
Move provider authentication status logic out of the CLI auth route so auth checks live with the provider implementations that understand each provider's install and credential model. Add provider-specific auth runtime classes for Claude, Codex, Cursor, and Gemini, and expose them through the shared provider contract as `provider.auth`. Add a provider auth service that resolves providers through the registry and delegates status checks via `auth.getStatus()`. Keep the existing `/api/cli/<provider>/status` endpoints, but make them thin route adapters over the new provider auth service. This removes duplicated route-local credential parsing and makes auth status a first-class provider capability beside MCP and message handling.
This commit is contained in:
100
server/modules/providers/list/codex/codex-auth.provider.ts
Normal file
100
server/modules/providers/list/codex/codex-auth.provider.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import spawn from 'cross-spawn';
|
||||
|
||||
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
||||
import type { ProviderAuthStatus } from '@/shared/types.js';
|
||||
import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
|
||||
|
||||
type CodexCredentialsStatus = {
|
||||
authenticated: boolean;
|
||||
email: string | null;
|
||||
method: string | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class CodexAuthProvider implements IProviderAuthRuntime {
|
||||
/**
|
||||
* Checks whether Codex is available to the server runtime.
|
||||
*/
|
||||
private checkInstalled(): boolean {
|
||||
try {
|
||||
spawn.sync('codex', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Codex SDK availability and credential status.
|
||||
*/
|
||||
async getStatus(): Promise<ProviderAuthStatus> {
|
||||
const installed = this.checkInstalled();
|
||||
const credentials = await this.checkCredentials();
|
||||
|
||||
return {
|
||||
installed,
|
||||
provider: 'codex',
|
||||
authenticated: credentials.authenticated,
|
||||
email: credentials.email,
|
||||
method: credentials.method,
|
||||
error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads Codex auth.json and checks OAuth tokens or an API key fallback.
|
||||
*/
|
||||
private async checkCredentials(): Promise<CodexCredentialsStatus> {
|
||||
try {
|
||||
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
||||
const content = await readFile(authPath, 'utf8');
|
||||
const auth = readObjectRecord(JSON.parse(content)) ?? {};
|
||||
const tokens = readObjectRecord(auth.tokens) ?? {};
|
||||
const idToken = readOptionalString(tokens.id_token);
|
||||
const accessToken = readOptionalString(tokens.access_token);
|
||||
|
||||
if (idToken || accessToken) {
|
||||
return {
|
||||
authenticated: true,
|
||||
email: idToken ? this.readEmailFromIdToken(idToken) : 'Authenticated',
|
||||
method: 'credentials_file',
|
||||
};
|
||||
}
|
||||
|
||||
if (readOptionalString(auth.OPENAI_API_KEY)) {
|
||||
return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
|
||||
}
|
||||
|
||||
return { authenticated: false, email: null, method: null, error: 'No valid tokens found' };
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
return {
|
||||
authenticated: false,
|
||||
email: null,
|
||||
method: null,
|
||||
error: code === 'ENOENT' ? 'Codex not configured' : error instanceof Error ? error.message : 'Failed to read Codex auth',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the user email from a Codex id_token when a readable JWT payload exists.
|
||||
*/
|
||||
private readEmailFromIdToken(idToken: string): string {
|
||||
try {
|
||||
const parts = idToken.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const payload = readObjectRecord(JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')));
|
||||
return readOptionalString(payload?.email) ?? readOptionalString(payload?.user) ?? 'Authenticated';
|
||||
}
|
||||
} catch {
|
||||
// Fall back to a generic authenticated marker if the token payload is not readable.
|
||||
}
|
||||
|
||||
return 'Authenticated';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user