126 lines
4.4 KiB
JavaScript
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")}`;
|
|
}
|