UNPKG

ailock

Version:

AI-Proof File Guard - Protect sensitive files from accidental AI modifications

227 lines 8.46 kB
import fetch from 'node-fetch'; import { loadUserConfig, saveUserConfig, getOfflineMode, shouldCollectAnalytics } from '../core/user-config.js'; import { getMachineUuid, sanitizeForLogging } from '../core/machine-id.js'; /** * CLI API service for integration with ailock-web growth system */ export class CliApiService { baseUrl; timeout; maxRetries; constructor(options) { // Default to ailock-web production URL, can be overridden via environment this.baseUrl = options?.baseUrl || process.env.AILOCK_API_URL || 'https://woodccjkyacwceitkjby.supabase.co/functions/v1'; this.timeout = options?.timeout || 10000; // 10 seconds this.maxRetries = options?.maxRetries || 3; } /** * Check if we're in offline mode or should skip API calls */ async shouldSkipApiCall() { const isOffline = await getOfflineMode(); const debugMode = process.env.AILOCK_DEBUG === 'true'; if (isOffline && debugMode) { console.log('Debug: Skipping API call (offline mode)'); } return isOffline; } /** * Make HTTP request with timeout and retry logic */ async makeRequest(endpoint, options) { const url = `${this.baseUrl}/${endpoint}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); let lastError = null; for (let attempt = 0; attempt < this.maxRetries; attempt++) { try { if (process.env.AILOCK_DEBUG === 'true') { console.log(`Debug: API request to ${sanitizeForLogging(url)} (attempt ${attempt + 1}/${this.maxRetries})`); } const response = await fetch(url, { method: options.method, headers: { 'Content-Type': 'application/json', ...options.headers }, body: options.body ? JSON.stringify(options.body) : undefined, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (process.env.AILOCK_DEBUG === 'true') { console.log(`Debug: API request failed (attempt ${attempt + 1}): ${sanitizeForLogging(lastError.message)}`); } // Don't retry on timeout or abort if (lastError.name === 'AbortError') { break; } // Wait before retry (exponential backoff) if (attempt < this.maxRetries - 1) { const delay = Math.min(1000 * Math.pow(2, attempt), 5000); await new Promise(resolve => setTimeout(resolve, delay)); } } } clearTimeout(timeoutId); throw lastError || new Error('Request failed'); } /** * Redeem an auth code to increase directory quota */ async redeemAuthCode(code) { if (await this.shouldSkipApiCall()) { return { success: false, error: 'Offline mode - cannot redeem auth code' }; } try { const machineUuid = await getMachineUuid(); const response = await this.makeRequest('cli-auth-redeem', { method: 'POST', body: { code: code.trim(), machine_uuid: machineUuid } }); // Update local config if redemption was successful if (response.success && response.directory_quota) { const config = await loadUserConfig(); config.directoryQuota = response.directory_quota; config.lastSyncAt = new Date(); await saveUserConfig(config); } return response; } catch (error) { return { success: false, error: `Failed to redeem auth code: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Track usage events for analytics */ async trackUsage(eventType, data) { if (await this.shouldSkipApiCall()) { // Don't log in offline mode unless debug is enabled if (process.env.AILOCK_DEBUG === 'true') { console.log(`Debug: Would track event ${sanitizeForLogging(eventType)} (offline mode)`); } return { success: true }; // Silently succeed in offline mode } // Check privacy settings before tracking if (!(await shouldCollectAnalytics())) { if (process.env.AILOCK_DEBUG === 'true') { console.log(`Debug: Analytics disabled by privacy settings, skipping event ${sanitizeForLogging(eventType)}`); } return { success: true }; // Silently succeed when analytics disabled } try { const machineUuid = await getMachineUuid(); const response = await this.makeRequest('cli-usage-track', { method: 'POST', body: { machine_uuid: machineUuid, event_type: eventType, directory_path: data?.directoryPath, // Web side will handle hashing for privacy total_locked_count: data?.totalLockedCount, metadata: data?.metadata } }); return response; } catch (error) { // Don't fail the main operation if analytics fail if (process.env.AILOCK_DEBUG === 'true') { console.log(`Debug: Failed to track usage event: ${sanitizeForLogging(error instanceof Error ? error.message : String(error))}`); } return { success: false, error: 'Analytics tracking failed' }; } } /** * Get user status including quota and available codes */ async getUserStatus(authCode) { if (await this.shouldSkipApiCall()) { // Return local config data in offline mode const config = await loadUserConfig(); return { success: true, directory_quota: config.directoryQuota, available_codes: 0, // Can't check available codes offline is_activated: !!config.authToken }; } try { if (!authCode) { return { success: false, error: 'Auth code required to check status' }; } const response = await this.makeRequest(`cli-status?code=${encodeURIComponent(authCode)}`, { method: 'GET' }); return response; } catch (error) { return { success: false, error: `Failed to get user status: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Test API connectivity */ async testConnectivity() { try { const response = await fetch(`${this.baseUrl}/cli-usage-track`, { method: 'OPTIONS' }); return response.ok; } catch { return false; } } /** * Get API configuration info for debugging */ getApiInfo() { return { baseUrl: this.baseUrl, timeout: this.timeout, maxRetries: this.maxRetries }; } } /** * Singleton instance for use throughout the application */ let apiServiceInstance = null; export function getApiService() { if (!apiServiceInstance) { apiServiceInstance = new CliApiService(); } return apiServiceInstance; } /** * Create a custom API service instance (useful for testing) */ export function createApiService(options) { return new CliApiService(options); } //# sourceMappingURL=CliApiService.js.map