claude-gemini-multimodal-bridge
Version:
Enterprise-grade AI integration bridge connecting Claude Code, Gemini CLI, and Google AI Studio with intelligent routing and advanced multimodal processing capabilities
447 lines (446 loc) • 19 kB
JavaScript
import { commandExists } from '../utils/platformUtils.js';
import { execSync } from 'child_process';
import { logger } from '../utils/logger.js';
import { safeExecute } from '../utils/errorHandler.js';
import { OAuthManager } from './OAuthManager.js';
import { AuthCache } from './AuthCache.js';
export class AuthVerifier {
oauthManager;
authCache;
constructor() {
this.oauthManager = new OAuthManager();
this.authCache = AuthCache.getInstance();
setInterval(() => {
this.authCache.cleanup();
}, 30 * 60 * 1000);
}
async verifyAllAuthentications() {
return safeExecute(async () => {
logger.info('Starting comprehensive authentication verification...');
const services = {
gemini: await this.verifyGeminiAuth(),
aistudio: await this.verifyAIStudioAuth(),
claude: await this.verifyClaudeCodeAuth(),
};
const overall = Object.values(services).every(result => result.success);
const recommendations = this.generateRecommendations(services);
logger.info('Authentication verification completed', {
overall,
servicesVerified: Object.keys(services).length,
successCount: Object.values(services).filter(r => r.success).length,
});
return {
overall,
services,
recommendations,
};
}, {
operationName: 'verify-all-auth',
layer: 'claude',
timeout: 30000,
});
}
async verifyGeminiAuth() {
const cachedResult = this.authCache.get('gemini');
if (cachedResult) {
return cachedResult;
}
return safeExecute(async () => {
logger.info('Verifying Gemini authentication (no cache)...');
try {
const oauthStatus = await this.oauthManager.checkGeminiAuthentication();
if (oauthStatus.isAuthenticated) {
const result = {
success: true,
status: oauthStatus,
requiresAction: false,
};
this.authCache.set('gemini', result);
return result;
}
const apiKey = process.env.GEMINI_API_KEY;
if (apiKey) {
logger.debug('OAuth failed, trying API Key fallback for Gemini');
try {
await this.testGeminiApiKey(apiKey);
const result = {
success: true,
status: {
isAuthenticated: true,
method: 'api_key',
userInfo: undefined,
},
requiresAction: false,
};
this.authCache.set('gemini', result);
return result;
}
catch (apiError) {
logger.warn('Gemini API key validation failed', { error: apiError.message });
}
}
const result = {
success: false,
status: {
isAuthenticated: false,
method: 'oauth',
userInfo: undefined,
},
error: 'Gemini not authenticated',
requiresAction: true,
actionInstructions: 'Run "gemini auth" for OAuth (recommended) or set GEMINI_API_KEY environment variable',
};
this.authCache.set('gemini', result);
const failureInfo = this.authCache.getFailureInfo('gemini');
if (failureInfo?.nextRetryTime) {
result.error += ` (Failure #${failureInfo.count}, retry after ${failureInfo.nextRetryTime.toLocaleTimeString()})`;
}
return result;
}
catch (error) {
logger.error('Gemini authentication verification failed', { error: error.message });
const result = {
success: false,
status: {
isAuthenticated: false,
method: 'oauth',
userInfo: undefined,
},
error: `Gemini verification failed: ${error.message}`,
requiresAction: true,
actionInstructions: 'Install Gemini CLI: npm install -g @google/gemini-cli && gemini auth',
};
return result;
}
}, {
operationName: 'verify-gemini-auth',
layer: 'gemini',
timeout: 10000,
});
}
async verifyAIStudioAuth() {
const cachedResult = this.authCache.get('aistudio');
if (cachedResult) {
return cachedResult;
}
return safeExecute(async () => {
logger.info('Verifying AI Studio authentication (no cache)...');
const preferredKey = process.env.AI_STUDIO_API_KEY;
const fallback1 = process.env.GOOGLE_AI_STUDIO_API_KEY;
const fallback2 = process.env.GEMINI_API_KEY;
const apiKey = preferredKey ?? fallback1 ?? fallback2;
logger.debug('AI Studio authentication verification', {
hasPreferredKey: !!preferredKey,
hasFallback1: !!fallback1,
hasFallback2: !!fallback2,
selectedKey: apiKey ? `${apiKey.substring(0, 8)}...` : 'none',
searchOrder: ['AI_STUDIO_API_KEY', 'GOOGLE_AI_STUDIO_API_KEY', 'GEMINI_API_KEY'],
errorReference: 'Addressing authentication issues from Error.md lines 70-86'
});
if (!preferredKey && fallback2) {
logger.warn('GEMINI_API_KEY is deprecated for AI Studio. Please use AI_STUDIO_API_KEY instead.', {
currentVar: 'GEMINI_API_KEY',
recommendedVar: 'AI_STUDIO_API_KEY',
migration: 'Update your .env file: GEMINI_API_KEY → AI_STUDIO_API_KEY',
reason: 'GEMINI_API_KEY is ambiguous - used for both Gemini CLI and AI Studio'
});
}
if (!preferredKey && fallback1) {
logger.warn('GOOGLE_AI_STUDIO_API_KEY is deprecated. Please use AI_STUDIO_API_KEY instead.', {
currentVar: 'GOOGLE_AI_STUDIO_API_KEY',
recommendedVar: 'AI_STUDIO_API_KEY',
migration: 'Update your .env file: GOOGLE_AI_STUDIO_API_KEY → AI_STUDIO_API_KEY'
});
}
if (!apiKey) {
logger.error('AI Studio API key missing - this causes the authentication failure seen in Error.md', {
issue: 'No API key found in any environment variable',
searchedVars: ['AI_STUDIO_API_KEY', 'GOOGLE_AI_STUDIO_API_KEY', 'GEMINI_API_KEY'],
errorContext: 'This is the root cause of AI Studio authentication failures',
setupUrl: 'https://aistudio.google.com/app/apikey'
});
const result = {
success: false,
status: {
isAuthenticated: false,
method: 'api_key',
userInfo: undefined,
},
error: 'AI Studio API key not found. This causes authentication failures as seen in Error.md.',
requiresAction: true,
actionInstructions: 'Set AI_STUDIO_API_KEY environment variable with your AI Studio API key. Get it from: https://aistudio.google.com/app/apikey',
};
this.authCache.set('aistudio', result);
const failureInfo = this.authCache.getFailureInfo('aistudio');
if (failureInfo?.nextRetryTime) {
result.error += ` (Failure #${failureInfo.count}, retry after ${failureInfo.nextRetryTime.toLocaleTimeString()})`;
}
return result;
}
if (!this.validateAIStudioApiKeyFormat(apiKey)) {
logger.error('Invalid AI Studio API key format detected', {
keyPrefix: apiKey.substring(0, 8),
keyLength: apiKey.length,
expectedFormat: 'Should start with "AI" and be at least 20 characters',
currentFormat: `Starts with "${apiKey.substring(0, 2)}", length: ${apiKey.length}`,
troubleshooting: 'Verify the key was copied correctly from AI Studio'
});
return {
success: false,
status: {
isAuthenticated: false,
method: 'api_key',
userInfo: undefined,
},
error: 'Invalid AI Studio API key format. Expected format: starts with "AI", minimum 20 characters.',
requiresAction: true,
actionInstructions: 'Verify your API key from https://aistudio.google.com/app/apikey and update AI_STUDIO_API_KEY in your .env file',
};
}
logger.info('AI Studio authentication verification successful', {
method: 'api_key',
keySource: preferredKey ? 'AI_STUDIO_API_KEY' : fallback1 ? 'GOOGLE_AI_STUDIO_API_KEY' : 'GEMINI_API_KEY',
status: 'ready'
});
const result = {
success: true,
status: {
isAuthenticated: true,
method: 'api_key',
userInfo: {
planType: 'free',
},
},
requiresAction: false,
};
this.authCache.set('aistudio', result);
return result;
}, {
operationName: 'verify-aistudio-auth',
layer: 'aistudio',
timeout: 10000,
});
}
validateAIStudioApiKeyFormat(apiKey) {
if (!apiKey || typeof apiKey !== 'string') {
return false;
}
return apiKey.length >= 20 && apiKey.startsWith('AI');
}
async verifyClaudeCodeAuth() {
const cachedResult = this.authCache.get('claude');
if (cachedResult) {
return cachedResult;
}
return safeExecute(async () => {
logger.info('Verifying Claude Code authentication...');
try {
const isInstalled = await this.checkClaudeCodeInstalled();
if (!isInstalled) {
const result = {
success: false,
status: {
isAuthenticated: false,
method: 'session',
userInfo: undefined,
},
error: 'Claude Code not installed',
requiresAction: true,
actionInstructions: 'Install Claude Code: npm install -g @anthropic-ai/claude-code',
};
this.authCache.set('claude', result);
return result;
}
const isWorking = await this.testClaudeCodeFunctionality();
if (!isWorking) {
const result = {
success: false,
status: {
isAuthenticated: false,
method: 'session',
userInfo: undefined,
},
error: 'Claude Code authentication required',
requiresAction: true,
actionInstructions: 'Run "claude auth" to authenticate Claude Code',
};
this.authCache.set('claude', result);
return result;
}
const result = {
success: true,
status: {
isAuthenticated: true,
method: 'session',
userInfo: {
planType: 'authenticated',
},
},
requiresAction: false,
};
this.authCache.set('claude', result);
return result;
}
catch (error) {
logger.error('Claude Code verification failed', { error: error.message });
return {
success: false,
status: {
isAuthenticated: false,
method: 'session',
userInfo: undefined,
},
error: `Claude Code verification failed: ${error.message}`,
requiresAction: true,
actionInstructions: 'Ensure Claude Code is properly installed and authenticated',
};
}
}, {
operationName: 'verify-claude-auth',
layer: 'claude',
timeout: 10000,
});
}
async verifyServiceAuth(service) {
switch (service) {
case 'gemini':
return this.verifyGeminiAuth();
case 'aistudio':
return this.verifyAIStudioAuth();
case 'claude':
return this.verifyClaudeCodeAuth();
default:
throw new Error(`Unknown service: ${service}`);
}
}
async testGeminiApiKey(apiKey) {
const { GoogleGenAI } = await import('@google/genai');
try {
const genAI = new GoogleGenAI({ apiKey });
const result = await genAI.models.generateContent({
model: 'gemini-pro',
contents: [{
parts: [{ text: 'Test' }]
}]
});
if (!result.candidates || result.candidates.length === 0) {
throw new Error('API key test failed - no response');
}
logger.debug('Gemini API key validation successful');
}
catch (error) {
const errorMessage = error.message;
if (errorMessage.includes('429') || errorMessage.includes('quota') || errorMessage.includes('Quota exceeded')) {
logger.warn('Gemini quota exceeded during authentication check', { error: errorMessage });
throw new Error(`Gemini API quota exceeded. Please wait before retrying or use a different model. Details: ${errorMessage}`);
}
logger.warn('Gemini API key validation failed', { error: errorMessage });
throw new Error(`Gemini API key invalid: ${errorMessage}`);
}
}
getAuthCacheStats() {
return this.authCache.getStats();
}
clearAuthCache(service) {
if (service) {
this.authCache.invalidate(service);
logger.info('Authentication cache cleared for service', { service });
}
else {
this.authCache.clear();
logger.info('All authentication cache cleared');
}
}
async forceRefreshAuth(service) {
this.authCache.forceRefresh(service);
return await this.verifyServiceAuth(service);
}
async checkClaudeCodeInstalled() {
if (commandExists('claude')) {
return true;
}
try {
execSync('claude --version', { stdio: 'ignore', timeout: 5000 });
return true;
}
catch {
return false;
}
}
async testClaudeCodeFunctionality() {
try {
const output = execSync('claude --help', {
encoding: 'utf8',
timeout: 5000,
stdio: 'pipe'
});
const needsAuth = output.toLowerCase().includes('auth') &&
(output.toLowerCase().includes('required') ||
output.toLowerCase().includes('login'));
return !needsAuth;
}
catch (error) {
logger.debug('Claude Code functionality test failed', { error: error.message });
return false;
}
}
generateRecommendations(services) {
const recommendations = [];
if (services.gemini && !services.gemini.success) {
if (services.gemini.actionInstructions) {
recommendations.push(`Gemini: ${services.gemini.actionInstructions}`);
}
}
if (services.aistudio && !services.aistudio.success) {
if (services.aistudio.actionInstructions) {
recommendations.push(`AI Studio: ${services.aistudio.actionInstructions}`);
}
}
if (services.claude && !services.claude.success) {
if (services.claude.actionInstructions) {
recommendations.push(`Claude Code: ${services.claude.actionInstructions}`);
}
}
if (recommendations.length === 0) {
recommendations.push('All services are properly authenticated and ready to use!');
}
else {
recommendations.push('For setup assistance, run: cgmb setup-guide');
}
return recommendations;
}
async hasAnyAuthentication() {
try {
const results = await this.verifyAllAuthentications();
return Object.values(results.services).some(service => service.success);
}
catch {
return false;
}
}
async getServiceStatus(service) {
try {
const result = await this.verifyServiceAuth(service);
if (result.success) {
const method = result.status.method;
const user = result.status.userInfo?.email ?? 'authenticated user';
return `✅ ${service}: Authenticated via ${method} (${user})`;
}
else {
return `❌ ${service}: ${result.error}`;
}
}
catch (error) {
return `❌ ${service}: Verification failed - ${error.message}`;
}
}
async serviceNeedsAttention(service) {
try {
const result = await this.verifyServiceAuth(service);
return !result.success && result.requiresAction;
}
catch {
return true;
}
}
}