fix(deploy): keep manager releases on stable runner
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { connect as connectHttp2 } from "node:http2";
|
||||
|
||||
import {
|
||||
DEVICE_EDGE_CHANNEL_LIMITS,
|
||||
DEVICE_EDGE_CHANNEL_PATH,
|
||||
createChannelEnvelope,
|
||||
createChannelEnvelopeDecoder,
|
||||
encodeChannelEnvelope,
|
||||
nextReconnectDelay,
|
||||
normalizeCertificateIdentities,
|
||||
normalizeCertificateFingerprint,
|
||||
} from "../../../packages/device-edge-channel-contract/src/index.mjs";
|
||||
import {
|
||||
normalizeAdapterMessage,
|
||||
normalizeDiscoverySignal,
|
||||
} from "../../../packages/device-protocol-contract/src/index.mjs";
|
||||
|
||||
// Runtime-owned transport implementation; kept inside the deployable Core context.
|
||||
const CHANNEL_TRACKER_SESSION_ID = "channel:control";
|
||||
const CHANNEL_PROFILE_REF = "channel.control.v1";
|
||||
|
||||
export function createDeviceGatewayCoreChannelClient(options = {}) {
|
||||
const config = normalizeConfig(options);
|
||||
const readyWaiters = new Set();
|
||||
let running = false;
|
||||
let state = null;
|
||||
let reconnectTimer = null;
|
||||
let reconnectAttempt = 0;
|
||||
let connectionSerial = 0;
|
||||
let totalConnectionAttempts = 0;
|
||||
let totalChannelsAccepted = 0;
|
||||
let totalReconnects = 0;
|
||||
let totalEventsAccepted = 0;
|
||||
let totalEventsRejected = 0;
|
||||
let totalProtocolFailures = 0;
|
||||
let lastErrorCode = null;
|
||||
|
||||
return Object.freeze({
|
||||
async start() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
void connectNow();
|
||||
},
|
||||
async stop() {
|
||||
running = false;
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
const current = state;
|
||||
state = null;
|
||||
if (current) closeConnection(current, false);
|
||||
rejectReadyWaiters("device_gateway_core_channel_stopped");
|
||||
},
|
||||
waitForReady(timeoutMs = 5_000) {
|
||||
if (state?.ready && !state.closed) return Promise.resolve(status());
|
||||
const normalizedTimeout = normalizeInteger(
|
||||
timeoutMs,
|
||||
10,
|
||||
120_000,
|
||||
5_000,
|
||||
"ready_timeout",
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiter = { resolve, reject, timer: null };
|
||||
waiter.timer = setTimeout(() => {
|
||||
readyWaiters.delete(waiter);
|
||||
reject(new Error("device_gateway_core_channel_ready_timeout"));
|
||||
}, normalizedTimeout);
|
||||
waiter.timer.unref?.();
|
||||
readyWaiters.add(waiter);
|
||||
});
|
||||
},
|
||||
status,
|
||||
disconnect() {
|
||||
if (state) closeConnection(state, true);
|
||||
},
|
||||
});
|
||||
|
||||
function status() {
|
||||
return Object.freeze({
|
||||
running,
|
||||
channel: state?.ready ? "accepted" : state ? "connecting" : "absent",
|
||||
edgeRegistrationId: state?.registration?.edgeRegistrationId ?? null,
|
||||
channelGeneration: state?.channelGeneration ?? null,
|
||||
edgeTrustGeneration: state?.observedEdgeIdentity?.generationRef ?? null,
|
||||
edgeCertificateFingerprint:
|
||||
state?.observedEdgeIdentity?.fingerprint ?? null,
|
||||
activeTrackerSessionChains: state?.sessionChains.size ?? 0,
|
||||
connectionAttempts: totalConnectionAttempts,
|
||||
channelsAccepted: totalChannelsAccepted,
|
||||
reconnects: totalReconnects,
|
||||
eventsAccepted: totalEventsAccepted,
|
||||
eventsRejected: totalEventsRejected,
|
||||
protocolFailures: totalProtocolFailures,
|
||||
lastErrorCode,
|
||||
trackerIngress: "remote-edge-only",
|
||||
commandTransport: "disabled",
|
||||
});
|
||||
}
|
||||
|
||||
async function connectNow() {
|
||||
if (!running || state) return;
|
||||
totalConnectionAttempts += 1;
|
||||
const serial = ++connectionSerial;
|
||||
let registration;
|
||||
try {
|
||||
registration = normalizeRegistration(await config.registrationProvider());
|
||||
if (registration.lifecycleState !== "active") {
|
||||
throw new Error("device_gateway_core_edge_registration_inactive");
|
||||
}
|
||||
} catch (error) {
|
||||
lastErrorCode = safeErrorCode(error);
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = {
|
||||
serial,
|
||||
registration,
|
||||
session: null,
|
||||
request: null,
|
||||
decoder: createChannelEnvelopeDecoder({
|
||||
direction: "edge-to-core",
|
||||
maxEnvelopeBytes: config.maxEnvelopeBytes,
|
||||
}),
|
||||
sessionChains: new Map(),
|
||||
channelGeneration: null,
|
||||
observedEdgeIdentity: null,
|
||||
edgeSequence: 0,
|
||||
coreSequence: 0,
|
||||
lastEdgeActivityAt: config.clock(),
|
||||
heartbeatTimer: null,
|
||||
ready: false,
|
||||
closed: false,
|
||||
};
|
||||
state = connection;
|
||||
const endpoint = new URL(registration.endpoint);
|
||||
const authority = `${endpoint.protocol}//${endpoint.host}`;
|
||||
const session = connectHttp2(authority, {
|
||||
key: config.tls.key,
|
||||
cert: config.tls.cert,
|
||||
ca: config.tls.ca,
|
||||
minVersion: "TLSv1.3",
|
||||
maxVersion: "TLSv1.3",
|
||||
rejectUnauthorized: true,
|
||||
servername: registration.servername,
|
||||
ALPNProtocols: ["h2"],
|
||||
settings: {
|
||||
enablePush: false,
|
||||
initialWindowSize: 1024 * 1024,
|
||||
},
|
||||
});
|
||||
connection.session = session;
|
||||
session.once("error", (error) => failConnection(connection, error));
|
||||
session.once("close", () => closeConnection(connection, true));
|
||||
session.once("connect", () => {
|
||||
try {
|
||||
verifyEdgePeer(connection);
|
||||
openChannelStream(connection);
|
||||
} catch (error) {
|
||||
failConnection(connection, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openChannelStream(connection) {
|
||||
assertCurrent(connection);
|
||||
const request = connection.session.request({
|
||||
":method": "POST",
|
||||
":path": DEVICE_EDGE_CHANNEL_PATH,
|
||||
"content-type": "application/x-ndjson",
|
||||
"cache-control": "no-store",
|
||||
}, { endStream: false });
|
||||
connection.request = request;
|
||||
request.once("response", (headers) => {
|
||||
if (Number(headers[":status"]) !== 200) {
|
||||
failConnection(connection, new Error(
|
||||
`device_gateway_core_channel_http_status_${headers[":status"]}`,
|
||||
));
|
||||
}
|
||||
});
|
||||
let processing = Promise.resolve();
|
||||
request.on("data", (chunk) => {
|
||||
request.pause();
|
||||
processing = processing
|
||||
.then(async () => {
|
||||
const envelopes = connection.decoder.push(chunk);
|
||||
for (const envelope of envelopes) {
|
||||
await handleEdgeEnvelope(connection, envelope);
|
||||
}
|
||||
})
|
||||
.catch((error) => failConnection(connection, error))
|
||||
.finally(() => {
|
||||
if (!connection.closed) request.resume();
|
||||
});
|
||||
});
|
||||
request.once("aborted", () => closeConnection(connection, true));
|
||||
request.once("close", () => closeConnection(connection, true));
|
||||
request.once("error", (error) => failConnection(connection, error));
|
||||
connection.heartbeatTimer = setInterval(
|
||||
() => checkChannelHealth(connection),
|
||||
config.keepaliveMs,
|
||||
);
|
||||
connection.heartbeatTimer.unref?.();
|
||||
}
|
||||
|
||||
async function handleEdgeEnvelope(connection, envelope) {
|
||||
assertCurrent(connection);
|
||||
if (
|
||||
envelope.edgeRegistrationId !== connection.registration.edgeRegistrationId
|
||||
|| envelope.sequence !== connection.edgeSequence + 1
|
||||
) {
|
||||
throw new Error("device_gateway_core_edge_envelope_mismatch");
|
||||
}
|
||||
if (
|
||||
connection.channelGeneration
|
||||
&& envelope.channelGeneration !== connection.channelGeneration
|
||||
) {
|
||||
throw new Error("device_gateway_core_channel_generation_mismatch");
|
||||
}
|
||||
connection.edgeSequence = envelope.sequence;
|
||||
connection.lastEdgeActivityAt = config.clock();
|
||||
|
||||
if (!connection.ready) {
|
||||
if (envelope.messageKind !== "channel.hello") {
|
||||
throw new Error("device_gateway_core_channel_hello_required");
|
||||
}
|
||||
if (
|
||||
envelope.channelGeneration !== connection.registration.channelGeneration
|
||||
) {
|
||||
throw new Error("device_gateway_core_channel_generation_mismatch");
|
||||
}
|
||||
if (
|
||||
envelope.payload?.status !== "ready"
|
||||
|| envelope.payload?.transport !== "http2-mtls"
|
||||
|| envelope.payload?.trustGeneration
|
||||
!== connection.observedEdgeIdentity?.generationRef
|
||||
|| envelope.payload?.commandTransport !== "disabled"
|
||||
) {
|
||||
throw new Error("device_gateway_core_channel_hello_invalid");
|
||||
}
|
||||
connection.channelGeneration = connection.registration.channelGeneration;
|
||||
send(connection, "channel.accepted", {
|
||||
status: "accepted",
|
||||
coreIdentity: config.coreIdentity,
|
||||
commandTransport: "disabled",
|
||||
}, {
|
||||
trackerSessionId: CHANNEL_TRACKER_SESSION_ID,
|
||||
adapterProfileRef: CHANNEL_PROFILE_REF,
|
||||
correlationId: envelope.correlationId,
|
||||
});
|
||||
connection.ready = true;
|
||||
reconnectAttempt = 0;
|
||||
totalChannelsAccepted += 1;
|
||||
lastErrorCode = null;
|
||||
resolveReadyWaiters();
|
||||
return;
|
||||
}
|
||||
if (envelope.messageKind === "channel.heartbeat") return;
|
||||
if (["discovery.observed", "adapter.message"].includes(envelope.messageKind)) {
|
||||
scheduleTrackerEvent(connection, envelope);
|
||||
return;
|
||||
}
|
||||
throw new Error("device_gateway_core_edge_message_unhandled");
|
||||
}
|
||||
|
||||
function scheduleTrackerEvent(connection, envelope) {
|
||||
if (envelope.trackerSessionId === CHANNEL_TRACKER_SESSION_ID) {
|
||||
throw new Error("device_gateway_core_tracker_session_invalid");
|
||||
}
|
||||
const previous = connection.sessionChains.get(envelope.trackerSessionId);
|
||||
if (!previous && connection.sessionChains.size >= 128) {
|
||||
throw new Error("device_gateway_core_tracker_session_limit_reached");
|
||||
}
|
||||
const work = (previous ?? Promise.resolve())
|
||||
.then(() => envelope.messageKind === "discovery.observed"
|
||||
? acceptDiscovery(connection, envelope)
|
||||
: acceptAdapterMessage(connection, envelope))
|
||||
.catch((error) => failConnection(connection, error))
|
||||
.finally(() => {
|
||||
if (connection.sessionChains.get(envelope.trackerSessionId) === work) {
|
||||
connection.sessionChains.delete(envelope.trackerSessionId);
|
||||
}
|
||||
});
|
||||
connection.sessionChains.set(envelope.trackerSessionId, work);
|
||||
}
|
||||
|
||||
async function acceptDiscovery(connection, envelope) {
|
||||
try {
|
||||
const signal = normalizeDiscoverySignal(envelope.payload?.signal);
|
||||
const discovery = await config.observeDiscovery(signal);
|
||||
sendEventResult(connection, envelope, { discovery });
|
||||
totalEventsAccepted += 1;
|
||||
} catch (error) {
|
||||
sendEventRejection(connection, envelope, error);
|
||||
totalEventsRejected += 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function acceptAdapterMessage(connection, envelope) {
|
||||
try {
|
||||
const message = normalizeAdapterMessage(envelope.payload?.message, {
|
||||
maxBytes: config.maxEnvelopeBytes,
|
||||
});
|
||||
const acceptance = await config.acceptMessage(message);
|
||||
sendEventResult(connection, envelope, { acceptance });
|
||||
totalEventsAccepted += 1;
|
||||
} catch (error) {
|
||||
sendEventRejection(connection, envelope, error);
|
||||
totalEventsRejected += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function sendEventResult(connection, envelope, result) {
|
||||
send(connection, "event.accepted", { result }, {
|
||||
trackerSessionId: envelope.trackerSessionId,
|
||||
adapterProfileRef: envelope.adapterProfileRef,
|
||||
correlationId: envelope.correlationId,
|
||||
});
|
||||
}
|
||||
|
||||
function sendEventRejection(connection, envelope, error) {
|
||||
send(connection, "event.rejected", {
|
||||
errorCode: safeErrorCode(error),
|
||||
}, {
|
||||
trackerSessionId: envelope.trackerSessionId,
|
||||
adapterProfileRef: envelope.adapterProfileRef,
|
||||
correlationId: envelope.correlationId,
|
||||
});
|
||||
}
|
||||
|
||||
function send(connection, messageKind, payload, metadata) {
|
||||
assertCurrent(connection);
|
||||
if (!connection.channelGeneration) {
|
||||
throw new Error("device_gateway_core_channel_generation_absent");
|
||||
}
|
||||
connection.coreSequence += 1;
|
||||
const now = config.now();
|
||||
const envelope = createChannelEnvelope({
|
||||
edgeRegistrationId: connection.registration.edgeRegistrationId,
|
||||
channelGeneration: connection.channelGeneration,
|
||||
trackerSessionId: metadata.trackerSessionId,
|
||||
adapterProfileRef: metadata.adapterProfileRef,
|
||||
sequence: connection.coreSequence,
|
||||
eventAt: metadata.eventAt ?? now,
|
||||
receivedAt: now,
|
||||
messageKind,
|
||||
correlationId: metadata.correlationId,
|
||||
payload,
|
||||
}, {
|
||||
direction: "core-to-edge",
|
||||
maxEnvelopeBytes: config.maxEnvelopeBytes,
|
||||
});
|
||||
connection.request.write(encodeChannelEnvelope(envelope, {
|
||||
direction: "core-to-edge",
|
||||
maxEnvelopeBytes: config.maxEnvelopeBytes,
|
||||
}));
|
||||
}
|
||||
|
||||
function checkChannelHealth(connection) {
|
||||
if (connection.closed || state !== connection) return;
|
||||
if (config.clock() - connection.lastEdgeActivityAt >= config.deadPeerMs) {
|
||||
failConnection(connection, new Error("device_gateway_core_edge_dead_peer"));
|
||||
return;
|
||||
}
|
||||
if (connection.ready) {
|
||||
try {
|
||||
send(connection, "channel.heartbeat", { status: "alive" }, {
|
||||
trackerSessionId: CHANNEL_TRACKER_SESSION_ID,
|
||||
adapterProfileRef: CHANNEL_PROFILE_REF,
|
||||
correlationId: `correlation:${randomUUID()}`,
|
||||
});
|
||||
} catch (error) {
|
||||
failConnection(connection, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function verifyEdgePeer(connection) {
|
||||
const socket = connection.session.socket;
|
||||
if (!socket?.authorized || socket.alpnProtocol !== "h2") {
|
||||
throw new Error("device_gateway_core_edge_tls_unauthorized");
|
||||
}
|
||||
const observed = normalizeCertificateFingerprint(
|
||||
socket.getPeerCertificate()?.fingerprint256,
|
||||
);
|
||||
const identity = connection.registration.certificateIdentities.find(
|
||||
(candidate) => candidate.fingerprint === observed,
|
||||
);
|
||||
if (!identity) {
|
||||
throw new Error("device_gateway_core_edge_identity_mismatch");
|
||||
}
|
||||
connection.observedEdgeIdentity = identity;
|
||||
}
|
||||
|
||||
function failConnection(connection, error) {
|
||||
if (connection.closed) return;
|
||||
totalProtocolFailures += 1;
|
||||
lastErrorCode = safeErrorCode(error);
|
||||
closeConnection(connection, true);
|
||||
}
|
||||
|
||||
function closeConnection(connection, reconnect) {
|
||||
if (connection.closed) return;
|
||||
connection.closed = true;
|
||||
clearInterval(connection.heartbeatTimer);
|
||||
connection.heartbeatTimer = null;
|
||||
connection.sessionChains.clear();
|
||||
try {
|
||||
connection.request?.close();
|
||||
} catch {}
|
||||
try {
|
||||
connection.session?.close();
|
||||
} catch {}
|
||||
if (state === connection) state = null;
|
||||
if (reconnect && running) scheduleReconnect();
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (!running || reconnectTimer || state) return;
|
||||
const delay = nextReconnectDelay(reconnectAttempt, {
|
||||
minimumMs: config.reconnectMinimumMs,
|
||||
maximumMs: config.reconnectMaximumMs,
|
||||
random: config.random,
|
||||
});
|
||||
reconnectAttempt += 1;
|
||||
totalReconnects += 1;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
void connectNow();
|
||||
}, delay);
|
||||
reconnectTimer.unref?.();
|
||||
}
|
||||
|
||||
function assertCurrent(connection) {
|
||||
if (!running || connection.closed || state !== connection) {
|
||||
throw new Error("device_gateway_core_channel_unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
function resolveReadyWaiters() {
|
||||
const value = status();
|
||||
for (const waiter of readyWaiters) {
|
||||
clearTimeout(waiter.timer);
|
||||
waiter.resolve(value);
|
||||
}
|
||||
readyWaiters.clear();
|
||||
}
|
||||
|
||||
function rejectReadyWaiters(code) {
|
||||
for (const waiter of readyWaiters) {
|
||||
clearTimeout(waiter.timer);
|
||||
waiter.reject(new Error(code));
|
||||
}
|
||||
readyWaiters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeConfig(options) {
|
||||
if (typeof options.observeDiscovery !== "function") {
|
||||
throw new TypeError("device_gateway_core_observe_discovery_invalid");
|
||||
}
|
||||
if (typeof options.acceptMessage !== "function") {
|
||||
throw new TypeError("device_gateway_core_accept_message_invalid");
|
||||
}
|
||||
const registrationProvider = typeof options.registrationProvider === "function"
|
||||
? options.registrationProvider
|
||||
: async () => options.registration;
|
||||
const tls = normalizeTls(options.tls);
|
||||
const keepaliveMs = normalizeInteger(
|
||||
options.keepaliveMs,
|
||||
10,
|
||||
120_000,
|
||||
DEVICE_EDGE_CHANNEL_LIMITS.keepaliveMs,
|
||||
"keepalive",
|
||||
);
|
||||
const deadPeerMs = normalizeInteger(
|
||||
options.deadPeerMs,
|
||||
keepaliveMs * 2,
|
||||
120_000,
|
||||
DEVICE_EDGE_CHANNEL_LIMITS.deadPeerMs,
|
||||
"dead_peer",
|
||||
);
|
||||
const reconnectMinimumMs = normalizeInteger(
|
||||
options.reconnectMinimumMs,
|
||||
10,
|
||||
120_000,
|
||||
DEVICE_EDGE_CHANNEL_LIMITS.reconnectMinimumMs,
|
||||
"reconnect_minimum",
|
||||
);
|
||||
const reconnectMaximumMs = normalizeInteger(
|
||||
options.reconnectMaximumMs,
|
||||
10,
|
||||
120_000,
|
||||
DEVICE_EDGE_CHANNEL_LIMITS.reconnectMaximumMs,
|
||||
"reconnect_maximum",
|
||||
);
|
||||
if (reconnectMaximumMs < reconnectMinimumMs) {
|
||||
throw new TypeError("device_gateway_core_reconnect_range_invalid");
|
||||
}
|
||||
return Object.freeze({
|
||||
registrationProvider,
|
||||
tls,
|
||||
coreIdentity: normalizeRef(options.coreIdentity, "core_identity"),
|
||||
observeDiscovery: options.observeDiscovery,
|
||||
acceptMessage: options.acceptMessage,
|
||||
keepaliveMs,
|
||||
deadPeerMs,
|
||||
reconnectMinimumMs,
|
||||
reconnectMaximumMs,
|
||||
maxEnvelopeBytes: normalizeInteger(
|
||||
options.maxEnvelopeBytes,
|
||||
256,
|
||||
DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes,
|
||||
DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes,
|
||||
"max_envelope_bytes",
|
||||
),
|
||||
random: typeof options.random === "function" ? options.random : Math.random,
|
||||
clock: typeof options.clock === "function" ? options.clock : Date.now,
|
||||
now: typeof options.now === "function"
|
||||
? () => new Date(options.now()).toISOString()
|
||||
: () => new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeRegistration(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new TypeError("device_gateway_core_edge_registration_invalid");
|
||||
}
|
||||
let endpoint;
|
||||
try {
|
||||
endpoint = new URL(String(value.endpoint || ""));
|
||||
} catch {
|
||||
throw new TypeError("device_gateway_core_edge_endpoint_invalid");
|
||||
}
|
||||
if (
|
||||
endpoint.protocol !== "https:"
|
||||
|| endpoint.username
|
||||
|| endpoint.password
|
||||
|| endpoint.pathname !== "/"
|
||||
|| endpoint.search
|
||||
|| endpoint.hash
|
||||
) {
|
||||
throw new TypeError("device_gateway_core_edge_endpoint_invalid");
|
||||
}
|
||||
if (!["active", "revoked", "disabled"].includes(value.lifecycleState)) {
|
||||
throw new TypeError("device_gateway_core_edge_lifecycle_invalid");
|
||||
}
|
||||
const servername = String(value.servername || "");
|
||||
if (!/^[A-Za-z0-9.-]{1,253}$/.test(servername)) {
|
||||
throw new TypeError("device_gateway_core_edge_servername_invalid");
|
||||
}
|
||||
return Object.freeze({
|
||||
edgeRegistrationId: normalizeRef(
|
||||
value.edgeRegistrationId,
|
||||
"edge_registration_id",
|
||||
),
|
||||
channelGeneration: normalizeRef(
|
||||
value.channelGeneration,
|
||||
"channel_generation",
|
||||
),
|
||||
endpoint: endpoint.toString(),
|
||||
servername,
|
||||
certificateIdentities: normalizeCertificateIdentities(
|
||||
value.certificateIdentities,
|
||||
),
|
||||
lifecycleState: value.lifecycleState,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeTls(value) {
|
||||
if (!value || typeof value !== "object") {
|
||||
throw new TypeError("device_gateway_core_channel_tls_invalid");
|
||||
}
|
||||
for (const key of ["key", "cert", "ca"]) {
|
||||
if (!(typeof value[key] === "string" || Buffer.isBuffer(value[key]))) {
|
||||
throw new TypeError(`device_gateway_core_channel_tls_${key}_invalid`);
|
||||
}
|
||||
}
|
||||
return Object.freeze({ key: value.key, cert: value.cert, ca: value.ca });
|
||||
}
|
||||
|
||||
function safeErrorCode(error) {
|
||||
const value = String(error?.message || error || "device_gateway_core_error")
|
||||
.toLowerCase()
|
||||
.replaceAll(/[^a-z0-9._:-]/g, "_")
|
||||
.slice(0, 128);
|
||||
return /^[a-z][a-z0-9._:-]{2,127}$/.test(value)
|
||||
? value
|
||||
: "device_gateway_core_event_rejected";
|
||||
}
|
||||
|
||||
function normalizeRef(value, field) {
|
||||
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
|
||||
throw new TypeError(`device_gateway_core_${field}_invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeInteger(value, minimum, maximum, fallback, field) {
|
||||
const number = value == null ? fallback : Number(value);
|
||||
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
||||
throw new TypeError(`device_gateway_core_${field}_invalid`);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../../packages/device-edge-channel-contract/src/index.mjs";
|
||||
import {
|
||||
createDeviceGatewayCoreChannelClient,
|
||||
} from "../../device-gateway-core/src/runtime.mjs";
|
||||
} from "./device-gateway-core-runtime.mjs";
|
||||
|
||||
const DEFAULT_TRUST_ROOT = "/run/nodedc-secrets/device-edge-channel/peers";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user