Files
NODEDC_PLATFORM/device-plane/services/device-control-core/src/gateway-ingest.mjs
T

126 lines
4.4 KiB
JavaScript

import { createHash } from "node:crypto";
import {
assertSafeProjection,
hashRestrictedIdentifier,
normalizeAdapterAcceptance,
normalizeAdapterMessage,
normalizeDiscoverySignal,
toSafeAdapterMessageView,
toSafeDiscoveryView,
} from "../../../packages/device-protocol-contract/src/index.mjs";
export function createDeviceGatewayIngest({ repository, identifierPepper } = {}) {
if (!repository || typeof repository.upsertQuarantineDiscovery !== "function") {
throw new TypeError("device_discovery_repository_required");
}
if (typeof repository.acceptAdapterMessage !== "function") {
throw new TypeError("device_gateway_message_repository_required");
}
if (typeof identifierPepper !== "string" || identifierPepper.length < 32) {
throw new TypeError("device_identifier_pepper_invalid");
}
return Object.freeze({
async observeDiscovery(input, context = {}) {
const receivedSignal = normalizeDiscoverySignal(input);
const identifierDigest = hashRestrictedIdentifier(
receivedSignal.identifier,
identifierPepper,
);
const routeRef = await resolveAuthenticatedRoute(repository, {
edgeRef: context.authenticatedEdgeRef,
modelProfileRef: receivedSignal.modelProfileRef,
protocol: receivedSignal.protocol,
identifierKind: receivedSignal.identifier.kind,
identifierDigest,
observedAt: receivedSignal.observedAt,
});
const signal = routeRef === undefined
? receivedSignal
: normalizeDiscoverySignal({
...withoutKeys(receivedSignal, ["routeRef"]),
...(routeRef ? { routeRef } : {}),
});
const safeView = assertSafeProjection(toSafeDiscoveryView(signal));
const discovery = await repository.upsertQuarantineDiscovery({
identifierDigest,
safeView,
sessionRef: signal.sessionRef,
routeRef: signal.routeRef ?? null,
});
return Object.freeze({
created: discovery.created === true,
value: assertSafeProjection(discovery.value),
claimedDeviceRef: discovery.claimedDeviceRef ?? null,
});
},
async acceptMessage(input, context = {}) {
const receivedMessage = normalizeAdapterMessage(input);
const identifierDigest = hashRestrictedIdentifier(
receivedMessage.identifier,
identifierPepper,
);
const routeRef = await resolveAuthenticatedRoute(repository, {
edgeRef: context.authenticatedEdgeRef,
modelProfileRef: receivedMessage.protocolProfileRef,
protocol: receivedMessage.protocol,
identifierKind: receivedMessage.identifier.kind,
identifierDigest,
observedAt: receivedMessage.observedAt,
});
const message = routeRef === undefined
? receivedMessage
: normalizeAdapterMessage({
...withoutKeys(receivedMessage, ["edgeRef", "routeRef"]),
edgeRef: context.authenticatedEdgeRef,
...(routeRef ? { routeRef } : {}),
});
const safeView = assertSafeProjection(toSafeAdapterMessageView(message));
const requestDigest = gatewayMessageRequestDigest({
edgeRef: safeView.edgeRef,
adapterRef: safeView.adapterRef,
protocolProfileRef: safeView.protocolProfileRef,
protocol: safeView.protocol,
routeRef: safeView.routeRef ?? null,
idempotencyKey: safeView.idempotencyKey,
identifierKind: safeView.identifier.kind,
identifierDigest,
payloadSchemaRef: safeView.payloadSchemaRef,
payload: safeView.payload,
});
const receipt = await repository.acceptAdapterMessage({
identifierDigest,
requestDigest,
safeView,
});
return Object.freeze({
value: normalizeAdapterAcceptance(receipt.acceptance),
claimedDeviceRef: receipt.claimedDeviceRef ?? null,
});
},
});
}
async function resolveAuthenticatedRoute(repository, input) {
if (input.edgeRef == null) return undefined;
if (typeof repository.resolveInboundRoute !== "function") {
throw new TypeError("device_inbound_route_repository_required");
}
return repository.resolveInboundRoute(input);
}
function withoutKeys(value, keys) {
const omitted = new Set(keys);
return Object.fromEntries(
Object.entries(value).filter(([key]) => !omitted.has(key)),
);
}
function gatewayMessageRequestDigest(value) {
return `sha256:${createHash("sha256")
.update(JSON.stringify(value), "utf8")
.digest("hex")}`;
}