feat(device-manager): add safe device enrollment

This commit is contained in:
Codex
2026-08-13 11:37:36 +03:00
parent a3385e83c4
commit d6c62da470
6 changed files with 396 additions and 14 deletions
@@ -89,6 +89,7 @@ export function createLocalPreviewDeviceCore() {
const modelProfiles = new Map();
const edges = new Map();
const routes = new Map();
const enrollments = new Map();
const bindings = new Map();
const grants = new Map();
const configurationRevisions = new Map();
@@ -131,7 +132,7 @@ export function createLocalPreviewDeviceCore() {
project: projectSummary(project),
devices: [],
discoveries: [],
enrollments: [],
enrollments: projectValues(enrollments, projectRef),
collections: projectValues(collections, projectRef)
.map(({ projectRef: _projectRef, ...collection }) => collection),
adapterPackages: [...adapterPackages.values()],
@@ -268,6 +269,12 @@ export function createLocalPreviewDeviceCore() {
);
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,
@@ -282,15 +289,25 @@ export function createLocalPreviewDeviceCore() {
}
if (command === "adapter-versions:register") {
requirePlatformOwner(actor);
if (!adapterPackages.has(input.adapterPackageRef)) {
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,
@@ -308,10 +325,21 @@ export function createLocalPreviewDeviceCore() {
}
if (command === "model-profiles:register") {
requirePlatformOwner(actor);
if (!adapterVersions.has(input.adapterVersionRef)) {
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,
@@ -323,7 +351,7 @@ export function createLocalPreviewDeviceCore() {
schemaArtifactRef: input.schemaArtifactRef,
profileDigest: input.profileDigest,
capabilities: input.capabilities ?? [],
lifecycleState: input.lifecycleState ?? "draft",
lifecycleState,
createdAt: existing?.createdAt || now(),
updatedAt: now(),
};
@@ -336,6 +364,12 @@ export function createLocalPreviewDeviceCore() {
(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,
@@ -360,6 +394,19 @@ export function createLocalPreviewDeviceCore() {
(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,
@@ -372,7 +419,7 @@ export function createLocalPreviewDeviceCore() {
listenerRef: input.listenerRef,
protocol: input.protocol,
direction: input.direction ?? "telemetry",
lifecycleState: input.lifecycleState ?? "draft",
lifecycleState,
sessionCount: 0,
activeSessionCount: 0,
createdAt: existing?.createdAt || now(),
@@ -432,7 +479,62 @@ export function createLocalPreviewDeviceCore() {
throw serviceError("device_configuration_revision_not_found", 404);
}
if (command === "enrollment-intents:ensure") {
throw serviceError("device_enrollment_secure_input_required", 409);
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);
@@ -449,6 +551,7 @@ export function createLocalPreviewDeviceCore() {
modelProfiles,
edges,
routes,
enrollments,
bindings,
grants,
configurationRevisions,
@@ -469,6 +572,37 @@ function requirePlatformOwner(actor) {
}
}
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",