UNPKG

api-stats-logger

Version:

SDK completo de logging e monitoramento de APIs com nova estrutura de logs organizada, auto-instrumentação, dashboard em tempo real e CLI para configuração automática. Suporta logs estruturados por contexto (HTTP, business, security, system) com campos op

463 lines (402 loc) 14.3 kB
const os = require('os'); const path = require('path'); const fs = require('fs'); // Constantes para identificação no keychain const SERVICE_NAME = 'api-stats-logger'; const ACCOUNT_TOKEN = 'auth-token'; const ACCOUNT_DEVICE = 'device-id'; class SecureStorage { constructor() { this.keytar = null; this.configDir = path.join(os.homedir(), '.config', 'api-stats-logger'); this.configFile = path.join(this.configDir, 'credentials'); this.initKeytar(); } initKeytar() { try { this.keytar = require('keytar'); console.log('🔐 Keychain/Keyring do sistema disponível'); } catch (error) { console.log('⚠️ Keychain não disponível, usando armazenamento local seguro'); this.keytar = null; } } async setCredentials(token, deviceId) { try { // Tentar keychain primeiro if (this.keytar) { await this.keytar.setPassword(SERVICE_NAME, ACCOUNT_TOKEN, token); if (deviceId) { await this.keytar.setPassword(SERVICE_NAME, ACCOUNT_DEVICE, deviceId); } console.log('✅ Credenciais salvas no keychain do sistema'); return true; } // Fallback para arquivo de configuração return this.setCredentialsToFile(token, deviceId); } catch (error) { console.warn('⚠️ Erro ao salvar no keychain, usando arquivo local:', error.message); return this.setCredentialsToFile(token, deviceId); } } async getCredentials() { try { // Tentar keychain primeiro if (this.keytar) { const token = await this.keytar.getPassword(SERVICE_NAME, ACCOUNT_TOKEN); const deviceId = await this.keytar.getPassword(SERVICE_NAME, ACCOUNT_DEVICE); if (token) { return { token, deviceId }; } } // Fallback para arquivo de configuração return this.getCredentialsFromFile(); } catch (error) { console.warn('⚠️ Erro ao ler keychain, tentando arquivo local:', error.message); return this.getCredentialsFromFile(); } } async clearCredentials() { try { // Limpar do keychain if (this.keytar) { try { await this.keytar.deletePassword(SERVICE_NAME, ACCOUNT_TOKEN); await this.keytar.deletePassword(SERVICE_NAME, ACCOUNT_DEVICE); await this.keytar.deletePassword(SERVICE_NAME, `${SERVICE_NAME}-user-data`); await this.keytar.deletePassword(SERVICE_NAME, `${SERVICE_NAME}-projects-cache`); } catch (error) { // Ignorar erros de credenciais não encontradas } } // Limpar arquivos locais this.clearCredentialsFromFile(); this.clearUserDataFromFile(); // Limpar cache de projetos try { const projectsCacheFile = path.join(this.configDir, 'projects-cache'); if (fs.existsSync(projectsCacheFile)) { fs.unlinkSync(projectsCacheFile); } } catch (error) { // Ignorar erros } // Limpar variáveis de ambiente da sessão delete process.env.API_STATS_CLI_TOKEN; delete process.env.API_STATS_CLI_DEVICE_ID; console.log('🧹 Credenciais removidas de todos os locais'); return true; } catch (error) { console.warn('⚠️ Erro ao limpar credenciais:', error.message); return false; } } // Métodos de fallback para arquivo local setCredentialsToFile(token, deviceId) { try { // Criar diretório se não existir if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 }); } const data = { token, deviceId, timestamp: Date.now(), service: SERVICE_NAME }; // Escrever com permissões restritas (apenas usuário) fs.writeFileSync(this.configFile, JSON.stringify(data), { mode: 0o600 }); console.log('✅ Credenciais salvas no arquivo de configuração local'); return true; } catch (error) { console.error('❌ Erro ao salvar credenciais no arquivo:', error.message); return false; } } getCredentialsFromFile() { try { if (fs.existsSync(this.configFile)) { const data = JSON.parse(fs.readFileSync(this.configFile, 'utf8')); // Verificar se os dados são válidos e não muito antigos (7 dias) const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); if (data.token && data.timestamp > sevenDaysAgo) { return { token: data.token, deviceId: data.deviceId }; } else { console.log('⚠️ Credenciais expiradas, removendo arquivo'); this.clearCredentialsFromFile(); } } return null; } catch (error) { console.warn('⚠️ Erro ao ler arquivo de credenciais:', error.message); this.clearCredentialsFromFile(); return null; } } clearCredentialsFromFile() { try { if (fs.existsSync(this.configFile)) { fs.unlinkSync(this.configFile); } if (fs.existsSync(this.configDir) && fs.readdirSync(this.configDir).length === 0) { fs.rmdirSync(this.configDir); } } catch (error) { // Ignorar erros de limpeza } } // Método para migrar credenciais antigas async migrateOldCredentials() { try { // Verificar se já temos credenciais novas const existingCredentials = await this.getCredentials(); if (existingCredentials) { return; } const os = require('os'); const legacyFiles = [ path.join(os.homedir(), '.api-stats-auth'), path.join(process.cwd(), '.api-stats-auth'), path.join(os.homedir(), '.api-stats-token') ]; for (const legacyFile of legacyFiles) { if (fs.existsSync(legacyFile)) { try { const data = fs.readFileSync(legacyFile, 'utf8').trim(); let legacyData = null; try { legacyData = JSON.parse(data); } catch { // Formato antigo apenas com token legacyData = { token: data, deviceId: null }; } if (legacyData && legacyData.token) { const success = await this.setCredentials(legacyData.token, legacyData.deviceId); if (success) { console.log('✅ Credenciais migradas com sucesso'); // Remover arquivo antigo apenas se a migração foi bem-sucedida fs.unlinkSync(legacyFile); console.log(`🧹 Arquivo antigo removido: ${path.basename(legacyFile)}`); // Parar após primeira migração bem-sucedida break; } } } catch (error) { console.warn(`⚠️ Erro ao migrar ${legacyFile}:`, error.message); } } } } catch (error) { // Ignorar erros de migração } } // Método para verificar se as credenciais ainda são válidas async validateCredentials(baseUrl) { try { const credentials = await this.getCredentials(); if (!credentials || !credentials.token) { return false; } // Fazer uma requisição de teste para verificar se o token ainda é válido const axios = require('axios'); const response = await axios.get(`${baseUrl}/auth/profile`, { headers: { 'Authorization': `Bearer ${credentials.token}`, 'x-device-id': credentials.deviceId || '', 'User-Agent': 'API-Stats-CLI-Secure/2.3.7' }, timeout: 5000 }); return response.status === 200; } catch (error) { // Se a validação falhar, limpar credenciais inválidas if (error.response && [401, 403].includes(error.response.status)) { console.log('⚠️ Credenciais inválidas, removendo...'); await this.clearCredentials(); } return false; } } // Métodos para cache de dados do usuário async setUserData(userData) { try { const userDataKey = `${SERVICE_NAME}-user-data`; // Tentar keychain primeiro if (this.keytar) { await this.keytar.setPassword(SERVICE_NAME, userDataKey, JSON.stringify(userData)); return true; } // Fallback para arquivo return this.setUserDataToFile(userData); } catch (error) { console.warn('⚠️ Erro ao salvar dados do usuário:', error.message); return this.setUserDataToFile(userData); } } async getUserData() { try { const userDataKey = `${SERVICE_NAME}-user-data`; // Tentar keychain primeiro if (this.keytar) { const userData = await this.keytar.getPassword(SERVICE_NAME, userDataKey); if (userData) { return JSON.parse(userData); } } // Fallback para arquivo return this.getUserDataFromFile(); } catch (error) { console.warn('⚠️ Erro ao ler dados do usuário:', error.message); return this.getUserDataFromFile(); } } async clearUserData() { try { const userDataKey = `${SERVICE_NAME}-user-data`; // Limpar do keychain if (this.keytar) { try { await this.keytar.deletePassword(SERVICE_NAME, userDataKey); } catch (error) { // Ignorar erros de dados não encontrados } } // Limpar arquivo local this.clearUserDataFromFile(); return true; } catch (error) { console.warn('⚠️ Erro ao limpar dados do usuário:', error.message); return false; } } // Métodos de fallback para dados do usuário setUserDataToFile(userData) { try { const userDataFile = path.join(this.configDir, 'user-data'); // Criar diretório se não existir if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 }); } const data = { ...userData, timestamp: Date.now(), service: SERVICE_NAME }; // Escrever com permissões restritas fs.writeFileSync(userDataFile, JSON.stringify(data), { mode: 0o600 }); return true; } catch (error) { console.error('❌ Erro ao salvar dados do usuário no arquivo:', error.message); return false; } } getUserDataFromFile() { try { const userDataFile = path.join(this.configDir, 'user-data'); if (fs.existsSync(userDataFile)) { const data = JSON.parse(fs.readFileSync(userDataFile, 'utf8')); // Verificar se os dados não são muito antigos (30 dias) const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); if (data.timestamp && data.timestamp > thirtyDaysAgo) { return data; } else { console.log('⚠️ Dados do usuário expirados, removendo arquivo'); this.clearUserDataFromFile(); } } return null; } catch (error) { console.warn('⚠️ Erro ao ler arquivo de dados do usuário:', error.message); this.clearUserDataFromFile(); return null; } } clearUserDataFromFile() { try { const userDataFile = path.join(this.configDir, 'user-data'); if (fs.existsSync(userDataFile)) { fs.unlinkSync(userDataFile); } } catch (error) { // Ignorar erros de limpeza } } // Método para cache de projetos async setProjectsCache(projects) { try { const projectsKey = `${SERVICE_NAME}-projects-cache`; const cacheData = { projects, timestamp: Date.now() }; // Tentar keychain primeiro if (this.keytar) { await this.keytar.setPassword(SERVICE_NAME, projectsKey, JSON.stringify(cacheData)); return true; } // Fallback para arquivo return this.setProjectsCacheToFile(cacheData); } catch (error) { console.warn('⚠️ Erro ao salvar cache de projetos:', error.message); return this.setProjectsCacheToFile(cacheData); } } async getProjectsCache() { try { const projectsKey = `${SERVICE_NAME}-projects-cache`; // Tentar keychain primeiro if (this.keytar) { const cacheData = await this.keytar.getPassword(SERVICE_NAME, projectsKey); if (cacheData) { const data = JSON.parse(cacheData); // Verificar se o cache não é muito antigo (1 hora) const oneHourAgo = Date.now() - (60 * 60 * 1000); if (data.timestamp > oneHourAgo) { return data.projects; } } } // Fallback para arquivo return this.getProjectsCacheFromFile(); } catch (error) { console.warn('⚠️ Erro ao ler cache de projetos:', error.message); return this.getProjectsCacheFromFile(); } } setProjectsCacheToFile(cacheData) { try { const projectsCacheFile = path.join(this.configDir, 'projects-cache'); // Criar diretório se não existir if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 }); } // Escrever com permissões restritas fs.writeFileSync(projectsCacheFile, JSON.stringify(cacheData), { mode: 0o600 }); return true; } catch (error) { console.error('❌ Erro ao salvar cache de projetos no arquivo:', error.message); return false; } } getProjectsCacheFromFile() { try { const projectsCacheFile = path.join(this.configDir, 'projects-cache'); if (fs.existsSync(projectsCacheFile)) { const data = JSON.parse(fs.readFileSync(projectsCacheFile, 'utf8')); // Verificar se o cache não é muito antigo (1 hora) const oneHourAgo = Date.now() - (60 * 60 * 1000); if (data.timestamp && data.timestamp > oneHourAgo) { return data.projects; } else { // Cache expirado, remover fs.unlinkSync(projectsCacheFile); } } return null; } catch (error) { console.warn('⚠️ Erro ao ler cache de projetos:', error.message); return null; } } } module.exports = SecureStorage;