Registers AppGate as a checkConfigOnly integration-health row and public sync route, matching the existing factory + is<Name>Configured() pattern. Committed now so Phase 13's worktree-isolated executors fork from a HEAD that includes this integration-health.ts entry, since Plan 13-02 inserts the PAX8 row immediately after it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
181 lines
6.3 KiB
TypeScript
181 lines
6.3 KiB
TypeScript
/**
|
|
* 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<T>(
|
|
path: string,
|
|
method: 'GET' | 'POST',
|
|
body?: unknown,
|
|
auth?: { token?: string },
|
|
): Promise<T> {
|
|
const url = new URL(`${this.config.baseUrl}${path}`);
|
|
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
|
return new Promise<T>((resolve, reject) => {
|
|
const headers: Record<string, string> = {
|
|
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<string> {
|
|
if (this.token && this.token.expiresAt - Date.now() > REFRESH_BUFFER_MS) {
|
|
return this.token.token;
|
|
}
|
|
const resp = await this.rawRequest<AppgateLoginResponse>('/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<T>(path: string): Promise<T> {
|
|
const token = await this.getToken();
|
|
return this.rawRequest<T>(`/admin${path}`, 'GET', undefined, { token });
|
|
}
|
|
|
|
// ─── Endpoints ─────────────────────────────────────────────────────────
|
|
|
|
async getActiveSessions(): Promise<AppgateActiveSession[]> {
|
|
const r = await this.get<AppgateResultList<AppgateActiveSession>>('/stats/active-sessions/dn');
|
|
return r.data ?? [];
|
|
}
|
|
|
|
async getUserLoginsLast24h(): Promise<AppgateHourlyLogins> {
|
|
const r = await this.get<{ data: AppgateHourlyLogins }>('/stats/user-logins');
|
|
return r.data ?? {};
|
|
}
|
|
|
|
async getOnBoardedDevices(): Promise<AppgateOnBoardedDevice[]> {
|
|
const r = await this.get<AppgateResultList<AppgateOnBoardedDevice>>('/on-boarded-devices');
|
|
return r.data ?? [];
|
|
}
|
|
|
|
async getAppliances(): Promise<AppgateAppliance[]> {
|
|
const r = await this.get<AppgateResultList<AppgateAppliance>>('/appliances');
|
|
return r.data ?? [];
|
|
}
|
|
|
|
async getLicense(): Promise<AppgateLicense> {
|
|
return this.get<AppgateLicense>('/license');
|
|
}
|
|
|
|
async getLicenseUsers(): Promise<AppgateUserLicense[]> {
|
|
const r = await this.get<AppgateResultList<AppgateUserLicense>>('/license/users');
|
|
return r.data ?? [];
|
|
}
|
|
|
|
// Cheap connectivity probe — does not require auth.
|
|
async ping(): Promise<{ ok: true }> {
|
|
await this.rawRequest<unknown>('/admin/identity-providers/names', 'GET');
|
|
return { ok: true };
|
|
}
|
|
}
|