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")}`; }