import { AddigyConfig, AddigyDevice, AddigyPolicy, AddigyOrganization, AddigyDeviceWithApps, AddigyApplication, AddigyAlert, AddigyMaintenanceItem, AddigyMonitoringItem, AddigyCustomFact, AddigySoftwareItem, AddigyVariable, AddigyApiResponse, AddigyApiError, QueryParams, } from '@/lib/types/addigy'; export class AddigyClient { private config: AddigyConfig; private rateLimiter: RateLimiter; constructor(config: AddigyConfig) { this.config = config; // Addigy rate limit: 1000 requests per 10 seconds = 100 requests per second this.rateLimiter = new RateLimiter(100); } private getAuthHeaders(): Record { return { 'x-api-key': this.config.apiToken, 'Content-Type': 'application/json', 'Accept': 'application/json', }; } private async makeApiCall( endpoint: string, options: RequestInit = {} ): Promise { await this.rateLimiter.throttle(); const url = `${this.config.apiUrl}${endpoint}`; try { const response = await fetch(url, { ...options, headers: { ...this.getAuthHeaders(), ...options.headers, }, }); const responseText = await response.text(); if (!response.ok) { console.error(`Addigy API error: ${response.status} - ${responseText}`); let errorMessage = `API Error: ${response.statusText}`; try { const errorData: AddigyApiError = JSON.parse(responseText); errorMessage = errorData.message || errorData.error || errorMessage; } catch (parseError) { errorMessage = responseText || errorMessage; } throw new Error(errorMessage); } try { return JSON.parse(responseText); } catch (parseError) { console.error('Failed to parse API response:', parseError); throw new Error('Invalid JSON response from Addigy API'); } } catch (error) { console.error('API call failed:', error); throw error; } } private buildQueryString(params: QueryParams): string { const queryParts: string[] = []; if (params.page !== undefined) { queryParts.push(`page=${params.page}`); } if (params.limit !== undefined) { queryParts.push(`limit=${params.limit}`); } if (params.filters && params.filters.length > 0) { const filtersJson = JSON.stringify({ filters: params.filters }); queryParts.push(`filters=${encodeURIComponent(filtersJson)}`); } return queryParts.length > 0 ? `?${queryParts.join('&')}` : ''; } // Device Methods async getAllDevices(params: QueryParams = {}): Promise { // Get organization ID - either from config or fetch from policies let orgId = this.config.organizationId; if (!orgId) { // Fetch first policy to get orgid const policies = await this.getAllPolicies({ limit: 1 }); if (policies.length > 0) { orgId = policies[0].orgid; console.log('Using orgid from first policy:', orgId); } else { throw new Error('No organization ID configured and no policies found'); } } // Addigy v2 /o/{orgid}/devices requires POST with JSON body const body: any = { page: params.page || 1, per_page: params.limit || 500, }; // Add filters if provided if (params.filters && params.filters.length > 0) { body.query = { filters: params.filters, }; } console.log('Addigy devices request body:', JSON.stringify(body)); console.log('Fetching devices from:', `${this.config.apiUrl}/o/${orgId}/devices`); const response = await this.makeApiCall<{ items?: any[]; data?: any[]; records?: any[] }>( `/o/${orgId}/devices`, { method: 'POST', body: JSON.stringify(body), } ); console.log('Addigy response keys:', Object.keys(response)); console.log('Addigy raw items count:', response.items?.length || 0); // Transform Addigy's nested facts structure to flat device objects const rawItems = response.items || response.data || response.records || []; // Field name mapping from Addigy to UI-expected format const fieldMapping: Record = { 'device_name': 'Device Name', 'device_model_name': 'Device Model Name', 'serial_number': 'Serial Number', 'mac_os_x_version': 'MAC OS X Version', 'ios_version': 'iOS Version', 'current_user': 'Current User', 'free_disk_percentage': 'Free Disk Percentage', 'battery_percentage': 'Battery Percentage', 'firewall_enabled': 'Firewall Enabled', 'filevault_enabled': 'FileVault Enabled', 'agent_version': 'Agent Version', }; const devices: AddigyDevice[] = rawItems.map((item: any) => { const device: any = {}; // Extract values from nested facts structure if (item.facts) { for (const [key, factObj] of Object.entries(item.facts)) { const fact = factObj as any; if (fact && typeof fact === 'object' && 'value' in fact) { // Use mapped field name if available, otherwise use original const mappedKey = fieldMapping[key] || key; device[mappedKey] = fact.value; } } } return device as AddigyDevice; }); console.log('Transformed devices count:', devices.length); if (devices.length > 0) { console.log('First device keys:', Object.keys(devices[0]).slice(0, 10)); } return devices; } async getDeviceById(deviceId: string): Promise { try { const response = await this.makeApiCall>( `/devices/${deviceId}`, { method: 'GET' } ); return response.data || null; } catch (error) { console.error(`Failed to get device ${deviceId}:`, error); return null; } } async getDevicesByPolicy(policyId: string): Promise { return this.getAllDevices({ filters: [ { audit_field: 'policy_id', type: 'string', operation: 'equals', value: policyId, }, ], }); } async getOnlineDevices(): Promise { return this.getAllDevices({ filters: [ { audit_field: 'online', type: 'boolean', operation: 'equals', value: true, }, ], }); } async getDeviceApplications(deviceId: string): Promise { try { const response = await this.makeApiCall<{ items?: AddigyApplication[]; data?: AddigyApplication[] }>( `/devices/${deviceId}/applications`, { method: 'GET' } ); return response.items || response.data || []; } catch (error) { console.error(`Failed to get applications for device ${deviceId}:`, error); return []; } } // Policy Methods async getAllPolicies(params: QueryParams = {}): Promise { const body: any = { page: params.page || 1, per_page: params.limit || 100, }; console.log('Fetching policies from:', `${this.config.apiUrl}/oa/policies/query`); // Addigy policies endpoint returns array directly, not wrapped in items/data const response = await this.makeApiCall( `/oa/policies/query`, { method: 'POST', body: JSON.stringify(body), } ); console.log('Policies response type:', Array.isArray(response) ? 'array' : typeof response); console.log('Policies count:', Array.isArray(response) ? response.length : 0); // Response is already an array return Array.isArray(response) ? response : []; } async getPolicyById(policyId: string): Promise { try { const response = await this.makeApiCall>( `/policies/${policyId}`, { method: 'GET' } ); return response.data || null; } catch (error) { console.error(`Failed to get policy ${policyId}:`, error); return null; } } async createPolicy(policy: Partial): Promise { const response = await this.makeApiCall>( '/policies', { method: 'POST', body: JSON.stringify(policy), } ); if (!response.data) { throw new Error('Failed to create policy'); } return response.data; } async updatePolicy( policyId: string, updates: Partial ): Promise { const response = await this.makeApiCall>( `/policies/${policyId}`, { method: 'PATCH', body: JSON.stringify(updates), } ); if (!response.data) { throw new Error('Failed to update policy'); } return response.data; } // Organization Methods async getOrganizations(params: QueryParams = {}): Promise { const queryString = this.buildQueryString(params); const response = await this.makeApiCall<{ items?: AddigyOrganization[]; data?: AddigyOrganization[] }>( `/organizations${queryString}`, { method: 'GET' } ); return response.items || response.data || []; } async getOrganizationById(orgId: string): Promise { try { const response = await this.makeApiCall>( `/organizations/${orgId}`, { method: 'GET' } ); return response.data || null; } catch (error) { console.error(`Failed to get organization ${orgId}:`, error); return null; } } // Alert Methods async getAlerts(params: QueryParams = {}): Promise { const queryString = this.buildQueryString(params); const response = await this.makeApiCall<{ items?: AddigyAlert[]; data?: AddigyAlert[] }>( `/alerts${queryString}`, { method: 'GET' } ); return response.items || response.data || []; } async getActiveAlerts(): Promise { return this.getAlerts({ filters: [ { audit_field: 'resolved', type: 'boolean', operation: 'equals', value: false, }, ], }); } async resolveAlert(alertId: string): Promise { await this.makeApiCall(`/alerts/${alertId}/resolve`, { method: 'POST', }); } // Maintenance Methods async getMaintenanceItems(params: QueryParams = {}): Promise { const queryString = this.buildQueryString(params); const response = await this.makeApiCall<{ items?: AddigyMaintenanceItem[]; data?: AddigyMaintenanceItem[] }>( `/maintenance${queryString}`, { method: 'GET' } ); return response.items || response.data || []; } // Monitoring Methods async getMonitoringItems(params: QueryParams = {}): Promise { const queryString = this.buildQueryString(params); const response = await this.makeApiCall<{ items?: AddigyMonitoringItem[]; data?: AddigyMonitoringItem[] }>( `/monitoring${queryString}`, { method: 'GET' } ); return response.items || response.data || []; } // Custom Facts / Variables async getCustomFacts(deviceId: string): Promise { try { const response = await this.makeApiCall<{ items?: AddigyCustomFact[]; data?: AddigyCustomFact[] }>( `/devices/${deviceId}/facts`, { method: 'GET' } ); return response.items || response.data || []; } catch (error) { console.error(`Failed to get custom facts for device ${deviceId}:`, error); return []; } } async createCustomFact( deviceId: string, fact: Partial ): Promise { const response = await this.makeApiCall>( `/devices/${deviceId}/facts`, { method: 'POST', body: JSON.stringify(fact), } ); if (!response.data) { throw new Error('Failed to create custom fact'); } return response.data; } async getVariables(params: QueryParams = {}): Promise { const queryString = this.buildQueryString(params); const response = await this.makeApiCall<{ items?: AddigyVariable[]; data?: AddigyVariable[] }>( `/variables${queryString}`, { method: 'GET' } ); return response.items || response.data || []; } // Software Methods async getSoftwareItems(params: QueryParams = {}): Promise { const queryString = this.buildQueryString(params); const response = await this.makeApiCall<{ items?: AddigySoftwareItem[]; data?: AddigySoftwareItem[] }>( `/software${queryString}`, { method: 'GET' } ); return response.items || response.data || []; } async getSoftwareVersions(softwareIdentifier: string): Promise { try { const response = await this.makeApiCall>( `/software/${softwareIdentifier}/versions`, { method: 'GET' } ); return response.data?.versions || []; } catch (error) { console.error(`Failed to get versions for software ${softwareIdentifier}:`, error); return []; } } // Device Actions async assignDeviceToPolicy(deviceId: string, policyId: string): Promise { await this.makeApiCall(`/devices/${deviceId}/policy`, { method: 'PATCH', body: JSON.stringify({ policy_id: policyId }), }); } async removeDevice(deviceId: string): Promise { await this.makeApiCall(`/devices/${deviceId}`, { method: 'DELETE', }); } // Utility Methods async testConnection(): Promise { try { await this.getAllPolicies({ limit: 1 }); return true; } catch (error) { console.error('Addigy connection test failed:', error); return false; } } // Paginated query to handle large datasets async getAllDevicesPaginated(pageSize: number = 100): Promise { const allDevices: AddigyDevice[] = []; let page = 1; let hasMore = true; while (hasMore) { console.log(`Fetching devices page ${page} (max ${pageSize} records)...`); const body = { page, per_page: pageSize, }; const response = await this.makeApiCall<{ items?: AddigyDevice[]; data?: AddigyDevice[]; pages?: number }>( `/devices`, { method: 'POST', body: JSON.stringify(body), } ); const devices = response.items || response.data || []; allDevices.push(...devices); console.log(`Fetched ${devices.length} devices, total so far: ${allDevices.length}`); // Check if we have more pages hasMore = devices.length === pageSize && (!response.pages || page < response.pages); page++; // Safety limit to prevent infinite loops if (page > 100) { console.warn('Reached page limit (100) for devices'); break; } } console.log(`Total devices fetched: ${allDevices.length}`); return allDevices; } } // Rate Limiter class class RateLimiter { private maxRequestsPerSecond: number; private requestTimes: number[]; constructor(maxRequestsPerSecond = 100) { this.maxRequestsPerSecond = maxRequestsPerSecond; this.requestTimes = []; } async throttle(): Promise { const now = Date.now(); const oneSecondAgo = now - 1000; // Remove old request times this.requestTimes = this.requestTimes.filter((t) => t > oneSecondAgo); // If at limit, wait if (this.requestTimes.length >= this.maxRequestsPerSecond) { const oldestRequest = this.requestTimes[0]; const waitTime = 1000 - (now - oldestRequest); if (waitTime > 0) { await new Promise((resolve) => setTimeout(resolve, waitTime)); } } this.requestTimes.push(Date.now()); } }