import * as plugins from './plugins.js'; import { logger } from './unifi.logger.js'; import { UnifiHttp } from './classes.unifihttp.js'; import { CameraManager } from './classes.cameramanager.js'; import type { IUnifiProtectOptions, IProtectBootstrap, IProtectCamera, IProtectNvr, THttpMethod, } from './interfaces/index.js'; /** * UniFi Protect - Entry point for Protect NVR API * * This class provides access to the UniFi Protect API for managing cameras, * recordings, and motion events. Supports two authentication methods: * 1. API Key (preferred) - Set X-API-Key header, no login required * 2. Session auth - Username/password login with session cookies + CSRF * * @example * ```typescript * // Using API key (no login required) * const protect = new UnifiProtect({ * host: '192.168.1.1', * apiKey: 'your-api-key', * }); * await protect.refreshBootstrap(); // Load camera data * const cameras = await protect.cameraManager.listCameras(); * * // Using session auth * const protect = new UnifiProtect({ * host: '192.168.1.1', * username: 'admin', * password: 'password', * }); * await protect.login(); * const cameras = await protect.cameraManager.listCameras(); * await protect.logout(); * ``` */ export class UnifiProtect { /** Protect host */ private host: string; /** API key for authentication */ private apiKey?: string; /** Username for session authentication */ private username?: string; /** Password for session authentication */ private password?: string; /** Whether to verify SSL certificates */ private verifySsl: boolean; /** HTTP client for Protect API */ private http: UnifiHttp; /** HTTP client for auth (console level) */ private authHttp: UnifiHttp; /** Whether currently authenticated */ private authenticated: boolean = false; /** CSRF token */ private csrfToken?: string; /** Bootstrap data (contains all cameras, NVR info, etc.) */ private bootstrap?: IProtectBootstrap; /** Camera manager instance */ public cameraManager: CameraManager; constructor(options: IUnifiProtectOptions) { this.host = options.host.replace(/\/$/, ''); this.apiKey = options.apiKey; this.username = options.username; this.password = options.password; this.verifySsl = options.verifySsl ?? false; // Build base URLs const baseHost = this.host.startsWith('http') ? this.host : `https://${this.host}`; // Auth happens at console level this.authHttp = new UnifiHttp(baseHost, this.verifySsl); // Protect API is behind /proxy/protect/api this.http = new UnifiHttp(`${baseHost}/proxy/protect/api`, this.verifySsl); // If API key provided, set it and mark as authenticated if (this.apiKey) { this.http.setHeader('X-API-Key', this.apiKey); this.authenticated = true; } // Initialize managers this.cameraManager = new CameraManager(this); logger.log('info', `UnifiProtect initialized for ${this.host}`); } /** * Login to Protect (only needed for session auth, not API key) */ public async login(): Promise { // If using API key, already authenticated if (this.apiKey) { logger.log('info', 'Using API key authentication, no login required'); this.authenticated = true; await this.fetchBootstrap(); return; } if (!this.username || !this.password) { throw new Error('Username and password required for session authentication'); } logger.log('info', `Logging in to UniFi Protect at ${this.host}`); // Login at console level const response = await this.authHttp.rawRequest('POST', '/api/auth/login', { username: this.username, password: this.password, }); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); throw new Error(`Protect login failed: HTTP ${response.status} - ${errorText}`); } // Store cookies const setCookieHeaders = response.headers?.['set-cookie']; if (setCookieHeaders && Array.isArray(setCookieHeaders)) { this.authHttp.setCookies(setCookieHeaders); this.http.setCookies(setCookieHeaders); } // Extract CSRF token const csrfHeader = response.headers?.['x-csrf-token']; if (csrfHeader) { this.csrfToken = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader; this.http.setHeader('X-CSRF-Token', this.csrfToken); } this.authenticated = true; // Fetch bootstrap to get cameras and NVR info await this.fetchBootstrap(); logger.log('info', 'Successfully logged in to UniFi Protect'); } /** * Logout from Protect (only for session auth) */ public async logout(): Promise { // If using API key, nothing to logout if (this.apiKey) { return; } if (!this.authenticated) { return; } logger.log('info', 'Logging out from UniFi Protect'); try { await this.authHttp.rawRequest('POST', '/api/auth/logout'); } catch (error) { logger.log('warn', `Protect logout error (may be expected): ${error}`); } this.authenticated = false; this.csrfToken = undefined; this.bootstrap = undefined; } /** * Check if logged in */ public isAuthenticated(): boolean { return this.authenticated; } /** * Fetch bootstrap data (cameras, NVR info, etc.) */ private async fetchBootstrap(): Promise { logger.log('debug', 'Fetching Protect bootstrap'); this.bootstrap = await this.http.request('GET', '/bootstrap'); logger.log('info', `Bootstrap loaded: ${this.bootstrap.cameras?.length || 0} cameras`); } /** * Refresh bootstrap data */ public async refreshBootstrap(): Promise { await this.fetchBootstrap(); } /** * Get NVR info from bootstrap */ public getNvrInfo(): IProtectNvr | undefined { return this.bootstrap?.nvr; } /** * Get cameras from bootstrap (cached) */ public getCamerasFromBootstrap(): IProtectCamera[] { return this.bootstrap?.cameras || []; } /** * Make a request to the Protect API */ public async request( method: THttpMethod, endpoint: string, data?: unknown ): Promise { if (!this.authenticated) { throw new Error('Not authenticated. Call login() first or provide API key.'); } return this.http.request(method, endpoint, data); } /** * Get all cameras (convenience method) */ public async getCameras() { return this.cameraManager.listCameras(); } /** * Get storage info from NVR */ public getStorageInfo(): { totalSize?: number; usedSpace?: number; freeSpace?: number } | undefined { const nvr = this.getNvrInfo(); if (!nvr?.storageInfo) return undefined; const totalSize = nvr.storageInfo.totalSize; const usedSpace = nvr.storageInfo.totalSpaceUsed; const freeSpace = totalSize && usedSpace ? totalSize - usedSpace : undefined; return { totalSize, usedSpace, freeSpace, }; } /** * Get NVR uptime */ public getNvrUptime(): number | undefined { return this.getNvrInfo()?.uptime; } /** * Check if NVR is connected to cloud */ public isCloudConnected(): boolean { return this.getNvrInfo()?.isConnectedToCloud === true; } /** * Get system events */ public async getSystemEvents(limit: number = 100): Promise { return this.request('GET', `/events?limit=${limit}`); } /** * Get liveviews */ public async getLiveviews(): Promise { return this.request('GET', '/liveviews'); } /** * Get users */ public async getUsers(): Promise { return this.request('GET', '/users'); } /** * Get groups */ public async getGroups(): Promise { return this.request('GET', '/groups'); } /** * Get lights (if any) */ public getLights() { return this.bootstrap?.lights || []; } /** * Get sensors (if any) */ public getSensors() { return this.bootstrap?.sensors || []; } /** * Get doorbells */ public getDoorbells() { return this.bootstrap?.doorbells || []; } /** * Get chimes */ public getChimes() { return this.bootstrap?.chimes || []; } /** * Get bridges */ public getBridges() { return this.bootstrap?.bridges || []; } /** * Get viewers */ public getViewers() { return this.bootstrap?.viewers || []; } }