const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); const path = require('path'); // Ruta del archivo JSON para persistencia const DB_FILE_PATH = path.join(__dirname, '../../data/api_keys.json'); // Sistema de base de datos en memoria con persistencia JSON class APIKeyDatabase { constructor() { this.apiKeys = new Map(); this.usage = new Map(); // Almacena estadísticas de uso por API key this.loadFromFile(); } // Cargar datos desde archivo JSON loadFromFile() { try { // Crear directorio data si no existe const dataDir = path.dirname(DB_FILE_PATH); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } // Cargar archivo si existe if (fs.existsSync(DB_FILE_PATH)) { const data = JSON.parse(fs.readFileSync(DB_FILE_PATH, 'utf8')); // Cargar API keys if (data.apiKeys) { // Asegurar que todas las keys tengan las propiedades de rate limit const completeApiKeys = data.apiKeys.map(([id, keyData]) => { return [id, { ...keyData, // Agregar propiedades de rate limit si no existen exemptFromRateLimit: keyData.exemptFromRateLimit !== undefined ? keyData.exemptFromRateLimit : false, rateLimitHistory: keyData.rateLimitHistory || [], rateLimitUntil: keyData.rateLimitUntil || null, rateLimitLevel: keyData.rateLimitLevel !== undefined ? keyData.rateLimitLevel : 0 }]; }); this.apiKeys = new Map(completeApiKeys); } // Cargar estadísticas de uso if (data.usage) { this.usage = new Map(); for (const [key, usage] of data.usage) { this.usage.set(key, { ...usage, dailyUsage: new Map(usage.dailyUsage || []) }); } } console.log(`[DB] Cargadas ${this.apiKeys.size} API keys desde ${DB_FILE_PATH}`); } else { console.log('[DB] No se encontró archivo de base de datos, iniciando con datos vacíos'); } } catch (error) { console.error('[DB] Error al cargar base de datos:', error.message); console.log('[DB] Iniciando con datos vacíos'); } } // Guardar datos en archivo JSON saveToFile() { try { const data = { apiKeys: Array.from(this.apiKeys.entries()), usage: Array.from(this.usage.entries()).map(([key, usage]) => [ key, { ...usage, dailyUsage: Array.from(usage.dailyUsage.entries()) } ]), savedAt: new Date().toISOString() }; fs.writeFileSync(DB_FILE_PATH, JSON.stringify(data, null, 2)); console.log(`[DB] Base de datos guardada en ${DB_FILE_PATH}`); } catch (error) { console.error('[DB] Error al guardar base de datos:', error.message); } } // Exportar datos a JSON (para descarga) exportToJSON() { const data = { apiKeys: Array.from(this.apiKeys.entries()), usage: Array.from(this.usage.entries()).map(([key, usage]) => [ key, { ...usage, dailyUsage: Array.from(usage.dailyUsage.entries()) } ]), exportedAt: new Date().toISOString(), version: "1.0" }; return JSON.stringify(data, null, 2); } // Importar datos desde JSON importFromJSON(jsonData) { try { const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData; let importedKeys = 0; // Importar API keys if (data.apiKeys && Array.isArray(data.apiKeys)) { for (const [id, keyData] of data.apiKeys) { // Verificar que no exista ya una key con el mismo valor const existingKey = this.getAPIKeyByValue(keyData.apiKey); if (!existingKey) { // Asegurar que las propiedades de rate limit existan const completeKeyData = { ...keyData, // Siempre habilitar rate limit para keys importadas exemptFromRateLimit: false, rateLimitHistory: keyData.rateLimitHistory || [], rateLimitUntil: keyData.rateLimitUntil || null, rateLimitLevel: keyData.rateLimitLevel !== undefined ? keyData.rateLimitLevel : 0 }; this.apiKeys.set(id, completeKeyData); importedKeys++; } } } // Importar estadísticas de uso if (data.usage && Array.isArray(data.usage)) { for (const [key, usage] of data.usage) { if (this.getAPIKeyByValue(key)) { this.usage.set(key, { ...usage, dailyUsage: new Map(usage.dailyUsage || []) }); } } } // Guardar cambios this.saveToFile(); return { success: true, importedKeys, message: `Se importaron ${importedKeys} API keys exitosamente` }; } catch (error) { return { success: false, error: error.message }; } } // Crear una nueva API key createAPIKey(name, description = '') { const id = uuidv4(); const apiKey = `sk-${uuidv4().replace(/-/g, '')}`; const keyData = { id, name, description, apiKey, enabled: true, exemptFromRateLimit: false, rateLimitHistory: [], rateLimitUntil: null, rateLimitLevel: 0, createdAt: new Date().toISOString(), lastUsed: null, totalRequests: 0 }; this.apiKeys.set(id, keyData); this.usage.set(apiKey, { requests: 0, lastRequest: null, dailyUsage: new Map() // Fecha -> número de requests }); // Guardar automáticamente this.saveToFile(); return keyData; } // Obtener todas las API keys getAllAPIKeys() { return Array.from(this.apiKeys.values()); } // Obtener API key por ID getAPIKeyById(id) { return this.apiKeys.get(id); } // Obtener API key por su valor getAPIKeyByValue(apiKey) { for (let [id, data] of this.apiKeys.entries()) { if (data.apiKey === apiKey && data.enabled) { return data; } } return null; } // Actualizar API key updateAPIKey(id, updates) { const existing = this.apiKeys.get(id); if (!existing) return null; const updated = { ...existing, ...updates }; this.apiKeys.set(id, updated); // Guardar automáticamente this.saveToFile(); return updated; } // Eliminar API key deleteAPIKey(id) { const existing = this.apiKeys.get(id); if (!existing) return false; this.apiKeys.delete(id); this.usage.delete(existing.apiKey); // Guardar automáticamente this.saveToFile(); return true; } // Registrar uso de una API key recordUsage(apiKey) { const keyData = this.getAPIKeyByValue(apiKey); if (!keyData) return false; const now = new Date(); const today = now.toISOString().split('T')[0]; // Actualizar estadísticas de la key keyData.lastUsed = now.toISOString(); keyData.totalRequests++; // Asegurar que todas las propiedades de rate limit existan if (keyData.exemptFromRateLimit === undefined) { keyData.exemptFromRateLimit = false; } // Actualizar historial de rate limit if (!keyData.rateLimitHistory) { keyData.rateLimitHistory = []; } if (keyData.rateLimitUntil === undefined) { keyData.rateLimitUntil = null; } if (keyData.rateLimitLevel === undefined) { keyData.rateLimitLevel = 0; } // Añadir la marca de tiempo actual al historial keyData.rateLimitHistory.push({ timestamp: now.toISOString() }); // Mantener solo las últimas 10 solicitudes en el historial if (keyData.rateLimitHistory.length > 10) { keyData.rateLimitHistory = keyData.rateLimitHistory.slice(-10); } this.apiKeys.set(keyData.id, keyData); // Actualizar estadísticas de uso const usage = this.usage.get(apiKey); if (usage) { usage.requests++; usage.lastRequest = now.toISOString(); // Actualizar uso diario const currentDaily = usage.dailyUsage.get(today) || 0; usage.dailyUsage.set(today, currentDaily + 1); // Limpiar datos antiguos (mantener solo últimos 30 días) const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); const thirtyDaysAgoStr = thirtyDaysAgo.toISOString().split('T')[0]; for (let [date] of usage.dailyUsage) { if (date < thirtyDaysAgoStr) { usage.dailyUsage.delete(date); } } } // Guardar cambios cada 10 requests para no saturar el disco if (keyData.totalRequests % 10 === 0) { this.saveToFile(); } return true; } // Obtener estadísticas de uso de una API key getUsageStats(apiKey) { const usage = this.usage.get(apiKey); const keyData = this.getAPIKeyByValue(apiKey); if (!usage || !keyData) return null; return { keyName: keyData.name, totalRequests: keyData.totalRequests, lastUsed: keyData.lastUsed, dailyUsage: Object.fromEntries(usage.dailyUsage) }; } // Obtener estadísticas generales getGeneralStats() { const totalKeys = this.apiKeys.size; const enabledKeys = Array.from(this.apiKeys.values()).filter(k => k.enabled).length; const totalRequests = Array.from(this.apiKeys.values()).reduce((sum, k) => sum + k.totalRequests, 0); const rateLimitedKeys = Array.from(this.apiKeys.values()).filter(k => k.rateLimitUntil && new Date(k.rateLimitUntil) > new Date()).length; const today = new Date().toISOString().split('T')[0]; let todayRequests = 0; for (let usage of this.usage.values()) { todayRequests += usage.dailyUsage.get(today) || 0; } return { totalKeys, enabledKeys, totalRequests, todayRequests, rateLimitedKeys }; } // Comprobar si una API key está actualmente limitada por rate limit isRateLimited(apiKey) { const keyData = this.getAPIKeyByValue(apiKey); if (!keyData) return { limited: true, reason: 'API key inválida' }; // Comprobar configuración global de rate limit const config = require('./config'); if (!config.rateLimitEnabled) { return { limited: false }; } // Si la key está exenta de rate limits, siempre devolver false if (keyData.exemptFromRateLimit) { return { limited: false }; } // Comprobar si hay un rate limit activo if (keyData.rateLimitUntil) { const now = new Date(); const limitUntil = new Date(keyData.rateLimitUntil); if (now < limitUntil) { // Calcular tiempo restante en minutos const remainingMs = limitUntil - now; const remainingMinutes = Math.ceil(remainingMs / (1000 * 60)); return { limited: true, until: keyData.rateLimitUntil, remainingMinutes, level: keyData.rateLimitLevel }; } } return { limited: false }; } // Comprobar y aplicar rate limit si es necesario checkAndApplyRateLimit(apiKey) { const keyData = this.getAPIKeyByValue(apiKey); if (!keyData) return { limited: true, reason: 'API key inválida' }; // Comprobar configuración global de rate limit const config = require('./config'); if (!config.rateLimitEnabled) { return { limited: false }; } // Si la key está exenta de rate limits, siempre permitir if (keyData.exemptFromRateLimit) { return { limited: false }; } const now = new Date(); // Comprobar si hay un rate limit activo if (keyData.rateLimitUntil) { const limitUntil = new Date(keyData.rateLimitUntil); if (now < limitUntil) { // Si intenta usar la API durante un rate limit, aumentar el nivel const newLevel = Math.min(keyData.rateLimitLevel + 1, 3); // Máximo nivel 3 let newDuration; switch (newLevel) { case 1: newDuration = 1 * 60 * 1000; break; // 1 minuto case 2: newDuration = 5 * 60 * 1000; break; // 5 minutos case 3: newDuration = 20 * 60 * 1000; break; // 20 minutos default: newDuration = 60 * 60 * 1000; // 1 hora } const newLimitUntil = new Date(now.getTime() + newDuration); // Actualizar el rate limit keyData.rateLimitLevel = newLevel; keyData.rateLimitUntil = newLimitUntil.toISOString(); this.apiKeys.set(keyData.id, keyData); this.saveToFile(); // Calcular tiempo restante en minutos const remainingMinutes = Math.ceil(newDuration / (1000 * 60)); return { limited: true, until: keyData.rateLimitUntil, remainingMinutes, level: newLevel, escalated: true }; } else { // Si el rate limit ha expirado, reiniciarlo keyData.rateLimitUntil = null; keyData.rateLimitLevel = 0; } } // Comprobar si ha hecho 3 solicitudes en menos de 2 minutos if (keyData.rateLimitHistory && keyData.rateLimitHistory.length >= 3) { const twoMinutesAgo = new Date(now.getTime() - 2 * 60 * 1000); const recentRequests = keyData.rateLimitHistory .filter(req => new Date(req.timestamp) > twoMinutesAgo) .length; if (recentRequests >= 3) { // Aplicar rate limit de 1 minuto const limitUntil = new Date(now.getTime() + 1 * 60 * 1000); keyData.rateLimitUntil = limitUntil.toISOString(); keyData.rateLimitLevel = 1; this.apiKeys.set(keyData.id, keyData); this.saveToFile(); return { limited: true, until: keyData.rateLimitUntil, remainingMinutes: 1, level: 1, escalated: false }; } } return { limited: false }; } // Resetear el rate limit de una API key resetRateLimit(id) { const keyData = this.apiKeys.get(id); if (!keyData) return false; keyData.rateLimitUntil = null; keyData.rateLimitLevel = 0; this.apiKeys.set(id, keyData); this.saveToFile(); return true; } // Obtener todas las API keys actualmente limitadas por rate limit getRateLimitedKeys() { const now = new Date(); const limitedKeys = []; for (let [id, keyData] of this.apiKeys.entries()) { if (keyData.rateLimitUntil && new Date(keyData.rateLimitUntil) > now) { const remainingMs = new Date(keyData.rateLimitUntil) - now; const remainingMinutes = Math.ceil(remainingMs / (1000 * 60)); limitedKeys.push({ ...keyData, remainingMinutes }); } } return limitedKeys; } // Limpiar todas las API keys (para reset) clearAll() { this.apiKeys.clear(); this.usage.clear(); this.saveToFile(); } } // Instancia singleton const db = new APIKeyDatabase(); module.exports = db;