feat(device-plane): add fail-closed deploy foundation

This commit is contained in:
Codex
2026-07-25 21:29:05 +03:00
parent e9e03143cd
commit e217723784
36 changed files with 3729 additions and 0 deletions
@@ -0,0 +1,152 @@
import { timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import {
assertSafeProjection,
hashRestrictedIdentifier,
normalizeDiscoverySignal,
toSafeDiscoveryView,
} from "../../../packages/device-protocol-contract/src/index.mjs";
export function createControlCoreApp({
repository,
gatewayToken = "",
identifierPepper = "",
discoveryIngestEnabled = false,
} = {}) {
if (!repository || typeof repository.health !== "function") {
throw new TypeError("device_repository_required");
}
if (discoveryIngestEnabled) {
if (typeof repository.upsertQuarantineDiscovery !== "function") {
throw new TypeError("device_discovery_repository_required");
}
if (typeof gatewayToken !== "string" || gatewayToken.length < 32) {
throw new TypeError("device_gateway_token_invalid");
}
if (typeof identifierPepper !== "string" || identifierPepper.length < 32) {
throw new TypeError("device_identifier_pepper_invalid");
}
}
const server = createServer(async (request, response) => {
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.setHeader("X-Content-Type-Options", "nosniff");
try {
const requestUrl = new URL(
request.url || "/",
`http://${request.headers.host || "127.0.0.1"}`,
);
if (request.method === "GET" && requestUrl.pathname === "/healthz") {
const database = await repository.health();
return writeJson(response, 200, {
ok: true,
service: "nodedc-device-control-core",
database,
discoveryIngest: discoveryIngestEnabled ? "enabled" : "disabled",
commandTransport: "disabled",
});
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/device-discoveries:observe"
) {
if (!discoveryIngestEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_discovery_ingest_disabled",
});
}
if (!matchesBearer(request.headers.authorization, gatewayToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_gateway_auth_required",
});
}
const input = await readJsonBody(request, 32 * 1024);
const signal = normalizeDiscoverySignal(input);
const identifierDigest = hashRestrictedIdentifier(
signal.identifier,
identifierPepper,
);
const safeView = assertSafeProjection(toSafeDiscoveryView(signal));
const discovery = await repository.upsertQuarantineDiscovery({
identifierDigest,
safeView,
});
return writeJson(response, discovery.created ? 201 : 200, {
ok: true,
created: discovery.created,
discovery: assertSafeProjection(discovery.value),
});
}
return writeJson(response, 404, {
ok: false,
error: "device_control_core_route_not_found",
});
} catch (error) {
const status = Number(error?.statusCode || 400);
return writeJson(
response,
Number.isInteger(status) && status >= 400 && status < 600
? status
: 500,
{
ok: false,
error: safeErrorCode(error),
},
);
}
});
return server;
}
function matchesBearer(header, expected) {
if (typeof header !== "string" || !header.startsWith("Bearer ")) return false;
const actual = Buffer.from(header.slice("Bearer ".length), "utf8");
const required = Buffer.from(expected, "utf8");
return (
actual.length === required.length
&& required.length > 0
&& timingSafeEqual(actual, required)
);
}
async function readJsonBody(request, maxBytes) {
const chunks = [];
let size = 0;
for await (const chunk of request) {
size += chunk.length;
if (size > maxBytes) {
const error = new Error("device_request_body_too_large");
error.statusCode = 413;
throw error;
}
chunks.push(chunk);
}
if (size === 0) throw new TypeError("device_request_body_required");
try {
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
} catch {
throw new TypeError("device_request_json_invalid");
}
}
function writeJson(response, status, body) {
response.statusCode = status;
return response.end(`${JSON.stringify(body)}\n`);
}
function safeErrorCode(error) {
const value = error instanceof Error ? error.message : "device_control_error";
return /^[a-z0-9_:-]{1,128}$/.test(value)
? value
: "device_control_error";
}
@@ -0,0 +1,73 @@
import { readFile } from "node:fs/promises";
export async function resolveDeviceDatabaseUrl(
environment = process.env,
readSecret = readFile,
) {
const explicit = optionalValue(environment.DEVICE_DATABASE_URL);
if (explicit) return explicit;
const host = restrictedValue(
environment.DEVICE_DATABASE_HOST,
/^[A-Za-z0-9.-]{1,253}$/,
"device_database_host_invalid",
);
const port = parsePort(environment.DEVICE_DATABASE_PORT, 5432);
const database = restrictedValue(
environment.DEVICE_DATABASE_NAME,
/^[A-Za-z_][A-Za-z0-9_-]{0,62}$/,
"device_database_name_invalid",
);
const user = restrictedValue(
environment.DEVICE_DATABASE_USER,
/^[A-Za-z_][A-Za-z0-9_-]{0,62}$/,
"device_database_user_invalid",
);
const passwordFile = requiredValue(
environment.DEVICE_DATABASE_PASSWORD_FILE,
"device_database_password_file_required",
);
const password = (await readSecret(passwordFile, "utf8")).trim();
if (password.length < 32 || password.length > 512) {
throw new Error("device_database_password_invalid");
}
return [
"postgresql://",
encodeURIComponent(user),
":",
encodeURIComponent(password),
"@",
host,
":",
String(port),
"/",
encodeURIComponent(database),
"?sslmode=disable",
].join("");
}
function optionalValue(value) {
if (typeof value !== "string") return "";
return value.trim();
}
function requiredValue(value, errorCode) {
const normalized = optionalValue(value);
if (!normalized) throw new Error(errorCode);
return normalized;
}
function restrictedValue(value, pattern, errorCode) {
const normalized = requiredValue(value, errorCode);
if (!pattern.test(normalized)) throw new Error(errorCode);
return normalized;
}
function parsePort(value, fallback) {
const parsed = Number(value || fallback);
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) {
throw new Error("device_database_port_invalid");
}
return parsed;
}
@@ -0,0 +1,128 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
import { ARUSNAVI_B2_MODEL_PROFILE } from "../../../packages/arusnavi-b2-adapter/src/index.mjs";
const { Pool } = pg;
const serviceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export class PostgresDeviceRepository {
constructor({ databaseUrl, poolSize = 10 } = {}) {
if (typeof databaseUrl !== "string" || databaseUrl.trim() === "") {
throw new TypeError("device_database_url_required");
}
this.pool = new Pool({
connectionString: databaseUrl,
max: normalizePoolSize(poolSize),
});
}
async migrate() {
const sql = await readFile(
resolve(serviceRoot, "migrations/001_device_plane_foundation.sql"),
"utf8",
);
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() {
await this.pool.query("select 1");
return "ready";
}
async upsertQuarantineDiscovery({ identifierDigest, safeView }) {
const result = await this.pool.query(
`insert into device_discoveries (
id,
identifier_kind,
identifier_digest,
identifier_masked,
model_profile_ref,
protocol,
lifecycle_state,
first_observed_at,
last_observed_at,
evidence
) values ($1, $2, $3, $4, $5, $6, 'quarantine', $7, $7, $8::jsonb)
on conflict (identifier_kind, identifier_digest, model_profile_ref)
do update set
last_observed_at = greatest(
device_discoveries.last_observed_at,
excluded.last_observed_at
),
evidence = excluded.evidence,
updated_at = now()
returning id, lifecycle_state, model_profile_ref, protocol,
identifier_kind, identifier_masked, first_observed_at,
last_observed_at, (xmax = 0) as created`,
[
randomUUID(),
safeView.identifier.kind,
identifierDigest,
safeView.identifier.masked,
safeView.modelProfileRef,
safeView.protocol,
safeView.observedAt,
JSON.stringify(safeView.evidence),
],
);
const row = result.rows[0];
return {
created: row.created === true,
value: {
schemaVersion: "nodedc.device.discovery-view.v1",
discoveryRef: `discovery:${row.id}`,
modelProfileRef: row.model_profile_ref,
protocol: row.protocol,
observedAt: new Date(row.last_observed_at).toISOString(),
lifecycleState: row.lifecycle_state,
identifier: {
kind: row.identifier_kind,
masked: row.identifier_masked,
},
evidence: safeView.evidence,
commandTransport: "disabled",
},
};
}
async close() {
await this.pool.end();
}
}
function normalizePoolSize(value) {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 50) {
throw new TypeError("device_database_pool_size_invalid");
}
return parsed;
}
@@ -0,0 +1,107 @@
import { readFile } from "node:fs/promises";
import { createControlCoreApp } from "./app.mjs";
import { resolveDeviceDatabaseUrl } from "./database-config.mjs";
import { PostgresDeviceRepository } from "./postgres-repository.mjs";
const config = await readConfig();
const repository = new PostgresDeviceRepository({
databaseUrl: config.databaseUrl,
poolSize: config.databasePoolSize,
});
await repository.migrate();
const server = createControlCoreApp({
repository,
gatewayToken: config.gatewayToken,
identifierPepper: config.identifierPepper,
discoveryIngestEnabled: config.discoveryIngestEnabled,
});
server.listen(config.port, config.host, () => {
console.log(JSON.stringify({
event: "device_control_core_started",
host: config.host,
port: config.port,
discoveryIngest: config.discoveryIngestEnabled,
commandTransport: "disabled",
}));
});
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
async function shutdown() {
server.close(async () => {
await repository.close();
process.exit(0);
});
}
async function readConfig() {
const discoveryIngestEnabled = parseBoolean(
process.env.DEVICE_DISCOVERY_INGEST_ENABLED,
false,
);
return {
host: String(process.env.HOST || "127.0.0.1").trim(),
port: parsePort(process.env.PORT, 18120),
databaseUrl: await resolveDeviceDatabaseUrl(process.env),
databasePoolSize: parsePositiveInt(
process.env.DEVICE_DATABASE_POOL_SIZE,
10,
),
discoveryIngestEnabled,
gatewayToken: discoveryIngestEnabled
? await readRequiredSecretFile(
process.env.DEVICE_GATEWAY_CORE_TOKEN_FILE,
"device_gateway_core_token_file_required",
)
: "",
identifierPepper: discoveryIngestEnabled
? await readRequiredSecretFile(
process.env.DEVICE_IDENTIFIER_PEPPER_FILE,
"device_identifier_pepper_file_required",
)
: "",
};
}
async function readRequiredSecretFile(path, errorCode) {
const normalized = requiredValue(path, errorCode);
const value = (await readFile(normalized, "utf8")).trim();
if (value.length < 32) throw new Error(errorCode);
return value;
}
function requiredValue(value, errorCode) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(errorCode);
}
return value.trim();
}
function parsePort(value, fallback) {
const parsed = Number(value || fallback);
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) {
throw new Error("device_control_port_invalid");
}
return parsed;
}
function parsePositiveInt(value, fallback) {
const parsed = Number(value || fallback);
if (!Number.isSafeInteger(parsed) || parsed < 1) {
throw new Error("device_positive_integer_invalid");
}
return parsed;
}
function parseBoolean(value, fallback) {
if (value === undefined || value === null || value === "") return fallback;
const normalized = String(value).trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) return true;
if (["0", "false", "no", "off"].includes(normalized)) return false;
throw new Error("device_boolean_invalid");
}