feat: establish standalone Device Core repository

This commit is contained in:
DCCONSTRUCTIONS
2026-08-21 11:51:21 +03:00
commit e0bac205d0
244 changed files with 51962 additions and 0 deletions
@@ -0,0 +1,12 @@
{
"name": "@nodedc/device-edge-channel-contract",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.mjs"
},
"engines": {
"node": ">=20"
}
}
@@ -0,0 +1,387 @@
export const DEVICE_EDGE_CHANNEL_SCHEMA =
"nodedc.device-edge.channel-envelope.v1";
export const DEVICE_EDGE_CHANNEL_PATH =
"/internal/v1/device-edge/channel";
export const DEVICE_EDGE_CHANNEL_LIMITS = Object.freeze({
maxEnvelopeBytes: 1024 * 1024,
maxPendingAcceptances: 128,
keepaliveMs: 15_000,
deadPeerMs: 45_000,
reconnectMinimumMs: 1_000,
reconnectMaximumMs: 30_000,
});
export const EDGE_TO_CORE_MESSAGE_KINDS = Object.freeze([
"channel.hello",
"channel.heartbeat",
"tracker.session-opened",
"tracker.session-closed",
"discovery.observed",
"adapter.message",
"delivery.acknowledged",
"command.status",
"channel.counters",
]);
export const CORE_TO_EDGE_MESSAGE_KINDS = Object.freeze([
"channel.accepted",
"channel.heartbeat",
"flow.window",
"session.disposition",
"event.accepted",
"event.rejected",
]);
const OPAQUE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
const forbiddenKeyFragments = Object.freeze([
"password",
"secret",
"credential",
"authorization",
"privatekey",
"bearertoken",
]);
export function normalizeChannelEnvelope(input, options = {}) {
assertPlainObject(input, "device_edge_channel_envelope");
const direction = normalizeDirection(options.direction);
const maxEnvelopeBytes = normalizeLimit(
options.maxEnvelopeBytes,
DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes,
);
const allowedKeys = new Set([
"schemaVersion",
"edgeRegistrationId",
"channelGeneration",
"trackerSessionId",
"adapterProfileRef",
"sequence",
"eventAt",
"receivedAt",
"payloadBytes",
"messageKind",
"correlationId",
"payload",
]);
rejectUnexpectedKeys(input, allowedKeys);
rejectForbiddenKeys(input);
if (input.schemaVersion !== DEVICE_EDGE_CHANNEL_SCHEMA) {
throw new TypeError("device_edge_channel_schema_invalid");
}
const allowedKinds = direction === "edge-to-core"
? EDGE_TO_CORE_MESSAGE_KINDS
: CORE_TO_EDGE_MESSAGE_KINDS;
if (!allowedKinds.includes(input.messageKind)) {
throw new TypeError("device_edge_channel_message_kind_invalid");
}
const sequence = Number(input.sequence);
if (!Number.isSafeInteger(sequence) || sequence < 1) {
throw new TypeError("device_edge_channel_sequence_invalid");
}
assertJsonValue(input.payload, 0);
const payload = cloneJsonValue(input.payload);
const payloadBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
if (payloadBytes > maxEnvelopeBytes) {
throw new TypeError("device_edge_channel_payload_too_large");
}
if (Number(input.payloadBytes) !== payloadBytes) {
throw new TypeError("device_edge_channel_payload_length_mismatch");
}
const normalized = {
schemaVersion: DEVICE_EDGE_CHANNEL_SCHEMA,
edgeRegistrationId: normalizeRef(
input.edgeRegistrationId,
"edge_registration_id",
),
channelGeneration: normalizeRef(
input.channelGeneration,
"channel_generation",
),
trackerSessionId: normalizeRef(
input.trackerSessionId,
"tracker_session_id",
),
adapterProfileRef: normalizeRef(
input.adapterProfileRef,
"adapter_profile_ref",
),
sequence,
eventAt: normalizeTimestamp(input.eventAt, "event_at"),
receivedAt: normalizeTimestamp(input.receivedAt, "received_at"),
payloadBytes,
messageKind: input.messageKind,
correlationId: normalizeRef(input.correlationId, "correlation_id"),
payload,
};
return deepFreeze(normalized);
}
export function createChannelEnvelope(fields, options = {}) {
assertPlainObject(fields, "device_edge_channel_fields");
const payload = fields.payload ?? {};
assertJsonValue(payload, 0);
const clonedPayload = cloneJsonValue(payload);
return normalizeChannelEnvelope({
...fields,
schemaVersion: DEVICE_EDGE_CHANNEL_SCHEMA,
payloadBytes: Buffer.byteLength(JSON.stringify(clonedPayload), "utf8"),
payload: clonedPayload,
}, options);
}
export function encodeChannelEnvelope(envelope, options = {}) {
const normalized = normalizeChannelEnvelope(envelope, options);
const encoded = Buffer.from(`${JSON.stringify(normalized)}\n`, "utf8");
const maxEnvelopeBytes = normalizeLimit(
options.maxEnvelopeBytes,
DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes,
);
if (encoded.length > maxEnvelopeBytes) {
throw new TypeError("device_edge_channel_envelope_too_large");
}
return encoded;
}
export function createChannelEnvelopeDecoder(options = {}) {
const direction = normalizeDirection(options.direction);
const maxEnvelopeBytes = normalizeLimit(
options.maxEnvelopeBytes,
DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes,
);
let buffered = Buffer.alloc(0);
return Object.freeze({
push(chunk) {
if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array)) {
throw new TypeError("device_edge_channel_chunk_invalid");
}
let incoming = Buffer.from(chunk);
const envelopes = [];
while (incoming.length > 0) {
const newlineIndex = incoming.indexOf(0x0a);
if (newlineIndex < 0) {
if (buffered.length + incoming.length > maxEnvelopeBytes) {
throw new TypeError("device_edge_channel_envelope_too_large");
}
buffered = buffered.length === 0
? incoming
: Buffer.concat([buffered, incoming]);
break;
}
if (buffered.length + newlineIndex === 0) {
throw new TypeError("device_edge_channel_envelope_empty");
}
if (buffered.length + newlineIndex + 1 > maxEnvelopeBytes) {
throw new TypeError("device_edge_channel_envelope_too_large");
}
const segment = incoming.subarray(0, newlineIndex);
const line = buffered.length === 0
? segment
: Buffer.concat([buffered, segment]);
buffered = Buffer.alloc(0);
incoming = incoming.subarray(newlineIndex + 1);
let parsed;
try {
parsed = JSON.parse(line.toString("utf8"));
} catch {
throw new TypeError("device_edge_channel_envelope_json_invalid");
}
envelopes.push(normalizeChannelEnvelope(parsed, {
direction,
maxEnvelopeBytes,
}));
}
return envelopes;
},
finish() {
if (buffered.length !== 0) {
throw new TypeError("device_edge_channel_envelope_truncated");
}
},
bufferedBytes() {
return buffered.length;
},
});
}
export function normalizeCertificateFingerprint(value) {
const compact = String(value || "").replaceAll(":", "").toUpperCase();
if (!/^[A-F0-9]{64}$/.test(compact)) {
throw new TypeError("device_edge_channel_certificate_fingerprint_invalid");
}
return compact.match(/.{2}/g).join(":");
}
export function normalizeCertificateIdentities(value) {
if (!Array.isArray(value) || value.length < 1 || value.length > 2) {
throw new TypeError("device_edge_channel_certificate_identities_invalid");
}
const generations = new Set();
const fingerprints = new Set();
let activeCount = 0;
const identities = value.map((identity) => {
assertPlainObject(identity, "device_edge_channel_certificate_identity");
rejectUnexpectedKeys(
identity,
new Set(["generationRef", "fingerprint", "status"]),
);
if (!["active", "staged"].includes(identity.status)) {
throw new TypeError("device_edge_channel_certificate_identity_status_invalid");
}
if (identity.status === "active") activeCount += 1;
const generationRef = normalizeRef(
identity.generationRef,
"certificate_generation_ref",
);
const fingerprint = normalizeCertificateFingerprint(identity.fingerprint);
if (generations.has(generationRef) || fingerprints.has(fingerprint)) {
throw new TypeError("device_edge_channel_certificate_identity_duplicate");
}
generations.add(generationRef);
fingerprints.add(fingerprint);
return Object.freeze({
generationRef,
fingerprint,
status: identity.status,
});
});
if (activeCount !== 1) {
throw new TypeError("device_edge_channel_active_certificate_identity_invalid");
}
return Object.freeze(identities);
}
export function nextReconnectDelay(attempt, options = {}) {
const normalizedAttempt = Number(attempt);
if (!Number.isSafeInteger(normalizedAttempt) || normalizedAttempt < 0) {
throw new TypeError("device_edge_channel_reconnect_attempt_invalid");
}
const minimumMs = normalizeReconnectDuration(
options.minimumMs,
DEVICE_EDGE_CHANNEL_LIMITS.reconnectMinimumMs,
);
const maximumMs = normalizeReconnectDuration(
options.maximumMs,
DEVICE_EDGE_CHANNEL_LIMITS.reconnectMaximumMs,
);
if (maximumMs < minimumMs) {
throw new TypeError("device_edge_channel_reconnect_range_invalid");
}
const random = options.random ?? Math.random;
if (typeof random !== "function") {
throw new TypeError("device_edge_channel_random_invalid");
}
const ceiling = Math.min(maximumMs, minimumMs * (2 ** normalizedAttempt));
const floor = Math.max(minimumMs, Math.floor(ceiling / 2));
const sample = Number(random());
if (!Number.isFinite(sample) || sample < 0 || sample > 1) {
throw new TypeError("device_edge_channel_random_invalid");
}
return Math.floor(floor + ((ceiling - floor) * sample));
}
function normalizeReconnectDuration(value, fallback) {
const number = value == null ? fallback : Number(value);
if (!Number.isSafeInteger(number) || number < 10 || number > 120_000) {
throw new TypeError("device_edge_channel_reconnect_duration_invalid");
}
return number;
}
function normalizeDirection(value) {
if (!['edge-to-core', 'core-to-edge'].includes(value)) {
throw new TypeError("device_edge_channel_direction_invalid");
}
return value;
}
function normalizeLimit(value, fallback) {
const number = value == null ? fallback : Number(value);
if (!Number.isSafeInteger(number) || number < 256 || number > 4 * 1024 * 1024) {
throw new TypeError("device_edge_channel_limit_invalid");
}
return number;
}
function normalizeRef(value, field) {
if (typeof value !== "string" || !OPAQUE_REF_RE.test(value)) {
throw new TypeError(`device_edge_channel_${field}_invalid`);
}
return value;
}
function normalizeTimestamp(value, field) {
if (typeof value !== "string" || !ISO_TIMESTAMP_RE.test(value)) {
throw new TypeError(`device_edge_channel_${field}_invalid`);
}
const date = new Date(value);
if (!Number.isFinite(date.getTime())) {
throw new TypeError(`device_edge_channel_${field}_invalid`);
}
return date.toISOString();
}
function assertPlainObject(value, name) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(`${name}_invalid`);
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError(`${name}_invalid`);
}
}
function rejectUnexpectedKeys(value, allowedKeys) {
for (const key of Object.keys(value)) {
if (!allowedKeys.has(key)) {
throw new TypeError(`device_edge_channel_field_unexpected:${key}`);
}
}
}
function rejectForbiddenKeys(value, depth = 0) {
if (depth > 16 || value == null || typeof value !== "object") return;
for (const [key, child] of Object.entries(value)) {
const compact = key.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
if (forbiddenKeyFragments.some((fragment) => compact.includes(fragment))) {
throw new TypeError(`device_edge_channel_forbidden_field:${key}`);
}
rejectForbiddenKeys(child, depth + 1);
}
}
function assertJsonValue(value, depth) {
if (depth > 16) {
throw new TypeError("device_edge_channel_payload_depth_invalid");
}
if (value == null || typeof value === "string" || typeof value === "boolean") {
return;
}
if (typeof value === "number" && Number.isFinite(value)) return;
if (Array.isArray(value)) {
for (const child of value) assertJsonValue(child, depth + 1);
return;
}
assertPlainObject(value, "device_edge_channel_payload");
for (const child of Object.values(value)) {
assertJsonValue(child, depth + 1);
}
}
function cloneJsonValue(value) {
return JSON.parse(JSON.stringify(value));
}
function deepFreeze(value) {
if (!value || typeof value !== "object" || Object.isFrozen(value)) {
return value;
}
Object.freeze(value);
for (const child of Object.values(value)) deepFreeze(child);
return value;
}
@@ -0,0 +1,106 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
createChannelEnvelope,
createChannelEnvelopeDecoder,
encodeChannelEnvelope,
nextReconnectDelay,
normalizeCertificateIdentities,
normalizeChannelEnvelope,
} from "../src/index.mjs";
const now = "2026-08-11T12:00:00.000Z";
function envelope(overrides = {}) {
return createChannelEnvelope({
edgeRegistrationId: "edge:pilot-1",
channelGeneration: "generation:pilot-1",
trackerSessionId: "channel:control",
adapterProfileRef: "channel.control.v1",
sequence: 1,
eventAt: now,
receivedAt: now,
messageKind: "channel.hello",
correlationId: "correlation:hello-1",
payload: { status: "ready" },
...overrides,
}, { direction: "edge-to-core" });
}
test("round-trips a bounded versioned Edge envelope", () => {
const decoder = createChannelEnvelopeDecoder({ direction: "edge-to-core" });
const encoded = encodeChannelEnvelope(envelope(), {
direction: "edge-to-core",
});
const split = Math.floor(encoded.length / 2);
assert.deepEqual(decoder.push(encoded.subarray(0, split)), []);
assert.deepEqual(decoder.push(encoded.subarray(split)), [envelope()]);
assert.equal(decoder.bufferedBytes(), 0);
decoder.finish();
});
test("fails closed on unknown kinds, payload mismatches and oversized frames", () => {
const valid = envelope();
assert.throws(() => normalizeChannelEnvelope({
...valid,
messageKind: "tcp.forward",
}, { direction: "edge-to-core" }), /message_kind_invalid/);
assert.throws(() => normalizeChannelEnvelope({
...valid,
payloadBytes: valid.payloadBytes + 1,
}, { direction: "edge-to-core" }), /payload_length_mismatch/);
const decoder = createChannelEnvelopeDecoder({
direction: "edge-to-core",
maxEnvelopeBytes: 256,
});
assert.throws(() => decoder.push(Buffer.alloc(257, 0x61)), /envelope_too_large/);
});
test("uses bounded jittered exponential reconnect delays", () => {
assert.equal(nextReconnectDelay(0, {
minimumMs: 1000,
maximumMs: 30_000,
random: () => 1,
}), 1000);
assert.equal(nextReconnectDelay(5, {
minimumMs: 1000,
maximumMs: 30_000,
random: () => 1,
}), 30_000);
assert.equal(nextReconnectDelay(5, {
minimumMs: 1000,
maximumMs: 30_000,
random: () => 0,
}), 15_000);
});
test("allows exactly one active and at most one staged certificate generation", () => {
const activeFingerprint = "AA:".repeat(31) + "AA";
const stagedFingerprint = "BB:".repeat(31) + "BB";
const identities = normalizeCertificateIdentities([
{
generationRef: "trust-generation:1",
fingerprint: activeFingerprint,
status: "active",
},
{
generationRef: "trust-generation:2",
fingerprint: stagedFingerprint,
status: "staged",
},
]);
assert.equal(identities.length, 2);
assert.equal(identities[0].status, "active");
assert.equal(identities[1].status, "staged");
assert.throws(() => normalizeCertificateIdentities([
{ ...identities[0], status: "staged" },
identities[1],
]), /active_certificate_identity_invalid/);
assert.throws(() => normalizeCertificateIdentities([
identities[0],
{ ...identities[1], status: "active" },
]), /active_certificate_identity_invalid/);
});