UNPKG

mcp-prompt-optimizer

Version:

Professional cloud-based MCP server for AI-powered prompt optimization with intelligent context detection, template auto-save, optimization insights, personal model configuration via WebUI, team collaboration, enterprise-grade features, production resilie

697 lines (603 loc) 26.7 kB
/** * Cloud API Key Manager for MCP Prompt Optimizer * Production-grade with enhanced network resilience and development mode * ALIGNED with backend API requirements */ const fs = require('fs').promises; const path = require('path'); const https = require('https'); const http = require('http'); const os = require('os'); const packageJson = require('../package.json'); class CloudApiKeyManager { constructor(apiKey, options = {}) { this.apiKey = apiKey; this.backendUrl = options.backendUrl || process.env.OPTIMIZER_BACKEND_URL || 'https://p01--project-optimizer--fvmrdk8m9k9j.code.run'; this.cacheFile = path.join(os.homedir(), '.mcp-cloud-api-cache.json'); this.healthFile = path.join(os.homedir(), '.mcp-cloud-health.json'); this.cacheExpiry = options.cacheExpiry || 24 * 60 * 60 * 1000; // 24 hours this.fallbackCacheExpiry = options.fallbackCacheExpiry || 7 * 24 * 60 * 60 * 1000; // 7 days this.logPrefix = '[CloudApiKeyManager]'; this.offlineMode = options.offlineMode || false; this.developmentMode = options.developmentMode || process.env.NODE_ENV === 'development' || process.env.OPTIMIZER_DEV_MODE === 'true'; this.maxRetries = options.maxRetries || 5; // Increased for production this.baseRetryDelay = options.baseRetryDelay || 1000; this.maxRetryDelay = options.maxRetryDelay || 30000; this.requestTimeout = options.requestTimeout || 15000; // Network health tracking this.networkHealth = { consecutiveFailures: 0, lastSuccessful: null, avgResponseTime: null, lastErrorType: null }; } log(message, level = 'info') { const timestamp = new Date().toISOString(); const prefix = `${timestamp} ${this.logPrefix}`; if (level === 'error') { console.error(`${prefix} ❌ ${message}`); } else if (level === 'warn') { console.warn(`${prefix} ⚠️ ${message}`); } else if (level === 'success') { console.log(`${prefix} ✅ ${message}`); } else { console.log(`${prefix} ℹ️ ${message}`); } } // Production-grade exponential backoff with jitter calculateRetryDelay(attempt) { const exponentialDelay = Math.min( this.baseRetryDelay * Math.pow(2, attempt - 1), this.maxRetryDelay ); // Add jitter to prevent thundering herd const jitter = Math.random() * 0.3 * exponentialDelay; return Math.floor(exponentialDelay + jitter); } // Enhanced API key format validation validateApiKeyFormat(apiKey) { if (!apiKey || typeof apiKey !== 'string') { return { valid: false, error: 'API key must be a string' }; } // Support development keys const validPrefixes = ['sk-opt-', 'sk-team-', 'sk-local-', 'sk-dev-']; const hasValidPrefix = validPrefixes.some(prefix => apiKey.startsWith(prefix)); if (!hasValidPrefix) { return { valid: false, error: 'Invalid API key format. Must start with "sk-opt-" (individual), "sk-team-" (team), "sk-local-" (development), or "sk-dev-" (testing)' }; } // Check minimum length for security if (apiKey.length < 20) { return { valid: false, error: 'API key too short' }; } // Determine type let keyType = 'unknown'; if (apiKey.startsWith('sk-opt-')) { keyType = 'individual'; } else if (apiKey.startsWith('sk-team-')) { keyType = 'team'; } else if (apiKey.startsWith('sk-local-')) { keyType = 'development'; } else if (apiKey.startsWith('sk-dev-')) { keyType = 'testing'; } return { valid: true, keyType: keyType }; } // Development mode mock responses generateMockValidation(keyType) { const mockResponses = { individual: { valid: true, tier: 'explorer', api_key_type: 'individual', quota: { limit: 5000, used: Math.floor(Math.random() * 1000), unlimited: false }, features: { ai_context_detection: true, template_management: true, optimization_insights: true } }, team: { valid: true, tier: 'creator', api_key_type: 'team', quota: { limit: 18000, used: Math.floor(Math.random() * 3000), unlimited: false }, features: { ai_context_detection: true, template_management: true, team_collaboration: true, optimization_insights: true } }, development: { valid: true, tier: 'development', api_key_type: 'development', quota: { unlimited: true }, features: { ai_context_detection: true, template_management: true, optimization_insights: true, development_mode: true } }, testing: { valid: true, tier: 'testing', api_key_type: 'testing', quota: { limit: 1000, used: Math.floor(Math.random() * 100), unlimited: false }, features: { ai_context_detection: true, template_management: true, optimization_insights: true, testing_mode: true } } }; const response = mockResponses[keyType] || mockResponses.development; response.mock_mode = true; response.backend_url = 'mock://development-mode'; return response; } // Enhanced API key validation with production resilience async validateApiKey() { this.log('Starting comprehensive API key validation...'); if (!this.apiKey) { throw new Error('API key is required. Set OPTIMIZER_API_KEY environment variable or provide key directly.'); } // Step 1: Format validation const formatCheck = this.validateApiKeyFormat(this.apiKey); if (!formatCheck.valid) { throw new Error(formatCheck.error); } this.log(`API key format valid: ${formatCheck.keyType}`); // Step 2: Development mode handling if (this.developmentMode || formatCheck.keyType === 'development' || formatCheck.keyType === 'testing') { this.log('Development/testing mode detected, using mock validation', 'warn'); const mockValidation = this.generateMockValidation(formatCheck.keyType); await this.cacheValidation(mockValidation); return mockValidation; } try { // Step 3: Backend validation with enhanced retry logic const validation = await this.validateWithBackendRetry(); // Step 4: Validate response structure if (validation && validation.valid) { await this.cacheValidation(validation); await this.updateNetworkHealth(true); this.log(`API key validated successfully: ${validation.tier}`, 'success'); return validation; } else { throw new Error(validation?.detail || validation?.error || 'API key validation failed'); } } catch (error) { this.log(`Backend validation failed: ${error.message}`, 'warn'); await this.updateNetworkHealth(false, error.message); // Enhanced fallback strategy const cachedValidation = await this.getCachedValidation(); if (cachedValidation && !this.isCacheExpired(cachedValidation)) { this.log('Using cached API key validation', 'warn'); return cachedValidation.data; } // Extended fallback for network issues if (cachedValidation && !this.isFallbackCacheExpired(cachedValidation)) { this.log('Using extended fallback cache due to network issues', 'warn'); const fallbackData = cachedValidation.data; fallbackData.fallback_mode = true; fallbackData.network_issue = error.message; return fallbackData; } // If we're in explicit offline mode and have any cache, use it if (this.offlineMode && cachedValidation) { this.log('Offline mode: using cached validation despite expiry', 'warn'); const offlineData = cachedValidation.data; offlineData.offline_mode = true; return offlineData; } throw new Error(`API key validation failed: ${error.message}`); } } // Production-grade retry logic with exponential backoff async validateWithBackendRetry() { let lastError; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { this.log(`Validation attempt ${attempt}/${this.maxRetries}...`); const startTime = Date.now(); const result = await this.validateWithBackend(); // Track response time for health monitoring const responseTime = Date.now() - startTime; if (this.networkHealth.avgResponseTime === null) { this.networkHealth.avgResponseTime = responseTime; } else { this.networkHealth.avgResponseTime = (this.networkHealth.avgResponseTime + responseTime) / 2; } return result; } catch (error) { lastError = error; this.log(`Attempt ${attempt} failed: ${error.message}`, 'warn'); if (attempt < this.maxRetries) { const delay = this.calculateRetryDelay(attempt); this.log(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } else { this.log('All retry attempts exhausted', 'error'); } } } throw lastError; } // Enhanced backend validation with better error handling - FIXED URL PATH async validateWithBackend() { return new Promise((resolve, reject) => { const url = `${this.backendUrl}/api/v1/mcp/mcp/validate-key`; const options = { method: 'GET', headers: { 'x-api-key': this.apiKey, 'Content-Type': 'application/json', 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`, 'Accept': 'application/json', 'Connection': 'close' // Ensure connection cleanup }, timeout: this.requestTimeout }; this.log(`Making request to: ${url}`); this.log(`Using API key: ${this.apiKey.substring(0, 16)}...`); const client = this.backendUrl.startsWith('https://') ? https : http; const req = client.request(url, options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { this.log(`Response status: ${res.statusCode}`); try { if (res.statusCode === 200) { const validation = JSON.parse(data); this.log(`Validation successful: ${JSON.stringify(validation, null, 2)}`); resolve(validation); } else if (res.statusCode === 401) { reject(new Error('Invalid API key or unauthorized access')); } else if (res.statusCode === 403) { reject(new Error('API key expired or quota exceeded')); } else if (res.statusCode === 429) { reject(new Error('Rate limit exceeded. Please try again later.')); } else if (res.statusCode === 500) { reject(new Error('Backend server error. Please try again later.')); } else if (res.statusCode === 503) { reject(new Error('Backend service temporarily unavailable. Please try again later.')); } else { let errorMessage; try { const error = JSON.parse(data); errorMessage = error.detail || error.message || `HTTP ${res.statusCode}`; } catch { errorMessage = `HTTP ${res.statusCode}: ${data}`; } reject(new Error(errorMessage)); } } catch (parseError) { this.log(`Parse error: ${parseError.message}`, 'error'); this.log(`Raw response: ${data}`, 'error'); reject(new Error(`Invalid response format: ${parseError.message}`)); } }); }); req.on('error', (error) => { this.log(`Network error: ${error.message}`, 'error'); // Enhanced error classification if (error.code === 'ENOTFOUND') { reject(new Error(`DNS resolution failed: Cannot resolve ${this.backendUrl.replace(/^https?:\/\//, '')}`)); } else if (error.code === 'ECONNREFUSED') { reject(new Error(`Connection refused: Backend server may be down`)); } else if (error.code === 'ETIMEDOUT') { reject(new Error(`Connection timeout: Backend server is not responding`)); } else if (error.code === 'ECONNRESET') { reject(new Error(`Connection reset: Network instability detected`)); } else { reject(new Error(`Network error: ${error.message}`)); } }); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout - backend may be unavailable')); }); req.setTimeout(this.requestTimeout); req.end(); }); } // Enhanced quota status retrieval - FIXED URL PATH async getQuotaStatus() { try { const url = `${this.backendUrl}/api/v1/mcp/mcp/quota-status`; const options = { method: 'GET', headers: { 'x-api-key': this.apiKey, 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`, 'Connection': 'close' }, timeout: this.requestTimeout }; return new Promise((resolve, reject) => { const client = this.backendUrl.startsWith('https://') ? https : http; const req = client.request(url, options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { if (res.statusCode === 200) { const result = JSON.parse(data); resolve(result); } else { let errorMessage; try { const error = JSON.parse(data); errorMessage = error.detail || `HTTP ${res.statusCode}`; } catch { errorMessage = `HTTP ${res.statusCode}: ${data}`; } reject(new Error(errorMessage)); } } catch (parseError) { reject(new Error(`Invalid response: ${parseError.message}`)); } }); }); req.on('error', (error) => { reject(new Error(`Network error: ${error.message}`)); }); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); req.setTimeout(this.requestTimeout); req.end(); }); } catch (error) { this.log(`Quota status check failed: ${error.message}`, 'warn'); throw error; } } // Network health tracking async updateNetworkHealth(success, errorMessage = null) { try { if (success) { this.networkHealth.consecutiveFailures = 0; this.networkHealth.lastSuccessful = Date.now(); this.networkHealth.lastErrorType = null; } else { this.networkHealth.consecutiveFailures++; this.networkHealth.lastErrorType = errorMessage; } // Save health metrics await fs.writeFile(this.healthFile, JSON.stringify(this.networkHealth, null, 2)); } catch (error) { this.log(`Failed to update network health: ${error.message}`, 'warn'); } } // Enhanced quota status checking async checkQuotaStatus(validation) { const quota = validation.quota || {}; if (quota.unlimited) { return { allowed: true, unlimited: true }; } const quotaUsed = quota.used || 0; const quotaLimit = quota.limit || 5000; const quotaRemaining = quota.remaining || (quotaLimit - quotaUsed); if (quotaUsed >= quotaLimit) { const tier = validation.tier || 'explorer'; const upgradeMessage = tier === 'explorer' ? 'Upgrade to Creator ($25.99/mo) for 18,000 optimizations: https://promptoptimizer-blog.vercel.app/pricing' : 'Quota will reset on your next billing cycle.'; throw new Error( `Monthly quota exceeded (${quotaUsed}/${quotaLimit}). ${upgradeMessage}` ); } return { allowed: true, unlimited: false, used: quotaUsed, limit: quotaLimit, remaining: quotaRemaining, usage_percentage: (quotaUsed / quotaLimit) * 100 }; } // Enhanced caching with metadata async cacheValidation(validation) { try { const cacheData = { timestamp: Date.now(), apiKeyPrefix: this.apiKey.substring(0, 20) + '...', // Safe prefix only data: validation, backendUrl: this.backendUrl, packageVersion: packageJson.version, networkHealth: { ...this.networkHealth } }; await fs.writeFile(this.cacheFile, JSON.stringify(cacheData, null, 2)); this.log('API key validation cached successfully'); } catch (error) { this.log(`Failed to cache validation: ${error.message}`, 'warn'); } } async getCachedValidation() { try { const cacheContent = await fs.readFile(this.cacheFile, 'utf8'); const cached = JSON.parse(cacheContent); // Validate cache structure if (!cached.timestamp || !cached.data) { this.log('Invalid cache structure, ignoring', 'warn'); return null; } return cached; } catch (error) { if (error.code !== 'ENOENT') { this.log(`Cache read error: ${error.message}`, 'warn'); } return null; } } isCacheExpired(cachedData) { if (!cachedData || !cachedData.timestamp) { return true; } const age = Date.now() - cachedData.timestamp; const expired = age > this.cacheExpiry; if (expired) { this.log(`Cache expired: ${Math.round(age / 1000 / 60)} minutes old`); } return expired; } // Extended fallback cache for network issues isFallbackCacheExpired(cachedData) { if (!cachedData || !cachedData.timestamp) { return true; } const age = Date.now() - cachedData.timestamp; const expired = age > this.fallbackCacheExpiry; if (expired) { this.log(`Fallback cache expired: ${Math.round(age / 1000 / 60 / 60)} hours old`); } return expired; } async clearCache() { try { await fs.unlink(this.cacheFile); this.log('API key cache cleared successfully'); } catch (error) { if (error.code !== 'ENOENT') { this.log(`Cache clear error: ${error.message}`, 'warn'); } } try { await fs.unlink(this.healthFile); this.log('Network health cache cleared successfully'); } catch (error) { if (error.code !== 'ENOENT') { this.log(`Health cache clear error: ${error.message}`, 'warn'); } } } // Enhanced validation and preparation async validateAndPrepare() { this.log('Starting comprehensive API key validation and preparation...'); try { // Step 1: Validate API key const validation = await this.validateApiKey(); // Step 2: Check quota (skip for development/mock modes) let quotaStatus; if (validation.mock_mode || validation.fallback_mode || validation.offline_mode) { quotaStatus = validation.quota || { allowed: true, unlimited: true }; } else { quotaStatus = await this.checkQuotaStatus(validation); } // Step 3: Log success const mode = validation.mock_mode ? '(mock)' : validation.fallback_mode ? '(fallback)' : validation.offline_mode ? '(offline)' : ''; if (quotaStatus.unlimited) { this.log(`API key valid: ${validation.tier} ${mode} (unlimited usage)`, 'success'); } else { this.log(`API key valid: ${validation.tier} ${mode} (${quotaStatus.remaining}/${quotaStatus.limit} remaining this month)`, 'success'); } return { validation, quotaStatus, tier: validation.tier, features: validation.features || {}, mode: { development: this.developmentMode, mock: validation.mock_mode || false, fallback: validation.fallback_mode || false, offline: validation.offline_mode || false } }; } catch (error) { this.log(`API key validation failed: ${error.message}`, 'error'); throw error; } } // Enhanced API key info with mode detection async getApiKeyInfo() { try { const validation = await this.validateApiKey(); const quotaStatus = await this.checkQuotaStatus(validation); return { tier: validation.tier, features: validation.features || {}, quota: quotaStatus, isValid: true, keyType: validation.api_key_type || this.validateApiKeyFormat(this.apiKey).keyType, mode: { mock: validation.mock_mode || false, fallback: validation.fallback_mode || false, offline: validation.offline_mode || false, development: this.developmentMode } }; } catch (error) { return { tier: null, features: {}, quota: { allowed: false }, isValid: false, error: error.message, keyType: 'unknown', mode: { mock: false, fallback: false, offline: false, development: this.developmentMode } }; } } // Static method to get API key from environment static getApiKey() { const envKey = process.env.OPTIMIZER_API_KEY; if (envKey) { return envKey; } throw new Error( 'API key required. Set the OPTIMIZER_API_KEY environment variable.\n' + 'Get your API key at: https://promptoptimizer-blog.vercel.app/pricing' ); } // Static method to create manager with environment key static fromEnvironment(options = {}) { const apiKey = CloudApiKeyManager.getApiKey(); return new CloudApiKeyManager(apiKey, options); } // Format key for display (hide sensitive parts) formatKeyForDisplay() { if (!this.apiKey) return 'No key'; return `${this.apiKey.substring(0, 8)}...${this.apiKey.slice(-4)}`; } } module.exports = CloudApiKeyManager;