/** * AppGate SDP Controller REST API client. * * Implements the subset of v22.5 endpoints Pulse needs: * - POST /admin/login (token acquisition) * - GET /admin/stats/active-sessions/dn (current sessions) * - GET /admin/stats/user-logins (24h login histogram) * - GET /admin/on-boarded-devices (device inventory) * - GET /admin/appliances (controllers / gateways) * - GET /admin/license (capacity + expiry) * - GET /admin/license/users (per-user license usage) * * Token caching: AppGate `LoginResponse` returns an explicit `expires` * timestamp; we re-authenticate ~60s before it lapses. * * TLS: AppGate controllers ship with a self-signed cert from the Controller * CA. Pulse goes through Node's `https` module (not global `fetch`) so it can * scope `rejectUnauthorized:false` to this client only — other outbound * HTTPS calls keep their strict verification. */ import * as https from 'node:https'; import { URL } from 'node:url'; import type { AppgateActiveSession, AppgateAppliance, AppgateHourlyLogins, AppgateLicense, AppgateLoginResponse, AppgateOnBoardedDevice, AppgateResultList, AppgateUserLicense, } from '@/lib/types/appgate'; export interface AppgateClientConfig { baseUrl: string; // e.g. https://wawnvaagp01.wulfconsulting.com:8443 username: string; password: string; providerName: string; // e.g. "local" deviceId: string; // any stable UUID for this Pulse instance insecureTls?: boolean; // default: true (self-signed) } interface CachedToken { token: string; expiresAt: number; // epoch ms } const ACCEPT_HEADER = 'application/vnd.appgate.peer-v22+json'; const REFRESH_BUFFER_MS = 60_000; const REQUEST_TIMEOUT_MS = 20_000; export class AppgateClient { private readonly config: AppgateClientConfig; private readonly agent: https.Agent; private token: CachedToken | null = null; constructor(config: AppgateClientConfig) { this.config = config; this.agent = new https.Agent({ rejectUnauthorized: config.insecureTls === false, keepAlive: true, }); } private rawRequest( path: string, method: 'GET' | 'POST', body?: unknown, auth?: { token?: string }, ): Promise { const url = new URL(`${this.config.baseUrl}${path}`); const payload = body !== undefined ? JSON.stringify(body) : undefined; return new Promise((resolve, reject) => { const headers: Record = { Accept: ACCEPT_HEADER, 'Content-Type': 'application/json', }; if (auth?.token) headers.Authorization = `Bearer ${auth.token}`; if (payload) headers['Content-Length'] = Buffer.byteLength(payload).toString(); const req = https.request( { hostname: url.hostname, port: url.port || 443, path: url.pathname + url.search, method, headers, agent: this.agent, timeout: REQUEST_TIMEOUT_MS, }, (res) => { const chunks: Buffer[] = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); const status = res.statusCode ?? 0; if (status < 200 || status >= 300) { reject(new Error(`AppGate ${method} ${path} → ${status}: ${text.slice(0, 300)}`)); return; } if (status === 204 || text.length === 0) { resolve(undefined as unknown as T); return; } try { resolve(JSON.parse(text) as T); } catch (e) { reject(new Error(`AppGate ${path}: invalid JSON response — ${(e as Error).message}`)); } }); }, ); req.on('error', (e) => reject(new Error(`AppGate ${path}: ${e.message}`))); req.on('timeout', () => { req.destroy(new Error(`AppGate ${path}: timeout after ${REQUEST_TIMEOUT_MS}ms`)); }); if (payload) req.write(payload); req.end(); }); } private async getToken(): Promise { if (this.token && this.token.expiresAt - Date.now() > REFRESH_BUFFER_MS) { return this.token.token; } const resp = await this.rawRequest('/admin/login', 'POST', { providerName: this.config.providerName, username: this.config.username, password: this.config.password, deviceId: this.config.deviceId, }); if (!resp.token || !resp.expires) { throw new Error('AppGate login response missing token/expires'); } this.token = { token: resp.token, expiresAt: new Date(resp.expires).getTime() }; return this.token.token; } private async get(path: string): Promise { const token = await this.getToken(); return this.rawRequest(`/admin${path}`, 'GET', undefined, { token }); } // ─── Endpoints ───────────────────────────────────────────────────────── async getActiveSessions(): Promise { const r = await this.get>('/stats/active-sessions/dn'); return r.data ?? []; } async getUserLoginsLast24h(): Promise { const r = await this.get<{ data: AppgateHourlyLogins }>('/stats/user-logins'); return r.data ?? {}; } async getOnBoardedDevices(): Promise { const r = await this.get>('/on-boarded-devices'); return r.data ?? []; } async getAppliances(): Promise { const r = await this.get>('/appliances'); return r.data ?? []; } async getLicense(): Promise { return this.get('/license'); } async getLicenseUsers(): Promise { const r = await this.get>('/license/users'); return r.data ?? []; } // Cheap connectivity probe — does not require auth. async ping(): Promise<{ ok: true }> { await this.rawRequest('/admin/identity-providers/names', 'GET'); return { ok: true }; } }