feat(device-core): add idempotent project management

This commit is contained in:
Codex
2026-08-10 17:27:07 +03:00
parent 336602c7ca
commit 70bafdd028
10 changed files with 2260 additions and 2 deletions
@@ -1,4 +1,4 @@
import { timingSafeEqual } from "node:crypto";
import { createHash, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import {
@@ -7,12 +7,25 @@ import {
normalizeDiscoverySignal,
toSafeDiscoveryView,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import {
normalizeManagementActor,
normalizeManagementCommand,
} from "./project-management.mjs";
const managementRoutes = new Map([
["/internal/v1/management/owner-scopes:ensure", "owner_scope.ensure"],
["/internal/v1/management/projects:ensure", "project.ensure"],
["/internal/v1/management/collections:ensure", "collection.ensure"],
["/internal/v1/management/project-grants:upsert", "project_grant.upsert"],
]);
export function createControlCoreApp({
repository,
gatewayToken = "",
identifierPepper = "",
discoveryIngestEnabled = false,
managementApiEnabled = false,
managementToken = "",
} = {}) {
if (!repository || typeof repository.health !== "function") {
throw new TypeError("device_repository_required");
@@ -28,6 +41,14 @@ export function createControlCoreApp({
throw new TypeError("device_identifier_pepper_invalid");
}
}
if (managementApiEnabled) {
if (typeof repository.executeManagementCommand !== "function") {
throw new TypeError("device_management_repository_required");
}
if (typeof managementToken !== "string" || managementToken.length < 32) {
throw new TypeError("device_management_token_invalid");
}
}
const server = createServer(async (request, response) => {
response.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -47,10 +68,56 @@ export function createControlCoreApp({
service: "nodedc-device-control-core",
database,
discoveryIngest: discoveryIngestEnabled ? "enabled" : "disabled",
managementApi: managementApiEnabled ? "enabled" : "disabled",
commandTransport: "disabled",
});
}
const managementCommandKind = managementRoutes.get(requestUrl.pathname);
if (request.method === "POST" && managementCommandKind) {
if (!managementApiEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_management_api_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, 64 * 1024);
const command = normalizeManagementCommand(managementCommandKind, input);
const requestDigest = managementRequestDigest({
actor,
commandKind: managementCommandKind,
command,
});
const execution = await repository.executeManagementCommand({
idempotencyKey,
commandKind: managementCommandKind,
requestDigest,
actor,
command,
});
response.setHeader("Idempotency-Key", idempotencyKey);
response.setHeader(
"Idempotency-Replayed",
execution.replayed ? "true" : "false",
);
return writeJson(response, 200, {
ok: true,
replayed: execution.replayed,
result: execution.result,
});
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/device-discoveries:observe"
@@ -108,6 +175,62 @@ export function createControlCoreApp({
return server;
}
function managementActorFromHeaders(headers) {
return normalizeManagementActor({
userRef: singleHeader(headers["x-nodedc-user-ref"]),
hubRole: singleHeader(headers["x-nodedc-hub-role"]),
groupRefs: commaSeparatedHeader(headers["x-nodedc-group-refs"]),
ownerScopes: ownerScopeHeader(headers["x-nodedc-owner-scopes"]),
});
}
function ownerScopeHeader(value) {
return commaSeparatedHeader(value).map((claim) => {
const separatorIndex = claim.indexOf("=");
if (separatorIndex < 1 || separatorIndex === claim.length - 1) {
throw new TypeError("device_actor_owner_scopes_invalid");
}
return {
scopeKind: claim.slice(0, separatorIndex),
ownerRef: claim.slice(separatorIndex + 1),
};
});
}
function commaSeparatedHeader(value) {
const header = singleHeader(value, true);
if (!header) return [];
return header.split(",").map((item) => item.trim()).filter(Boolean);
}
function singleHeader(value, optional = false) {
if (Array.isArray(value)) throw new TypeError("device_management_header_invalid");
if (value == null || value === "") {
if (optional) return "";
throw new TypeError("device_management_header_required");
}
if (typeof value !== "string" || value.length > 4096) {
throw new TypeError("device_management_header_invalid");
}
return value.trim();
}
function normalizeIdempotencyKey(value) {
const key = singleHeader(value);
if (!/^[\x21-\x7e]{8,256}$/.test(key)) {
const error = new Error("device_idempotency_key_invalid");
error.statusCode = 400;
throw error;
}
return key;
}
function managementRequestDigest(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");