feat(api): add device identity guard and Prometheus metrics

- Persist device identity observations and expose fleet identity summaries.

- Block device control actions when identity verification fails.

- Add metrics collection and a Prometheus scrape endpoint.
This commit is contained in:
chick
2026-09-07 00:44:51 +08:00
parent 8406f14469
commit 961eb928c4
14 changed files with 1619 additions and 4 deletions
@@ -20,6 +20,11 @@ import {
NotificationServiceError,
type InstanceNotificationService,
} from '../../application/notifications/instance-notification-service.js';
import type {
DeviceIdentity,
DeviceIdentityService,
IdentityReason,
} from '../../application/identity/device-identity-service.js';
export interface FleetMessageDevice {
readonly id: string;
readonly name: string;
@@ -68,6 +73,14 @@ export interface FleetRoutesOptions {
readonly connections?: ConnectionProbe;
/** Offline send queue; absent means sends fail fast instead of waiting for the device. */
readonly outbox?: SmsOutboxService;
/** Identity guard; absent means the fleet list has no opinion about hardware drift. */
readonly identities?: DeviceIdentityService;
}
export interface FleetIdentitySummary {
readonly tracked: boolean;
readonly status: 'confirmed' | 'pending';
readonly reasons: readonly IdentityReason[];
}
/** One queued send, with the node label the queue table itself does not store. */
@@ -184,6 +197,11 @@ function connectionSummary(state: ConnectionState | undefined): FleetConnectionS
};
}
function identitySummary(identity: DeviceIdentity | undefined): FleetIdentitySummary {
if (!identity) return { tracked: false, status: 'confirmed', reasons: [] };
return { tracked: true, status: identity.status, reasons: identity.reasons };
}
function record(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
return value as Record<string, unknown>;
@@ -357,6 +375,7 @@ export function registerFleetRoutes(app: FastifyInstance, options: FleetRoutesOp
...instance,
connection: connectionSummary(reachability?.get(instance.id)),
resources: await options.resources.get(instance.id),
identity: identitySummary(options.identities?.get(instance.id)),
}));
return { items };
});