feat(device-plane): supervise pinned edge channels in core
This commit is contained in:
@@ -27,6 +27,12 @@ test("health reports database readiness and disabled command transport", async (
|
||||
database: "ready",
|
||||
discoveryIngest: "disabled",
|
||||
managementApi: "disabled",
|
||||
edgeChannels: {
|
||||
enabled: false,
|
||||
configured: 0,
|
||||
accepted: 0,
|
||||
degraded: 0,
|
||||
},
|
||||
commandTransport: "disabled",
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
const migrationUrl = new URL(
|
||||
"../migrations/013_device_edge_channels.sql",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
test("Edge channel migration extends the canonical Edge without storing keys", async () => {
|
||||
const sql = await readFile(migrationUrl, "utf8");
|
||||
|
||||
assert.match(sql, /alter table device_edges/);
|
||||
assert.match(sql, /channel_endpoint text/);
|
||||
assert.match(sql, /channel_generation_ref text/);
|
||||
assert.match(sql, /channel_trust_bundle_ref text/);
|
||||
assert.match(sql, /channel_certificate_identities jsonb/);
|
||||
assert.match(sql, /channel_lifecycle_state in \('disabled', 'active', 'revoked'\)/);
|
||||
assert.match(sql, /where channel_lifecycle_state = 'active'/);
|
||||
assert.doesNotMatch(sql, /private[_ ]?key/i);
|
||||
assert.doesNotMatch(sql, /password/i);
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
createDeviceEdgeChannelSupervisor,
|
||||
} from "../src/edge-channel-supervisor.mjs";
|
||||
|
||||
test("supervisor reconciles one in-process client per active Edge", async () => {
|
||||
let registrations = [registration("channel-generation:1")];
|
||||
const clients = [];
|
||||
const supervisor = createDeviceEdgeChannelSupervisor({
|
||||
repository: {
|
||||
listActiveEdgeChannelRegistrations: async () => registrations,
|
||||
},
|
||||
gatewayIngest: {
|
||||
observeDiscovery: async () => ({}),
|
||||
acceptMessage: async () => ({}),
|
||||
},
|
||||
coreIdentity: {
|
||||
identityRef: "workload:device-control-core",
|
||||
key: "test-key",
|
||||
cert: "test-cert",
|
||||
},
|
||||
readPeerTrust: async () => "test-edge-certificate",
|
||||
clientFactory: (options) => {
|
||||
const state = { started: 0, stopped: 0, options };
|
||||
clients.push(state);
|
||||
return {
|
||||
async start() { state.started += 1; },
|
||||
async stop() { state.stopped += 1; },
|
||||
status: () => ({ channel: "accepted", lastErrorCode: null }),
|
||||
};
|
||||
},
|
||||
reconcileIntervalMs: 300_000,
|
||||
});
|
||||
|
||||
await supervisor.start();
|
||||
assert.equal(clients.length, 1);
|
||||
assert.equal(supervisor.status().accepted, 1);
|
||||
assert.equal(clients[0].options.registration.channelGeneration, "channel-generation:1");
|
||||
|
||||
await supervisor.reconcile();
|
||||
assert.equal(clients.length, 1);
|
||||
|
||||
registrations = [registration("channel-generation:2")];
|
||||
await supervisor.reconcile();
|
||||
assert.equal(clients.length, 2);
|
||||
assert.equal(clients[0].stopped, 1);
|
||||
assert.equal(clients[1].started, 1);
|
||||
|
||||
registrations = [];
|
||||
await supervisor.reconcile();
|
||||
assert.equal(clients[1].stopped, 1);
|
||||
assert.equal(supervisor.status().configured, 0);
|
||||
assert.equal(JSON.stringify(supervisor.status()).includes("155.212"), false);
|
||||
await supervisor.stop();
|
||||
});
|
||||
|
||||
test("supervisor keeps a failed trust enrollment isolated from other Edges", async () => {
|
||||
const supervisor = createDeviceEdgeChannelSupervisor({
|
||||
repository: {
|
||||
listActiveEdgeChannelRegistrations: async () => [
|
||||
registration("channel-generation:1", "edge:good"),
|
||||
registration("channel-generation:1", "edge:bad"),
|
||||
],
|
||||
},
|
||||
gatewayIngest: {
|
||||
observeDiscovery: async () => ({}),
|
||||
acceptMessage: async () => ({}),
|
||||
},
|
||||
coreIdentity: {
|
||||
identityRef: "workload:device-control-core",
|
||||
key: "test-key",
|
||||
cert: "test-cert",
|
||||
},
|
||||
readPeerTrust: async ({ registration: value }) => {
|
||||
if (value.edgeRegistrationId === "edge:bad") {
|
||||
throw new Error("device_edge_channel_trust_bundle_identity_mismatch");
|
||||
}
|
||||
return "test-edge-certificate";
|
||||
},
|
||||
clientFactory: () => ({
|
||||
start: async () => undefined,
|
||||
stop: async () => undefined,
|
||||
status: () => ({ channel: "accepted", lastErrorCode: null }),
|
||||
}),
|
||||
reconcileIntervalMs: 300_000,
|
||||
});
|
||||
|
||||
await supervisor.start();
|
||||
const status = supervisor.status();
|
||||
assert.equal(status.configured, 2);
|
||||
assert.equal(status.accepted, 1);
|
||||
assert.equal(status.degraded, 1);
|
||||
assert.equal(status.commandTransport, "disabled");
|
||||
assert.equal(status.edges.find((item) => item.edgeRegistrationId === "edge:bad")
|
||||
.lastErrorCode, "device_edge_channel_trust_bundle_identity_mismatch");
|
||||
await supervisor.stop();
|
||||
});
|
||||
|
||||
function registration(channelGeneration, edgeRegistrationId = "edge:pilot") {
|
||||
return {
|
||||
edgeRegistrationId,
|
||||
endpoint: "https://155.212.211.15:8443/",
|
||||
servername: "155.212.211.15",
|
||||
channelGeneration,
|
||||
trustBundleRef: "edge-trust:moscow-edge",
|
||||
certificateIdentities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
lifecycleState: "active",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
||||
DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
} from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
import { createDeviceGatewayIngest } from "../src/gateway-ingest.mjs";
|
||||
|
||||
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
|
||||
const rawImei = "000000000000001";
|
||||
|
||||
test("shared gateway ingest masks identifiers for HTTP and Edge callers", async () => {
|
||||
const stored = [];
|
||||
const ingest = createDeviceGatewayIngest({
|
||||
identifierPepper,
|
||||
repository: {
|
||||
async upsertQuarantineDiscovery(value) {
|
||||
stored.push(value);
|
||||
return {
|
||||
created: true,
|
||||
value: { ...value.safeView, discoveryRef: "discovery:test" },
|
||||
};
|
||||
},
|
||||
async acceptAdapterMessage(value) {
|
||||
stored.push(value);
|
||||
return {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test",
|
||||
idempotencyKey: value.safeView.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const discovery = await ingest.observeDiscovery(discoverySignal());
|
||||
const acceptance = await ingest.acceptMessage(adapterMessage());
|
||||
|
||||
assert.equal(discovery.value.identifier.masked, "***********0001");
|
||||
assert.equal(acceptance.status, "accepted");
|
||||
assert.match(stored[0].identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.match(stored[1].requestDigest, /^sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(JSON.stringify(stored).includes(rawImei), false);
|
||||
});
|
||||
|
||||
function discoverySignal() {
|
||||
return {
|
||||
schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
||||
sessionRef: "session:test",
|
||||
modelProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
observedAt: "2026-08-11T12:00:00.000Z",
|
||||
identifier: { kind: "imei", value: rawImei },
|
||||
evidence: {
|
||||
transport: "tcp",
|
||||
bytesObserved: 16,
|
||||
framingStatus: "verified",
|
||||
specificationRef: "arusnavi.internal.framing.test-v1",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function adapterMessage() {
|
||||
return {
|
||||
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
||||
edgeRef: "edge:test",
|
||||
adapterRef: "arusnavi-b2",
|
||||
protocolProfileRef: "arusnavi.b2.internal.v1",
|
||||
protocol: "INTERNAL",
|
||||
sessionRef: "session:test",
|
||||
messageRef: "package:1:test",
|
||||
messageType: "telemetry.package",
|
||||
sequence: 1,
|
||||
observedAt: "2026-08-11T12:00:00.000Z",
|
||||
idempotencyKey: `sha256:${"a".repeat(64)}`,
|
||||
identifier: { kind: "imei", value: rawImei },
|
||||
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
|
||||
payload: {
|
||||
packageNumber: 1,
|
||||
packetCount: 1,
|
||||
packageDigest: `sha256:${"b".repeat(64)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -143,6 +143,62 @@ test("shared catalog and Edge authority requires the Hub owner ceiling", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes only a pinned Core-initiated public Edge channel", () => {
|
||||
const command = normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "moscow-edge",
|
||||
displayName: "Moscow Edge",
|
||||
deploymentRef: "deployment:device-edge/moscow-1",
|
||||
lifecycleState: "active",
|
||||
channel: {
|
||||
endpoint: "https://155.212.211.15:8443/",
|
||||
servername: "155.212.211.15",
|
||||
generationRef: "channel-generation:1",
|
||||
trustBundleRef: "edge-trust:moscow-edge",
|
||||
certificateIdentities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
lifecycleState: "active",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(command.channel.endpoint, "https://155.212.211.15:8443/");
|
||||
assert.equal(command.channel.lifecycleState, "active");
|
||||
assert.equal(command.channel.certificateIdentities.length, 1);
|
||||
for (const endpoint of [
|
||||
"https://127.0.0.1:8443/",
|
||||
"https://192.168.1.1:8443/",
|
||||
"https://155.212.211.15:9921/",
|
||||
"http://155.212.211.15:8443/",
|
||||
]) {
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "bad-edge",
|
||||
displayName: "Bad Edge",
|
||||
channel: { ...command.channel, endpoint },
|
||||
}),
|
||||
/device_edge_channel_endpoint_invalid/,
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "bad-edge",
|
||||
displayName: "Bad Edge",
|
||||
channel: { ...command.channel, servername: "example.invalid" },
|
||||
}),
|
||||
/device_edge_channel_servername_mismatch/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeInfrastructureManagementCommand("edge.ensure", {
|
||||
edgeKey: "bad-edge",
|
||||
displayName: "Bad Edge",
|
||||
channel: { lifecycleState: "disabled", endpoint: command.channel.endpoint },
|
||||
}),
|
||||
/device_edge_channel_disabled_configuration_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
function actor(hubRole) {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:platform-admin",
|
||||
|
||||
@@ -192,6 +192,48 @@ test("stores enrollment digest but returns and audits only its masked projection
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
test("lists only bounded active Edge channel registrations without secrets", async () => {
|
||||
const client = scriptedClient([
|
||||
step("begin transaction read only"),
|
||||
step("from device_edges", {
|
||||
rows: [{
|
||||
id: edgeId,
|
||||
channel_endpoint: "https://155.212.211.15:8443/",
|
||||
channel_servername: "155.212.211.15",
|
||||
channel_generation_ref: "channel-generation:1",
|
||||
channel_trust_bundle_ref: "edge-trust:moscow-edge",
|
||||
channel_certificate_identities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
channel_lifecycle_state: "active",
|
||||
}],
|
||||
}),
|
||||
step("commit"),
|
||||
]);
|
||||
const repository = repositoryWithClient(client);
|
||||
|
||||
const registrations = await repository.listActiveEdgeChannelRegistrations(8);
|
||||
|
||||
assert.deepEqual(registrations[0], {
|
||||
edgeRegistrationId: `edge:${edgeId}`,
|
||||
endpoint: "https://155.212.211.15:8443/",
|
||||
servername: "155.212.211.15",
|
||||
channelGeneration: "channel-generation:1",
|
||||
trustBundleRef: "edge-trust:moscow-edge",
|
||||
certificateIdentities: [{
|
||||
generationRef: "edge-identity:1",
|
||||
fingerprint: "AA:".repeat(31) + "AA",
|
||||
status: "active",
|
||||
}],
|
||||
lifecycleState: "active",
|
||||
});
|
||||
assert.equal(JSON.stringify(registrations).includes("private"), false);
|
||||
assert.equal(client.remaining(), 0);
|
||||
assert.equal(client.released, true);
|
||||
});
|
||||
|
||||
function managementActor(hubRole) {
|
||||
return normalizeManagementActor({
|
||||
userRef: "user:platform-owner",
|
||||
|
||||
Reference in New Issue
Block a user