mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 12:21:20 +00:00
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:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user