refactor(cliproxy): address code review feedback for token handling

- Remove module-level mutable state (lastTokenSourcePath)
- Add GeminiCredsWithSource interface for explicit source path passing
- Add isValidCliproxyToken() for proper type validation
- Use explicit path comparison instead of .includes('.gemini') heuristic
- Add descriptive comment for directory scan error handling

Addresses review feedback from PR #396
This commit is contained in:
kaitranntt
2026-01-29 15:10:20 -05:00
parent 9d96535d28
commit cddf931fe6
+63 -38
View File
@@ -46,6 +46,12 @@ interface GeminiOAuthCreds {
id_token?: string; id_token?: string;
} }
/** Gemini credentials with source path for write-back */
interface GeminiCredsWithSource {
creds: GeminiOAuthCreds;
sourcePath: string;
}
/** CLIProxyAPI Gemini token structure (from GeminiTokenStorage Go struct) */ /** CLIProxyAPI Gemini token structure (from GeminiTokenStorage Go struct) */
interface CliproxyGeminiToken { interface CliproxyGeminiToken {
token: { token: {
@@ -58,9 +64,6 @@ interface CliproxyGeminiToken {
type: 'gemini'; type: 'gemini';
} }
/** Tracks the source path of the last read token (for write-back) */
let lastTokenSourcePath: string | null = null;
/** Token refresh response from Google */ /** Token refresh response from Google */
interface TokenRefreshResponse { interface TokenRefreshResponse {
access_token?: string; access_token?: string;
@@ -90,10 +93,22 @@ function mapCliproxyToGeminiCreds(cliproxy: CliproxyGeminiToken): GeminiOAuthCre
} }
/** /**
* Read Gemini token from CLIProxy auth directory * Validate CLIProxyAPI token structure has required fields
* Returns null if no valid token found
*/ */
function readCliproxyGeminiCreds(): GeminiOAuthCreds | null { function isValidCliproxyToken(data: unknown): data is CliproxyGeminiToken {
if (typeof data !== 'object' || data === null) return false;
const obj = data as Record<string, unknown>;
if (obj.type !== 'gemini') return false;
if (typeof obj.token !== 'object' || obj.token === null) return false;
const token = obj.token as Record<string, unknown>;
return typeof token.access_token === 'string';
}
/**
* Read Gemini token from CLIProxy auth directory
* Returns credentials with source path, or null if no valid token found
*/
function readCliproxyGeminiCreds(): GeminiCredsWithSource | null {
const authDir = getProviderAuthDir('gemini'); const authDir = getProviderAuthDir('gemini');
if (!fs.existsSync(authDir)) return null; if (!fs.existsSync(authDir)) return null;
@@ -127,6 +142,7 @@ function readCliproxyGeminiCreds(): GeminiOAuthCreds | null {
} }
} }
} catch { } catch {
// Directory read failed - continue to return null
return null; return null;
} }
} }
@@ -135,12 +151,14 @@ function readCliproxyGeminiCreds(): GeminiOAuthCreds | null {
try { try {
const content = fs.readFileSync(tokenPath, 'utf8'); const content = fs.readFileSync(tokenPath, 'utf8');
const data = JSON.parse(content); const data: unknown = JSON.parse(content);
// Check if this is CLIProxyAPI format (has nested token object) // Validate CLIProxyAPI format with proper type checking
if (data.type === 'gemini' && data.token) { if (isValidCliproxyToken(data)) {
lastTokenSourcePath = tokenPath; return {
return mapCliproxyToGeminiCreds(data as CliproxyGeminiToken); creds: mapCliproxyToGeminiCreds(data),
sourcePath: tokenPath,
};
} }
return null; return null;
@@ -152,12 +170,13 @@ function readCliproxyGeminiCreds(): GeminiOAuthCreds | null {
/** /**
* Read Gemini OAuth credentials * Read Gemini OAuth credentials
* Priority: CLIProxy auth dir first, then ~/.gemini/oauth_creds.json * Priority: CLIProxy auth dir first, then ~/.gemini/oauth_creds.json
* Returns credentials with source path for correct write-back
*/ */
function readGeminiCreds(): GeminiOAuthCreds | null { function readGeminiCreds(): GeminiCredsWithSource | null {
// 1. Try CLIProxy auth directory first (CCS-managed tokens) // 1. Try CLIProxy auth directory first (CCS-managed tokens)
const cliproxyCreds = readCliproxyGeminiCreds(); const cliproxyResult = readCliproxyGeminiCreds();
if (cliproxyCreds) { if (cliproxyResult) {
return cliproxyCreds; return cliproxyResult;
} }
// 2. Fall back to standard Gemini CLI location // 2. Fall back to standard Gemini CLI location
@@ -166,9 +185,11 @@ function readGeminiCreds(): GeminiOAuthCreds | null {
return null; return null;
} }
try { try {
lastTokenSourcePath = oauthPath;
const content = fs.readFileSync(oauthPath, 'utf8'); const content = fs.readFileSync(oauthPath, 'utf8');
return JSON.parse(content) as GeminiOAuthCreds; return {
creds: JSON.parse(content) as GeminiOAuthCreds,
sourcePath: oauthPath,
};
} catch { } catch {
return null; return null;
} }
@@ -199,23 +220,26 @@ function writeCliproxyGeminiCreds(tokenPath: string, creds: GeminiOAuthCreds): s
/** /**
* Write Gemini OAuth credentials * Write Gemini OAuth credentials
* Writes back to the source location (CLIProxy or ~/.gemini) * Writes back to the specified source location (CLIProxy or ~/.gemini)
* @param creds - The credentials to write
* @param sourcePath - The path where credentials were originally read from
* @returns error message if write failed, undefined on success * @returns error message if write failed, undefined on success
*/ */
function writeGeminiCreds(creds: GeminiOAuthCreds): string | undefined { function writeGeminiCreds(creds: GeminiOAuthCreds, sourcePath: string): string | undefined {
// If we read from CLIProxy, write back there in CLIProxy format const geminiOAuthPath = getGeminiOAuthPath();
if (lastTokenSourcePath && !lastTokenSourcePath.includes('.gemini')) {
return writeCliproxyGeminiCreds(lastTokenSourcePath, creds); // If source is not the standard Gemini path, write to CLIProxy format
if (sourcePath !== geminiOAuthPath) {
return writeCliproxyGeminiCreds(sourcePath, creds);
} }
// Otherwise write to standard Gemini CLI location // Otherwise write to standard Gemini CLI location
const oauthPath = getGeminiOAuthPath(); const dir = path.dirname(geminiOAuthPath);
const dir = path.dirname(oauthPath);
try { try {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
} }
fs.writeFileSync(oauthPath, JSON.stringify(creds, null, 2), { mode: 0o600 }); fs.writeFileSync(geminiOAuthPath, JSON.stringify(creds, null, 2), { mode: 0o600 });
return undefined; return undefined;
} catch (err) { } catch (err) {
return err instanceof Error ? err.message : 'Failed to write credentials'; return err instanceof Error ? err.message : 'Failed to write credentials';
@@ -226,14 +250,14 @@ function writeGeminiCreds(creds: GeminiOAuthCreds): string | undefined {
* Check if Gemini token is expired or expiring soon * Check if Gemini token is expired or expiring soon
*/ */
export function isGeminiTokenExpiringSoon(): boolean { export function isGeminiTokenExpiringSoon(): boolean {
const creds = readGeminiCreds(); const result = readGeminiCreds();
if (!creds || !creds.access_token) { if (!result || !result.creds.access_token) {
return true; // No token = needs auth return true; // No token = needs auth
} }
if (!creds.expiry_date) { if (!result.creds.expiry_date) {
return false; // No expiry info = assume valid return false; // No expiry info = assume valid
} }
const expiresIn = creds.expiry_date - Date.now(); const expiresIn = result.creds.expiry_date - Date.now();
return expiresIn < REFRESH_LEAD_TIME_MS; return expiresIn < REFRESH_LEAD_TIME_MS;
} }
@@ -246,11 +270,12 @@ export async function refreshGeminiToken(): Promise<{
error?: string; error?: string;
expiresAt?: number; expiresAt?: number;
}> { }> {
const creds = readGeminiCreds(); const result = readGeminiCreds();
if (!creds || !creds.refresh_token) { if (!result || !result.creds.refresh_token) {
return { success: false, error: 'No refresh token available' }; return { success: false, error: 'No refresh token available' };
} }
const { creds, sourcePath } = result;
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); const timeoutId = setTimeout(() => controller.abort(), 10000);
@@ -263,7 +288,7 @@ export async function refreshGeminiToken(): Promise<{
}, },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
refresh_token: creds.refresh_token, refresh_token: creds.refresh_token as string, // Already validated above
client_id: GEMINI_CLIENT_ID, client_id: GEMINI_CLIENT_ID,
client_secret: GEMINI_CLIENT_SECRET, client_secret: GEMINI_CLIENT_SECRET,
}).toString(), }).toString(),
@@ -291,7 +316,7 @@ export async function refreshGeminiToken(): Promise<{
access_token: data.access_token, access_token: data.access_token,
expiry_date: expiresAt, expiry_date: expiresAt,
}; };
const writeError = writeGeminiCreds(updatedCreds); const writeError = writeGeminiCreds(updatedCreds, sourcePath);
if (writeError) { if (writeError) {
return { success: false, error: `Token refreshed but failed to save: ${writeError}` }; return { success: false, error: `Token refreshed but failed to save: ${writeError}` };
} }
@@ -316,8 +341,8 @@ export async function ensureGeminiTokenValid(verbose = false): Promise<{
refreshed: boolean; refreshed: boolean;
error?: string; error?: string;
}> { }> {
const creds = readGeminiCreds(); const result = readGeminiCreds();
if (!creds || !creds.access_token) { if (!result || !result.creds.access_token) {
return { valid: false, refreshed: false, error: 'No Gemini credentials found' }; return { valid: false, refreshed: false, error: 'No Gemini credentials found' };
} }
@@ -330,13 +355,13 @@ export async function ensureGeminiTokenValid(verbose = false): Promise<{
console.log('[i] Gemini token expired or expiring soon, refreshing...'); console.log('[i] Gemini token expired or expiring soon, refreshing...');
} }
const result = await refreshGeminiToken(); const refreshResult = await refreshGeminiToken();
if (result.success) { if (refreshResult.success) {
if (verbose) { if (verbose) {
console.log('[OK] Gemini token refreshed successfully'); console.log('[OK] Gemini token refreshed successfully');
} }
return { valid: true, refreshed: true }; return { valid: true, refreshed: true };
} }
return { valid: false, refreshed: false, error: result.error }; return { valid: false, refreshed: false, error: refreshResult.error };
} }