/** * Core API client for Omada Controller REST API */ import https from 'https'; import { Authentication, AuthConfig } from '../auth/Authentication.js'; import { OmadaApiError, OmadaNetworkError, OmadaDeviceNotFoundError, OmadaConfigurationError, } from '../errors/OmadaErrors.js'; import { ApiResponse, ApiRequestOptions, PaginatedResponse } from '../types/api.js'; export interface OmadaClientConfig extends AuthConfig { siteId?: string; } /** * Main client class for interacting with Omada Controller API */ export class OmadaClient { private auth: Authentication; private config: OmadaClientConfig; private httpsAgent: https.Agent; private siteId: string | null = null; constructor(config: OmadaClientConfig) { this.config = config; this.auth = new Authentication(config); this.httpsAgent = new https.Agent({ rejectUnauthorized: config.verifySSL ?? true, }); this.siteId = config.siteId || null; } /** * Get the current site ID (auto-detect if not set) */ async getSiteId(): Promise { if (this.siteId) { return this.siteId; } // Auto-detect site ID by getting the default site const sites = await this.request>('GET', '/sites'); if (!sites || sites.length === 0) { throw new OmadaApiError('No sites found in Omada Controller'); } // Use the first site as default this.siteId = sites[0].id; return this.siteId; } /** * Set the site ID explicitly */ setSiteId(siteId: string): void { this.siteId = siteId; } /** * Make an authenticated API request */ async request( method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', endpoint: string, options: Omit = {} ): Promise { const token = await this.auth.getAccessToken(); const url = this.buildUrl(endpoint, options.params); const headers: Record = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, ...options.headers, }; try { const response = await fetch(url, { method, headers, body: options.body ? JSON.stringify(options.body) : undefined, // Note: Native fetch in Node.js 18+ doesn't support agent directly // For SSL certificate handling, ensure verifySSL config is set correctly // @ts-expect-error - agent may not be supported by all fetch implementations agent: this.httpsAgent, }); const text = await response.text(); let data: ApiResponse; try { data = JSON.parse(text); } catch (parseError) { throw new OmadaApiError( `Invalid JSON response: ${text.substring(0, 200)}`, response.status ); } if (!response.ok) { throw new OmadaApiError( data.msg || `HTTP ${response.status}: ${response.statusText}`, response.status, data ); } // Check Omada API error code (0 = success) if (data.errorCode !== 0) { const errorMsg = data.msg || `API error code: ${data.errorCode}`; // Handle specific error cases if (data.errorCode === 10001) { // Token expired or invalid this.auth.clearToken(); throw new OmadaApiError('Authentication token expired', 401, data); } if (data.errorCode === 10002 || data.errorCode === 10003) { throw new OmadaDeviceNotFoundError(endpoint); } if (data.errorCode >= 10000 && data.errorCode < 20000) { throw new OmadaConfigurationError(errorMsg, data); } throw new OmadaApiError(errorMsg, response.status, data); } return data.result as T; } catch (error) { if (error instanceof OmadaApiError || error instanceof OmadaDeviceNotFoundError || error instanceof OmadaConfigurationError) { throw error; } throw new OmadaNetworkError( `Request failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined ); } } /** * Make a paginated API request */ async requestPaginated( method: 'GET' | 'POST', endpoint: string, options: Omit = {} ): Promise> { const result = await this.request>(method, endpoint, options); return result; } /** * Build full URL with query parameters */ private buildUrl(endpoint: string, params?: Record): string { const baseUrl = this.config.baseUrl.replace(/\/$/, ''); let url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`; if (params && Object.keys(params).length > 0) { const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { searchParams.append(key, String(value)); } url += `?${searchParams.toString()}`; } return url; } /** * Get authentication instance (for advanced use cases) */ getAuth(): Authentication { return this.auth; } }