ailock
Version:
AI-Proof File Guard - Protect sensitive files from accidental AI modifications
297 lines • 11.7 kB
JavaScript
import { loadUserConfig, saveUserConfig, getOfflineMode, shouldCollectAnalytics } from '../core/user-config.js';
import { getMachineUuid, sanitizeForLogging } from '../core/machine-id.js';
/**
* Rate limiter to prevent API abuse
*/
class RateLimiter {
requests = [];
maxRequests;
windowMs;
constructor(maxRequests = 10, windowMs = 60000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
}
canMakeRequest() {
const now = Date.now();
// Remove old requests outside the window
this.requests = this.requests.filter(time => now - time < this.windowMs);
if (this.requests.length >= this.maxRequests) {
return false;
}
this.requests.push(now);
return true;
}
getTimeUntilNextRequest() {
if (this.requests.length === 0)
return 0;
const oldestRequest = this.requests[0];
const now = Date.now();
const timeElapsed = now - oldestRequest;
if (timeElapsed >= this.windowMs) {
return 0;
}
return this.windowMs - timeElapsed;
}
}
/**
* CLI API service for integration with ailock-web growth system
*/
export class CliApiService {
baseUrl;
timeout;
maxRetries;
anonKey;
rateLimiter;
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;
// Initialize rate limiter (10 requests per minute by default)
const maxRequests = options?.maxRequestsPerMinute ||
parseInt(process.env.AILOCK_MAX_REQUESTS_PER_MINUTE || '10', 10);
this.rateLimiter = new RateLimiter(maxRequests, 60000);
// Supabase anon key for Edge Functions
// The anon key is public and safe to expose as it's limited by RLS policies
// Can be overridden via environment variable for different environments
const defaultAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Indvb2RjY2preWFjd2NlaXRramJ5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ1ODU0NTQsImV4cCI6MjA3MDE2MTQ1NH0.34H_wio0aV0tdjBNTq9XUoxC2Qobmg-af2TW2n470O4';
// Use environment variable if available, otherwise use default
// This allows for easy configuration in different environments while maintaining
// a working default for end users who don't need to configure anything
this.anonKey = process.env.AILOCK_ANON_KEY || defaultAnonKey;
// Log configuration source in debug mode
if (process.env.AILOCK_DEBUG === 'true') {
console.log(`Debug: Using ${process.env.AILOCK_ANON_KEY ? 'environment' : 'default'} API key configuration`);
console.log(`Debug: Rate limit set to ${maxRequests} requests per minute`);
}
}
/**
* 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) {
// Check rate limit before making request
if (!this.rateLimiter.canMakeRequest()) {
const waitTime = this.rateLimiter.getTimeUntilNextRequest();
const waitSeconds = Math.ceil(waitTime / 1000);
throw new Error(`Rate limit exceeded. Please wait ${waitSeconds} seconds before making another request.`);
}
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',
'Authorization': `Bearer ${this.anonKey}`,
...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 auth code is provided, use it for full status
if (authCode) {
const response = await this.makeRequest(`cli-status?code=${encodeURIComponent(authCode)}`, {
method: 'GET'
});
return response;
}
// Otherwise, just get basic status with machine UUID
const machineUuid = await getMachineUuid();
const response = await this.makeRequest('cli-status', {
method: 'POST',
body: {
machine_uuid: machineUuid
}
});
return response;
}
catch (error) {
return {
success: false,
error: `Failed to get user status: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Alias for getUserStatus - for backwards compatibility with tests
*/
async checkStatus(authCode) {
return this.getUserStatus(authCode);
}
/**
* 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