Files
NODEDC_DEVICE_CORE/apps/device-manager/server/device-core-client.mjs
T

1168 lines
45 KiB
JavaScript

import { randomUUID } from "node:crypto";
const commandRoutes = new Map([
["owner-scopes:ensure", "/internal/v1/management/owner-scopes:ensure"],
["projects:ensure", "/internal/v1/management/projects:ensure"],
["collections:ensure", "/internal/v1/management/collections:ensure"],
["project-grants:upsert", "/internal/v1/management/project-grants:upsert"],
["adapter-packages:ensure", "/internal/v1/management/adapter-packages:ensure"],
["adapter-versions:register", "/internal/v1/management/adapter-versions:register"],
["model-profiles:register", "/internal/v1/management/model-profiles:register"],
["edges:ensure", "/internal/v1/management/edges:ensure"],
["routes:ensure", "/internal/v1/management/routes:ensure"],
["enrollment-intents:ensure", "/internal/v1/management/enrollment-intents:ensure"],
["devices:claim", "/internal/v1/management/devices:claim"],
["devices:update", "/internal/v1/management/devices:update"],
["assets:ensure", "/internal/v1/management/assets:ensure"],
["asset-bindings:ensure", "/internal/v1/management/asset-bindings:ensure"],
["asset-bindings:close", "/internal/v1/management/asset-bindings:close"],
["infrastructure-hosts:ensure", "/internal/v1/management/infrastructure-hosts:ensure"],
["infrastructure-endpoints:ensure", "/internal/v1/management/infrastructure-endpoints:ensure"],
["infrastructure-deployments:ensure", "/internal/v1/management/infrastructure-deployments:ensure"],
["infrastructure-service-instances:ensure", "/internal/v1/management/infrastructure-service-instances:ensure"],
["health-observations:record", "/internal/v1/management/health-observations:record"],
["device-bindings:ensure", "/internal/v1/management/device-bindings:ensure"],
["device-bindings:revoke", "/internal/v1/management/device-bindings:revoke"],
[
"device-configuration-revisions:create",
"/internal/v1/management/device-configuration-revisions:create",
],
[
"device-configurations:set-desired",
"/internal/v1/management/device-configurations:set-desired",
],
["commands:service-ping", "/internal/v1/commands:service-ping"],
]);
export function createDeviceCoreClient({ baseUrl, token, fetchImpl = fetch } = {}) {
const endpoint = normalizeBaseUrl(baseUrl);
if (typeof token !== "string" || token.length < 32) {
throw serviceError("device_core_token_invalid", 503);
}
async function request(pathname, actor, init = {}) {
const response = await fetchImpl(new URL(pathname, endpoint), {
...init,
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
...actorHeaders(actor),
...(init.headers ?? {}),
},
signal: AbortSignal.timeout(10_000),
});
const body = await response.json().catch(() => null);
if (!response.ok || body?.ok !== true) {
throw serviceError(
safeCoreError(body?.error),
response.status >= 400 && response.status < 600 ? response.status : 502,
);
}
return body;
}
return {
configured: true,
async listProjects(actor) {
return request("/internal/v1/query/projects", actor)
.then((body) => body.projects);
},
async getWorkspace(actor, projectRef) {
const projectId = entityId(projectRef, "project");
const [workspace, ontology] = await Promise.all([
request(`/internal/v1/query/projects/${projectId}/workspace`, actor)
.then((body) => body.workspace),
request(`/internal/v1/query/projects/${projectId}/ontology`, actor)
.then((body) => body.projection),
]);
return { ...workspace, ontology };
},
async execute(command, actor, input, idempotencyKey) {
const pathname = commandRoutes.get(command);
if (!pathname) throw serviceError("device_manager_command_invalid", 404);
if (!/^[\x21-\x7e]{8,256}$/.test(idempotencyKey || "")) {
throw serviceError("device_idempotency_key_invalid", 400);
}
return request(pathname, actor, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(input),
}).then(({ replayed, result }) => ({ replayed, result }));
},
};
}
export function createLocalPreviewDeviceCore({ fixture = null } = {}) {
const ownerScopes = new Map();
const projects = new Map();
const collections = new Map();
const adapterPackages = new Map();
const adapterVersions = new Map();
const modelProfiles = new Map();
const edges = new Map();
const routes = new Map();
const enrollments = new Map();
const devices = new Map();
const sessions = new Map();
const bindings = new Map();
const grants = new Map();
const configurationRevisions = new Map();
const configurationStates = new Map();
const auditEvents = [];
const commands = new Map();
const assets = new Map();
const assetBindings = new Map();
const hosts = new Map();
const endpoints = new Map();
const deployments = new Map();
const serviceInstances = new Map();
const healthObservations = new Map();
function now() {
return new Date().toISOString();
}
function projectValues(store, projectRef) {
return [...store.values()].filter((value) => value.projectRef === projectRef);
}
function audit(actor, projectRef, eventType, refs = {}) {
auditEvents.unshift({
auditEventRef: `audit-event:${randomUUID()}`,
eventType,
actorRef: actor.userRef,
deviceRef: refs.deviceRef ?? null,
discoveryRef: refs.discoveryRef ?? null,
projectRef,
occurredAt: now(),
});
}
function projectSummary(project) {
const projectCollections = [...collections.values()]
.filter((collection) => collection.projectRef === project.projectRef);
return {
...project,
counts: {
devices: projectValues(devices, project.projectRef).length,
collections: projectCollections.length,
discoveries: 0,
},
};
}
function workspace(projectRef) {
const project = projects.get(projectRef);
if (!project) throw serviceError("device_project_not_found", 404);
return {
project: projectSummary(project),
devices: projectValues(devices, projectRef)
.map(({ projectRef: _projectRef, ...device }) => device),
discoveries: [],
enrollments: projectValues(enrollments, projectRef),
collections: projectValues(collections, projectRef)
.map(({ projectRef: _projectRef, ...collection }) => collection),
adapterPackages: [...adapterPackages.values()],
adapterVersions: [...adapterVersions.values()],
modelProfiles: [...modelProfiles.values()],
edges: [...edges.values()],
routes: projectValues(routes, projectRef),
sessions: projectValues(sessions, projectRef)
.map(({ projectRef: _projectRef, ...session }) => session),
bindings: projectValues(bindings, projectRef),
configurationRevisions: projectValues(configurationRevisions, projectRef),
configurationStates: projectValues(configurationStates, projectRef),
commands: projectValues(commands, projectRef),
auditEvents: auditEvents.filter((event) => event.projectRef === projectRef),
grants: projectValues(grants, projectRef),
policies: {
commandTransport: fixture === "arusnavi-b2"
? "typed-service-ping-v1"
: "disabled",
commandPlanningApi: fixture === "arusnavi-b2" ? "enabled" : "disabled",
identifierProjection: fixture === "arusnavi-b2" ? "authorized-full" : "masked-only",
auditPayloadProjection: "metadata-only",
},
ontology: ontologyProjection(projectRef),
};
}
function ontologyProjection(projectRef) {
const projectHosts = projectValues(hosts, projectRef);
const projectServices = projectValues(serviceInstances, projectRef);
const withHealth = (subjectKind, value, subjectRef) => ({
...value,
health: latestPreviewHealth(projectRef, subjectKind, subjectRef),
});
return {
ontology: {
catalogHash: "229c61c02a790906",
packages: ["asset", "device", "infrastructure", "observation"],
},
assets: projectValues(assets, projectRef),
assetBindings: projectValues(assetBindings, projectRef),
hosts: projectHosts.map((host) => withHealth("host", host, host.hostRef)),
endpoints: projectValues(endpoints, projectRef),
deployments: projectValues(deployments, projectRef),
serviceInstances: projectServices.map((service) =>
withHealth("service-instance", service, service.serviceInstanceRef)),
policies: {
restrictedIdentifiers: "masked-only",
managementCredentials: "opaque-reference-only",
missingHealthEvidence: "unobserved-not-unhealthy",
arbitraryConsole: "disabled",
},
};
}
function latestPreviewHealth(projectRef, subjectKind, subjectRef) {
const values = projectValues(healthObservations, projectRef)
.filter((item) => item.subjectKind === subjectKind && item.subjectRef === subjectRef)
.sort((left, right) => right.observedAt.localeCompare(left.observedAt));
const latest = values[0];
if (!latest) return { state: "unobserved", freshness: "missing", observationRef: null };
const fresh = new Date(latest.expiresAt).valueOf() > Date.now();
return {
state: fresh ? latest.observedState : "unobserved",
freshness: fresh ? "fresh" : "stale",
lastObservedState: latest.observedState,
evidenceClass: latest.evidenceClass,
observedAt: latest.observedAt,
expiresAt: latest.expiresAt,
observationRef: latest.healthObservationRef,
};
}
if (fixture === "arusnavi-b2") seedArusnaviB2Preview({
ownerScopes,
projects,
devices,
modelProfiles,
edges,
routes,
sessions,
configurationStates,
});
else if (fixture != null && fixture !== "") {
throw serviceError("device_manager_preview_fixture_invalid", 400);
}
return {
configured: true,
async listProjects() {
return [...projects.values()].map(projectSummary);
},
async getWorkspace(_actor, projectRef) {
return workspace(projectRef);
},
async execute(command, actor, input) {
if (command === "commands:service-ping") {
if (fixture !== "arusnavi-b2") {
throw serviceError("device_command_transport_disabled", 409);
}
const device = devices.get(input.deviceRef);
if (!device || device.projectRef !== input.projectRef) {
throw serviceError("device_command_route_unavailable", 409);
}
if (typeof input.accessCode !== "string" || !/^\d{6}$/.test(input.accessCode)) {
throw serviceError("device_service_ping_access_code_invalid", 400);
}
const commandRef = `command:${randomUUID()}`;
const at = now();
const view = {
commandRef,
projectRef: input.projectRef,
deviceRef: input.deviceRef,
deviceName: device.displayName,
commandKey: `preview-service-ping-${randomUUID()}`,
commandCatalogRef: "arusnavi.b2.internal.v1:service-ping",
commandType: "service.ping",
riskClass: "low",
lifecycleState: "queued",
plannedAt: at,
expiresAt: new Date(Date.now() + Number(input.expiresInSeconds) * 1000).toISOString(),
confirmedAt: null,
dispatchedAt: null,
acknowledgedAt: null,
terminalAt: null,
terminalReasonCode: null,
createdAt: at,
updatedAt: at,
};
commands.set(commandRef, view);
return { replayed: false, result: view };
}
if (command === "owner-scopes:ensure") {
const key = `${input.scopeKind}:${input.ownerRef}`;
const created = !ownerScopes.has(key);
const scope = {
ownerScopeRef: ownerScopes.get(key)?.ownerScopeRef || `owner-scope:${randomUUID()}`,
scopeKind: input.scopeKind,
ownerRef: input.ownerRef,
displayName: input.displayName,
lifecycleState: "active",
};
ownerScopes.set(key, scope);
return { replayed: false, result: { created, ownerScope: scope } };
}
if (command === "projects:ensure") {
const scope = ownerScopes.get(`${input.scopeKind}:${input.ownerRef}`);
if (!scope) throw serviceError("device_owner_scope_not_found", 404);
const existing = [...projects.values()].find((project) =>
project.ownerScope.ownerRef === input.ownerRef
&& project.projectKey === input.projectKey
);
const projectRef = existing?.projectRef || `project:${randomUUID()}`;
const project = {
projectRef,
projectKey: input.projectKey,
name: input.name,
description: input.description ?? null,
lifecycleState: "active",
ownerScope: scope,
access: { projectRole: "owner", capabilities: ownerCapabilities },
counts: { devices: 0, collections: 0, discoveries: 0 },
createdAt: existing?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
projects.set(projectRef, project);
if (!existing) {
const grantRef = `grant:${randomUUID()}`;
grants.set(grantRef, {
grantRef,
projectRef,
principalKind: "user",
principalRef: actor.userRef,
projectRole: "owner",
capabilityAllow: [],
capabilityDeny: [],
lifecycleState: "active",
});
audit(actor, projectRef, "project.created");
}
return { replayed: false, result: { created: !existing, project } };
}
if (command === "collections:ensure") {
const projectRef = input.projectRef;
if (!projects.has(projectRef)) throw serviceError("device_project_not_found", 404);
const existing = [...collections.values()].find((collection) =>
collection.projectRef === projectRef
&& collection.collectionKey === input.collectionKey
);
const collectionRef = existing?.collectionRef || `collection:${randomUUID()}`;
const collection = {
collectionRef,
projectRef,
collectionKey: input.collectionKey,
name: input.name,
description: input.description ?? null,
lifecycleState: "active",
memberCount: existing?.memberCount || 0,
createdAt: existing?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
collections.set(collectionRef, collection);
audit(actor, projectRef, createdEvent(existing, "collection"));
return { replayed: false, result: { created: !existing, collection } };
}
if (command === "project-grants:upsert") {
if (!projects.has(input.projectRef)) {
throw serviceError("device_project_not_found", 404);
}
const existing = [...grants.values()].find((grant) =>
grant.projectRef === input.projectRef
&& grant.principalKind === input.principalKind
&& grant.principalRef === input.principalRef
);
const grantRef = existing?.grantRef || `grant:${randomUUID()}`;
const grant = {
grantRef,
projectRef: input.projectRef,
principalKind: input.principalKind,
principalRef: input.principalRef,
projectRole: input.projectRole,
capabilityAllow: input.capabilityAllow ?? [],
capabilityDeny: input.capabilityDeny ?? [],
lifecycleState: input.lifecycleState ?? "active",
};
grants.set(grantRef, grant);
audit(actor, input.projectRef, createdEvent(existing, "project_grant"));
return { replayed: false, result: { created: !existing, grant } };
}
if (command === "adapter-packages:ensure") {
requirePlatformOwner(actor);
const existing = [...adapterPackages.values()].find(
(entry) => entry.packageKey === input.packageKey,
);
const adapterPackageRef = existing?.adapterPackageRef
|| `adapter-package:${randomUUID()}`;
assertPreviewTransition(
existing?.lifecycleState,
input.lifecycleState ?? "active",
previewTransitions.adapterPackage,
"device_adapter_package_transition_invalid",
);
const adapterPackage = {
adapterPackageRef,
packageKey: input.packageKey,
displayName: input.displayName,
publisherRef: input.publisherRef,
lifecycleState: input.lifecycleState ?? "active",
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
adapterPackages.set(adapterPackageRef, adapterPackage);
return { replayed: false, result: { created: !existing, adapterPackage } };
}
if (command === "adapter-versions:register") {
requirePlatformOwner(actor);
const adapterPackage = adapterPackages.get(input.adapterPackageRef);
if (!adapterPackage) {
throw serviceError("device_adapter_package_not_found", 404);
}
if (adapterPackage.lifecycleState !== "active") {
throw serviceError("device_adapter_package_inactive", 409);
}
const existing = [...adapterVersions.values()].find((entry) =>
entry.adapterPackageRef === input.adapterPackageRef
&& entry.version === input.version
);
const adapterVersionRef = existing?.adapterVersionRef
|| `adapter-version:${randomUUID()}`;
assertPreviewTransition(
existing?.lifecycleState,
input.lifecycleState ?? "draft",
previewTransitions.catalogVersion,
"device_adapter_version_transition_invalid",
);
const adapterVersion = {
adapterVersionRef,
adapterPackageRef: input.adapterPackageRef,
version: input.version,
runtimePackageRef: input.runtimePackageRef,
contentDigest: input.contentDigest,
contractVersion: input.contractVersion,
capabilities: input.capabilities ?? [],
lifecycleState: input.lifecycleState ?? "draft",
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
adapterVersions.set(adapterVersionRef, adapterVersion);
return { replayed: false, result: { created: !existing, adapterVersion } };
}
if (command === "model-profiles:register") {
requirePlatformOwner(actor);
const adapterVersion = adapterVersions.get(input.adapterVersionRef);
if (!adapterVersion) {
throw serviceError("device_adapter_version_not_found", 404);
}
const existing = modelProfiles.get(input.profileRef);
const lifecycleState = input.lifecycleState ?? "draft";
if (lifecycleState === "active" && adapterVersion.lifecycleState !== "active") {
throw serviceError("device_model_profile_adapter_not_active", 409);
}
assertPreviewTransition(
existing?.lifecycleState,
lifecycleState,
previewTransitions.catalogVersion,
"device_model_profile_transition_invalid",
);
const modelProfile = {
modelProfileRef: input.profileRef,
adapterVersionRef: input.adapterVersionRef,
schemaVersion: input.schemaVersion,
vendor: input.vendor,
model: input.model,
deviceType: input.deviceType,
protocol: input.protocol,
schemaArtifactRef: input.schemaArtifactRef,
profileDigest: input.profileDigest,
capabilities: input.capabilities ?? [],
lifecycleState,
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
modelProfiles.set(input.profileRef, modelProfile);
return { replayed: false, result: { created: !existing, modelProfile } };
}
if (command === "edges:ensure") {
requirePlatformOwner(actor);
const existing = [...edges.values()].find(
(entry) => entry.edgeKey === input.edgeKey,
);
const edgeRef = existing?.edgeRef || `edge:${randomUUID()}`;
assertPreviewTransition(
existing?.lifecycleState,
input.lifecycleState ?? "provisioning",
previewTransitions.edge,
"device_edge_transition_invalid",
);
const edge = {
edgeRef,
edgeKey: input.edgeKey,
displayName: input.displayName,
deploymentRef: input.deploymentRef ?? null,
lifecycleState: input.lifecycleState ?? "provisioning",
channel: existing?.channel ?? {
lifecycleState: "disabled",
generationRef: null,
runtimeState: "disabled",
lastErrorCode: null,
},
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
edges.set(edgeRef, edge);
return { replayed: false, result: { created: !existing, edge } };
}
if (command === "routes:ensure") {
if (!projects.has(input.projectRef)) {
throw serviceError("device_project_not_found", 404);
}
const edge = edges.get(input.edgeRef);
const profile = modelProfiles.get(input.modelProfileRef);
if (!edge) throw serviceError("device_edge_not_found", 404);
if (!profile) throw serviceError("device_model_profile_not_found", 404);
const existing = projectValues(routes, input.projectRef).find(
(entry) => entry.routeKey === input.routeKey,
);
const routeRef = existing?.routeRef || `route:${randomUUID()}`;
const lifecycleState = input.lifecycleState ?? "draft";
if (
lifecycleState === "active"
&& (edge.lifecycleState !== "active" || profile.lifecycleState !== "active")
) {
throw serviceError("device_route_dependency_not_active", 409);
}
assertPreviewTransition(
existing?.lifecycleState,
lifecycleState,
previewTransitions.route,
"device_route_transition_invalid",
);
const route = {
routeRef,
projectRef: input.projectRef,
routeKey: input.routeKey,
displayName: input.displayName,
edgeRef: input.edgeRef,
edgeName: edge.displayName,
modelProfileRef: input.modelProfileRef,
profileName: `${profile.vendor} ${profile.model}`,
listenerRef: input.listenerRef,
protocol: input.protocol,
direction: input.direction ?? "telemetry",
lifecycleState,
sessionCount: 0,
activeSessionCount: 0,
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
routes.set(routeRef, route);
audit(actor, input.projectRef, createdEvent(existing, "route"));
return { replayed: false, result: { created: !existing, route } };
}
if (command === "device-bindings:ensure") {
if (!projects.has(input.projectRef)) {
throw serviceError("device_project_not_found", 404);
}
if (input.source?.kind !== "collection" || !collections.has(input.source.ref)) {
throw serviceError("device_binding_source_not_found", 404);
}
const existing = projectValues(bindings, input.projectRef).find(
(entry) => entry.bindingKey === input.bindingKey,
);
const bindingRef = existing?.bindingRef || `binding:${randomUUID()}`;
const source = collections.get(input.source.ref);
const binding = {
bindingRef,
projectRef: input.projectRef,
bindingKey: input.bindingKey,
displayName: input.displayName,
source: {
kind: input.source.kind,
ref: input.source.ref,
displayName: source.name,
},
target: { kind: input.targetKind, ref: input.targetRef },
capabilities: input.capabilities,
lifecycleState: "pending_external_approval",
sourceApprovedAt: now(),
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
bindings.set(bindingRef, binding);
audit(actor, input.projectRef, createdEvent(existing, "device_binding"));
return { replayed: false, result: { created: !existing, binding } };
}
if (command === "device-bindings:revoke") {
const binding = bindings.get(input.bindingRef);
if (!binding || binding.projectRef !== input.projectRef) {
throw serviceError("device_binding_not_found", 404);
}
const revoked = { ...binding, lifecycleState: "revoked", updatedAt: now() };
bindings.set(binding.bindingRef, revoked);
audit(actor, input.projectRef, "device_binding.revoked");
return { replayed: false, result: { revoked: true, binding: revoked } };
}
if (command === "device-configuration-revisions:create") {
throw serviceError("device_not_found", 404);
}
if (command === "device-configurations:set-desired") {
throw serviceError("device_configuration_revision_not_found", 404);
}
if (command === "enrollment-intents:ensure") {
if (!projects.has(input.projectRef)) {
throw serviceError("device_project_not_found", 404);
}
const route = routes.get(input.routeRef);
if (!route || route.projectRef !== input.projectRef) {
throw serviceError("device_route_not_found", 404);
}
if (route.lifecycleState !== "active") {
throw serviceError("device_enrollment_route_inactive", 409);
}
if (route.modelProfileRef !== input.modelProfileRef) {
throw serviceError("device_enrollment_profile_mismatch", 409);
}
if (
input.identifier?.kind !== "imei"
|| typeof input.identifier.value !== "string"
|| !/^\d{15}$/.test(input.identifier.value)
) {
throw serviceError("restricted_identifier_imei_invalid", 400);
}
const existing = projectValues(enrollments, input.projectRef).find(
(entry) => entry.enrollmentKey === input.enrollmentKey,
);
const enrollmentIntentRef = existing?.enrollmentIntentRef
|| `enrollment-intent:${randomUUID()}`;
const enrollment = {
enrollmentIntentRef,
projectRef: input.projectRef,
enrollmentKey: input.enrollmentKey,
displayName: input.displayName,
routeRef: input.routeRef,
modelProfileRef: input.modelProfileRef,
expectedIdentifier: {
kind: "imei",
masked: `***********${input.identifier.value.slice(-4)}`,
},
lifecycleState: "pending",
observedDiscoveryRef: null,
claimedDeviceRef: null,
expiresAt: input.expiresAt ?? null,
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
enrollments.set(enrollmentIntentRef, enrollment);
audit(actor, input.projectRef, createdEvent(existing, "enrollment_intent"));
const { expectedIdentifier, ...safeEnrollment } = enrollment;
return {
replayed: false,
result: {
created: !existing,
enrollmentIntent: {
...safeEnrollment,
identifier: expectedIdentifier,
},
},
};
}
if (command === "devices:claim") {
throw serviceError("device_discovery_not_found", 404);
}
if (command === "assets:ensure") {
if (!projects.has(input.projectRef)) throw serviceError("device_project_not_found", 404);
const existing = projectValues(assets, input.projectRef)
.find((item) => item.assetKey === input.assetKey);
const assetRef = existing?.assetRef || `asset:${randomUUID()}`;
const asset = {
assetRef,
projectRef: input.projectRef,
assetKey: input.assetKey,
displayName: input.displayName,
assetTypeRef: input.assetTypeRef,
lifecycleState: input.lifecycleState ?? "active",
ontology: previewOntology("asset.asset"),
};
assets.set(assetRef, asset);
audit(actor, input.projectRef, createdEvent(existing, "asset"));
return { replayed: false, result: { created: !existing, asset } };
}
if (command === "asset-bindings:ensure") {
const device = devices.get(input.deviceRef);
const asset = assets.get(input.assetRef);
if (!device || device.projectRef !== input.projectRef) {
throw serviceError("device_not_found", 404);
}
if (!asset || asset.projectRef !== input.projectRef) {
throw serviceError("device_asset_not_found", 404);
}
const existing = projectValues(assetBindings, input.projectRef)
.find((item) => item.bindingKey === input.bindingKey);
const assetBindingRef = existing?.assetBindingRef || `asset-binding:${randomUUID()}`;
const assetBinding = {
assetBindingRef,
projectRef: input.projectRef,
bindingKey: input.bindingKey,
deviceRef: input.deviceRef,
deviceName: device.displayName,
assetRef: input.assetRef,
assetName: asset.displayName,
bindingKind: input.bindingKind ?? "tracking",
validFrom: input.validFrom,
validTo: null,
provenanceRef: input.provenanceRef,
ontology: previewOntology("device.asset_binding"),
};
assetBindings.set(assetBindingRef, assetBinding);
audit(actor, input.projectRef, createdEvent(existing, "asset_binding"));
return { replayed: false, result: { created: !existing, assetBinding } };
}
if (command === "asset-bindings:close") {
const existing = assetBindings.get(input.assetBindingRef);
if (!existing || existing.projectRef !== input.projectRef || existing.validTo) {
throw serviceError("device_asset_binding_not_closable", 409);
}
const assetBinding = { ...existing, validTo: input.validTo };
assetBindings.set(existing.assetBindingRef, assetBinding);
audit(actor, input.projectRef, "asset_binding.closed");
return { replayed: false, result: { closed: true, assetBinding } };
}
if (command === "infrastructure-hosts:ensure") {
if (!projects.has(input.projectRef)) throw serviceError("device_project_not_found", 404);
const existing = projectValues(hosts, input.projectRef)
.find((item) => item.hostKey === input.hostKey);
const hostRef = existing?.hostRef || `host:${randomUUID()}`;
const host = {
hostRef,
projectRef: input.projectRef,
hostKey: input.hostKey,
displayName: input.displayName,
providerRef: input.providerRef ?? null,
externalRef: input.externalRef ?? null,
managementCredentialConfigured: Boolean(input.managementCredentialRef),
lifecycleState: input.lifecycleState ?? "provisioning",
ontology: previewOntology("infrastructure.host"),
};
hosts.set(hostRef, host);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_host"));
return { replayed: false, result: { created: !existing, host } };
}
if (command === "infrastructure-endpoints:ensure") {
const host = hosts.get(input.hostRef);
if (!host || host.projectRef !== input.projectRef) throw serviceError("device_host_not_found", 404);
const existing = projectValues(endpoints, input.projectRef)
.find((item) => item.hostRef === input.hostRef && item.endpointKey === input.endpointKey);
const endpointRef = existing?.endpointRef || `endpoint:${randomUUID()}`;
const endpoint = {
endpointRef,
projectRef: input.projectRef,
hostRef: input.hostRef,
endpointKey: input.endpointKey,
purpose: input.purpose,
endpointUri: input.endpointUri,
lifecycleState: input.lifecycleState ?? "active",
ontology: previewOntology("infrastructure.endpoint"),
};
endpoints.set(endpointRef, endpoint);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_endpoint"));
return { replayed: false, result: { created: !existing, endpoint } };
}
if (command === "infrastructure-deployments:ensure") {
const host = hosts.get(input.hostRef);
if (!host || host.projectRef !== input.projectRef) throw serviceError("device_host_not_found", 404);
const existing = projectValues(deployments, input.projectRef)
.find((item) => item.deploymentKey === input.deploymentKey);
const deploymentRef = existing?.deploymentRef || `deployment:${randomUUID()}`;
const deployment = {
deploymentRef,
projectRef: input.projectRef,
hostRef: input.hostRef,
deploymentKey: input.deploymentKey,
displayName: input.displayName,
artifactRef: input.artifactRef,
artifactDigest: input.artifactDigest,
lifecycleState: input.lifecycleState ?? "desired",
ontology: previewOntology("infrastructure.deployment"),
};
deployments.set(deploymentRef, deployment);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_deployment"));
return { replayed: false, result: { created: !existing, deployment } };
}
if (command === "infrastructure-service-instances:ensure") {
const host = hosts.get(input.hostRef);
const deployment = deployments.get(input.deploymentRef);
if (!host || host.projectRef !== input.projectRef) throw serviceError("device_host_not_found", 404);
if (!deployment || deployment.projectRef !== input.projectRef) {
throw serviceError("device_deployment_not_found", 404);
}
const existing = projectValues(serviceInstances, input.projectRef)
.find((item) => item.hostRef === input.hostRef && item.serviceKey === input.serviceKey);
const serviceInstanceRef = existing?.serviceInstanceRef || `service-instance:${randomUUID()}`;
const serviceInstance = {
serviceInstanceRef,
projectRef: input.projectRef,
hostRef: input.hostRef,
deploymentRef: input.deploymentRef,
edgeRef: input.edgeRef ?? null,
serviceKey: input.serviceKey,
displayName: input.displayName,
serviceRole: input.serviceRole,
lifecycleState: input.lifecycleState ?? "provisioning",
ontology: previewOntology("infrastructure.service_instance"),
};
serviceInstances.set(serviceInstanceRef, serviceInstance);
audit(actor, input.projectRef, createdEvent(existing, "infrastructure_service_instance"));
return { replayed: false, result: { created: !existing, serviceInstance } };
}
if (command === "health-observations:record") {
const healthObservationRef = `health-observation:${randomUUID()}`;
const healthObservation = {
healthObservationRef,
projectRef: input.projectRef,
subjectKind: input.subjectKind,
subjectRef: input.subjectRef,
observedState: input.observedState,
evidenceClass: input.evidenceClass,
observedAt: input.observedAt,
expiresAt: input.expiresAt,
};
healthObservations.set(healthObservationRef, healthObservation);
audit(actor, input.projectRef, "health_observation.recorded");
return { replayed: false, result: { recorded: true, healthObservation } };
}
if (command === "devices:update") {
const device = devices.get(input.deviceRef);
if (!device || device.projectRef !== input.projectRef) {
throw serviceError("device_not_found", 404);
}
if (typeof input.displayName !== "string" || !input.displayName.trim()) {
throw serviceError("device_display_name_invalid", 400);
}
const updated = {
...device,
displayName: input.displayName.trim(),
integrationDeviceId: typeof input.integrationDeviceId === "string"
? input.integrationDeviceId.trim() || null
: null,
updatedAt: now(),
};
devices.set(input.deviceRef, updated);
audit(actor, input.projectRef, "device.updated");
return { replayed: false, result: { updated: true, device: updated } };
}
throw serviceError("device_manager_command_invalid", 404);
},
snapshot() {
return {
ownerScopes,
projects,
collections,
adapterPackages,
adapterVersions,
modelProfiles,
edges,
routes,
enrollments,
devices,
sessions,
bindings,
grants,
configurationRevisions,
configurationStates,
commands,
auditEvents,
};
},
};
}
function createdEvent(existing, resource) {
return `${resource}.${existing ? "updated" : "created"}`;
}
function previewOntology(entityId) {
return { entityId, catalogHash: "229c61c02a790906" };
}
function requirePlatformOwner(actor) {
if (actor?.hubRole !== "owner") {
throw serviceError("device_platform_catalog_access_denied", 403);
}
}
function seedArusnaviB2Preview({
ownerScopes,
projects,
devices,
modelProfiles,
edges,
routes,
sessions,
configurationStates,
}) {
const timestamp = new Date().toISOString();
const ownerScopeRef = "owner-scope:78da71d5-f48f-4de0-8e47-729f6d644151";
const projectRef = "project:ad7b357c-c7ac-4bf8-a638-c7f956e9aa71";
const deviceRef = "device:b6a55921-7888-44b5-a93e-241aa2fdd3d7";
const edgeRef = "edge:73da0c42-a641-4559-b8f7-23509b60bfe9";
const routeRef = "route:fef9b7a0-a462-4d68-9991-af026203368b";
const sessionRef = "session:57ead610-47de-45f7-a42d-fbe4fa0aba38";
const scope = {
ownerScopeRef,
scopeKind: "personal",
ownerRef: "user:local-device-admin",
displayName: "Local Device Admin",
lifecycleState: "active",
};
ownerScopes.set("personal:user:local-device-admin", scope);
projects.set(projectRef, {
projectRef,
projectKey: "arusnavi-b2-preview",
name: "ARUSNAVI B2 preview",
description: "Локальная визуальная фикстура пилотного ARUSNAVI B2",
lifecycleState: "active",
ownerScope: scope,
access: { projectRole: "owner", capabilities: ownerCapabilities },
counts: { devices: 1, collections: 0, discoveries: 0 },
createdAt: timestamp,
updatedAt: timestamp,
});
modelProfiles.set("arusnavi.b2.internal.v1", {
modelProfileRef: "arusnavi.b2.internal.v1",
adapterVersionRef: null,
schemaVersion: "1.0.0",
vendor: "ARUSNAVI",
model: "B2",
deviceType: "tracker",
protocol: "INTERNAL",
schemaArtifactRef: "schema:arusnavi.b2.internal.v1",
profileDigest: null,
capabilities: ["telemetry", "configuration", "commands"],
lifecycleState: "active",
createdAt: timestamp,
updatedAt: timestamp,
});
edges.set(edgeRef, {
edgeRef,
edgeKey: "preview-edge",
displayName: "Preview VPS edge",
deploymentRef: "deployment:preview",
lifecycleState: "active",
channel: {
lifecycleState: "active",
generationRef: "channel-generation:preview",
runtimeState: "accepted",
lastErrorCode: null,
},
createdAt: timestamp,
updatedAt: timestamp,
});
routes.set(routeRef, {
routeRef,
projectRef,
routeKey: "preview-b2-route",
displayName: "B2 direct preview",
edgeRef,
edgeName: "Preview VPS edge",
modelProfileRef: "arusnavi.b2.internal.v1",
profileName: "ARUSNAVI B2",
listenerRef: "listener:preview",
protocol: "INTERNAL",
direction: "bidirectional",
lifecycleState: "active",
sessionCount: 1,
activeSessionCount: 1,
createdAt: timestamp,
updatedAt: timestamp,
});
devices.set(deviceRef, {
projectRef,
deviceRef,
deviceKey: "pilot-b2-preview",
displayName: "Пилотный B2",
integrationDeviceId: "8028",
modelProfileRef: "arusnavi.b2.internal.v1",
lifecycleState: "active",
identifier: {
kind: "imei",
masked: "***********1088",
value: "863151070211088",
},
session: { state: "online", lastSeenAt: timestamp },
reported: {
observedAt: timestamp,
identity: {
imei: "863151070211088",
iccid1: "****************1111",
iccid2: "****************2222",
},
firmware: { currentVersion: "0.02", appliedAt: timestamp, availableVersion: "0.05" },
configuration: {
monitoring: {
servers: [
{ host: "legacy.example.invalid", port: 20623, protocol: "INTERNAL", identity: "0" },
{ host: "direct.example.invalid", port: 9921, protocol: "INTERNAL", identity: "0" },
],
},
transmission: { navigation: { position: true, motion: true, hdop: false } },
trajectory: {
normal: { courseDeltaDegrees: 15, speedDeltaKph: 10, distanceMeters: 15, parkingIntervalSeconds: 15 },
roaming: { courseDeltaDegrees: 20, speedDeltaKph: 50, distanceMeters: 1000, parkingIntervalSeconds: 300 },
},
navigation: {
sources: { satellite: true, wifi: false, lbs: false, tag: false },
constellations: { gps: true, glonass: true, galileo: false, beidou: false },
filter: { minimumSatellites: 4, maximumHdopTimesTen: 30 },
},
},
telemetry: {
navigation: {
latitude: "55.7500",
longitude: "37.6200",
speedKph: 18,
altitudeMeters: 156,
satellites: 12,
courseDegrees: 84,
hdop: 1.2,
},
gsm: { signal: 79, operator: "preview", lac: "masked", cid: "masked" },
system: { externalVoltageMv: 13240, internalVoltageMv: 4120, status: "Норма" },
},
},
createdAt: timestamp,
updatedAt: timestamp,
});
sessions.set(sessionRef, {
sessionRef,
projectRef,
routeRef,
routeName: "B2 direct preview",
deviceRef,
deviceName: "Пилотный B2",
protocol: "INTERNAL",
lifecycleState: "online",
connectedAt: timestamp,
lastSeenAt: timestamp,
disconnectedAt: null,
closeReasonCode: null,
frameCount: 1842,
byteCount: 734208,
});
configurationStates.set(deviceRef, {
projectRef,
deviceRef,
deviceName: "Пилотный B2",
desiredConfigurationRevisionRef: null,
appliedConfigurationRevisionRef: null,
appliedAt: null,
updatedAt: timestamp,
});
}
const previewTransitions = Object.freeze({
adapterPackage: Object.freeze({
active: Object.freeze(["active", "retired"]),
retired: Object.freeze(["retired"]),
}),
catalogVersion: Object.freeze({
draft: Object.freeze(["draft", "active", "retired"]),
active: Object.freeze(["active", "retired"]),
retired: Object.freeze(["retired"]),
}),
edge: Object.freeze({
provisioning: Object.freeze(["provisioning", "active", "retired"]),
active: Object.freeze(["active", "suspended", "retired"]),
suspended: Object.freeze(["suspended", "active", "retired"]),
retired: Object.freeze(["retired"]),
}),
route: Object.freeze({
draft: Object.freeze(["draft", "active", "retired"]),
active: Object.freeze(["active", "suspended", "retired"]),
suspended: Object.freeze(["suspended", "active", "retired"]),
retired: Object.freeze(["retired"]),
}),
});
function assertPreviewTransition(previous, next, transitions, code) {
if (!previous) return;
if (!transitions[previous]?.includes(next)) {
throw serviceError(code, 409);
}
}
const ownerCapabilities = Object.freeze([
"project.read",
"project.manage",
"access.manage",
"inventory.read",
"asset.manage",
"device.enroll",
"device.claim",
"device.transfer",
"collection.manage",
"route.manage",
"binding.manage",
"telemetry.observe",
"observation.write",
"infrastructure.read",
"infrastructure.manage",
"configuration.read",
"configuration.manage",
"command.plan",
"command.confirm",
"command.dispatch",
"credential.manage",
"audit.read",
]);
function actorHeaders(actor) {
if (!actor || typeof actor !== "object") throw serviceError("device_actor_required", 401);
return {
"X-NODEDC-User-Ref": actor.userRef,
"X-NODEDC-Hub-Role": actor.hubRole,
"X-NODEDC-Group-Refs": (actor.groupRefs ?? []).join(","),
"X-NODEDC-Owner-Scopes": (actor.ownerScopes ?? [])
.map((scope) => `${scope.scopeKind}=${scope.ownerRef}`)
.join(","),
};
}
function entityId(value, prefix) {
const match = String(value || "").match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw serviceError(`device_${prefix}_ref_invalid`, 400);
return match[1].toLowerCase();
}
function normalizeBaseUrl(value) {
if (typeof value !== "string" || value.trim() === "") {
throw serviceError("device_core_url_required", 503);
}
const url = new URL(value);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
throw serviceError("device_core_url_invalid", 503);
}
url.pathname = url.pathname.replace(/\/$/, "") || "/";
return url;
}
function safeCoreError(value) {
return typeof value === "string" && /^device_[a-z0-9._:-]{2,120}$/.test(value)
? value
: "device_core_unavailable";
}
function serviceError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}