ailock
Version:
AI-Proof File Guard - Protect sensitive files from accidental AI modifications
227 lines • 8.46 kB
JavaScript
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