Initial project documentation and status
This commit is contained in:
@@ -0,0 +1,708 @@
|
||||
/**
|
||||
* 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 {
|
||||
MeshCtrlClient,
|
||||
} from './meshctrl-client';
|
||||
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: MeshCtrlClient | 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',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.config.adapterType === 'real' && (!hasUsername || !hasPassword)) {
|
||||
throw new BridgeError(
|
||||
ErrorCodes.ConfigurationInvalid,
|
||||
'Real adapter requires authentication credentials',
|
||||
);
|
||||
}
|
||||
|
||||
// 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 MeshCtrlClient({
|
||||
url: this.config.meshCentralUrl,
|
||||
timeoutMs: this.config.timeoutMs,
|
||||
});
|
||||
|
||||
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 server version first to verify connection
|
||||
try {
|
||||
const versionResponse = await this.client!.sendRequest({
|
||||
action: 'serverversion',
|
||||
});
|
||||
|
||||
if (versionResponse.err) {
|
||||
this._auditLog(AuditAction.MESHCENTRAL_REQUEST, AuditStatus.FAILURE, {
|
||||
actor: this._getActorName(),
|
||||
resource: 'serverversion',
|
||||
error: versionResponse.err,
|
||||
});
|
||||
|
||||
throw new BridgeError(
|
||||
ErrorCodes.MeshCentralError,
|
||||
'Failed to establish connection with MeshCentral server',
|
||||
);
|
||||
} else {
|
||||
this._auditLog(AuditAction.MESHCENTRAL_RESPONSE, AuditStatus.SUCCESS, {
|
||||
actor: this._getActorName(),
|
||||
resource: 'serverversion',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await this._disconnect();
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 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; 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 nodes = (nodesResponse.nodes ?? []) 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; 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();
|
||||
|
||||
const versionResponse = await this.client!.sendRequest({
|
||||
action: 'serverversion',
|
||||
});
|
||||
|
||||
const healthy = !versionResponse.err;
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user