feat: establish standalone Device Core repository
This commit is contained in:
@@ -0,0 +1,765 @@
|
||||
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("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,
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { normalizeRuntimeDocument } from "../src/server.mjs";
|
||||
|
||||
const fingerprint = Array.from({ length: 32 }, () => "AB").join(":");
|
||||
|
||||
test("runtime document accepts one generation-bound Core identity", () => {
|
||||
const result = normalizeRuntimeDocument({
|
||||
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
|
||||
edgeRegistrationId: "edge:moscow-vps-1",
|
||||
channelGeneration: "channel:1",
|
||||
trustGeneration: "trust:1",
|
||||
allowedCoreFingerprints: [fingerprint],
|
||||
});
|
||||
|
||||
assert.equal(result.edgeRegistrationId, "edge:moscow-vps-1");
|
||||
assert.deepEqual(result.allowedCoreFingerprints, [fingerprint]);
|
||||
});
|
||||
|
||||
test("runtime document rejects hidden authority and missing identity", () => {
|
||||
assert.throws(() => normalizeRuntimeDocument({
|
||||
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
|
||||
edgeRegistrationId: "edge:moscow-vps-1",
|
||||
channelGeneration: "channel:1",
|
||||
trustGeneration: "trust:1",
|
||||
allowedCoreFingerprints: [fingerprint],
|
||||
endpoint: "https://attacker.invalid",
|
||||
}), /runtime_config_key_invalid/);
|
||||
|
||||
assert.throws(() => normalizeRuntimeDocument({
|
||||
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
|
||||
edgeRegistrationId: "edge:moscow-vps-1",
|
||||
channelGeneration: "channel:1",
|
||||
trustGeneration: "trust:1",
|
||||
allowedCoreFingerprints: [],
|
||||
}), /runtime_core_identity_invalid/);
|
||||
|
||||
assert.throws(() => normalizeRuntimeDocument({
|
||||
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
|
||||
edgeRegistrationId: "edge:moscow-vps-1",
|
||||
channelGeneration: "channel:1",
|
||||
trustGeneration: "trust:1",
|
||||
allowedCoreFingerprints: [fingerprint, fingerprint],
|
||||
}), /runtime_core_identity_invalid/);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
normalizeTrackerIngressConfiguration,
|
||||
} from "../../../vps/edge-process/device-edge-runtime.mjs";
|
||||
|
||||
test("VPS tracker ingress resolves one allowlisted profile within bounded limits", () => {
|
||||
const config = normalizeTrackerIngressConfiguration({}, "edge:moscow-vps-1");
|
||||
|
||||
assert.equal(config.protocolProfileRef, "arusnavi.b2.internal.v1");
|
||||
assert.equal(config.tcpHost, "0.0.0.0");
|
||||
assert.equal(config.tcpPort, 9921);
|
||||
assert.equal(config.maxConcurrentSessions, 128);
|
||||
assert.equal(config.maxSessionsPerAddress, 16);
|
||||
assert.equal(config.maxConnectionsPerMinutePerAddress, 60);
|
||||
assert.equal(config.maxAggregateBufferedBytes, 32 * 1024 * 1024);
|
||||
});
|
||||
|
||||
test("VPS tracker ingress rejects hidden bind and non-allowlisted adapters", () => {
|
||||
assert.throws(() => normalizeTrackerIngressConfiguration({
|
||||
DEVICE_GATEWAY_TCP_HOST: "127.0.0.1",
|
||||
}, "edge:moscow-vps-1"), /public_host_invalid/);
|
||||
|
||||
assert.throws(() => normalizeTrackerIngressConfiguration({
|
||||
DEVICE_GATEWAY_PROTOCOL_PROFILE_REF: "vendor.unknown.v1",
|
||||
}, "edge:moscow-vps-1"), /profile_not_allowlisted/);
|
||||
|
||||
assert.throws(() => normalizeTrackerIngressConfiguration({
|
||||
DEVICE_GATEWAY_MAX_SESSIONS: "129",
|
||||
}, "edge:moscow-vps-1"), /session_limit_invalid/);
|
||||
});
|
||||
Reference in New Issue
Block a user