UNPKG

magically-sdk

Version:

Official SDK for Magically - Build mobile apps with AI

198 lines (197 loc) 8.02 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.APIClient = void 0; const Logger_1 = require("./Logger"); class APIClient { constructor(config, loggerPrefix = 'MagicallyAPI') { this.apiKey = null; // Infinite loop detection this.callHistory = new Map(); this.LOOP_THRESHOLD = 10; // Max calls per method in time window this.TIME_WINDOW = 5000; // 5 seconds this.lastCleanup = 0; // Track last garbage collection this.CLEANUP_INTERVAL = 30000; // Clean up every 30 seconds this.config = config; this.logger = new Logger_1.Logger(config.debug || false, loggerPrefix); this.baseUrl = config.apiUrl || 'https://trymagically.com'; // Use API key from config if provided (for edge functions) if (config.apiKey) { this.apiKey = config.apiKey; this.logger.info('API key provided in config - using API key authentication'); } else if (typeof globalThis !== 'undefined' && 'MAGICALLY_API_KEY' in globalThis) { // Fallback to global environment this.apiKey = globalThis.MAGICALLY_API_KEY; this.logger.info('API key detected in environment - using API key authentication'); } } /** * Make an authenticated API request with automatic logging */ async request(endpoint, options, token) { // Check for infinite loops before making request this.checkForInfiniteLoop(options.operation || endpoint); const startTime = Date.now(); let requestId = ''; const url = `${this.baseUrl}${endpoint}`; const headers = { 'Content-Type': 'application/json', ...options.headers, }; // Use API key if available (edge environment), otherwise use provided token if (this.apiKey) { headers['Authorization'] = `Bearer ${this.apiKey}`; this.logger.debug('Using API key authentication for request'); } else if (token) { headers['Authorization'] = `Bearer ${token}`; } const requestConfig = { method: options.method, headers, body: options.body ? JSON.stringify(options.body) : undefined, }; // Log the request requestId = this.logger.networkRequest(options.method, url, { headers, body: options.body, operation: options.operation }); try { const response = await fetch(url, requestConfig); const responseData = await response.json(); const duration = Date.now() - startTime; if (!response.ok) { // Log error response this.logger.networkError(requestId, responseData, { duration, operation: options.operation }); // Throw structured error this.handleAPIError(responseData, `${options.operation || 'Request'} failed`); } // Log successful response this.logger.networkResponse(requestId, { status: response.status, statusText: response.statusText, duration, data: this.sanitizeResponseData(responseData, options.operation), operation: options.operation }); return responseData; } catch (error) { // Log network errors if (requestId) { this.logger.networkError(requestId, error, { duration: Date.now() - startTime, operation: options.operation }); } throw error; } } /** * Check if running in edge environment (has API key) */ isEdgeEnvironment() { return this.apiKey !== null; } /** * Sanitize response data for logging (avoid logging large arrays) */ sanitizeResponseData(data, operation) { if (operation === 'query' && data.data && Array.isArray(data.data)) { return { ...data, dataCount: data.data.length, data: '[Array of items]' }; } return data; } /** * Check for infinite loops by tracking method call frequency */ checkForInfiniteLoop(methodName) { const now = Date.now(); const cleanMethodName = methodName.replace(/[^a-zA-Z0-9:_-]/g, '_'); // Get or create call history for this method if (!this.callHistory.has(cleanMethodName)) { this.callHistory.set(cleanMethodName, []); } const calls = this.callHistory.get(cleanMethodName); // Auto garbage collection - clean up stale method entries periodically if (now - this.lastCleanup > this.CLEANUP_INTERVAL) { this.garbageCollectCallHistory(now); this.lastCleanup = now; } // Remove calls outside the time window const cutoffTime = now - this.TIME_WINDOW; const recentCalls = calls.filter(timestamp => timestamp > cutoffTime); // Add current call recentCalls.push(now); // If no recent calls, remove the method entry entirely if (recentCalls.length === 1) { this.callHistory.set(cleanMethodName, recentCalls); } else { this.callHistory.set(cleanMethodName, recentCalls); } // Check if we've exceeded the threshold if (recentCalls.length > this.LOOP_THRESHOLD) { // Use logger for structured warning this.logger.error('🚨 INFINITE LOOP DETECTED', { method: methodName, callCount: recentCalls.length, timeWindow: `${this.TIME_WINDOW / 1000}s`, threshold: this.LOOP_THRESHOLD, message: `Method '${methodName}' called ${recentCalls.length} times in ${this.TIME_WINDOW / 1000}s` }); // Clear the history to prevent further spam this.callHistory.set(cleanMethodName, []); // Throw error to stop the loop throw new Error(`Infinite loop detected in method: ${methodName}. Too many rapid calls (${recentCalls.length} in ${this.TIME_WINDOW / 1000}s)`); } } /** * Garbage collect stale call history entries to prevent memory leaks */ garbageCollectCallHistory(now) { const cutoffTime = now - this.TIME_WINDOW; const methodsToDelete = []; // Check each method's call history for (const [methodName, timestamps] of this.callHistory.entries()) { // Filter out old calls const recentCalls = timestamps.filter(timestamp => timestamp > cutoffTime); if (recentCalls.length === 0) { // No recent calls - mark for deletion methodsToDelete.push(methodName); } else { // Update with only recent calls this.callHistory.set(methodName, recentCalls); } } // Remove stale method entries methodsToDelete.forEach(methodName => { this.callHistory.delete(methodName); }); if (methodsToDelete.length > 0) { this.logger.debug('Garbage collected call history', { removedMethods: methodsToDelete.length, totalMethods: this.callHistory.size }); } } /** * Handle API errors with structured error information */ handleAPIError(errorData, fallbackMessage) { // Create an error with the response data attached for the Logger const error = new Error(errorData.error || errorData.message || fallbackMessage); error.responseData = errorData; // Preserve the original error response for Logger throw error; } } exports.APIClient = APIClient;