845 lines
26 KiB
JavaScript
845 lines
26 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { randomUUID, X509Certificate } from "node:crypto";
|
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { connect as connectHttp2 } from "node:http2";
|
|
import { createServer as createTcpServer } from "node:net";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { after, before, test } from "node:test";
|
|
|
|
import {
|
|
DEVICE_ADAPTER_ACCEPTANCE_SCHEMA,
|
|
DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
|
DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
|
} from "../../../packages/device-protocol-contract/src/index.mjs";
|
|
import { createDeviceGatewayCoreChannelClient } from
|
|
"../../device-gateway-core/src/runtime.mjs";
|
|
import { createDeviceGatewayIngest } from
|
|
"../../device-control-core/src/gateway-ingest.mjs";
|
|
import { createDeviceEdgeChannelServer } from "../src/runtime.mjs";
|
|
|
|
let fixtureDirectory;
|
|
let certificates;
|
|
|
|
before(async () => {
|
|
fixtureDirectory = await mkdtemp(join(tmpdir(), "nodedc-edge-channel-"));
|
|
certificates = await generateCertificateFixture(fixtureDirectory);
|
|
});
|
|
|
|
after(async () => {
|
|
await rm(fixtureDirectory, { recursive: true, force: true });
|
|
});
|
|
|
|
test("accepts synthetic discovery and durable message results over Core-initiated mTLS", async () => {
|
|
const acceptedMessages = new Map();
|
|
let messageCalls = 0;
|
|
const pair = await startPair({
|
|
observeDiscovery: productionDiscoveryObserver(),
|
|
acceptMessage: async (message) => {
|
|
messageCalls += 1;
|
|
const previous = acceptedMessages.get(message.idempotencyKey);
|
|
if (previous) return { ...previous, replayed: true };
|
|
const acceptance = acceptanceFor(message, false);
|
|
acceptedMessages.set(message.idempotencyKey, acceptance);
|
|
return acceptance;
|
|
},
|
|
});
|
|
|
|
try {
|
|
const discovery = await pair.edge.submitDiscovery(discoverySignal());
|
|
assert.equal(discovery.lifecycleState, "quarantine");
|
|
assert.equal(discovery.identifier.masked, "***********0001");
|
|
assert.equal("created" in discovery, false);
|
|
assert.equal("value" in discovery, false);
|
|
|
|
const message = adapterMessage();
|
|
const first = await pair.edge.submitAdapterMessage(message);
|
|
const replay = await pair.edge.submitAdapterMessage(message);
|
|
assert.equal(first.replayed, false);
|
|
assert.equal(replay.replayed, true);
|
|
assert.equal(messageCalls, 2);
|
|
assert.equal(pair.edge.status().eventsAccepted, 3);
|
|
assert.equal(pair.core.status().eventsAccepted, 3);
|
|
assert.equal(pair.edge.status().trackerIngress, "disabled");
|
|
assert.equal(pair.core.status().commandTransport, "disabled");
|
|
} finally {
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("delivers a normalized host observation over the existing pinned mTLS channel", async () => {
|
|
const recorded = [];
|
|
const pair = await startPair({
|
|
recordHostTelemetry: async (snapshot, context) => {
|
|
recorded.push({ snapshot, context });
|
|
return { status: "recorded" };
|
|
},
|
|
});
|
|
|
|
try {
|
|
const observation = hostTelemetrySnapshot();
|
|
assert.deepEqual(await pair.edge.submitHostTelemetry(observation), {
|
|
status: "recorded",
|
|
});
|
|
assert.equal(recorded.length, 1);
|
|
assert.equal(recorded[0].snapshot.hostKey, "robot2b-b2-edge-vps");
|
|
assert.equal(
|
|
recorded[0].context.authenticatedEdgeRef,
|
|
"edge:pilot-1",
|
|
);
|
|
assert.equal(pair.edge.status().eventsAccepted, 1);
|
|
assert.equal(pair.core.status().eventsAccepted, 1);
|
|
} finally {
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("recovers the claimed device from telemetry after a Core restart and completes a typed command", async () => {
|
|
const commandRef = "command:11111111-1111-4111-8111-111111111111";
|
|
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
|
|
const transportMessageRef = "edge-command:33333333-3333-4333-8333-333333333333";
|
|
const recorded = [];
|
|
let offers = 0;
|
|
const pair = await startPair({
|
|
commandTransport: "typed-service-ping-v1",
|
|
acceptMessage: async (message) => ({
|
|
value: acceptanceFor(message, false),
|
|
claimedDeviceRef: deviceRef,
|
|
}),
|
|
offerCommand: async (offeredDeviceRef) => {
|
|
assert.equal(offeredDeviceRef, deviceRef);
|
|
offers += 1;
|
|
return {
|
|
commandRef,
|
|
commandType: "service.ping",
|
|
accessCode: "123456",
|
|
transportMessageRef,
|
|
};
|
|
},
|
|
recordCommandStatus: async (status) => recorded.push(status),
|
|
});
|
|
|
|
try {
|
|
const receipt = await pair.edge.submitAdapterMessage(adapterMessage());
|
|
assert.equal(receipt.status, "accepted");
|
|
assert.deepEqual(receipt.commandOffer, {
|
|
commandRef,
|
|
commandType: "service.ping",
|
|
accessCode: "123456",
|
|
transportMessageRef,
|
|
});
|
|
assert.equal(offers, 1);
|
|
|
|
await pair.edge.submitCommandStatus({
|
|
commandRef,
|
|
transportMessageRef,
|
|
lifecycleState: "acknowledged",
|
|
resultCode: "serv_ok",
|
|
observedAt: "2026-08-12T17:30:00.000Z",
|
|
sessionRef: "session:pilot-1",
|
|
adapterProfileRef: "arusnavi.internal.b2.v1",
|
|
});
|
|
assert.equal(recorded.length, 1);
|
|
assert.equal(recorded[0].resultCode, "serv_ok");
|
|
assert.equal(pair.core.status().negotiatedCommandTransport, "typed-service-ping-v1");
|
|
} finally {
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("keeps the channel alive and reconnects without losing idempotency", async () => {
|
|
const acceptedMessages = new Map();
|
|
const pair = await startPair({
|
|
keepaliveMs: 20,
|
|
deadPeerMs: 60,
|
|
reconnectMinimumMs: 20,
|
|
reconnectMaximumMs: 40,
|
|
acceptMessage: async (message) => {
|
|
const previous = acceptedMessages.get(message.idempotencyKey);
|
|
if (previous) return { ...previous, replayed: true };
|
|
const acceptance = acceptanceFor(message, false);
|
|
acceptedMessages.set(message.idempotencyKey, acceptance);
|
|
return acceptance;
|
|
},
|
|
});
|
|
|
|
try {
|
|
await delay(120);
|
|
assert.equal(pair.edge.status().channel, "accepted");
|
|
const message = adapterMessage();
|
|
assert.equal((await pair.edge.submitAdapterMessage(message)).replayed, false);
|
|
|
|
pair.edge.disconnectActiveChannel();
|
|
await waitFor(() => pair.core.status().connectionAttempts >= 2
|
|
&& pair.edge.status().channel === "accepted", 2_000);
|
|
assert.equal((await pair.edge.submitAdapterMessage(message)).replayed, true);
|
|
assert.ok(pair.core.status().reconnects >= 1);
|
|
} finally {
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("bounds a stalled TCP/TLS handshake and reconnects", async () => {
|
|
const sockets = new Set();
|
|
const server = createTcpServer((socket) => {
|
|
sockets.add(socket);
|
|
socket.once("close", () => sockets.delete(socket));
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
const core = createCoreClient({
|
|
address: server.address(),
|
|
connectTimeoutMs: 30,
|
|
reconnectMinimumMs: 20,
|
|
reconnectMaximumMs: 20,
|
|
});
|
|
try {
|
|
await core.start();
|
|
await waitFor(() => core.status().connectionAttempts >= 2, 1_000);
|
|
assert.match(core.status().lastErrorCode, /connect_timeout/);
|
|
assert.ok(core.status().reconnects >= 1);
|
|
} finally {
|
|
await core.stop();
|
|
for (const socket of sockets) socket.destroy();
|
|
await new Promise((resolve) => server.close(resolve));
|
|
}
|
|
});
|
|
|
|
test("rotates the Edge certificate through staged overlap and rejects retired identity", async () => {
|
|
const edge = createEdgeServer();
|
|
const address = await edge.start();
|
|
let registration = edgeRegistration(address, [
|
|
{
|
|
generationRef: "trust-generation:1",
|
|
fingerprint: certificates.edge.fingerprint,
|
|
status: "active",
|
|
},
|
|
{
|
|
generationRef: "trust-generation:2",
|
|
fingerprint: certificates.edgeNext.fingerprint,
|
|
status: "staged",
|
|
},
|
|
]);
|
|
const core = createCoreClient({
|
|
address,
|
|
registrationProvider: async () => registration,
|
|
});
|
|
try {
|
|
await core.start();
|
|
await core.waitForReady(2_000);
|
|
await waitFor(() => edge.status().channel === "accepted", 2_000);
|
|
assert.equal(core.status().edgeTrustGeneration, "trust-generation:1");
|
|
|
|
edge.rotateTrust({
|
|
generationRef: "trust-generation:2",
|
|
key: certificates.edgeNext.key,
|
|
cert: certificates.edgeNext.cert,
|
|
ca: certificates.ca,
|
|
allowedCoreFingerprints: [certificates.core.fingerprint],
|
|
});
|
|
await waitFor(() => core.status().connectionAttempts >= 2
|
|
&& core.status().channel === "accepted"
|
|
&& core.status().edgeTrustGeneration === "trust-generation:2"
|
|
&& edge.status().channel === "accepted", 2_000);
|
|
|
|
registration = edgeRegistration(address, [{
|
|
generationRef: "trust-generation:2",
|
|
fingerprint: certificates.edgeNext.fingerprint,
|
|
status: "active",
|
|
}]);
|
|
assert.equal((await edge.submitAdapterMessage(adapterMessage())).status, "accepted");
|
|
|
|
const failuresBeforeRollback = core.status().protocolFailures;
|
|
edge.rotateTrust({
|
|
generationRef: "trust-generation:3",
|
|
key: certificates.edge.key,
|
|
cert: certificates.edge.cert,
|
|
ca: certificates.ca,
|
|
allowedCoreFingerprints: [certificates.core.fingerprint],
|
|
});
|
|
await waitFor(() => core.status().protocolFailures > failuresBeforeRollback, 2_000);
|
|
assert.notEqual(core.status().channel, "accepted");
|
|
} finally {
|
|
await core.stop();
|
|
await edge.stop();
|
|
}
|
|
});
|
|
|
|
test("rejects a revoked Edge registration before opening a channel", async () => {
|
|
const edge = createEdgeServer();
|
|
const address = await edge.start();
|
|
const core = createCoreClient({
|
|
address,
|
|
lifecycleState: "revoked",
|
|
});
|
|
try {
|
|
await core.start();
|
|
await waitFor(() => core.status().connectionAttempts >= 1, 500);
|
|
assert.equal(core.status().channel, "absent");
|
|
assert.equal(edge.status().channelsAccepted, 0);
|
|
assert.match(core.status().lastErrorCode, /registration_inactive/);
|
|
} finally {
|
|
await core.stop();
|
|
await edge.stop();
|
|
}
|
|
});
|
|
|
|
test("rejects an Edge hello from an unexpected channel generation", async () => {
|
|
const edge = createEdgeServer({
|
|
channelGeneration: "generation:edge-unexpected",
|
|
});
|
|
const address = await edge.start();
|
|
const core = createCoreClient({ address });
|
|
try {
|
|
await core.start();
|
|
await waitFor(() => core.status().protocolFailures >= 1, 2_000);
|
|
assert.notEqual(core.status().channel, "accepted");
|
|
assert.match(core.status().lastErrorCode, /channel_generation_mismatch/);
|
|
assert.equal(edge.status().channelsAccepted, 0);
|
|
} finally {
|
|
await core.stop();
|
|
await edge.stop();
|
|
}
|
|
});
|
|
|
|
test("rejects an authenticated but non-allowlisted Core certificate", async () => {
|
|
const edge = createEdgeServer();
|
|
const address = await edge.start();
|
|
const core = createCoreClient({
|
|
address,
|
|
clientCertificate: certificates.intruder,
|
|
});
|
|
try {
|
|
await core.start();
|
|
await waitFor(() => edge.status().channelsRejected >= 1, 2_000);
|
|
assert.equal(edge.status().channelsAccepted, 0);
|
|
assert.equal(edge.status().channel, "absent");
|
|
} finally {
|
|
await core.stop();
|
|
await edge.stop();
|
|
}
|
|
});
|
|
|
|
test("rejects an unknown Edge certificate fingerprint", async () => {
|
|
const edge = createEdgeServer();
|
|
const address = await edge.start();
|
|
const core = createCoreClient({
|
|
address,
|
|
expectedEdgeFingerprint: certificates.intruder.fingerprint,
|
|
});
|
|
try {
|
|
await core.start();
|
|
await waitFor(() => core.status().protocolFailures >= 1, 2_000);
|
|
assert.equal(core.status().channelsAccepted, 0);
|
|
assert.match(core.status().lastErrorCode, /edge_identity_mismatch/);
|
|
} finally {
|
|
await core.stop();
|
|
await edge.stop();
|
|
}
|
|
});
|
|
|
|
test("returns a conclusive rejection when Core cannot accept a package", async () => {
|
|
const pair = await startPair({
|
|
acceptMessage: async () => {
|
|
throw new Error("device_control_core_unavailable");
|
|
},
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
pair.edge.submitAdapterMessage(adapterMessage()),
|
|
/device_control_core_unavailable/,
|
|
);
|
|
assert.equal(pair.edge.status().eventsRejected, 1);
|
|
assert.equal(pair.core.status().eventsRejected, 1);
|
|
} finally {
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("rejects a discovery when Core does not return its durable receipt", async () => {
|
|
const pair = await startPair({
|
|
observeDiscovery: async () => ({
|
|
schemaVersion: "nodedc.device.discovery-view.v1",
|
|
lifecycleState: "quarantine",
|
|
commandTransport: "disabled",
|
|
identifier: { kind: "imei", masked: "***********1088" },
|
|
}),
|
|
});
|
|
try {
|
|
await assert.rejects(
|
|
pair.edge.submitDiscovery(discoverySignal()),
|
|
/device_gateway_core_discovery_receipt_invalid/,
|
|
);
|
|
assert.equal(pair.edge.status().eventsRejected, 1);
|
|
assert.equal(pair.core.status().eventsRejected, 1);
|
|
} finally {
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("isolates tracker session ordering while allowing cross-session progress", async () => {
|
|
let releaseSlow;
|
|
const slowGate = new Promise((resolve) => {
|
|
releaseSlow = resolve;
|
|
});
|
|
const calls = [];
|
|
const pair = await startPair({
|
|
acceptMessage: async (message) => {
|
|
calls.push(message.sessionRef);
|
|
if (message.sessionRef === "session:slow") await slowGate;
|
|
return acceptanceFor(message, false);
|
|
},
|
|
});
|
|
try {
|
|
let slowResolved = false;
|
|
const slow = pair.edge.submitAdapterMessage(adapterMessage({
|
|
sessionRef: "session:slow",
|
|
messageRef: "message:slow-1",
|
|
idempotencyKey: `sha256:${"c".repeat(64)}`,
|
|
})).then((value) => {
|
|
slowResolved = true;
|
|
return value;
|
|
});
|
|
await waitFor(() => calls.includes("session:slow"), 500);
|
|
const fast = await pair.edge.submitAdapterMessage(adapterMessage({
|
|
sessionRef: "session:fast",
|
|
messageRef: "message:fast-1",
|
|
idempotencyKey: `sha256:${"d".repeat(64)}`,
|
|
}));
|
|
assert.equal(fast.status, "accepted");
|
|
assert.equal(slowResolved, false);
|
|
releaseSlow();
|
|
assert.equal((await slow).status, "accepted");
|
|
} finally {
|
|
releaseSlow?.();
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("preserves per-session order and applies a bounded acceptance window", async () => {
|
|
let releaseFirst;
|
|
const firstGate = new Promise((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
const calls = [];
|
|
const pair = await startPair({
|
|
maxPendingAcceptances: 2,
|
|
acceptMessage: async (message) => {
|
|
calls.push(message.messageRef);
|
|
if (message.messageRef === "message:ordered-1") await firstGate;
|
|
return acceptanceFor(message, false);
|
|
},
|
|
});
|
|
try {
|
|
const first = pair.edge.submitAdapterMessage(adapterMessage({
|
|
sessionRef: "session:ordered",
|
|
messageRef: "message:ordered-1",
|
|
idempotencyKey: `sha256:${"e".repeat(64)}`,
|
|
sequence: 1,
|
|
}));
|
|
await waitFor(() => calls.length === 1, 500);
|
|
const second = pair.edge.submitAdapterMessage(adapterMessage({
|
|
sessionRef: "session:ordered",
|
|
messageRef: "message:ordered-2",
|
|
idempotencyKey: `sha256:${"f".repeat(64)}`,
|
|
sequence: 2,
|
|
}));
|
|
await delay(30);
|
|
assert.deepEqual(calls, ["message:ordered-1"]);
|
|
await assert.rejects(
|
|
pair.edge.submitAdapterMessage(adapterMessage({
|
|
sessionRef: "session:overflow",
|
|
messageRef: "message:overflow-1",
|
|
idempotencyKey: `sha256:${"1".repeat(64)}`,
|
|
})),
|
|
/acceptance_window_full/,
|
|
);
|
|
releaseFirst();
|
|
await Promise.all([first, second]);
|
|
assert.deepEqual(calls, ["message:ordered-1", "message:ordered-2"]);
|
|
} finally {
|
|
releaseFirst?.();
|
|
await stopPair(pair);
|
|
}
|
|
});
|
|
|
|
test("closes the logical session on an unknown message kind", async () => {
|
|
const edge = createEdgeServer();
|
|
const address = await edge.start();
|
|
const session = connectHttp2(`https://127.0.0.1:${address.port}`, {
|
|
key: certificates.core.key,
|
|
cert: certificates.core.cert,
|
|
ca: certificates.ca,
|
|
servername: "localhost",
|
|
minVersion: "TLSv1.3",
|
|
maxVersion: "TLSv1.3",
|
|
rejectUnauthorized: true,
|
|
});
|
|
try {
|
|
await onceEvent(session, "connect");
|
|
const request = session.request({
|
|
":method": "POST",
|
|
":path": "/internal/v1/device-edge/channel",
|
|
}, { endStream: false });
|
|
const hello = await readFirstEnvelope(request);
|
|
const invalid = {
|
|
schemaVersion: hello.schemaVersion,
|
|
edgeRegistrationId: hello.edgeRegistrationId,
|
|
channelGeneration: hello.channelGeneration,
|
|
trackerSessionId: "channel:control",
|
|
adapterProfileRef: "channel.control.v1",
|
|
sequence: 1,
|
|
eventAt: new Date().toISOString(),
|
|
receivedAt: new Date().toISOString(),
|
|
payloadBytes: 2,
|
|
messageKind: "tcp.forward",
|
|
correlationId: `correlation:${randomUUID()}`,
|
|
payload: {},
|
|
};
|
|
request.write(`${JSON.stringify(invalid)}\n`);
|
|
await waitFor(() => edge.status().protocolFailures >= 1, 1_000);
|
|
assert.equal(edge.status().channel, "absent");
|
|
request.close();
|
|
} finally {
|
|
session.close();
|
|
await edge.stop();
|
|
}
|
|
});
|
|
|
|
async function startPair(options = {}) {
|
|
const edge = createEdgeServer(options);
|
|
const address = await edge.start();
|
|
const core = createCoreClient({ address, ...options });
|
|
await core.start();
|
|
await core.waitForReady(2_000);
|
|
await waitFor(() => edge.status().channel === "accepted", 2_000);
|
|
return { edge, core };
|
|
}
|
|
|
|
function createEdgeServer(options = {}) {
|
|
const edgeCertificate = options.edgeCertificate ?? certificates.edge;
|
|
return createDeviceEdgeChannelServer({
|
|
edgeRegistrationId: "edge:pilot-1",
|
|
channelGeneration: options.channelGeneration ?? "generation:pilot-1",
|
|
trustGeneration: options.edgeTrustGeneration ?? "trust-generation:1",
|
|
host: "127.0.0.1",
|
|
port: 0,
|
|
tls: {
|
|
key: edgeCertificate.key,
|
|
cert: edgeCertificate.cert,
|
|
ca: certificates.ca,
|
|
allowedCoreFingerprints: options.allowedCoreFingerprints
|
|
?? [certificates.core.fingerprint],
|
|
},
|
|
keepaliveMs: options.keepaliveMs ?? 50,
|
|
deadPeerMs: options.deadPeerMs ?? 150,
|
|
acceptanceTimeoutMs: 500,
|
|
maxPendingAcceptances: options.maxPendingAcceptances,
|
|
commandTransport: options.commandTransport,
|
|
});
|
|
}
|
|
|
|
function createCoreClient(options) {
|
|
const clientCertificate = options.clientCertificate ?? certificates.core;
|
|
const registration = edgeRegistration(options.address,
|
|
options.edgeCertificateIdentities ?? [{
|
|
generationRef: "trust-generation:1",
|
|
fingerprint: options.expectedEdgeFingerprint
|
|
?? certificates.edge.fingerprint,
|
|
status: "active",
|
|
}],
|
|
options.lifecycleState ?? "active");
|
|
return createDeviceGatewayCoreChannelClient({
|
|
...(options.registrationProvider
|
|
? { registrationProvider: options.registrationProvider }
|
|
: { registration }),
|
|
tls: {
|
|
key: clientCertificate.key,
|
|
cert: clientCertificate.cert,
|
|
ca: certificates.ca,
|
|
},
|
|
coreIdentity: "workload:device-gateway-core",
|
|
keepaliveMs: options.keepaliveMs ?? 50,
|
|
deadPeerMs: options.deadPeerMs ?? 150,
|
|
reconnectMinimumMs: options.reconnectMinimumMs ?? 20,
|
|
reconnectMaximumMs: options.reconnectMaximumMs ?? 80,
|
|
connectTimeoutMs: options.connectTimeoutMs,
|
|
random: () => 0,
|
|
observeDiscovery: options.observeDiscovery ?? productionDiscoveryObserver(),
|
|
acceptMessage: options.acceptMessage ?? (async (message) =>
|
|
acceptanceFor(message, false)),
|
|
commandTransport: options.commandTransport,
|
|
offerCommand: options.offerCommand,
|
|
recordCommandStatus: options.recordCommandStatus,
|
|
recordHostTelemetry: options.recordHostTelemetry,
|
|
});
|
|
}
|
|
|
|
function hostTelemetrySnapshot() {
|
|
return {
|
|
schemaVersion: "nodedc.infrastructure.host-telemetry.v1",
|
|
profile: "linux-host-telegraf-v1",
|
|
hostKey: "robot2b-b2-edge-vps",
|
|
observedAt: new Date().toISOString(),
|
|
source: {
|
|
agent: "telegraf",
|
|
agentVersion: "1.38.4",
|
|
collectorRef: "service:nodedc-host-telemetry-agent",
|
|
},
|
|
hardware: {
|
|
hostname: "koffyvngij",
|
|
architecture: "x64",
|
|
platform: "linux",
|
|
kernelRelease: "6.8.0",
|
|
cpuModel: "KVM CPU",
|
|
logicalProcessors: 1,
|
|
},
|
|
cpu: { usagePercent: 20, load1: 0.2, load5: 0.1, load15: 0.05 },
|
|
memory: {
|
|
totalBytes: 1024,
|
|
availableBytes: 700,
|
|
freeBytes: null,
|
|
usedBytes: 324,
|
|
usedPercent: 31.6,
|
|
},
|
|
swap: {
|
|
totalBytes: 0,
|
|
availableBytes: null,
|
|
freeBytes: 0,
|
|
usedBytes: 0,
|
|
usedPercent: 0,
|
|
},
|
|
system: {
|
|
uptimeSeconds: 100,
|
|
users: 1,
|
|
processes: {
|
|
total: 10,
|
|
running: 1,
|
|
sleeping: 9,
|
|
blocked: 0,
|
|
zombies: 0,
|
|
},
|
|
},
|
|
disks: [],
|
|
network: [],
|
|
services: [],
|
|
};
|
|
}
|
|
|
|
function productionDiscoveryObserver() {
|
|
return createDeviceGatewayIngest({
|
|
identifierPepper: "test-only-device-edge-channel-identifier-pepper",
|
|
repository: {
|
|
async upsertQuarantineDiscovery(value) {
|
|
return {
|
|
created: true,
|
|
value: {
|
|
...value.safeView,
|
|
discoveryRef: "discovery:pilot-1",
|
|
},
|
|
};
|
|
},
|
|
async acceptAdapterMessage() {
|
|
throw new Error("device_edge_channel_test_unexpected_ingest_message");
|
|
},
|
|
},
|
|
}).observeDiscovery;
|
|
}
|
|
|
|
function edgeRegistration(address, certificateIdentities, lifecycleState = "active") {
|
|
return {
|
|
edgeRegistrationId: "edge:pilot-1",
|
|
channelGeneration: "generation:pilot-1",
|
|
endpoint: `https://127.0.0.1:${address.port}/`,
|
|
servername: "localhost",
|
|
certificateIdentities,
|
|
lifecycleState,
|
|
};
|
|
}
|
|
|
|
async function stopPair(pair) {
|
|
await pair.core.stop();
|
|
await pair.edge.stop();
|
|
}
|
|
|
|
function discoverySignal() {
|
|
return {
|
|
schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
|
|
sessionRef: "session:pilot-1",
|
|
modelProfileRef: "arusnavi.internal.b2.v1",
|
|
protocol: "INTERNAL",
|
|
observedAt: new Date().toISOString(),
|
|
identifier: { kind: "imei", value: "860000000000001" },
|
|
evidence: {
|
|
transport: "tcp",
|
|
bytesObserved: 10,
|
|
framingStatus: "verified",
|
|
specificationRef: "arusnavi.internal.protocol.v1",
|
|
},
|
|
};
|
|
}
|
|
|
|
function adapterMessage(overrides = {}) {
|
|
return {
|
|
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
|
edgeRef: "edge:pilot-1",
|
|
adapterRef: "arusnavi-b2",
|
|
protocolProfileRef: "arusnavi.internal.b2.v1",
|
|
protocol: "INTERNAL",
|
|
sessionRef: "session:pilot-1",
|
|
messageRef: "message:pilot-1",
|
|
messageType: "telemetry.package",
|
|
sequence: 1,
|
|
observedAt: new Date().toISOString(),
|
|
idempotencyKey: `sha256:${"a".repeat(64)}`,
|
|
identifier: { kind: "imei", value: "860000000000001" },
|
|
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
|
|
payload: {
|
|
packageNumber: 1,
|
|
packetCount: 1,
|
|
byteLength: 11,
|
|
packageDigest: `sha256:${"b".repeat(64)}`,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function acceptanceFor(message, replayed) {
|
|
return {
|
|
schemaVersion: DEVICE_ADAPTER_ACCEPTANCE_SCHEMA,
|
|
acceptanceRef: "acceptance:pilot-1",
|
|
idempotencyKey: message.idempotencyKey,
|
|
status: "accepted",
|
|
replayed,
|
|
acceptedAt: "2026-08-11T12:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
async function generateCertificateFixture(directory) {
|
|
runOpenSsl(directory, [
|
|
"req", "-x509", "-newkey", "rsa:2048", "-nodes", "-sha256",
|
|
"-days", "1", "-subj", "/CN=NODEDC Test Device Edge CA",
|
|
"-addext", "basicConstraints=critical,CA:TRUE",
|
|
"-addext", "keyUsage=critical,keyCertSign,cRLSign",
|
|
"-keyout", "ca.key", "-out", "ca.crt",
|
|
]);
|
|
await issueCertificate(directory, "edge", "localhost", [
|
|
"subjectAltName=DNS:localhost,IP:127.0.0.1",
|
|
"extendedKeyUsage=serverAuth",
|
|
]);
|
|
await issueCertificate(directory, "edge-next", "localhost", [
|
|
"subjectAltName=DNS:localhost,IP:127.0.0.1",
|
|
"extendedKeyUsage=serverAuth",
|
|
]);
|
|
await issueCertificate(directory, "core", "nodedc-device-gateway-core", [
|
|
"extendedKeyUsage=clientAuth",
|
|
]);
|
|
await issueCertificate(directory, "intruder", "unapproved-core", [
|
|
"extendedKeyUsage=clientAuth",
|
|
]);
|
|
const ca = await readFile(join(directory, "ca.crt"));
|
|
return {
|
|
ca,
|
|
edge: await readCertificate(directory, "edge"),
|
|
edgeNext: await readCertificate(directory, "edge-next"),
|
|
core: await readCertificate(directory, "core"),
|
|
intruder: await readCertificate(directory, "intruder"),
|
|
};
|
|
}
|
|
|
|
async function issueCertificate(directory, name, commonName, extensions) {
|
|
runOpenSsl(directory, [
|
|
"req", "-newkey", "rsa:2048", "-nodes", "-sha256",
|
|
"-subj", `/CN=${commonName}`,
|
|
"-keyout", `${name}.key`, "-out", `${name}.csr`,
|
|
]);
|
|
const extensionFile = `${name}.ext`;
|
|
await writeFile(join(directory, extensionFile), [
|
|
"basicConstraints=critical,CA:FALSE",
|
|
"keyUsage=critical,digitalSignature,keyEncipherment",
|
|
...extensions,
|
|
].join("\n"));
|
|
runOpenSsl(directory, [
|
|
"x509", "-req", "-sha256", "-days", "1",
|
|
"-in", `${name}.csr`, "-CA", "ca.crt", "-CAkey", "ca.key",
|
|
"-CAcreateserial", "-extfile", extensionFile, "-out", `${name}.crt`,
|
|
]);
|
|
}
|
|
|
|
async function readCertificate(directory, name) {
|
|
const cert = await readFile(join(directory, `${name}.crt`));
|
|
return {
|
|
key: await readFile(join(directory, `${name}.key`)),
|
|
cert,
|
|
fingerprint: new X509Certificate(cert).fingerprint256,
|
|
};
|
|
}
|
|
|
|
function runOpenSsl(directory, arguments_) {
|
|
const result = spawnSync("openssl", arguments_, {
|
|
cwd: directory,
|
|
encoding: "utf8",
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(`openssl_failed:${result.stderr}`);
|
|
}
|
|
}
|
|
|
|
function readFirstEnvelope(stream) {
|
|
return new Promise((resolve, reject) => {
|
|
let buffered = "";
|
|
const onData = (chunk) => {
|
|
buffered += chunk.toString("utf8");
|
|
const newline = buffered.indexOf("\n");
|
|
if (newline < 0) return;
|
|
cleanup();
|
|
resolve(JSON.parse(buffered.slice(0, newline)));
|
|
};
|
|
const onError = (error) => {
|
|
cleanup();
|
|
reject(error);
|
|
};
|
|
const cleanup = () => {
|
|
stream.off("data", onData);
|
|
stream.off("error", onError);
|
|
};
|
|
stream.on("data", onData);
|
|
stream.once("error", onError);
|
|
});
|
|
}
|
|
|
|
function onceEvent(emitter, event) {
|
|
return new Promise((resolve, reject) => {
|
|
const onEvent = (...args) => {
|
|
cleanup();
|
|
resolve(args);
|
|
};
|
|
const onError = (error) => {
|
|
cleanup();
|
|
reject(error);
|
|
};
|
|
const cleanup = () => {
|
|
emitter.off(event, onEvent);
|
|
emitter.off("error", onError);
|
|
};
|
|
emitter.once(event, onEvent);
|
|
emitter.once("error", onError);
|
|
});
|
|
}
|
|
|
|
async function waitFor(predicate, timeoutMs) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
if (predicate()) return;
|
|
await delay(10);
|
|
}
|
|
assert.fail("condition_timeout");
|
|
}
|
|
|
|
function delay(milliseconds) {
|
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
}
|