feat(device-plane): add typed B2 service ping transport
This commit is contained in:
@@ -90,7 +90,7 @@ export function createDeviceEdgeChannelServer(options = {}) {
|
||||
status: "ready",
|
||||
transport: "http2-mtls",
|
||||
trustGeneration: trust.generationRef,
|
||||
commandTransport: "disabled",
|
||||
commandTransport: config.commandTransport,
|
||||
}, {
|
||||
trackerSessionId: CHANNEL_TRACKER_SESSION_ID,
|
||||
adapterProfileRef: CHANNEL_PROFILE_REF,
|
||||
@@ -156,7 +156,10 @@ export function createDeviceEdgeChannelServer(options = {}) {
|
||||
if (!result?.discovery) {
|
||||
throw new Error("device_edge_channel_discovery_acceptance_invalid");
|
||||
}
|
||||
return result.discovery;
|
||||
return Object.freeze({
|
||||
...result.discovery,
|
||||
...(result.commandOffer ? { commandOffer: result.commandOffer } : {}),
|
||||
});
|
||||
},
|
||||
async submitAdapterMessage(message) {
|
||||
const normalized = normalizeAdapterMessage(message, {
|
||||
@@ -173,7 +176,24 @@ export function createDeviceEdgeChannelServer(options = {}) {
|
||||
if (acceptance.idempotencyKey !== normalized.idempotencyKey) {
|
||||
throw new Error("device_edge_channel_acceptance_mismatch");
|
||||
}
|
||||
return acceptance;
|
||||
return Object.freeze({
|
||||
...acceptance,
|
||||
...(result.commandOffer ? { commandOffer: result.commandOffer } : {}),
|
||||
});
|
||||
},
|
||||
async submitCommandStatus(status) {
|
||||
const normalized = normalizeCommandStatus(status);
|
||||
const result = await submitEvent("command.status", {
|
||||
status: normalized,
|
||||
}, {
|
||||
trackerSessionId: normalized.sessionRef,
|
||||
adapterProfileRef: normalized.adapterProfileRef,
|
||||
eventAt: normalized.observedAt,
|
||||
});
|
||||
if (result?.status !== "recorded") {
|
||||
throw new Error("device_edge_channel_command_status_invalid");
|
||||
}
|
||||
return Object.freeze({ status: "recorded" });
|
||||
},
|
||||
status() {
|
||||
return Object.freeze({
|
||||
@@ -191,7 +211,7 @@ export function createDeviceEdgeChannelServer(options = {}) {
|
||||
eventsRejected: totalEventsRejected,
|
||||
protocolFailures: totalProtocolFailures,
|
||||
trackerIngress: "disabled",
|
||||
commandTransport: "disabled",
|
||||
commandTransport: config.commandTransport,
|
||||
});
|
||||
},
|
||||
rotateTrust(next) {
|
||||
@@ -271,7 +291,10 @@ export function createDeviceEdgeChannelServer(options = {}) {
|
||||
if (envelope.messageKind !== "channel.accepted") {
|
||||
throw new Error("device_edge_channel_acceptance_required");
|
||||
}
|
||||
if (envelope.payload?.status !== "accepted") {
|
||||
if (
|
||||
envelope.payload?.status !== "accepted"
|
||||
|| envelope.payload?.commandTransport !== config.commandTransport
|
||||
) {
|
||||
throw new Error("device_edge_channel_acceptance_invalid");
|
||||
}
|
||||
state.accepted = true;
|
||||
@@ -410,6 +433,7 @@ function normalizeConfig(options) {
|
||||
return Object.freeze({
|
||||
edgeRegistrationId,
|
||||
channelGeneration,
|
||||
commandTransport: normalizeCommandTransport(options.commandTransport),
|
||||
trust,
|
||||
host: normalizeHost(options.host ?? "127.0.0.1"),
|
||||
port: normalizePort(options.port ?? 443),
|
||||
@@ -442,6 +466,50 @@ function normalizeConfig(options) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeCommandTransport(value) {
|
||||
const normalized = value ?? "disabled";
|
||||
if (!["disabled", "typed-service-ping-v1"].includes(normalized)) {
|
||||
throw new TypeError("device_edge_channel_command_transport_invalid");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeCommandStatus(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new TypeError("device_edge_channel_command_status_invalid");
|
||||
}
|
||||
const commandRef = normalizeRef(value.commandRef, "command_ref");
|
||||
const transportMessageRef = normalizeRef(
|
||||
value.transportMessageRef,
|
||||
"transport_message_ref",
|
||||
);
|
||||
const sessionRef = normalizeRef(value.sessionRef, "tracker_session_ref");
|
||||
const adapterProfileRef = normalizeRef(
|
||||
value.adapterProfileRef,
|
||||
"adapter_profile_ref",
|
||||
);
|
||||
if (!["acknowledged", "unknown"].includes(value.lifecycleState)) {
|
||||
throw new TypeError("device_edge_channel_command_lifecycle_invalid");
|
||||
}
|
||||
const resultCode = String(value.resultCode || "");
|
||||
if (!/^[a-z][a-z0-9._-]{1,63}$/.test(resultCode)) {
|
||||
throw new TypeError("device_edge_channel_command_result_invalid");
|
||||
}
|
||||
const observedAt = new Date(value.observedAt);
|
||||
if (Number.isNaN(observedAt.getTime())) {
|
||||
throw new TypeError("device_edge_channel_command_observed_at_invalid");
|
||||
}
|
||||
return Object.freeze({
|
||||
commandRef,
|
||||
transportMessageRef,
|
||||
sessionRef,
|
||||
adapterProfileRef,
|
||||
lifecycleState: value.lifecycleState,
|
||||
resultCode,
|
||||
observedAt: observedAt.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeTrust(value) {
|
||||
if (!value || typeof value !== "object") {
|
||||
throw new TypeError("device_edge_channel_tls_invalid");
|
||||
|
||||
@@ -68,6 +68,59 @@ test("accepts synthetic discovery and durable message results over Core-initiate
|
||||
}
|
||||
});
|
||||
|
||||
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({
|
||||
@@ -458,6 +511,7 @@ function createEdgeServer(options = {}) {
|
||||
deadPeerMs: options.deadPeerMs ?? 150,
|
||||
acceptanceTimeoutMs: 500,
|
||||
maxPendingAcceptances: options.maxPendingAcceptances,
|
||||
commandTransport: options.commandTransport,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -490,6 +544,9 @@ function createCoreClient(options) {
|
||||
observeDiscovery: options.observeDiscovery ?? productionDiscoveryObserver(),
|
||||
acceptMessage: options.acceptMessage ?? (async (message) =>
|
||||
acceptanceFor(message, false)),
|
||||
commandTransport: options.commandTransport,
|
||||
offerCommand: options.offerCommand,
|
||||
recordCommandStatus: options.recordCommandStatus,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -536,7 +593,7 @@ function discoverySignal() {
|
||||
modelProfileRef: "arusnavi.internal.b2.v1",
|
||||
protocol: "INTERNAL",
|
||||
observedAt: new Date().toISOString(),
|
||||
identifier: { kind: "imei", value: "863151070211088" },
|
||||
identifier: { kind: "imei", value: "860000000000001" },
|
||||
evidence: {
|
||||
transport: "tcp",
|
||||
bytesObserved: 10,
|
||||
@@ -559,7 +616,7 @@ function adapterMessage(overrides = {}) {
|
||||
sequence: 1,
|
||||
observedAt: new Date().toISOString(),
|
||||
idempotencyKey: `sha256:${"a".repeat(64)}`,
|
||||
identifier: { kind: "imei", value: "863151070211088" },
|
||||
identifier: { kind: "imei", value: "860000000000001" },
|
||||
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
|
||||
payload: {
|
||||
packageNumber: 1,
|
||||
|
||||
Reference in New Issue
Block a user