feat(device-plane): add typed B2 service ping transport

This commit is contained in:
Codex
2026-08-12 22:57:09 +03:00
parent bdb85cb4f7
commit 4b73a15765
24 changed files with 1322 additions and 88 deletions
@@ -56,6 +56,7 @@ export function createControlCoreApp({
managementToken = "",
gatewayIngest = null,
edgeChannelStatusProvider = null,
typedCommandRuntime = null,
} = {}) {
if (!repository || typeof repository.health !== "function") {
throw new TypeError("device_repository_required");
@@ -97,6 +98,15 @@ export function createControlCoreApp({
) {
throw new TypeError("device_gateway_ingest_invalid");
}
if (
typedCommandRuntime != null
&& (
typeof typedCommandRuntime.planServicePing !== "function"
|| typeof typedCommandRuntime.status !== "function"
)
) {
throw new TypeError("device_typed_command_runtime_invalid");
}
if (
edgeChannelStatusProvider != null
&& typeof edgeChannelStatusProvider !== "function"
@@ -126,7 +136,47 @@ export function createControlCoreApp({
edgeChannels: edgeChannelStatusProvider
? edgeChannelStatusProvider()
: { enabled: false, configured: 0, accepted: 0, degraded: 0 },
commandTransport: "disabled",
commandTransport: typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
});
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/commands:service-ping"
) {
if (!managementApiEnabled || !typedCommandRuntime) {
return writeJson(response, 404, {
ok: false,
error: "device_command_transport_disabled",
});
}
if (!matchesBearer(request.headers.authorization, managementToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_management_auth_required",
});
}
const idempotencyKey = normalizeIdempotencyKey(
request.headers["idempotency-key"],
);
const actor = managementActorFromHeaders(request.headers);
const input = await readJsonBody(request, 8 * 1024);
const execution = await typedCommandRuntime.planServicePing({
idempotencyKey,
actor,
input,
});
response.setHeader("Idempotency-Key", idempotencyKey);
response.setHeader(
"Idempotency-Replayed",
execution.replayed ? "true" : "false",
);
return writeJson(response, 200, {
ok: true,
replayed: execution.replayed,
result: execution.command,
});
}
@@ -232,6 +282,11 @@ export function createControlCoreApp({
const workspace = await repository.getProjectWorkspace(
actor,
workspaceProjectId,
{
commandTransport: typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
},
);
return writeJson(response, 200, { ok: true, workspace });
}
@@ -280,7 +335,8 @@ export function createControlCoreApp({
}
const input = await readJsonBody(request, 1024 * 1024);
const acceptance = await ingest.acceptMessage(input);
const receipt = await ingest.acceptMessage(input);
const acceptance = receipt.value;
return writeJson(response, acceptance.replayed ? 200 : 201, {
ok: true,
acceptance,
@@ -14,6 +14,7 @@ import {
import {
DEVICE_DISCOVERY_VIEW_SCHEMA,
assertSafeProjection,
normalizeAdapterAcceptance,
normalizeAdapterMessage,
normalizeDiscoverySignal,
} from "../../../packages/device-protocol-contract/src/index.mjs";
@@ -88,6 +89,7 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
edgeTrustGeneration: state?.observedEdgeIdentity?.generationRef ?? null,
edgeCertificateFingerprint:
state?.observedEdgeIdentity?.fingerprint ?? null,
negotiatedCommandTransport: state?.negotiatedCommandTransport ?? null,
activeTrackerSessionChains: state?.sessionChains.size ?? 0,
connectionAttempts: totalConnectionAttempts,
channelsAccepted: totalChannelsAccepted,
@@ -97,7 +99,7 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
protocolFailures: totalProtocolFailures,
lastErrorCode,
trackerIngress: "remote-edge-only",
commandTransport: "disabled",
commandTransport: config.commandTransport,
});
}
@@ -127,7 +129,9 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
maxEnvelopeBytes: config.maxEnvelopeBytes,
}),
sessionChains: new Map(),
trackerDevices: new Map(),
channelGeneration: null,
negotiatedCommandTransport: null,
observedEdgeIdentity: null,
edgeSequence: 0,
coreSequence: 0,
@@ -245,15 +249,19 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
|| envelope.payload?.transport !== "http2-mtls"
|| envelope.payload?.trustGeneration
!== connection.observedEdgeIdentity?.generationRef
|| envelope.payload?.commandTransport !== "disabled"
|| !isCompatibleCommandTransport(
config.commandTransport,
envelope.payload?.commandTransport,
)
) {
throw new Error("device_gateway_core_channel_hello_invalid");
}
connection.negotiatedCommandTransport = envelope.payload.commandTransport;
connection.channelGeneration = connection.registration.channelGeneration;
send(connection, "channel.accepted", {
status: "accepted",
coreIdentity: config.coreIdentity,
commandTransport: "disabled",
commandTransport: connection.negotiatedCommandTransport,
}, {
trackerSessionId: CHANNEL_TRACKER_SESSION_ID,
adapterProfileRef: CHANNEL_PROFILE_REF,
@@ -269,7 +277,7 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
return;
}
if (envelope.messageKind === "channel.heartbeat") return;
if (["discovery.observed", "adapter.message"].includes(envelope.messageKind)) {
if (["discovery.observed", "adapter.message", "command.status"].includes(envelope.messageKind)) {
scheduleTrackerEvent(connection, envelope);
return;
}
@@ -287,7 +295,9 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
const work = (previous ?? Promise.resolve())
.then(() => envelope.messageKind === "discovery.observed"
? acceptDiscovery(connection, envelope)
: acceptAdapterMessage(connection, envelope))
: envelope.messageKind === "adapter.message"
? acceptAdapterMessage(connection, envelope)
: acceptCommandStatus(connection, envelope))
.catch((error) => failConnection(connection, error))
.finally(() => {
if (connection.sessionChains.get(envelope.trackerSessionId) === work) {
@@ -300,10 +310,27 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
async function acceptDiscovery(connection, envelope) {
try {
const signal = normalizeDiscoverySignal(envelope.payload?.signal);
const discovery = normalizeDiscoveryReceipt(
const receipt = normalizeDiscoveryReceipt(
await config.observeDiscovery(signal),
);
sendEventResult(connection, envelope, { discovery });
if (receipt.claimedDeviceRef) {
connection.trackerDevices.set(
envelope.trackerSessionId,
receipt.claimedDeviceRef,
);
} else {
connection.trackerDevices.delete(envelope.trackerSessionId);
}
const commandOffer = (
connection.negotiatedCommandTransport === "typed-service-ping-v1"
&& receipt.claimedDeviceRef
)
? await config.offerCommand(receipt.claimedDeviceRef)
: null;
sendEventResult(connection, envelope, {
discovery: receipt.discovery,
...(commandOffer ? { commandOffer } : {}),
});
totalEventsAccepted += 1;
} catch (error) {
sendEventRejection(connection, envelope, error);
@@ -316,8 +343,38 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
const message = normalizeAdapterMessage(envelope.payload?.message, {
maxBytes: config.maxEnvelopeBytes,
});
const acceptance = await config.acceptMessage(message);
sendEventResult(connection, envelope, { acceptance });
const receipt = normalizeAdapterReceipt(
await config.acceptMessage(message),
);
if (receipt.claimedDeviceRef) {
connection.trackerDevices.set(
envelope.trackerSessionId,
receipt.claimedDeviceRef,
);
}
const claimedDeviceRef = receipt.claimedDeviceRef
?? connection.trackerDevices.get(envelope.trackerSessionId);
const commandOffer = (
connection.negotiatedCommandTransport === "typed-service-ping-v1"
&& claimedDeviceRef
)
? await config.offerCommand(claimedDeviceRef)
: null;
sendEventResult(connection, envelope, {
acceptance: receipt.acceptance,
...(commandOffer ? { commandOffer } : {}),
});
totalEventsAccepted += 1;
} catch (error) {
sendEventRejection(connection, envelope, error);
totalEventsRejected += 1;
}
}
async function acceptCommandStatus(connection, envelope) {
try {
await config.recordCommandStatus(envelope.payload?.status);
sendEventResult(connection, envelope, { status: "recorded" });
totalEventsAccepted += 1;
} catch (error) {
sendEventRejection(connection, envelope, error);
@@ -489,7 +546,25 @@ function normalizeDiscoveryReceipt(input) {
) {
throw new TypeError("device_gateway_core_discovery_receipt_invalid");
}
return discovery;
return Object.freeze({
discovery,
claimedDeviceRef: input.claimedDeviceRef ?? null,
});
}
function normalizeAdapterReceipt(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new TypeError("device_gateway_core_adapter_receipt_invalid");
}
const acceptanceValue = input.value ?? (
input.schemaVersion === "nodedc.device-adapter-acceptance.v1"
? input
: null
);
return Object.freeze({
acceptance: normalizeAdapterAcceptance(acceptanceValue),
claimedDeviceRef: input.claimedDeviceRef ?? null,
});
}
function normalizeConfig(options) {
@@ -499,6 +574,15 @@ function normalizeConfig(options) {
if (typeof options.acceptMessage !== "function") {
throw new TypeError("device_gateway_core_accept_message_invalid");
}
const commandTransport = options.commandTransport ?? "disabled";
if (!["disabled", "typed-service-ping-v1"].includes(commandTransport)) {
throw new TypeError("device_gateway_core_command_transport_invalid");
}
const offerCommand = options.offerCommand ?? (async () => null);
const recordCommandStatus = options.recordCommandStatus ?? (async () => undefined);
if (typeof offerCommand !== "function" || typeof recordCommandStatus !== "function") {
throw new TypeError("device_gateway_core_command_runtime_invalid");
}
const registrationProvider = typeof options.registrationProvider === "function"
? options.registrationProvider
: async () => options.registration;
@@ -540,6 +624,9 @@ function normalizeConfig(options) {
coreIdentity: normalizeRef(options.coreIdentity, "core_identity"),
observeDiscovery: options.observeDiscovery,
acceptMessage: options.acceptMessage,
commandTransport,
offerCommand,
recordCommandStatus,
keepaliveMs,
deadPeerMs,
connectTimeoutMs: normalizeInteger(
@@ -633,6 +720,11 @@ function safeErrorCode(error) {
: "device_gateway_core_event_rejected";
}
function isCompatibleCommandTransport(configured, offered) {
if (offered === configured) return true;
return configured === "typed-service-ping-v1" && offered === "disabled";
}
function normalizeRef(value, field) {
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
throw new TypeError(`device_gateway_core_${field}_invalid`);
@@ -133,7 +133,7 @@ export async function observeQuarantineDiscovery({
returning id, lifecycle_state, model_profile_ref, protocol,
identifier_kind, identifier_masked, first_observed_at,
last_observed_at, evidence, project_id, route_id,
enrollment_intent_id, (xmax = 0) as created`,
enrollment_intent_id, claimed_device_id, (xmax = 0) as created`,
[
randomUUID(),
safeView.identifier.kind,
@@ -179,6 +179,9 @@ export async function observeQuarantineDiscovery({
return {
created: row.created === true,
value: discoveryView(row),
claimedDeviceRef: row.claimed_device_id
? `device:${row.claimed_device_id}`
: null,
};
} catch (error) {
await client.query("rollback").catch(() => undefined);
@@ -110,6 +110,11 @@ export function createDeviceEdgeChannelSupervisor(options = {}) {
coreIdentity: config.coreIdentity.identityRef,
observeDiscovery: config.gatewayIngest.observeDiscovery,
acceptMessage: config.gatewayIngest.acceptMessage,
commandTransport: config.typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
offerCommand: config.typedCommandRuntime?.offerForDevice,
recordCommandStatus: config.typedCommandRuntime?.recordStatus,
});
assertClient(client);
clients.set(registration.edgeRegistrationId, { client, digest });
@@ -169,7 +174,9 @@ export function createDeviceEdgeChannelSupervisor(options = {}) {
degraded,
reconciliationFailures,
lastErrorCode,
commandTransport: "disabled",
commandTransport: config.typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
edges: Object.freeze(edges),
});
}
@@ -245,6 +252,7 @@ function normalizeConfiguration(options) {
return Object.freeze({
repository: options.repository,
gatewayIngest: options.gatewayIngest,
typedCommandRuntime: normalizeTypedCommandRuntime(options.typedCommandRuntime),
coreIdentity: Object.freeze({ ...coreIdentity }),
maxEdges,
reconcileIntervalMs,
@@ -254,6 +262,17 @@ function normalizeConfiguration(options) {
});
}
function normalizeTypedCommandRuntime(value) {
if (value == null) return null;
if (
typeof value.offerForDevice !== "function"
|| typeof value.recordStatus !== "function"
) {
throw new TypeError("device_edge_channel_typed_command_runtime_invalid");
}
return value;
}
function normalizeRegistration(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_edge_channel_registration_invalid");
@@ -38,6 +38,7 @@ export function createDeviceGatewayIngest({ repository, identifierPepper } = {})
return Object.freeze({
created: discovery.created === true,
value: assertSafeProjection(discovery.value),
claimedDeviceRef: discovery.claimedDeviceRef ?? null,
});
},
@@ -60,13 +61,15 @@ export function createDeviceGatewayIngest({ repository, identifierPepper } = {})
payloadSchemaRef: safeView.payloadSchemaRef,
payload: safeView.payload,
});
return normalizeAdapterAcceptance(
await repository.acceptAdapterMessage({
identifierDigest,
requestDigest,
safeView,
}),
);
const receipt = await repository.acceptAdapterMessage({
identifierDigest,
requestDigest,
safeView,
});
return Object.freeze({
value: normalizeAdapterAcceptance(receipt.acceptance),
claimedDeviceRef: receipt.claimedDeviceRef ?? null,
});
},
});
}
@@ -15,6 +15,13 @@ export async function acceptGatewayMessage({
const route = routeId == null
? null
: await findActiveRoute(client, routeId, safeView);
const claimedDeviceRef = route == null
? null
: await findClaimedDeviceRef(client, {
identifierDigest,
route,
safeView,
});
const id = randomUUID();
const inserted = await client.query(
`insert into device_gateway_message_receipts (
@@ -67,7 +74,7 @@ export async function acceptGatewayMessage({
);
if (inserted.rows[0]) {
await client.query("commit");
return acceptanceView(inserted.rows[0], false);
return receiptView(inserted.rows[0], false, claimedDeviceRef);
}
const existing = await client.query(
@@ -83,7 +90,7 @@ export async function acceptGatewayMessage({
throw domainError("device_gateway_idempotency_conflict", 409);
}
await client.query("commit");
return acceptanceView(row, true);
return receiptView(row, true, claimedDeviceRef);
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw error;
@@ -92,6 +99,36 @@ export async function acceptGatewayMessage({
}
}
async function findClaimedDeviceRef(client, {
identifierDigest,
route,
safeView,
}) {
const result = await client.query(
`select claimed_device_id
from device_discoveries
where identifier_kind = $1
and identifier_digest = $2
and model_profile_ref = $3
and lifecycle_state = 'claimed'
and project_id = $4
and route_id = $5
and claimed_device_id is not null
for share`,
[
safeView.identifier.kind,
identifierDigest,
safeView.protocolProfileRef,
route.project_id,
route.id,
],
);
const row = result.rows[0];
return row?.claimed_device_id
? `device:${row.claimed_device_id}`
: null;
}
async function findActiveRoute(client, routeId, safeView) {
const edgeId = parseEntityRef(safeView.edgeRef, "edge");
const result = await client.query(
@@ -133,14 +170,17 @@ async function findActiveRoute(client, routeId, safeView) {
return route;
}
function acceptanceView(row, replayed) {
function receiptView(row, replayed, claimedDeviceRef) {
return {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: `acceptance:${row.id}`,
idempotencyKey: row.idempotency_key,
status: "accepted",
replayed,
acceptedAt: new Date(row.accepted_at).toISOString(),
acceptance: {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: `acceptance:${row.id}`,
idempotencyKey: row.idempotency_key,
status: "accepted",
replayed,
acceptedAt: new Date(row.accepted_at).toISOString(),
},
claimedDeviceRef,
};
}
@@ -41,6 +41,11 @@ import {
assertProjectCapability,
toProjectRef,
} from "./project-management.mjs";
import {
dispatchTypedCommand,
planTypedServicePing,
recordTypedCommandStatus,
} from "./typed-command-repository.mjs";
const { Pool } = pg;
const serviceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -163,9 +168,9 @@ export class PostgresDeviceRepository {
);
}
async getProjectWorkspace(actor, projectId) {
async getProjectWorkspace(actor, projectId, options) {
return this.#executeRead((client) =>
getDeviceProjectWorkspace(client, actor, projectId)
getDeviceProjectWorkspace(client, actor, projectId, options)
);
}
@@ -199,6 +204,33 @@ export class PostgresDeviceRepository {
});
}
async planTypedServicePing(input) {
return this.#executeWrite((client) => planTypedServicePing(client, input));
}
async dispatchTypedCommand(input) {
return this.#executeWrite((client) => dispatchTypedCommand(client, input));
}
async recordTypedCommandStatus(input) {
return this.#executeWrite((client) => recordTypedCommandStatus(client, input));
}
async #executeWrite(operation) {
const client = await this.pool.connect();
try {
await client.query("begin");
const result = await operation(client);
await client.query("commit");
return result;
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw mapPostgresError(error);
} finally {
client.release();
}
}
async #executeRead(operation) {
const client = await this.pool.connect();
try {
@@ -46,7 +46,12 @@ export async function listAccessibleDeviceProjects(client, actor) {
});
}
export async function getDeviceProjectWorkspace(client, actor, projectId) {
export async function getDeviceProjectWorkspace(
client,
actor,
projectId,
{ commandTransport = "disabled" } = {},
) {
const project = await findProjectWithCapability(
client,
actor,
@@ -339,8 +344,8 @@ export async function getDeviceProjectWorkspace(client, actor, projectId) {
auditEvents: auditEvents.rows.map(auditEventView),
grants: projectGrants.rows.map(storedGrantView),
policies: {
commandTransport: "disabled",
commandPlanningApi: "disabled",
commandTransport,
commandPlanningApi: commandTransport === "disabled" ? "disabled" : "enabled",
identifierProjection: "masked-only",
auditPayloadProjection: "metadata-only",
},
@@ -13,6 +13,7 @@ import {
} from "./edge-channel-supervisor.mjs";
import { createDeviceGatewayIngest } from "./gateway-ingest.mjs";
import { PostgresDeviceRepository } from "./postgres-repository.mjs";
import { createTypedCommandRuntime } from "./typed-command-runtime.mjs";
const config = await readConfig();
const repository = new PostgresDeviceRepository({
@@ -21,6 +22,9 @@ const repository = new PostgresDeviceRepository({
});
await repository.migrate();
const typedCommandRuntime = config.managementApiEnabled && config.edgeChannelEnabled
? createTypedCommandRuntime({ repository })
: null;
const gatewayIngest = config.discoveryIngestEnabled || config.edgeChannelEnabled
? createDeviceGatewayIngest({
@@ -36,6 +40,7 @@ const edgeChannelSupervisor = config.edgeChannelEnabled
trustRoot: config.edgeChannelTrustRoot,
maxEdges: config.edgeChannelMaxEdges,
reconcileIntervalMs: config.edgeChannelReconcileIntervalMs,
typedCommandRuntime,
})
: null;
await edgeChannelSupervisor?.start();
@@ -51,6 +56,7 @@ const server = createControlCoreApp({
edgeChannelStatusProvider: edgeChannelSupervisor
? () => edgeChannelSupervisor.status()
: null,
typedCommandRuntime,
});
server.listen(config.port, config.host, () => {
@@ -61,7 +67,7 @@ server.listen(config.port, config.host, () => {
discoveryIngest: config.discoveryIngestEnabled,
managementApi: config.managementApiEnabled,
edgeChannels: config.edgeChannelEnabled,
commandTransport: "disabled",
commandTransport: typedCommandRuntime ? "typed-service-ping-v1" : "disabled",
}));
});
@@ -0,0 +1,299 @@
import { createHash, randomUUID } from "node:crypto";
import { assertProjectCapability } from "./project-management.mjs";
const SERVICE_PING_CATALOG = "arusnavi.b2.internal.v1:service-ping";
export async function planTypedServicePing(client, {
idempotencyKey,
requestDigest,
actor,
projectId,
deviceId,
expiresAt,
}) {
const commandKey = `service-ping-${createHash("sha256")
.update(`${actor.userRef}\0${idempotencyKey}`, "utf8")
.digest("hex").slice(0, 32)}`;
const project = await findProject(client, projectId);
const grants = await projectGrants(client, projectId);
assertProjectCapability(actor, grants, "command.plan");
assertProjectCapability(actor, grants, "command.dispatch");
const device = await findCommandableDevice(client, projectId, deviceId);
const existing = await client.query(
`select dc.*, di.display_name as device_name
from device_commands dc
join device_instances di on di.id = dc.device_id
where dc.project_id = $1 and dc.command_key = $2
for update of dc`,
[projectId, commandKey],
);
if (existing.rows[0]) {
const row = existing.rows[0];
if (
row.device_id !== deviceId
|| row.command_catalog_ref !== SERVICE_PING_CATALOG
|| row.command_type !== "service.ping"
|| row.parameters_digest !== requestDigest
) {
throw domainError("device_command_idempotency_conflict", 409);
}
return { replayed: true, commandId: row.id, command: commandView(row) };
}
const commandId = randomUUID();
const plannedAt = new Date();
await client.query(
`insert into device_commands (
id, owner_scope_id, project_id, device_id, command_key,
command_catalog_ref, command_type, risk_class,
parameters_digest, parameters_projection, lifecycle_state,
planned_by_ref, planned_at, expires_at
) values (
$1, $2, $3, $4, $5, $6, 'service.ping', 'low',
$7, $8::jsonb, 'queued', $9, $10, $11
)`,
[
commandId,
project.owner_scope_id,
projectId,
deviceId,
commandKey,
SERVICE_PING_CATALOG,
requestDigest,
JSON.stringify({ operation: "service.ping", profileRef: device.model_profile_ref }),
actor.userRef,
plannedAt,
expiresAt,
],
);
await insertEvent(client, commandId, deviceId, projectId, 1, null, "draft", actor.userRef, "operator_requested");
await insertEvent(client, commandId, deviceId, projectId, 2, "draft", "planned", actor.userRef, "typed_policy_approved");
await insertEvent(client, commandId, deviceId, projectId, 3, "planned", "queued", actor.userRef, "awaiting_tracker_session");
await audit(client, {
eventType: "command.queued",
actorRef: actor.userRef,
projectId,
deviceId,
payload: { commandRef: `command:${commandId}`, commandType: "service.ping", riskClass: "low" },
});
return {
replayed: false,
commandId,
command: commandView({
id: commandId,
device_id: deviceId,
device_name: device.display_name,
command_key: commandKey,
command_catalog_ref: SERVICE_PING_CATALOG,
command_type: "service.ping",
risk_class: "low",
lifecycle_state: "queued",
planned_at: plannedAt,
expires_at: expiresAt,
created_at: plannedAt,
updated_at: plannedAt,
}),
};
}
export async function dispatchTypedCommand(client, {
commandId,
transportMessageRef,
now,
}) {
const result = await client.query(
`select dc.*, di.display_name as device_name
from device_commands dc
join device_instances di on di.id = dc.device_id
where dc.id = $1
for update of dc`,
[commandId],
);
const row = result.rows[0];
if (!row) throw domainError("device_command_not_found", 404);
if (row.lifecycle_state !== "queued") return null;
if (new Date(row.expires_at).getTime() <= now.getTime()) {
await client.query(
`update device_commands set lifecycle_state = 'expired',
terminal_at = $2, terminal_reason_code = 'ttl_elapsed', updated_at = $2
where id = $1`,
[commandId, now],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 4, "queued", "expired", "workload:device-control-core", "ttl_elapsed");
return null;
}
await client.query(
`update device_commands set lifecycle_state = 'dispatched',
dispatched_at = $2, transport_message_ref = $3, updated_at = $2
where id = $1`,
[commandId, now, transportMessageRef],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 4, "queued", "dispatched", "workload:device-control-core", "tracker_session_allocated", transportMessageRef);
await audit(client, {
eventType: "command.dispatched",
actorRef: "workload:device-control-core",
projectId: row.project_id,
deviceId: row.device_id,
payload: { commandRef: `command:${commandId}`, commandType: row.command_type },
});
return commandView({ ...row, lifecycle_state: "dispatched", dispatched_at: now, transport_message_ref: transportMessageRef, updated_at: now });
}
export async function recordTypedCommandStatus(client, {
commandId,
transportMessageRef,
lifecycleState,
resultCode,
observedAt,
}) {
const result = await client.query(
`select * from device_commands where id = $1 for update`,
[commandId],
);
const row = result.rows[0];
if (!row) throw domainError("device_command_not_found", 404);
if (row.transport_message_ref !== transportMessageRef) {
throw domainError("device_command_transport_mismatch", 409);
}
if (["verified", "failed", "expired", "unknown"].includes(row.lifecycle_state)) {
return;
}
if (lifecycleState === "acknowledged" && row.lifecycle_state === "dispatched") {
await client.query(
`update device_commands set lifecycle_state = 'acknowledged',
acknowledged_at = $2, updated_at = $2 where id = $1`,
[commandId, observedAt],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 5, "dispatched", "acknowledged", "workload:device-edge", "protocol_reply_received", `result:${resultCode}`);
await client.query(
`update device_commands set lifecycle_state = 'verified',
terminal_at = $2, terminal_reason_code = 'protocol_reply_serv_ok',
updated_at = $2 where id = $1`,
[commandId, observedAt],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 6, "acknowledged", "verified", "workload:device-control-core", "protocol_reply_serv_ok", `result:${resultCode}`);
await audit(client, {
eventType: "command.verified",
actorRef: "workload:device-control-core",
projectId: row.project_id,
deviceId: row.device_id,
payload: { commandRef: `command:${commandId}`, commandType: row.command_type, resultCode },
});
return;
}
if (lifecycleState === "unknown" && row.lifecycle_state === "dispatched") {
await client.query(
`update device_commands set lifecycle_state = 'unknown', terminal_at = $2,
terminal_reason_code = $3, updated_at = $2 where id = $1`,
[commandId, observedAt, resultCode],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 5, "dispatched", "unknown", "workload:device-edge", resultCode);
}
}
async function findProject(client, projectId) {
const result = await client.query(
`select id, owner_scope_id, lifecycle_state from device_projects where id = $1 for share`,
[projectId],
);
const row = result.rows[0];
if (!row) throw domainError("device_project_not_found", 404);
if (row.lifecycle_state !== "active") throw domainError("device_project_inactive", 409);
return row;
}
async function findCommandableDevice(client, projectId, deviceId) {
const result = await client.query(
`select di.id, di.display_name, di.model_profile_ref, di.lifecycle_state
from device_instances di
where di.project_id = $1 and di.id = $2
and exists (
select 1 from device_routes dr
where dr.project_id = di.project_id
and dr.model_profile_ref = di.model_profile_ref
and dr.direction = 'bidirectional'
and dr.lifecycle_state = 'active'
)
for share`,
[projectId, deviceId],
);
const row = result.rows[0];
if (!row) throw domainError("device_command_route_unavailable", 409);
if (row.model_profile_ref !== "arusnavi.b2.internal.v1") {
throw domainError("device_command_profile_unsupported", 409);
}
if (["suspended", "retired"].includes(row.lifecycle_state)) {
throw domainError("device_command_device_inactive", 409);
}
return row;
}
async function projectGrants(client, projectId) {
const result = await client.query(
`select id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state
from device_project_grants where project_id = $1 for share`,
[projectId],
);
return result.rows.map((row) => ({
grantRef: `grant:${row.id}`,
principalKind: row.principal_kind,
principalRef: row.principal_ref,
projectRole: row.project_role,
capabilityAllow: row.capability_allow ?? [],
capabilityDeny: row.capability_deny ?? [],
lifecycleState: row.lifecycle_state,
}));
}
async function insertEvent(client, commandId, deviceId, projectId, sequence, from, to, actor, reason, evidence = null) {
await client.query(
`insert into device_command_events (
id, command_id, device_id, project_id, sequence_number,
from_state, to_state, actor_ref, reason_code, evidence_ref
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[randomUUID(), commandId, deviceId, projectId, sequence, from, to, actor, reason, evidence],
);
}
async function audit(client, { eventType, actorRef, projectId, deviceId, payload }) {
await client.query(
`insert into device_audit_events (
id, event_type, actor_ref, project_id, device_id, payload
) values ($1,$2,$3,$4,$5,$6::jsonb)`,
[randomUUID(), eventType, actorRef, projectId, deviceId, JSON.stringify(payload)],
);
}
function commandView(row) {
return Object.freeze({
commandRef: `command:${row.id}`,
deviceRef: `device:${row.device_id}`,
deviceName: row.device_name,
commandKey: row.command_key,
commandCatalogRef: row.command_catalog_ref,
commandType: row.command_type,
riskClass: row.risk_class,
lifecycleState: row.lifecycle_state,
plannedAt: iso(row.planned_at),
expiresAt: iso(row.expires_at),
dispatchedAt: iso(row.dispatched_at),
acknowledgedAt: iso(row.acknowledged_at),
terminalAt: iso(row.terminal_at),
terminalReasonCode: row.terminal_reason_code ?? null,
createdAt: iso(row.created_at),
updatedAt: iso(row.updated_at),
});
}
function iso(value) {
return value == null ? null : new Date(value).toISOString();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,149 @@
import { createHash, randomUUID } from "node:crypto";
export function createTypedCommandRuntime({ repository, now = () => new Date() } = {}) {
if (
!repository
|| typeof repository.planTypedServicePing !== "function"
|| typeof repository.dispatchTypedCommand !== "function"
|| typeof repository.recordTypedCommandStatus !== "function"
) {
throw new TypeError("device_typed_command_repository_required");
}
const credentials = new Map();
return Object.freeze({
async planServicePing({ idempotencyKey, actor, input }) {
const normalized = normalizeInput(input);
const expiresAt = new Date(now().getTime() + normalized.expiresInSeconds * 1000);
const requestDigest = `sha256:${createHash("sha256").update(JSON.stringify({
actorRef: actor.userRef,
projectId: normalized.projectId,
deviceId: normalized.deviceId,
operation: "service.ping",
expiresInSeconds: normalized.expiresInSeconds,
}), "utf8").digest("hex")}`;
const execution = await repository.planTypedServicePing({
idempotencyKey,
requestDigest,
actor,
projectId: normalized.projectId,
deviceId: normalized.deviceId,
expiresAt,
});
if (!execution.replayed && execution.command.lifecycleState === "queued") {
credentials.set(execution.commandId, Object.freeze({
deviceId: normalized.deviceId,
accessCode: normalized.accessCode,
expiresAt: new Date(execution.command.expiresAt).getTime(),
}));
}
return { replayed: execution.replayed, command: execution.command };
},
async offerForDevice(deviceRef) {
const deviceId = entityId(deviceRef, "device");
const at = now();
for (const [commandId, secret] of credentials) {
if (secret.expiresAt <= at.getTime()) {
await repository.dispatchTypedCommand({
commandId,
transportMessageRef: `edge-command:${randomUUID()}`,
now: at,
});
credentials.delete(commandId);
continue;
}
if (secret.deviceId !== deviceId) continue;
const transportMessageRef = `edge-command:${randomUUID()}`;
const dispatched = await repository.dispatchTypedCommand({
commandId,
transportMessageRef,
now: at,
});
if (!dispatched) {
credentials.delete(commandId);
continue;
}
return Object.freeze({
commandRef: `command:${commandId}`,
commandType: "service.ping",
accessCode: secret.accessCode,
transportMessageRef,
});
}
return null;
},
async recordStatus(status) {
const commandId = entityId(status?.commandRef, "command");
const normalized = normalizeStatus(status);
await repository.recordTypedCommandStatus({ commandId, ...normalized });
credentials.delete(commandId);
},
status() {
return Object.freeze({
commandTransport: "typed-service-ping-v1",
transientAuthorizations: credentials.size,
});
},
});
}
function normalizeInput(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw domainError("device_service_ping_input_invalid", 400);
}
const keys = Object.keys(input).sort().join(",");
if (keys !== "accessCode,deviceRef,expiresInSeconds,projectRef") {
throw domainError("device_service_ping_input_invalid", 400);
}
if (typeof input.accessCode !== "string" || !/^\d{6}$/.test(input.accessCode)) {
throw domainError("device_service_ping_access_code_invalid", 400);
}
const expiresInSeconds = Number(input.expiresInSeconds);
if (!Number.isSafeInteger(expiresInSeconds) || expiresInSeconds < 30 || expiresInSeconds > 1800) {
throw domainError("device_service_ping_ttl_invalid", 400);
}
return {
projectId: entityId(input.projectRef, "project"),
deviceId: entityId(input.deviceRef, "device"),
accessCode: input.accessCode,
expiresInSeconds,
};
}
function normalizeStatus(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_command_status_invalid");
}
if (!['acknowledged', 'unknown'].includes(value.lifecycleState)) {
throw new TypeError("device_command_status_invalid");
}
if (typeof value.transportMessageRef !== "string" || !/^edge-command:[0-9a-f-]{36}$/i.test(value.transportMessageRef)) {
throw new TypeError("device_command_status_invalid");
}
if (typeof value.resultCode !== "string" || !/^[a-z][a-z0-9._-]{1,63}$/.test(value.resultCode)) {
throw new TypeError("device_command_status_invalid");
}
const observedAt = new Date(value.observedAt);
if (Number.isNaN(observedAt.getTime())) throw new TypeError("device_command_status_invalid");
return {
transportMessageRef: value.transportMessageRef.toLowerCase(),
lifecycleState: value.lifecycleState,
resultCode: value.resultCode,
observedAt,
};
}
function entityId(value, prefix) {
const match = String(value || "").match(new RegExp(`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`, "i"));
if (!match) throw domainError(`device_${prefix}_ref_invalid`, 400);
return match[1].toLowerCase();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -255,6 +255,58 @@ test("management API exposes a repository idempotency conflict without retrying"
}
});
test("typed service ping accepts a transient access code and never echoes it", async () => {
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
let planned;
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => ({ replayed: false, result: {} }),
},
typedCommandRuntime: {
status: () => ({ commandTransport: "typed-service-ping-v1" }),
planServicePing: async (value) => {
planned = value;
return {
replayed: false,
command: {
commandRef: "command:33333333-3333-4333-8333-333333333333",
deviceRef,
commandType: "service.ping",
lifecycleState: "queued",
},
};
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/commands:service-ping`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify({
projectRef,
deviceRef,
accessCode: "654321",
expiresInSeconds: 300,
}),
},
);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.result.lifecycleState, "queued");
assert.equal(JSON.stringify(body).includes("654321"), false);
assert.equal(planned.input.accessCode, "654321");
assert.equal(planned.idempotencyKey, "phase2-test-0001");
} finally {
await runtime.close();
}
});
test("project query is service-authenticated and forwards only the trusted actor", async () => {
let queriedActor;
const runtime = await startTestServer({
@@ -424,12 +476,15 @@ test("gateway message endpoint returns acceptance only after repository commit",
acceptAdapterMessage: async (value) => {
stored = value;
return {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: "acceptance:test-001",
idempotencyKey: value.safeView.idempotencyKey,
status: "accepted",
replayed: false,
acceptedAt: "2026-08-11T12:00:00.000Z",
acceptance: {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: "acceptance:test-001",
idempotencyKey: value.safeView.idempotencyKey,
status: "accepted",
replayed: false,
acceptedAt: "2026-08-11T12:00:00.000Z",
},
claimedDeviceRef: "device:11111111-1111-4111-8111-111111111111",
};
},
},
@@ -25,12 +25,15 @@ test("shared gateway ingest masks identifiers for HTTP and Edge callers", async
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",
acceptance: {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: "acceptance:test",
idempotencyKey: value.safeView.idempotencyKey,
status: "accepted",
replayed: false,
acceptedAt: "2026-08-11T12:00:00.000Z",
},
claimedDeviceRef: "device:11111111-1111-4111-8111-111111111111",
};
},
},
@@ -40,7 +43,11 @@ test("shared gateway ingest masks identifiers for HTTP and Edge callers", async
const acceptance = await ingest.acceptMessage(adapterMessage());
assert.equal(discovery.value.identifier.masked, "***********0001");
assert.equal(acceptance.status, "accepted");
assert.equal(acceptance.value.status, "accepted");
assert.equal(
acceptance.claimedDeviceRef,
"device:11111111-1111-4111-8111-111111111111",
);
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);
@@ -22,10 +22,11 @@ test("commits a gateway receipt before returning Core acceptance", async () => {
const result = await acceptGatewayMessage(messageInput(client));
assert.equal(result.status, "accepted");
assert.equal(result.replayed, false);
assert.equal(result.idempotencyKey, idempotencyKey);
assert.equal(result.acceptedAt, acceptedAt.toISOString());
assert.equal(result.acceptance.status, "accepted");
assert.equal(result.acceptance.replayed, false);
assert.equal(result.acceptance.idempotencyKey, idempotencyKey);
assert.equal(result.acceptance.acceptedAt, acceptedAt.toISOString());
assert.equal(result.claimedDeviceRef, null);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
@@ -47,8 +48,8 @@ test("replays one durable receipt for the same normalized request", async () =>
const result = await acceptGatewayMessage(messageInput(client));
assert.equal(result.status, "accepted");
assert.equal(result.replayed, true);
assert.equal(result.acceptance.status, "accepted");
assert.equal(result.acceptance.replayed, true);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
@@ -0,0 +1,106 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createTypedCommandRuntime } from "../src/typed-command-runtime.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
test("keeps the B2 access code transient and emits only a typed offer", async () => {
const planned = [];
const dispatched = [];
const runtime = createTypedCommandRuntime({
now: () => new Date("2026-08-12T18:00:00.000Z"),
repository: {
async planTypedServicePing(value) {
planned.push(value);
return {
replayed: false,
commandId: "33333333-3333-4333-8333-333333333333",
command: {
lifecycleState: "queued",
expiresAt: "2026-08-12T18:05:00.000Z",
},
};
},
async dispatchTypedCommand(value) {
dispatched.push(value);
return { lifecycleState: "dispatched" };
},
async recordTypedCommandStatus() {},
},
});
await runtime.planServicePing({
idempotencyKey: "idem-00000001",
actor: { userRef: "user:test" },
input: { projectRef, deviceRef, accessCode: "123456", expiresInSeconds: 300 },
});
assert.equal(JSON.stringify(planned).includes("123456"), false);
const offer = await runtime.offerForDevice(deviceRef);
assert.equal(offer.commandType, "service.ping");
assert.equal(offer.accessCode, "123456");
assert.match(offer.transportMessageRef, /^edge-command:/);
assert.equal(dispatched.length, 1);
});
test("expires a transient authorization through the durable ledger", async () => {
let current = new Date("2026-08-12T18:00:00.000Z");
const dispatches = [];
const runtime = createTypedCommandRuntime({
now: () => current,
repository: {
async planTypedServicePing() {
return {
replayed: false,
commandId: "33333333-3333-4333-8333-333333333333",
command: {
lifecycleState: "queued",
expiresAt: "2026-08-12T18:00:30.000Z",
},
};
},
async dispatchTypedCommand(value) {
dispatches.push(value);
return null;
},
async recordTypedCommandStatus() {},
},
});
await runtime.planServicePing({
idempotencyKey: "idem-00000002",
actor: { userRef: "user:test" },
input: { projectRef, deviceRef, accessCode: "123456", expiresInSeconds: 30 },
});
current = new Date("2026-08-12T18:00:31.000Z");
assert.equal(await runtime.offerForDevice(deviceRef), null);
assert.equal(dispatches.length, 1);
assert.equal(runtime.status().transientAuthorizations, 0);
});
test("does not recreate a transient authorization on an idempotent replay", async () => {
const runtime = createTypedCommandRuntime({
repository: {
async planTypedServicePing() {
return {
replayed: true,
commandId: "33333333-3333-4333-8333-333333333333",
command: {
lifecycleState: "queued",
expiresAt: "2026-08-12T18:05:00.000Z",
},
};
},
async dispatchTypedCommand() {
throw new Error("must_not_dispatch_replayed_secret");
},
async recordTypedCommandStatus() {},
},
});
await runtime.planServicePing({
idempotencyKey: "idem-00000003",
actor: { userRef: "user:test" },
input: { projectRef, deviceRef, accessCode: "654321", expiresInSeconds: 300 },
});
assert.equal(runtime.status().transientAuthorizations, 0);
assert.equal(await runtime.offerForDevice(deviceRef), null);
});