feat(device-plane): add universal adapter acceptance boundary

This commit is contained in:
Codex
2026-08-11 19:11:48 +03:00
parent 227c7c26c1
commit 393741f1bd
26 changed files with 1887 additions and 127 deletions
@@ -5,9 +5,12 @@ import {
assertSafeProjection,
hashRestrictedIdentifier,
maskRestrictedIdentifier,
normalizeAdapterAcceptance,
normalizeAdapterMessage,
normalizeDiscoverySignal,
normalizeRestrictedIdentifier,
toSafeDiscoveryView,
toSafeAdapterMessageView,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import {
normalizeManagementActor,
@@ -64,6 +67,9 @@ export function createControlCoreApp({
if (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 gatewayToken !== "string" || gatewayToken.length < 32) {
throw new TypeError("device_gateway_token_invalid");
}
@@ -249,6 +255,55 @@ export function createControlCoreApp({
});
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/gateway/messages:accept"
) {
if (!discoveryIngestEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_gateway_message_ingest_disabled",
});
}
if (!matchesBearer(request.headers.authorization, gatewayToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_gateway_auth_required",
});
}
const input = await readJsonBody(request, 1024 * 1024);
const message = normalizeAdapterMessage(input);
const identifierDigest = hashRestrictedIdentifier(
message.identifier,
identifierPepper,
);
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 acceptance = normalizeAdapterAcceptance(
await repository.acceptAdapterMessage({
identifierDigest,
requestDigest,
safeView,
}),
);
return writeJson(response, acceptance.replayed ? 200 : 201, {
ok: true,
acceptance,
});
}
return writeJson(response, 404, {
ok: false,
error: "device_control_core_route_not_found",
@@ -366,6 +421,12 @@ function managementRequestDigest(value) {
.digest("hex")}`;
}
function gatewayMessageRequestDigest(value) {
return `sha256:${createHash("sha256")
.update(JSON.stringify(value), "utf8")
.digest("hex")}`;
}
function matchesBearer(header, expected) {
if (typeof header !== "string" || !header.startsWith("Bearer ")) return false;
const actual = Buffer.from(header.slice("Bearer ".length), "utf8");
@@ -0,0 +1,163 @@
import { randomUUID } from "node:crypto";
export async function acceptGatewayMessage({
pool,
identifierDigest,
requestDigest,
safeView,
}) {
const routeId = safeView.routeRef == null
? null
: parseEntityRef(safeView.routeRef, "route");
const client = await pool.connect();
try {
await client.query("begin");
const route = routeId == null
? null
: await findActiveRoute(client, routeId, safeView);
const id = randomUUID();
const inserted = await client.query(
`insert into device_gateway_message_receipts (
id,
idempotency_key,
request_digest,
edge_ref,
adapter_ref,
protocol_profile_ref,
protocol,
route_id,
project_id,
session_ref,
message_ref,
message_type,
sequence,
identifier_kind,
identifier_digest,
identifier_masked,
payload_schema_ref,
payload,
observed_at
) values (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18::jsonb, $19
)
on conflict (idempotency_key) do nothing
returning id, idempotency_key, accepted_at`,
[
id,
safeView.idempotencyKey,
requestDigest,
safeView.edgeRef,
safeView.adapterRef,
safeView.protocolProfileRef,
safeView.protocol,
route?.id ?? null,
route?.project_id ?? null,
safeView.sessionRef,
safeView.messageRef,
safeView.messageType,
safeView.sequence,
safeView.identifier.kind,
identifierDigest,
safeView.identifier.masked,
safeView.payloadSchemaRef,
JSON.stringify(safeView.payload),
safeView.observedAt,
],
);
if (inserted.rows[0]) {
await client.query("commit");
return acceptanceView(inserted.rows[0], false);
}
const existing = await client.query(
`select id, idempotency_key, request_digest, accepted_at
from device_gateway_message_receipts
where idempotency_key = $1
for share`,
[safeView.idempotencyKey],
);
const row = existing.rows[0];
if (!row) throw domainError("device_gateway_receipt_missing", 409);
if (row.request_digest !== requestDigest) {
throw domainError("device_gateway_idempotency_conflict", 409);
}
await client.query("commit");
return acceptanceView(row, true);
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw error;
} finally {
client.release();
}
}
async function findActiveRoute(client, routeId, safeView) {
const edgeId = parseEntityRef(safeView.edgeRef, "edge");
const result = await client.query(
`select r.id, r.project_id, r.edge_id, r.model_profile_ref,
r.protocol, r.lifecycle_state,
e.lifecycle_state as edge_lifecycle_state,
p.lifecycle_state as profile_lifecycle_state,
ap.package_key as adapter_ref,
ap.lifecycle_state as adapter_lifecycle_state,
av.lifecycle_state as adapter_version_lifecycle_state
from device_routes r
join device_edges e on e.id = r.edge_id
join device_model_profiles p on p.profile_ref = r.model_profile_ref
join device_adapter_versions av on av.id = p.adapter_version_id
join device_adapter_packages ap on ap.id = av.adapter_package_id
where r.id = $1
for share`,
[routeId],
);
const route = result.rows[0];
if (!route) throw domainError("device_gateway_route_not_found", 404);
if (
route.lifecycle_state !== "active"
|| route.edge_lifecycle_state !== "active"
|| route.profile_lifecycle_state !== "active"
|| route.adapter_lifecycle_state !== "active"
|| route.adapter_version_lifecycle_state !== "active"
) {
throw domainError("device_gateway_route_not_active", 409);
}
if (
route.edge_id !== edgeId
|| route.model_profile_ref !== safeView.protocolProfileRef
|| route.protocol !== safeView.protocol
|| route.adapter_ref !== safeView.adapterRef
) {
throw domainError("device_gateway_route_contract_mismatch", 409);
}
return route;
}
function acceptanceView(row, replayed) {
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(),
};
}
function parseEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = 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 new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -5,8 +5,8 @@ import { fileURLToPath } from "node:url";
import pg from "pg";
import { ARUSNAVI_B2_MODEL_PROFILE } from "../../../packages/arusnavi-b2-adapter/src/index.mjs";
import { observeQuarantineDiscovery } from "./discovery-repository.mjs";
import { acceptGatewayMessage } from "./gateway-message-repository.mjs";
import {
applyControlResourceManagementCommand,
authorizeControlResourceManagementReplay,
@@ -56,6 +56,7 @@ const migrationFiles = [
"009_device_sensitive_reference_commands.sql",
"010_device_control_resources.sql",
"011_device_control_resource_commands.sql",
"012_device_gateway_message_receipts.sql",
];
export class PostgresDeviceRepository {
@@ -88,30 +89,6 @@ export class PostgresDeviceRepository {
);
await this.pool.query(sql);
}
await this.pool.query(
`insert into device_model_profiles (
profile_ref,
schema_version,
vendor,
model,
device_type,
protocol,
profile
) values ($1, $2, $3, $4, $5, $6, $7::jsonb)
on conflict (profile_ref) do update set
schema_version = excluded.schema_version,
profile = excluded.profile,
updated_at = now()`,
[
ARUSNAVI_B2_MODEL_PROFILE.profileRef,
ARUSNAVI_B2_MODEL_PROFILE.schemaVersion,
ARUSNAVI_B2_MODEL_PROFILE.vendor,
ARUSNAVI_B2_MODEL_PROFILE.model,
ARUSNAVI_B2_MODEL_PROFILE.deviceType,
ARUSNAVI_B2_MODEL_PROFILE.protocol,
JSON.stringify(ARUSNAVI_B2_MODEL_PROFILE),
],
);
}
async health() {
@@ -126,6 +103,13 @@ export class PostgresDeviceRepository {
});
}
async acceptAdapterMessage(input) {
return acceptGatewayMessage({
pool: this.pool,
...input,
});
}
async executeManagementCommand({
idempotencyKey,
commandKind,