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
JavaScript
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;