708 lines
21 KiB
TypeScript
708 lines
21 KiB
TypeScript
/**
|
|
* Adapter pattern for MeshCentral integration.
|
|
*
|
|
* Defines the boundary where a real MeshCentral client can be swapped in.
|
|
*/
|
|
|
|
import {
|
|
MeshCentralAdapter,
|
|
Device,
|
|
DeviceGroup,
|
|
DiagnosticResult,
|
|
ListDevicesRequest,
|
|
RunDiagnosticRequest,
|
|
BridgeError,
|
|
ErrorCodes,
|
|
defaultConfig,
|
|
BridgeConfig,
|
|
DeviceStatus,
|
|
validateCredentials,
|
|
} from './contracts';
|
|
import { createMockAdapter } from './mock-adapter';
|
|
import { UpstreamMeshTransport } from './upstream-transport';
|
|
import {
|
|
createAuditEntry,
|
|
AuditAction,
|
|
AuditStatus,
|
|
} from './audit';
|
|
import { Logger, getGlobalLogger } from './logger';
|
|
|
|
/**
|
|
* Real MeshCentral adapter using the official meshctrl.js protocol.
|
|
*
|
|
* Connects to MeshCentral's control.ashx WebSocket and uses the
|
|
* same protocol as meshctrl.js for inventory and diagnostics.
|
|
*/
|
|
export class MeshCentralRealAdapter implements MeshCentralAdapter {
|
|
readonly name = 'meshcentral-real';
|
|
readonly version = '0.1.0';
|
|
|
|
private config: BridgeConfig;
|
|
private client: UpstreamMeshTransport | null = null;
|
|
private credentials: { username: string; password: string; totpToken?: string } | null = null;
|
|
private connected = false;
|
|
private logger: Logger;
|
|
|
|
constructor(config?: Partial<BridgeConfig>) {
|
|
this.config = { ...defaultConfig, ...(config ?? {}) };
|
|
// Use a dedicated logger for this adapter instance
|
|
// In production, you could inject a custom logger via config
|
|
this.logger = getGlobalLogger();
|
|
// Set the logger name to be more descriptive
|
|
this.logger.setName('meshcentral-adapter');
|
|
|
|
// Validate configuration and credentials
|
|
this._validateConfig();
|
|
}
|
|
|
|
/**
|
|
* Set credentials for authentication.
|
|
* Must be called before connecting to the real adapter.
|
|
*/
|
|
setCredentials(credentials: { username: string; password: string; totpToken?: string }): void {
|
|
// Log audit entry for credential update attempt
|
|
this._auditLog(AuditAction.CREDENTIAL_UPDATE, AuditStatus.PENDING, {
|
|
actor: this._getActorName(),
|
|
resource: 'adapter-credentials',
|
|
});
|
|
|
|
// Validate credentials before storing
|
|
validateCredentials(credentials);
|
|
|
|
this.credentials = {
|
|
username: credentials.username,
|
|
password: credentials.password,
|
|
totpToken: credentials.totpToken,
|
|
};
|
|
|
|
// Log successful credential update (redacted)
|
|
this._auditLog(AuditAction.CREDENTIAL_UPDATE, AuditStatus.SUCCESS, {
|
|
actor: this._getActorName(),
|
|
resource: 'adapter-credentials',
|
|
detailsUpdated: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate a redacted credentials summary for logging.
|
|
*/
|
|
private _getCredentialsSummary(): { hasCredentials: boolean; hasTotp: boolean } {
|
|
if (!this.credentials) {
|
|
return { hasCredentials: false, hasTotp: false };
|
|
}
|
|
|
|
return {
|
|
hasCredentials: !!this.credentials.username && !!this.credentials.password,
|
|
hasTotp: !!this.credentials.totpToken,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a safe actor name for audit logs (no secrets).
|
|
*/
|
|
private _getActorName(): string {
|
|
if (!this.credentials?.username) {
|
|
return 'unknown';
|
|
}
|
|
|
|
// Use username without exposing password or totp
|
|
return this.credentials.username;
|
|
}
|
|
|
|
/**
|
|
* Log an audit entry.
|
|
*/
|
|
private _auditLog(
|
|
action: AuditAction,
|
|
status: AuditStatus,
|
|
extraDetails?: Record<string, unknown>,
|
|
): void {
|
|
const details = {
|
|
adapter: this.name,
|
|
version: this.version,
|
|
configUrl: this.config.meshCentralUrl,
|
|
...extraDetails,
|
|
};
|
|
|
|
// Create and format audit entry (secrets already redacted by sanitizeForLogging)
|
|
const entry = createAuditEntry({
|
|
action,
|
|
status,
|
|
actor: typeof extraDetails?.actor === 'string' ? extraDetails.actor : this._getActorName(),
|
|
resource: typeof extraDetails?.resource === 'string'
|
|
? extraDetails.resource
|
|
: 'meshcentral-adapter',
|
|
details: Object.fromEntries(
|
|
Object.entries(details).filter(([key]) => key !== 'actor' && key !== 'resource'),
|
|
),
|
|
});
|
|
|
|
// Output audit log via structured logger
|
|
this.logger.audit(`AUDIT - ${entry.action} - ${entry.status}`, {
|
|
action: entry.action,
|
|
status: entry.status,
|
|
actor: typeof extraDetails?.actor === 'string' ? extraDetails.actor : this._getActorName(),
|
|
resource: typeof extraDetails?.resource === 'string'
|
|
? extraDetails.resource
|
|
: 'meshcentral-adapter',
|
|
...Object.fromEntries(
|
|
Object.entries(details).filter(([key]) => key !== 'actor' && key !== 'resource'),
|
|
),
|
|
});
|
|
|
|
if (process.env.AUDIT_DEBUG === 'true') {
|
|
this.logger.debug(JSON.stringify(entry, null, 2));
|
|
}
|
|
}
|
|
private _validateConfig(): void {
|
|
// Log config load attempt
|
|
this._auditLog(AuditAction.CONFIG_LOAD, AuditStatus.PENDING, {
|
|
adapter: this.name,
|
|
resource: 'adapter-configuration',
|
|
});
|
|
|
|
if (!this.config.meshCentralUrl) {
|
|
this._auditLog(AuditAction.CONFIG_LOAD, AuditStatus.FAILURE, {
|
|
adapter: this.name,
|
|
resource: 'adapter-configuration',
|
|
error: 'meshCentralUrl is required',
|
|
});
|
|
|
|
throw new BridgeError(
|
|
ErrorCodes.ConfigurationInvalid,
|
|
'Configuration is incomplete: meshCentralUrl is required',
|
|
);
|
|
}
|
|
|
|
// Check for credentials in environment/config
|
|
const hasUsername = !!this.config.username;
|
|
const hasPassword = !!this.config.password;
|
|
|
|
// Generic error message that doesn't reveal which credential is missing
|
|
if (hasUsername !== hasPassword) {
|
|
throw new BridgeError(
|
|
ErrorCodes.ConfigurationInvalid,
|
|
'Configuration is incomplete: authentication credentials must be provided together',
|
|
);
|
|
}
|
|
|
|
// Real-adapter credentials may be supplied later through setCredentials().
|
|
// Keep the constructor usable for protected runtime secret injection;
|
|
// _ensureConnected() remains the enforcement point before any connection.
|
|
|
|
// Validate credential formats if present
|
|
if (hasUsername && hasPassword) {
|
|
try {
|
|
validateCredentials({
|
|
username: this.config.username,
|
|
password: this.config.password,
|
|
totpToken: this.config.totpToken,
|
|
});
|
|
} catch {
|
|
// Re-throw as generic error to prevent credential enumeration
|
|
throw new BridgeError(
|
|
ErrorCodes.ConfigurationInvalid,
|
|
'Authentication credentials are invalid',
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate allowlist formats if provided
|
|
this._validateAllowlist(this.config.pveAllowlist, 'pveAllowlist');
|
|
this._validateAllowlist(this.config.deviceAllowlist, 'deviceAllowlist');
|
|
this._validateAllowlist(this.config.groupAllowlist, 'groupAllowlist');
|
|
}
|
|
|
|
/**
|
|
* Validate allowlist entries.
|
|
*/
|
|
private _validateAllowlist(allowlist: string[] | undefined, name: string): void {
|
|
if (!allowlist) {
|
|
return;
|
|
}
|
|
|
|
for (const entry of allowlist) {
|
|
if (!entry || typeof entry !== 'string' || entry.trim() === '') {
|
|
throw new BridgeError(
|
|
ErrorCodes.ConfigurationInvalid,
|
|
`Allowlist entry in ${name} is empty or invalid`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a device is in the allowlist.
|
|
*/
|
|
private _isDeviceAllowed(deviceId: string): boolean {
|
|
const { deviceAllowlist, groupAllowlist } = this.config;
|
|
|
|
// If no allowlists configured, all devices are allowed
|
|
if (!deviceAllowlist && !groupAllowlist) {
|
|
return true;
|
|
}
|
|
|
|
// Check device allowlist first (specific devices)
|
|
if (deviceAllowlist && deviceAllowlist.includes(deviceId)) {
|
|
return true;
|
|
}
|
|
|
|
// If group allowlist is configured, device must be in an allowed group
|
|
if (groupAllowlist && groupAllowlist.length > 0) {
|
|
// We'll check this after we have device info
|
|
return true; // Will be checked in getDevice
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private _isGroupAllowed(groupId: string): boolean {
|
|
return !this.config.groupAllowlist || this.config.groupAllowlist.length === 0
|
|
|| this.config.groupAllowlist.includes(groupId);
|
|
}
|
|
|
|
/**
|
|
* Connect to MeshCentral if not already connected.
|
|
*/
|
|
private async _ensureConnected(): Promise<void> {
|
|
if (this.connected) {
|
|
return;
|
|
}
|
|
|
|
const credSummary = this._getCredentialsSummary();
|
|
|
|
// Log connection attempt (redacted)
|
|
this._auditLog(AuditAction.CONNECTION_ATTEMPT, AuditStatus.PENDING, {
|
|
actor: this._getActorName(),
|
|
resource: `meshcentral://${this.config.meshCentralUrl}`,
|
|
hasCredentials: credSummary.hasCredentials,
|
|
hasTotp: credSummary.hasTotp,
|
|
});
|
|
|
|
if (!this.credentials) {
|
|
this._auditLog(AuditAction.CONNECTION_ATTEMPT, AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: `meshcentral://${this.config.meshCentralUrl}`,
|
|
error: 'Adapter is not configured for authentication',
|
|
});
|
|
|
|
throw new BridgeError(
|
|
ErrorCodes.ConfigurationInvalid,
|
|
'Adapter is not configured for authentication',
|
|
);
|
|
}
|
|
|
|
this.client = new UpstreamMeshTransport(this.config.meshCentralUrl);
|
|
|
|
try {
|
|
await this.client.connect(this.credentials);
|
|
this.connected = true;
|
|
|
|
// Log successful connection
|
|
this._auditLog(AuditAction.CONNECTION_ESTABLISHED, AuditStatus.SUCCESS, {
|
|
actor: this._getActorName(),
|
|
resource: `meshcentral://${this.config.meshCentralUrl}`,
|
|
});
|
|
} catch (error) {
|
|
this._auditLog(AuditAction.CONNECTION_ATTEMPT, AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: `meshcentral://${this.config.meshCentralUrl}`,
|
|
error: error instanceof Error ? error.message : 'unknown',
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disconnect from MeshCentral.
|
|
*/
|
|
private async _disconnect(): Promise<void> {
|
|
if (this.connected && this.client) {
|
|
try {
|
|
await this.client.disconnect();
|
|
this._auditLog(AuditAction.CONNECTION_CLOSED, AuditStatus.SUCCESS, {
|
|
actor: this._getActorName(),
|
|
resource: `meshcentral://${this.config.meshCentralUrl}`,
|
|
});
|
|
} finally {
|
|
this.connected = false;
|
|
this.client = null;
|
|
}
|
|
} else if (this.connected) {
|
|
this._auditLog(AuditAction.CONNECTION_CLOSED, AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: `meshcentral://${this.config.meshCentralUrl}`,
|
|
error: 'Client was null when attempting disconnect',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert MeshCtrl node response to Device type.
|
|
*/
|
|
private _mapNodeToDevice(node: { id: string; name: string; online: boolean; device?: { os?: string; uptime?: number; lastseen?: number; ip?: string[]; agentver?: string } }, group: { id: string; name: string }): Device {
|
|
const status = node.online ? DeviceStatus.Online : DeviceStatus.Offline;
|
|
|
|
return {
|
|
id: node.id,
|
|
name: node.name,
|
|
groupId: group.id,
|
|
groupName: group.name,
|
|
status,
|
|
os: node.device?.os ?? undefined,
|
|
uptimeSeconds: node.device?.uptime ?? undefined,
|
|
lastSeen: node.device?.lastseen
|
|
? new Date(node.device.lastseen * 1000).toISOString()
|
|
: undefined,
|
|
ipAddresses: node.device?.ip ?? undefined,
|
|
agentVersion: node.device?.agentver ?? undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert MeshCtrl meshes response to DeviceGroup type.
|
|
*/
|
|
private _mapMeshToGroup(mesh: { id: string; name: string; description?: string }): DeviceGroup {
|
|
return {
|
|
id: mesh.id,
|
|
name: mesh.name,
|
|
description: mesh.description ?? undefined,
|
|
deviceCount: 0, // Will be populated on demand
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Filter an array by allowlist.
|
|
*/
|
|
private _filterByAllowlist<T>(items: T[], getId: (item: T) => string, allowlist?: string[]): T[] {
|
|
if (!allowlist || allowlist.length === 0) {
|
|
return items;
|
|
}
|
|
|
|
const allowedIds = new Set(allowlist);
|
|
return items.filter((item) => allowedIds.has(getId(item)));
|
|
}
|
|
|
|
async listDevices(request?: ListDevicesRequest): Promise<Device[]> {
|
|
await this._ensureConnected();
|
|
|
|
// Log device listing attempt
|
|
this._auditLog(AuditAction.DEVICE_LISTED, AuditStatus.PENDING, {
|
|
actor: this._getActorName(),
|
|
resource: 'all-devices',
|
|
groupId: request?.groupId,
|
|
});
|
|
|
|
// Get meshes (device groups)
|
|
const meshesResponse = await this.client!.sendRequest({
|
|
action: 'meshes',
|
|
});
|
|
|
|
if (meshesResponse.err) {
|
|
throw new BridgeError(
|
|
ErrorCodes.MeshCentralError,
|
|
'Failed to retrieve device groups from MeshCentral server',
|
|
);
|
|
}
|
|
|
|
const meshes = ((meshesResponse.meshes ?? []) as Array<{ id?: string; _id?: string; name: string; description?: string; desc?: string }>).map((mesh) => ({
|
|
...mesh,
|
|
id: mesh.id ?? mesh._id ?? '',
|
|
})).filter((mesh) => mesh.id !== '') as Array<{ id: string; name: string }>;
|
|
|
|
// If group filter provided, find matching mesh
|
|
if (request?.groupId) {
|
|
const targetMesh = meshes.find((m) => m.id === request.groupId);
|
|
if (!targetMesh) {
|
|
throw new BridgeError(
|
|
ErrorCodes.ResourceNotFound,
|
|
'Requested device group could not be found',
|
|
);
|
|
}
|
|
|
|
if (!this._isGroupAllowed(targetMesh.id)) {
|
|
throw new BridgeError(
|
|
ErrorCodes.ResourceNotFound,
|
|
'Requested device group could not be found',
|
|
);
|
|
}
|
|
|
|
return this._getDevicesForMesh(targetMesh);
|
|
}
|
|
|
|
// Get all devices from all meshes
|
|
const allDevices: Device[] = [];
|
|
|
|
for (const mesh of meshes) {
|
|
const devices = await this._getDevicesForMesh(mesh);
|
|
allDevices.push(...devices);
|
|
}
|
|
|
|
return allDevices;
|
|
}
|
|
|
|
// Get devices for a specific mesh/group.
|
|
private async _getDevicesForMesh(mesh: { id: string; name: string }): Promise<Device[]> {
|
|
const nodesResponse = await this.client!.sendRequest({
|
|
action: 'nodes',
|
|
meshid: mesh.id,
|
|
});
|
|
|
|
if (nodesResponse.err) {
|
|
throw new BridgeError(
|
|
ErrorCodes.MeshCentralError,
|
|
`Failed to retrieve devices for group: ${mesh.name}`,
|
|
);
|
|
}
|
|
|
|
const groupedNodes = (nodesResponse.nodes ?? {}) as Record<string, Array<{
|
|
id?: string;
|
|
_id?: string;
|
|
name: string;
|
|
meshid?: string;
|
|
online?: boolean;
|
|
conn?: number;
|
|
osdesc?: string;
|
|
ip?: string[];
|
|
device?: { os?: string; uptime?: number; lastseen?: number; ip?: string[]; agentver?: string };
|
|
}>>;
|
|
const nodes = Object.entries(groupedNodes).flatMap(([groupId, groupNodes]) =>
|
|
groupNodes.map((node) => ({
|
|
id: node.id ?? node._id ?? '',
|
|
name: node.name,
|
|
meshid: node.meshid ?? groupId,
|
|
online: node.online ?? node.conn === 1,
|
|
device: node.device ?? {
|
|
os: node.osdesc,
|
|
ip: node.ip,
|
|
},
|
|
})),
|
|
).filter((node) => node.id !== '') as Array<{ id: string; name: string; online: boolean; meshid?: string; device?: { os?: string; uptime?: number; lastseen?: number; ip?: string[]; agentver?: string } }>;
|
|
|
|
if (!this._isGroupAllowed(mesh.id)) {
|
|
return [];
|
|
}
|
|
|
|
return nodes
|
|
.map((node) => this._mapNodeToDevice(node, mesh))
|
|
.filter((device) => this._isDeviceAllowed(device.id));
|
|
}
|
|
|
|
async getDevice(deviceId: string): Promise<Device> {
|
|
await this._ensureConnected();
|
|
|
|
// Log device access attempt
|
|
this._auditLog(AuditAction.DEVICE_ACCESS, AuditStatus.PENDING, {
|
|
actor: this._getActorName(),
|
|
resource: deviceId,
|
|
});
|
|
|
|
// First get all devices to validate allowlist and find the device
|
|
const allDevices = await this.listDevices();
|
|
|
|
const device = allDevices.find((d) => d.id === deviceId);
|
|
|
|
if (!device) {
|
|
this._auditLog(AuditAction.DEVICE_ACCESS, AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: deviceId,
|
|
error: 'Device could not be found',
|
|
});
|
|
|
|
throw new BridgeError(
|
|
ErrorCodes.ResourceNotFound,
|
|
'Device could not be found',
|
|
);
|
|
}
|
|
|
|
// Check allowlist again after getting device info
|
|
if (!this._isDeviceAllowed(deviceId)) {
|
|
this._auditLog(AuditAction.ACCESS_DENIED, AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: deviceId,
|
|
error: 'Device is not authorized for access',
|
|
});
|
|
|
|
throw new BridgeError(
|
|
ErrorCodes.AdapterNotFound,
|
|
'Device is not authorized for access',
|
|
);
|
|
}
|
|
|
|
this._auditLog(AuditAction.DEVICE_ACCESS, AuditStatus.SUCCESS, {
|
|
actor: this._getActorName(),
|
|
resource: deviceId,
|
|
});
|
|
|
|
return device;
|
|
}
|
|
|
|
async listGroups(): Promise<DeviceGroup[]> {
|
|
await this._ensureConnected();
|
|
|
|
const meshesResponse = await this.client!.sendRequest({
|
|
action: 'meshes',
|
|
});
|
|
|
|
if (meshesResponse.err) {
|
|
throw new BridgeError(
|
|
ErrorCodes.MeshCentralError,
|
|
'Failed to retrieve device groups from MeshCentral server',
|
|
);
|
|
}
|
|
|
|
const meshes = ((meshesResponse.meshes ?? []) as Array<{
|
|
id?: string;
|
|
_id?: string;
|
|
name: string;
|
|
description?: string;
|
|
desc?: string;
|
|
}>).map((mesh) => ({
|
|
...mesh,
|
|
id: mesh.id ?? mesh._id ?? '',
|
|
description: mesh.description ?? mesh.desc,
|
|
})).filter((mesh) => mesh.id !== '') as Array<{ id: string; name: string; description?: string }>;
|
|
|
|
// Filter by group allowlist if configured
|
|
const allowedMeshes = this._filterByAllowlist(
|
|
meshes,
|
|
(m) => m.id,
|
|
this.config.groupAllowlist,
|
|
);
|
|
|
|
return allowedMeshes.map((mesh) => this._mapMeshToGroup(mesh));
|
|
}
|
|
|
|
async getGroup(groupId: string): Promise<DeviceGroup> {
|
|
const groups = await this.listGroups();
|
|
|
|
const group = groups.find((g) => g.id === groupId);
|
|
|
|
if (!group) {
|
|
throw new BridgeError(
|
|
ErrorCodes.ResourceNotFound,
|
|
'Device group could not be found',
|
|
);
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
async runDiagnostic(request: RunDiagnosticRequest): Promise<DiagnosticResult> {
|
|
const { deviceId, checkType } = request;
|
|
|
|
// Log diagnostic attempt
|
|
this._auditLog(AuditAction.MESHCENTRAL_REQUEST, AuditStatus.PENDING, {
|
|
actor: this._getActorName(),
|
|
resource: deviceId,
|
|
checkType,
|
|
});
|
|
|
|
// Validate device is in allowlist
|
|
if (!this._isDeviceAllowed(deviceId)) {
|
|
this._auditLog(AuditAction.ACCESS_DENIED, AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: deviceId,
|
|
checkType,
|
|
error: 'Device is not authorized for access',
|
|
});
|
|
|
|
throw new BridgeError(
|
|
ErrorCodes.AdapterNotFound,
|
|
'Device is not authorized for access',
|
|
);
|
|
}
|
|
|
|
// Get device to verify it exists and get group info
|
|
await this.getDevice(deviceId);
|
|
|
|
// For now, return a placeholder diagnostic result
|
|
// Real implementation would execute commands via MeshCtrl's deviceinfo or events API
|
|
const result = {
|
|
deviceId,
|
|
checkType,
|
|
timestamp: new Date().toISOString(),
|
|
success: true,
|
|
output: '[Real adapter diagnostic not yet implemented - device is online]',
|
|
exitCode: 0,
|
|
outputLength: 49,
|
|
maxOutputLength: request.maxOutputLength ?? this.config.maxOutputLength,
|
|
truncated: false,
|
|
};
|
|
|
|
this._auditLog(AuditAction.MESHCENTRAL_RESPONSE, AuditStatus.SUCCESS, {
|
|
actor: this._getActorName(),
|
|
resource: deviceId,
|
|
checkType,
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
async healthCheck(): Promise<boolean> {
|
|
// Log health check attempt
|
|
this._auditLog(AuditAction.SERVICE_START, AuditStatus.PENDING, {
|
|
actor: this._getActorName(),
|
|
resource: 'health-check',
|
|
});
|
|
|
|
if (this.config.adapterType !== 'real') {
|
|
this._auditLog(AuditAction.SERVICE_START, AuditStatus.SUCCESS, {
|
|
actor: this._getActorName(),
|
|
resource: 'health-check',
|
|
adapterType: this.config.adapterType,
|
|
});
|
|
return true; // Mock adapter is always healthy
|
|
}
|
|
|
|
try {
|
|
await this._ensureConnected();
|
|
|
|
// `meshes` is the least-privileged authenticated inventory operation
|
|
// and is available without the server-upgrade site permission required
|
|
// by MeshCentral's `serverversion` command.
|
|
const meshesResponse = await this.client!.sendRequest({ action: 'meshes' });
|
|
const healthy = Array.isArray(meshesResponse.meshes);
|
|
this._auditLog(AuditAction.SERVICE_START, healthy ? AuditStatus.SUCCESS : AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: 'health-check',
|
|
healthy,
|
|
});
|
|
|
|
return healthy;
|
|
} catch {
|
|
this._auditLog(AuditAction.SERVICE_START, AuditStatus.FAILURE, {
|
|
actor: this._getActorName(),
|
|
resource: 'health-check',
|
|
healthy: false,
|
|
});
|
|
|
|
await this._disconnect();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup on destroy.
|
|
*/
|
|
async destroy(): Promise<void> {
|
|
await this._disconnect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Factory function to create the appropriate adapter based on configuration.
|
|
*/
|
|
export function createMeshCentralAdapter(config: Partial<BridgeConfig> = {}): MeshCentralAdapter {
|
|
const adapterType = config.adapterType ?? 'mock';
|
|
|
|
switch (adapterType) {
|
|
case 'real':
|
|
return new MeshCentralRealAdapter(config);
|
|
case 'mock':
|
|
default:
|
|
return createMockAdapter(config);
|
|
}
|
|
}
|