@attraccess/plugin-wago 0.1.0-nightly.990.1 → 0.1.0-nightly.991.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -52544,14 +52544,7 @@ function parseAnnouncement(payload) {
|
|
|
52544
52544
|
return { ...heartbeat, pairingCode: input.pairingCode.trim() };
|
|
52545
52545
|
}
|
|
52546
52546
|
function parseHeartbeat(payload) {
|
|
52547
|
-
|
|
52548
|
-
try {
|
|
52549
|
-
value = JSON.parse(payload.toString("utf8"));
|
|
52550
|
-
} catch {
|
|
52551
|
-
throw new Error("announcement is not valid JSON");
|
|
52552
|
-
}
|
|
52553
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("announcement must be an object");
|
|
52554
|
-
const input = value;
|
|
52547
|
+
const input = parseObject(payload, "announcement");
|
|
52555
52548
|
const required = ["hardwareId", "protocolVersion", "runtimeVersion"];
|
|
52556
52549
|
for (const key of required)
|
|
52557
52550
|
if (typeof input[key] !== "string" || !input[key].trim()) throw new Error(`announcement ${key} is required`);
|
|
@@ -52612,23 +52605,7 @@ function parseOperationalMessage(prefix, topic, payload) {
|
|
|
52612
52605
|
throw new Error("operational streamId is invalid");
|
|
52613
52606
|
const streamId = value.streamId;
|
|
52614
52607
|
if (suffix === "state") {
|
|
52615
|
-
|
|
52616
|
-
throw new Error("invalid state message");
|
|
52617
|
-
return {
|
|
52618
|
-
hardwareId,
|
|
52619
|
-
message: {
|
|
52620
|
-
category: "state",
|
|
52621
|
-
timestamp,
|
|
52622
|
-
streamId,
|
|
52623
|
-
sequence,
|
|
52624
|
-
connected: value.connected,
|
|
52625
|
-
revision: value.revision,
|
|
52626
|
-
contentHash: value.contentHash,
|
|
52627
|
-
outputs: value.outputs,
|
|
52628
|
-
...value.inputs !== void 0 ? { inputs: value.inputs } : {},
|
|
52629
|
-
...value.readiness !== void 0 ? { readiness: { hardwareAvailable: value.readiness.hardwareAvailable } } : {}
|
|
52630
|
-
}
|
|
52631
|
-
};
|
|
52608
|
+
return { hardwareId, message: parseStateMessage(value, { timestamp, streamId, sequence }) };
|
|
52632
52609
|
}
|
|
52633
52610
|
if (suffix === "measurements") {
|
|
52634
52611
|
return {
|
|
@@ -52673,6 +52650,20 @@ function parseOperationalMessage(prefix, topic, payload) {
|
|
|
52673
52650
|
}
|
|
52674
52651
|
};
|
|
52675
52652
|
}
|
|
52653
|
+
function parseStateMessage(value, envelope) {
|
|
52654
|
+
if (typeof value.connected !== "boolean" || !isNullableInteger(value.revision) || !isNullableString(value.contentHash) || !isBooleanRecord(value.outputs) || value.inputs !== void 0 && !isBooleanRecord(value.inputs) || value.readiness !== void 0 && (!value.readiness || typeof value.readiness !== "object" || Array.isArray(value.readiness) || typeof value.readiness.hardwareAvailable !== "boolean"))
|
|
52655
|
+
throw new Error("invalid state message");
|
|
52656
|
+
return {
|
|
52657
|
+
category: "state",
|
|
52658
|
+
...envelope,
|
|
52659
|
+
connected: value.connected,
|
|
52660
|
+
revision: value.revision,
|
|
52661
|
+
contentHash: value.contentHash,
|
|
52662
|
+
outputs: value.outputs,
|
|
52663
|
+
...value.inputs !== void 0 ? { inputs: value.inputs } : {},
|
|
52664
|
+
...value.readiness !== void 0 ? { readiness: { hardwareAvailable: value.readiness.hardwareAvailable } } : {}
|
|
52665
|
+
};
|
|
52666
|
+
}
|
|
52676
52667
|
function configurationReportedHardwareId(prefix, topic) {
|
|
52677
52668
|
const topicPrefix = `${normalizeOperationalPrefix(prefix)}/v${CONFIGURATION_PROTOCOL_VERSION}/controllers/`;
|
|
52678
52669
|
const topicSuffix = "/configuration/reported";
|
|
@@ -52973,30 +52964,33 @@ function validateModbusBindings(snapshot) {
|
|
|
52973
52964
|
if (Array.isArray(snapshot.logicalChannels))
|
|
52974
52965
|
for (const channel of snapshot.logicalChannels) {
|
|
52975
52966
|
if (channel?.physicalPointId !== point.id) continue;
|
|
52976
|
-
|
|
52977
|
-
if (capabilities.includes("output") && action && device) {
|
|
52978
|
-
for (let offset = 0; offset < registerCount(action); offset++) {
|
|
52979
|
-
const key = JSON.stringify([
|
|
52980
|
-
device.connectionId,
|
|
52981
|
-
device.unitId,
|
|
52982
|
-
action.functionCode === 5 ? "coil" : "register",
|
|
52983
|
-
wireAddress(action) + offset
|
|
52984
|
-
]);
|
|
52985
|
-
if (outputOwners.has(key)) fail("each physical Modbus output must have a single logical owner");
|
|
52986
|
-
outputOwners.add(key);
|
|
52987
|
-
}
|
|
52988
|
-
}
|
|
52989
|
-
if (capabilities.includes("input") && !capabilities.includes("measurement"))
|
|
52990
|
-
fail(
|
|
52991
|
-
measurement ? "Modbus register inputs require measurement capability and its named measurement transform" : "input requires named measurement"
|
|
52992
|
-
);
|
|
52993
|
-
if (capabilities.includes("output") && !action) fail("output requires named action");
|
|
52994
|
-
if (capabilities.includes("measurement") && (!measurement || channel.measurement?.unit !== measurement.unit || (channel.measurement?.kind ?? "live") !== measurement.kind || channel.measurement?.scale !== 1 || channel.measurement?.offset !== 0))
|
|
52995
|
-
fail("measurement channel must match profile unit/kind with identity transform");
|
|
52967
|
+
validateChannelBinding(channel, measurement, action, device, outputOwners, fail);
|
|
52996
52968
|
}
|
|
52997
52969
|
}
|
|
52998
52970
|
return errors;
|
|
52999
52971
|
}
|
|
52972
|
+
function validateChannelBinding(channel, measurement, action, device, outputOwners, fail) {
|
|
52973
|
+
const capabilities = Array.isArray(channel.capabilities) ? channel.capabilities : [];
|
|
52974
|
+
if (capabilities.includes("output") && action && device) {
|
|
52975
|
+
for (let offset = 0; offset < registerCount(action); offset++) {
|
|
52976
|
+
const key = JSON.stringify([
|
|
52977
|
+
device.connectionId,
|
|
52978
|
+
device.unitId,
|
|
52979
|
+
action.functionCode === 5 ? "coil" : "register",
|
|
52980
|
+
wireAddress(action) + offset
|
|
52981
|
+
]);
|
|
52982
|
+
if (outputOwners.has(key)) fail("each physical Modbus output must have a single logical owner");
|
|
52983
|
+
outputOwners.add(key);
|
|
52984
|
+
}
|
|
52985
|
+
}
|
|
52986
|
+
if (capabilities.includes("input") && !capabilities.includes("measurement"))
|
|
52987
|
+
fail(
|
|
52988
|
+
measurement ? "Modbus register inputs require measurement capability and its named measurement transform" : "input requires named measurement"
|
|
52989
|
+
);
|
|
52990
|
+
if (capabilities.includes("output") && !action) fail("output requires named action");
|
|
52991
|
+
if (capabilities.includes("measurement") && (!measurement || channel.measurement?.unit !== measurement.unit || (channel.measurement?.kind ?? "live") !== measurement.kind || channel.measurement?.scale !== 1 || channel.measurement?.offset !== 0))
|
|
52992
|
+
fail("measurement channel must match profile unit/kind with identity transform");
|
|
52993
|
+
}
|
|
53000
52994
|
|
|
53001
52995
|
// backend/configuration.ts
|
|
53002
52996
|
var HARDWARE_PROFILES = ["751-9301", "879-3000", "879-1300", "modbus"];
|
|
@@ -53897,13 +53891,7 @@ var WagoCommandHandler = class {
|
|
|
53897
53891
|
__publicField(this, "dependencies", dependencies);
|
|
53898
53892
|
__publicField(this, "pending", /* @__PURE__ */ new Map());
|
|
53899
53893
|
}
|
|
53900
|
-
async
|
|
53901
|
-
const controllers = await this.dependencies.controllers().find({ where: { trustState: "claimed" }, order: { name: "ASC" } });
|
|
53902
|
-
const controllerId = positiveInteger2(config.controllerId);
|
|
53903
|
-
const revision2 = controllerId ? await this.dependencies.appliedRevision(controllerId) : null;
|
|
53904
|
-
const snapshot = revision2 ? JSON.parse(revision2.snapshot) : null;
|
|
53905
|
-
const channelId2 = typeof config.channelId === "string" ? config.channelId : void 0;
|
|
53906
|
-
const outputChannels = snapshot?.logicalChannels.filter((item) => item.capabilities.includes("output")) ?? [];
|
|
53894
|
+
async channelNames(controllerId, revision2) {
|
|
53907
53895
|
let names = {};
|
|
53908
53896
|
if (controllerId) {
|
|
53909
53897
|
const draft = await this.dependencies.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId });
|
|
@@ -53913,6 +53901,16 @@ var WagoCommandHandler = class {
|
|
|
53913
53901
|
} catch {
|
|
53914
53902
|
}
|
|
53915
53903
|
}
|
|
53904
|
+
return names;
|
|
53905
|
+
}
|
|
53906
|
+
async schema(config, resourceId) {
|
|
53907
|
+
const controllers = await this.dependencies.controllers().find({ where: { trustState: "claimed" }, order: { name: "ASC" } });
|
|
53908
|
+
const controllerId = positiveInteger2(config.controllerId);
|
|
53909
|
+
const revision2 = controllerId ? await this.dependencies.appliedRevision(controllerId) : null;
|
|
53910
|
+
const snapshot = revision2 ? JSON.parse(revision2.snapshot) : null;
|
|
53911
|
+
const channelId2 = typeof config.channelId === "string" ? config.channelId : void 0;
|
|
53912
|
+
const outputChannels = snapshot?.logicalChannels.filter((item) => item.capabilities.includes("output")) ?? [];
|
|
53913
|
+
const names = await this.channelNames(controllerId, revision2);
|
|
53916
53914
|
const channel = outputChannels.find((item) => item.id === channelId2);
|
|
53917
53915
|
const references = channelId2 && controllerId ? await this.references(controllerId, channelId2, resourceId) : [];
|
|
53918
53916
|
const properties = {
|
|
@@ -54345,6 +54343,30 @@ var faultCodes = /* @__PURE__ */ new Set([
|
|
|
54345
54343
|
function identifier(value) {
|
|
54346
54344
|
return typeof value === "string" && value.trim().length > 0 && value.length <= 256 && !Array.from(value).some((character) => character.charCodeAt(0) < 32);
|
|
54347
54345
|
}
|
|
54346
|
+
function isObject(value) {
|
|
54347
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
54348
|
+
}
|
|
54349
|
+
function validStatePayload(data, kind, canonical) {
|
|
54350
|
+
if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
|
|
54351
|
+
return false;
|
|
54352
|
+
if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
|
|
54353
|
+
return false;
|
|
54354
|
+
if (canonical && kind === "state" && (typeof data.connected !== "boolean" || !(data.revision === null || Number.isSafeInteger(data.revision) && data.revision >= 0) || !isObject(data.outputs) || data.inputs !== void 0 && !isObject(data.inputs) || ![...Object.values(data.outputs), ...Object.values(isObject(data.inputs) ? data.inputs : {})].every(
|
|
54355
|
+
(value) => typeof value === "boolean"
|
|
54356
|
+
)))
|
|
54357
|
+
return false;
|
|
54358
|
+
return true;
|
|
54359
|
+
}
|
|
54360
|
+
function validEventPayload(data, kind, canonical) {
|
|
54361
|
+
if (canonical && kind === "configuration/reported" && (!Number.isSafeInteger(data.revision) || data.revision < 1 || typeof data.contentHash !== "string" || !/^[0-9a-f]{64}$/i.test(data.contentHash) || !Array.isArray(data.errors)))
|
|
54362
|
+
return false;
|
|
54363
|
+
if (kind === "measurements" && (!identifier(data.channelId) || typeof data.value !== "number" || !Number.isFinite(data.value) || (canonical ? !Number.isSafeInteger(data.value) || !CANONICAL_UNITS.includes(data.unit) || !["live", "cumulative"].includes(data.kind) : !["ampere", "volt", "watt", "percent"].includes(data.unit))))
|
|
54364
|
+
return false;
|
|
54365
|
+
if (kind === "faults" && !identifier(data.channelId)) return false;
|
|
54366
|
+
if (kind === "acknowledgements" && (!identifier(data.id) || !["accepted", "duplicate", "rejected"].includes(data.status)))
|
|
54367
|
+
return false;
|
|
54368
|
+
return true;
|
|
54369
|
+
}
|
|
54348
54370
|
var WagoDiagnosticsStore = class {
|
|
54349
54371
|
constructor(now = Date.now) {
|
|
54350
54372
|
__publicField(this, "now", now);
|
|
@@ -54436,27 +54458,40 @@ var WagoDiagnosticsStore = class {
|
|
|
54436
54458
|
if (!DIAGNOSTIC_CATEGORIES.includes(kind)) return false;
|
|
54437
54459
|
const canonical = canonicalEnvelope(data, kind);
|
|
54438
54460
|
if (canonical && !validEnvelope(data, this.now())) return false;
|
|
54439
|
-
|
|
54440
|
-
if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
|
|
54441
|
-
return false;
|
|
54442
|
-
if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
|
|
54443
|
-
return false;
|
|
54444
|
-
if (canonical && kind === "configuration/reported" && (!Number.isSafeInteger(data.revision) || data.revision < 1 || typeof data.contentHash !== "string" || !/^[0-9a-f]{64}$/i.test(data.contentHash) || !Array.isArray(data.errors)))
|
|
54445
|
-
return false;
|
|
54446
|
-
if (canonical && kind === "state" && (typeof data.connected !== "boolean" || !(data.revision === null || Number.isSafeInteger(data.revision) && data.revision >= 0) || !isObject(data.outputs) || data.inputs !== void 0 && !isObject(data.inputs) || ![...Object.values(data.outputs), ...Object.values(isObject(data.inputs) ? data.inputs : {})].every(
|
|
54447
|
-
(value) => typeof value === "boolean"
|
|
54448
|
-
)))
|
|
54449
|
-
return false;
|
|
54450
|
-
if (kind === "measurements" && (!identifier(data.channelId) || typeof data.value !== "number" || !Number.isFinite(data.value) || (canonical ? !Number.isSafeInteger(data.value) || !CANONICAL_UNITS.includes(data.unit) || !["live", "cumulative"].includes(data.kind) : !["ampere", "volt", "watt", "percent"].includes(data.unit))))
|
|
54451
|
-
return false;
|
|
54452
|
-
if (kind === "faults" && !identifier(data.channelId)) return false;
|
|
54453
|
-
if (kind === "acknowledgements" && (!identifier(data.id) || !["accepted", "duplicate", "rejected"].includes(data.status)))
|
|
54454
|
-
return false;
|
|
54461
|
+
if (!validStatePayload(data, kind, canonical) || !validEventPayload(data, kind, canonical)) return false;
|
|
54455
54462
|
const oldest = [...this.controllers].find(([, value]) => !value.activeStream)?.[0];
|
|
54456
54463
|
if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest === void 0) return false;
|
|
54457
54464
|
if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest !== void 0)
|
|
54458
54465
|
this.controllers.delete(oldest);
|
|
54459
54466
|
const state = this.read(id);
|
|
54467
|
+
if (!this.admitIncoming(id, state, kind, data, canonical)) return false;
|
|
54468
|
+
state.touched = this.now();
|
|
54469
|
+
const receivedAt = new Date(this.now()).toISOString();
|
|
54470
|
+
const metadata2 = {
|
|
54471
|
+
sourceAt: canonical ? data.timestamp : null,
|
|
54472
|
+
receivedAt,
|
|
54473
|
+
streamId: canonical ? state.activeStream : null,
|
|
54474
|
+
sequence: canonical ? data.sequence : null
|
|
54475
|
+
};
|
|
54476
|
+
if (kind === "heartbeat") state.heartbeatAt = canonical ? data.timestamp : receivedAt;
|
|
54477
|
+
if (kind === "state") this.applyState(state, data, canonical, metadata2);
|
|
54478
|
+
this.applyEvents(id, state, kind, data, canonical, metadata2);
|
|
54479
|
+
for (const collection2 of [
|
|
54480
|
+
state.cumulativeMeasurements,
|
|
54481
|
+
state.inputs,
|
|
54482
|
+
state.outputs,
|
|
54483
|
+
state.measurements,
|
|
54484
|
+
state.faults,
|
|
54485
|
+
state.acknowledgements
|
|
54486
|
+
]) {
|
|
54487
|
+
while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
|
|
54488
|
+
}
|
|
54489
|
+
state.events.push({ kind, receivedAt });
|
|
54490
|
+
state.events = state.events.slice(-50);
|
|
54491
|
+
this.controllers.set(id, state);
|
|
54492
|
+
return true;
|
|
54493
|
+
}
|
|
54494
|
+
admitIncoming(id, state, kind, data, canonical) {
|
|
54460
54495
|
if (!canonical && kind === "heartbeat" && typeof data.sequence === "number") {
|
|
54461
54496
|
if (data.sequence < (state.legacyHeartbeatSequence ?? 0)) return false;
|
|
54462
54497
|
state.legacyHeartbeatSequence = data.sequence;
|
|
@@ -54496,43 +54531,38 @@ var WagoDiagnosticsStore = class {
|
|
|
54496
54531
|
}
|
|
54497
54532
|
return false;
|
|
54498
54533
|
}
|
|
54499
|
-
|
|
54500
|
-
|
|
54501
|
-
|
|
54502
|
-
|
|
54503
|
-
|
|
54504
|
-
|
|
54505
|
-
|
|
54506
|
-
|
|
54507
|
-
|
|
54508
|
-
|
|
54509
|
-
|
|
54510
|
-
|
|
54511
|
-
|
|
54512
|
-
|
|
54513
|
-
|
|
54514
|
-
|
|
54515
|
-
|
|
54516
|
-
|
|
54517
|
-
|
|
54518
|
-
|
|
54519
|
-
|
|
54520
|
-
|
|
54521
|
-
|
|
54522
|
-
|
|
54523
|
-
|
|
54524
|
-
|
|
54525
|
-
|
|
54526
|
-
|
|
54527
|
-
|
|
54528
|
-
|
|
54529
|
-
|
|
54530
|
-
|
|
54531
|
-
for (const [channelId2, value] of Object.entries(data.inputs).slice(0, MAX_CHANNELS)) {
|
|
54532
|
-
if (identifier(channelId2) && typeof value === "boolean")
|
|
54533
|
-
state.inputs[channelId2] = { kind: "input", value, ...metadata2 };
|
|
54534
|
-
}
|
|
54535
|
-
}
|
|
54534
|
+
return true;
|
|
54535
|
+
}
|
|
54536
|
+
applyState(state, data, canonical, metadata2) {
|
|
54537
|
+
const contentHash = typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash) ? data.contentHash : void 0;
|
|
54538
|
+
const hardwareAvailable = isObject(data.readiness) && typeof data.readiness.hardwareAvailable === "boolean" ? data.readiness.hardwareAvailable : void 0;
|
|
54539
|
+
if (state.connected !== data.connected || state.revision !== data.revision || state.contentHash !== contentHash || state.hardwareAvailable !== hardwareAvailable) {
|
|
54540
|
+
if (state.stateSourceAt || canonical && state.connected === false)
|
|
54541
|
+
state.measurementAfter = canonical ? sourceTime(data.timestamp) : this.now();
|
|
54542
|
+
state.measurements = /* @__PURE__ */ Object.create(null);
|
|
54543
|
+
state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
|
|
54544
|
+
}
|
|
54545
|
+
state.inputs = /* @__PURE__ */ Object.create(null);
|
|
54546
|
+
state.outputs = /* @__PURE__ */ Object.create(null);
|
|
54547
|
+
if (typeof data.connected === "boolean") state.connected = data.connected;
|
|
54548
|
+
state.revision = Number.isSafeInteger(data.revision) ? data.revision : void 0;
|
|
54549
|
+
state.contentHash = contentHash;
|
|
54550
|
+
state.hardwareAvailable = hardwareAvailable;
|
|
54551
|
+
state.stateSourceAt = canonical ? data.timestamp : void 0;
|
|
54552
|
+
if (data.outputs && typeof data.outputs === "object" && !Array.isArray(data.outputs)) {
|
|
54553
|
+
for (const [channelId2, value] of Object.entries(data.outputs).slice(0, MAX_CHANNELS)) {
|
|
54554
|
+
if (identifier(channelId2) && typeof value === "boolean")
|
|
54555
|
+
state.outputs[channelId2] = { kind: "output", value, ...metadata2 };
|
|
54556
|
+
}
|
|
54557
|
+
}
|
|
54558
|
+
if (isObject(data.inputs))
|
|
54559
|
+
for (const [channelId2, value] of Object.entries(data.inputs).slice(0, MAX_CHANNELS)) {
|
|
54560
|
+
if (identifier(channelId2) && typeof value === "boolean")
|
|
54561
|
+
state.inputs[channelId2] = { kind: "input", value, ...metadata2 };
|
|
54562
|
+
}
|
|
54563
|
+
}
|
|
54564
|
+
applyEvents(id, state, kind, data, canonical, metadata2) {
|
|
54565
|
+
const { receivedAt } = metadata2;
|
|
54536
54566
|
if (kind === "measurements" && identifier(data.channelId) && typeof data.value === "number" && Number.isFinite(data.value) && (canonical ? CANONICAL_UNITS : ["ampere", "volt", "watt", "percent"]).includes(data.unit)) {
|
|
54537
54567
|
const collection2 = canonical && data.kind === "cumulative" ? state.cumulativeMeasurements : state.measurements;
|
|
54538
54568
|
collection2[data.channelId] = {
|
|
@@ -54566,20 +54596,6 @@ var WagoDiagnosticsStore = class {
|
|
|
54566
54596
|
receivedAt,
|
|
54567
54597
|
errors: safeValidationSummaries(data.errors)
|
|
54568
54598
|
};
|
|
54569
|
-
for (const collection2 of [
|
|
54570
|
-
state.cumulativeMeasurements,
|
|
54571
|
-
state.inputs,
|
|
54572
|
-
state.outputs,
|
|
54573
|
-
state.measurements,
|
|
54574
|
-
state.faults,
|
|
54575
|
-
state.acknowledgements
|
|
54576
|
-
]) {
|
|
54577
|
-
while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
|
|
54578
|
-
}
|
|
54579
|
-
state.events.push({ kind, receivedAt });
|
|
54580
|
-
state.events = state.events.slice(-50);
|
|
54581
|
-
this.controllers.set(id, state);
|
|
54582
|
-
return true;
|
|
54583
54599
|
}
|
|
54584
54600
|
};
|
|
54585
54601
|
|
|
@@ -56184,14 +56200,14 @@ function wagoFw31IdentityRead(withRoot = false) {
|
|
|
56184
56200
|
(path, i) => `printf 'source${i}\\n' && /bin/bash -o pipefail -c ${quotedRead} bash "${withRoot ? "$root" : ""}/${path}"`
|
|
56185
56201
|
).join(" && ")} && printf 'complete\\n'; }`;
|
|
56186
56202
|
}
|
|
56187
|
-
function
|
|
56188
|
-
if (output.length + (output.endsWith("\n") ? 0 : 1) > 5e4) return
|
|
56203
|
+
function parseIdentityFrames(output) {
|
|
56204
|
+
if (output.length + (output.endsWith("\n") ? 0 : 1) > 5e4) return null;
|
|
56189
56205
|
const chunks = [[], [], []];
|
|
56190
56206
|
let source = -1;
|
|
56191
56207
|
let bytes = 0;
|
|
56192
56208
|
let complete = false;
|
|
56193
56209
|
for (const line of (output.endsWith("\n") ? output.slice(0, -1) : output).split("\n")) {
|
|
56194
|
-
if (complete) return
|
|
56210
|
+
if (complete) return null;
|
|
56195
56211
|
if (line === `source${source + 1}` && source < 2) {
|
|
56196
56212
|
source++;
|
|
56197
56213
|
continue;
|
|
@@ -56201,25 +56217,35 @@ function isCc100Fw31Identity(output) {
|
|
|
56201
56217
|
continue;
|
|
56202
56218
|
}
|
|
56203
56219
|
if (source >= 0 && /^ *$/.test(line)) continue;
|
|
56204
|
-
if (source < 0 || !/^ *[0-9]+(?: +[0-9]+)* *$/.test(line)) return
|
|
56220
|
+
if (source < 0 || !/^ *[0-9]+(?: +[0-9]+)* *$/.test(line)) return null;
|
|
56205
56221
|
for (const token of line.trim().split(/ +/)) {
|
|
56206
56222
|
const byte = Number(token);
|
|
56207
|
-
if (++bytes > 8192 || byte > 255) return
|
|
56223
|
+
if (++bytes > 8192 || byte > 255) return null;
|
|
56208
56224
|
chunks[source].push(byte);
|
|
56209
56225
|
}
|
|
56210
56226
|
}
|
|
56211
|
-
if (!complete) return
|
|
56227
|
+
if (!complete) return null;
|
|
56228
|
+
return chunks;
|
|
56229
|
+
}
|
|
56230
|
+
function parseIdentityFields(text) {
|
|
56231
|
+
if (/[^\n\x20-\x7e]/.test(text)) return null;
|
|
56232
|
+
const fields2 = /* @__PURE__ */ new Map();
|
|
56233
|
+
for (const line of text.split("\n")) {
|
|
56234
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
56235
|
+
const match = /^([A-Z][A-Z0-9_]*)=(?:"([^"\\]*)"|([^"\\\s]+))$/.exec(line);
|
|
56236
|
+
if (!match || fields2.has(match[1])) return null;
|
|
56237
|
+
fields2.set(match[1], match[2] ?? match[3]);
|
|
56238
|
+
}
|
|
56239
|
+
return fields2;
|
|
56240
|
+
}
|
|
56241
|
+
function isCc100Fw31Identity(output) {
|
|
56242
|
+
const chunks = parseIdentityFrames(output);
|
|
56243
|
+
if (!chunks) return false;
|
|
56212
56244
|
const texts = chunks.map((chunk) => Buffer.from(chunk).toString("latin1"));
|
|
56213
56245
|
if (texts[2] !== "CC100-751-9301\0") return false;
|
|
56214
56246
|
for (let i = 0; i < 2; i++) {
|
|
56215
|
-
|
|
56216
|
-
|
|
56217
|
-
for (const line of texts[i].split("\n")) {
|
|
56218
|
-
if (line === "" || line.startsWith("#")) continue;
|
|
56219
|
-
const match = /^([A-Z][A-Z0-9_]*)=(?:"([^"\\]*)"|([^"\\\s]+))$/.exec(line);
|
|
56220
|
-
if (!match || fields2.has(match[1])) return false;
|
|
56221
|
-
fields2.set(match[1], match[2] ?? match[3]);
|
|
56222
|
-
}
|
|
56247
|
+
const fields2 = parseIdentityFields(texts[i]);
|
|
56248
|
+
if (!fields2) return false;
|
|
56223
56249
|
if (i === 0) {
|
|
56224
56250
|
if (!["PTXDIST_PLATFORM_NAME", "VERSION", "VERSION_ID"].every((key) => fields2.has(key))) return false;
|
|
56225
56251
|
for (const [key, value] of fields2) {
|
|
@@ -56939,6 +56965,36 @@ WagoCredentialRotationEntity = __decorateClass([
|
|
|
56939
56965
|
// backend/wago-credential-rotation.ts
|
|
56940
56966
|
var CAPABILITY = "credential-rotation-v1";
|
|
56941
56967
|
var WAIT_MS = 3e4;
|
|
56968
|
+
function assertRotationController(controller, retry) {
|
|
56969
|
+
if (controller.trustState !== "claimed" || !controller.mqttServerId || controller.compatibilityError)
|
|
56970
|
+
throw new import_common4.ConflictException("A compatible claimed controller is required.");
|
|
56971
|
+
if ("credentialMqttServerId" in controller && controller.credentialMqttServerId != null && controller.credentialMqttServerId !== controller.mqttServerId)
|
|
56972
|
+
throw new import_common4.ConflictException("Use the original credential broker before rotating credentials.");
|
|
56973
|
+
let capabilities;
|
|
56974
|
+
try {
|
|
56975
|
+
capabilities = JSON.parse(controller.capabilities);
|
|
56976
|
+
} catch {
|
|
56977
|
+
capabilities = [];
|
|
56978
|
+
}
|
|
56979
|
+
if (!Array.isArray(capabilities) || !capabilities.includes(CAPABILITY))
|
|
56980
|
+
throw new import_common4.ConflictException("Install a runtime supporting credential-rotation-v1 before rotating credentials.");
|
|
56981
|
+
const heartbeatAt = sourceTime(controller.lastHeartbeatAt);
|
|
56982
|
+
const now = Date.now();
|
|
56983
|
+
if (!retry && (heartbeatAt === null || heartbeatAt > now || now - heartbeatAt >= 9e4))
|
|
56984
|
+
throw new import_common4.ConflictException("Wait for a fresh permanent controller heartbeat before rotating credentials.");
|
|
56985
|
+
}
|
|
56986
|
+
function checkPendingRotation(previous, credentialEpoch, retry, mqttServerId, prefix) {
|
|
56987
|
+
if (previous && previous.credentialEpoch !== credentialEpoch)
|
|
56988
|
+
throw new import_common4.ConflictException("Recover the previous credential registration before rotating again.");
|
|
56989
|
+
if (previous && previous.phase !== "completed" && !retry)
|
|
56990
|
+
throw new import_common4.ConflictException("Retry the pending credential handoff instead of rotating again.");
|
|
56991
|
+
if (retry && (!previous || previous.phase === "provisioning"))
|
|
56992
|
+
throw new import_common4.ConflictException("Broker rotation completion is uncertain. Recover broker credentials before retrying.");
|
|
56993
|
+
if (retry && previous?.phase === "completed") return { state: "completed", revision: previous.revision };
|
|
56994
|
+
if (previous && (previous.mqttServerId !== mqttServerId || previous.prefix !== prefix))
|
|
56995
|
+
throw new import_common4.ConflictException("Credential rotation must use its original broker and namespace.");
|
|
56996
|
+
return void 0;
|
|
56997
|
+
}
|
|
56942
56998
|
var WagoCredentialRotationUncertainError = class extends import_common4.ConflictException {
|
|
56943
56999
|
constructor() {
|
|
56944
57000
|
super("Credential rotation is incomplete. Inspect its recovery state and retry the pending handoff.");
|
|
@@ -56965,22 +57021,7 @@ var WagoCredentialRotationService = class {
|
|
|
56965
57021
|
await guard.assertOwned();
|
|
56966
57022
|
const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
|
|
56967
57023
|
if (!controller) throw new import_common4.NotFoundException("WAGO controller not found");
|
|
56968
|
-
|
|
56969
|
-
throw new import_common4.ConflictException("A compatible claimed controller is required.");
|
|
56970
|
-
if ("credentialMqttServerId" in controller && controller.credentialMqttServerId != null && controller.credentialMqttServerId !== controller.mqttServerId)
|
|
56971
|
-
throw new import_common4.ConflictException("Use the original credential broker before rotating credentials.");
|
|
56972
|
-
let capabilities;
|
|
56973
|
-
try {
|
|
56974
|
-
capabilities = JSON.parse(controller.capabilities);
|
|
56975
|
-
} catch {
|
|
56976
|
-
capabilities = [];
|
|
56977
|
-
}
|
|
56978
|
-
if (!Array.isArray(capabilities) || !capabilities.includes(CAPABILITY))
|
|
56979
|
-
throw new import_common4.ConflictException("Install a runtime supporting credential-rotation-v1 before rotating credentials.");
|
|
56980
|
-
const heartbeatAt = sourceTime(controller.lastHeartbeatAt);
|
|
56981
|
-
const now = Date.now();
|
|
56982
|
-
if (!retry && (heartbeatAt === null || heartbeatAt > now || now - heartbeatAt >= 9e4))
|
|
56983
|
-
throw new import_common4.ConflictException("Wait for a fresh permanent controller heartbeat before rotating credentials.");
|
|
57024
|
+
assertRotationController(controller, retry);
|
|
56984
57025
|
const [epochRow] = await this.repository.query(
|
|
56985
57026
|
"SELECT credential_epoch AS epoch FROM plugin_wago_controllers WHERE id = ?",
|
|
56986
57027
|
[controllerId]
|
|
@@ -56991,17 +57032,8 @@ var WagoCredentialRotationService = class {
|
|
|
56991
57032
|
prefix = normalizeOperationalPrefix(prefix);
|
|
56992
57033
|
const identity = `wago-controller-${controller.hardwareId}`;
|
|
56993
57034
|
const previous = await this.repository.createQueryBuilder("rotation").addSelect("rotation.encryptedCredentials").where("rotation.controllerId = :controllerId", { controllerId }).getOne();
|
|
56994
|
-
|
|
56995
|
-
|
|
56996
|
-
if (previous && previous.phase !== "completed" && !retry)
|
|
56997
|
-
throw new import_common4.ConflictException("Retry the pending credential handoff instead of rotating again.");
|
|
56998
|
-
if (retry && (!previous || previous.phase === "provisioning"))
|
|
56999
|
-
throw new import_common4.ConflictException(
|
|
57000
|
-
"Broker rotation completion is uncertain. Recover broker credentials before retrying."
|
|
57001
|
-
);
|
|
57002
|
-
if (retry && previous?.phase === "completed") return { state: "completed", revision: previous.revision };
|
|
57003
|
-
if (previous && (previous.mqttServerId !== controller.mqttServerId || previous.prefix !== prefix))
|
|
57004
|
-
throw new import_common4.ConflictException("Credential rotation must use its original broker and namespace.");
|
|
57035
|
+
const completed = checkPendingRotation(previous, credentialEpoch, retry, controller.mqttServerId, prefix);
|
|
57036
|
+
if (completed) return completed;
|
|
57005
57037
|
const lifecycle = new WagoAudit(this.context).begin(principal, controllerId, "credential_rotation");
|
|
57006
57038
|
await lifecycle.attempt();
|
|
57007
57039
|
let uncertain = retry;
|
|
@@ -58717,6 +58749,9 @@ WagoRuntimeArtifactsService = __decorateClass([
|
|
|
58717
58749
|
// backend/wago-commissioning-readiness.ts
|
|
58718
58750
|
var import_common7 = require("@nestjs/common");
|
|
58719
58751
|
var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
|
|
58752
|
+
function isReadiness(value) {
|
|
58753
|
+
return !!value && typeof value === "object" && "configurationAccepted" in value && typeof value.configurationAccepted === "boolean" && "hardwareAvailable" in value && typeof value.hardwareAvailable === "boolean" && "ready" in value && typeof value.ready === "boolean";
|
|
58754
|
+
}
|
|
58720
58755
|
var WagoCommissioningReadiness = class {
|
|
58721
58756
|
constructor(context) {
|
|
58722
58757
|
__publicField(this, "context", context);
|
|
@@ -58755,7 +58790,7 @@ var WagoCommissioningReadiness = class {
|
|
|
58755
58790
|
const now = Date.now();
|
|
58756
58791
|
const value = JSON.parse(message.payload.toString("utf8"));
|
|
58757
58792
|
const readiness = value.readiness;
|
|
58758
|
-
if (timestamp > now || event.contentHash !== null && !/^[a-f0-9]{64}$/i.test(event.contentHash) || !
|
|
58793
|
+
if (timestamp > now || event.contentHash !== null && !/^[a-f0-9]{64}$/i.test(event.contentHash) || !isReadiness(readiness)) {
|
|
58759
58794
|
current.state = void 0;
|
|
58760
58795
|
return;
|
|
58761
58796
|
}
|
|
@@ -59114,34 +59149,36 @@ function assertManagementKey(key) {
|
|
|
59114
59149
|
const inner = read();
|
|
59115
59150
|
if (offset !== bytes.length || inner.length % 8 || !inner.subarray(0, 4).equals(inner.subarray(4, 8)))
|
|
59116
59151
|
throw new Error();
|
|
59117
|
-
|
|
59118
|
-
const part = () => {
|
|
59119
|
-
const length = inner.readUInt32BE(cursor);
|
|
59120
|
-
cursor += 4;
|
|
59121
|
-
if (length > inner.length - cursor) throw new Error();
|
|
59122
|
-
const value = inner.subarray(cursor, cursor + length);
|
|
59123
|
-
cursor += length;
|
|
59124
|
-
return value;
|
|
59125
|
-
};
|
|
59126
|
-
if (part().toString() !== algorithm) throw new Error();
|
|
59127
|
-
const pub = part(), priv = part();
|
|
59128
|
-
if (pub.length !== 32 || priv.length !== 64 || !priv.subarray(32).equals(pub) || !publicBlob(pub).equals(blob) || part().length)
|
|
59129
|
-
throw new Error();
|
|
59130
|
-
const derived = (0, import_node_crypto12.createPublicKey)(
|
|
59131
|
-
(0, import_node_crypto12.createPrivateKey)({
|
|
59132
|
-
key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), priv.subarray(0, 32)]),
|
|
59133
|
-
format: "der",
|
|
59134
|
-
type: "pkcs8"
|
|
59135
|
-
})
|
|
59136
|
-
).export({ format: "jwk" });
|
|
59137
|
-
if (typeof derived.x !== "string" || !Buffer.from(derived.x, "base64url").equals(pub)) throw new Error();
|
|
59138
|
-
const padding = inner.subarray(cursor);
|
|
59139
|
-
if (!padding.length || padding.length > 8 || !padding.every((value, index) => value === index + 1))
|
|
59140
|
-
throw new Error();
|
|
59152
|
+
assertPrivateKeyPayload(inner, blob);
|
|
59141
59153
|
} catch {
|
|
59142
59154
|
throw new Error("invalid_key");
|
|
59143
59155
|
}
|
|
59144
59156
|
}
|
|
59157
|
+
function assertPrivateKeyPayload(inner, blob) {
|
|
59158
|
+
let cursor = 8;
|
|
59159
|
+
const part = () => {
|
|
59160
|
+
const length = inner.readUInt32BE(cursor);
|
|
59161
|
+
cursor += 4;
|
|
59162
|
+
if (length > inner.length - cursor) throw new Error();
|
|
59163
|
+
const value = inner.subarray(cursor, cursor + length);
|
|
59164
|
+
cursor += length;
|
|
59165
|
+
return value;
|
|
59166
|
+
};
|
|
59167
|
+
if (part().toString() !== algorithm) throw new Error();
|
|
59168
|
+
const pub = part(), priv = part();
|
|
59169
|
+
if (pub.length !== 32 || priv.length !== 64 || !priv.subarray(32).equals(pub) || !publicBlob(pub).equals(blob) || part().length)
|
|
59170
|
+
throw new Error();
|
|
59171
|
+
const derived = (0, import_node_crypto12.createPublicKey)(
|
|
59172
|
+
(0, import_node_crypto12.createPrivateKey)({
|
|
59173
|
+
key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), priv.subarray(0, 32)]),
|
|
59174
|
+
format: "der",
|
|
59175
|
+
type: "pkcs8"
|
|
59176
|
+
})
|
|
59177
|
+
).export({ format: "jwk" });
|
|
59178
|
+
if (typeof derived.x !== "string" || !Buffer.from(derived.x, "base64url").equals(pub)) throw new Error();
|
|
59179
|
+
const padding = inner.subarray(cursor);
|
|
59180
|
+
if (!padding.length || padding.length > 8 || !padding.every((value, index) => value === index + 1)) throw new Error();
|
|
59181
|
+
}
|
|
59145
59182
|
function restoreManagementKey(privateKey, expectedFingerprint) {
|
|
59146
59183
|
try {
|
|
59147
59184
|
if (privateKey.length > 4096) throw new Error();
|
|
@@ -59571,15 +59608,7 @@ var WagoManagementService = class {
|
|
|
59571
59608
|
await this.adapter.installKey(tx, input.temporarySsh, key.publicKey);
|
|
59572
59609
|
await this.step(record2, owner, "verifying_key");
|
|
59573
59610
|
await this.verify(record2, key.privateKey);
|
|
59574
|
-
if (record2.mode === "baseline")
|
|
59575
|
-
await this.step(record2, owner, "restricting_access");
|
|
59576
|
-
await this.adapter.restrictAccess(tx, input.temporarySsh, key.privateKey);
|
|
59577
|
-
await this.step(record2, owner, "verifying_baseline");
|
|
59578
|
-
await this.verify(record2, key.privateKey);
|
|
59579
|
-
const result = await this.adapter.verifyBaseline(tx, key.privateKey);
|
|
59580
|
-
if (!result.passwordDisabled || !result.defaultAccessDisabled || !result.minimumPrivileges || !result.wbmSecure && !record2.exceptions.includes("wbm_exposed") || !result.otherManagementSecure && !record2.exceptions.includes("other_services_exposed"))
|
|
59581
|
-
throw new Error();
|
|
59582
|
-
}
|
|
59611
|
+
if (record2.mode === "baseline") await this.enforceBaseline(record2, owner, input.temporarySsh, key.privateKey);
|
|
59583
59612
|
await this.step(record2, owner, "committing");
|
|
59584
59613
|
await this.adapter.commit(tx, input.temporarySsh, key.privateKey);
|
|
59585
59614
|
record2.state = record2.mode === "baseline" && record2.exceptions.length === 0 ? "hardened" : "key_enrolled";
|
|
@@ -59596,6 +59625,16 @@ var WagoManagementService = class {
|
|
|
59596
59625
|
assertOwned
|
|
59597
59626
|
);
|
|
59598
59627
|
}
|
|
59628
|
+
async enforceBaseline(record2, owner, credential, privateKey) {
|
|
59629
|
+
const tx = record2.transaction;
|
|
59630
|
+
await this.step(record2, owner, "restricting_access");
|
|
59631
|
+
await this.adapter.restrictAccess(tx, credential, privateKey);
|
|
59632
|
+
await this.step(record2, owner, "verifying_baseline");
|
|
59633
|
+
await this.verify(record2, privateKey);
|
|
59634
|
+
const result = await this.adapter.verifyBaseline(tx, privateKey);
|
|
59635
|
+
if (!result.passwordDisabled || !result.defaultAccessDisabled || !result.minimumPrivileges || !result.wbmSecure && !record2.exceptions.includes("wbm_exposed") || !result.otherManagementSecure && !record2.exceptions.includes("other_services_exposed"))
|
|
59636
|
+
throw new Error();
|
|
59637
|
+
}
|
|
59599
59638
|
async recover(controllerId, input, assertOwned) {
|
|
59600
59639
|
exactKeys2(input, ["confirm", "temporarySsh"]);
|
|
59601
59640
|
if (input.confirm !== true) throw new ManagementError("invalid_request");
|
|
@@ -60671,15 +60710,22 @@ var WagoCommissioningService = class {
|
|
|
60671
60710
|
(result) => result.state === "awaiting_discovery"
|
|
60672
60711
|
);
|
|
60673
60712
|
}
|
|
60674
|
-
async
|
|
60713
|
+
async loadDeliverableSession(id) {
|
|
60675
60714
|
const session = await this.sessions.findOneBy({ id });
|
|
60676
60715
|
if (!session) throw new import_common8.NotFoundException("commissioning session not found");
|
|
60677
60716
|
if (!["awaiting_delivery", "delivering", "awaiting_codesys_confirmation", "delivery_failed"].includes(session.state))
|
|
60678
60717
|
throw new import_common8.ConflictException("commissioning session cannot be delivered in its current state");
|
|
60679
|
-
|
|
60680
|
-
|
|
60718
|
+
return session;
|
|
60719
|
+
}
|
|
60720
|
+
async requireRuntimeArtifact(session) {
|
|
60681
60721
|
if (!isRuntimeArtifactConfigured() && !session.runtimeArtifactDigest && !await this.artifacts?.has())
|
|
60682
60722
|
throw new import_common8.ConflictException("Import a signed CC100 runtime release before installation.");
|
|
60723
|
+
}
|
|
60724
|
+
async deliverWhileLocked(id, input, principal = null) {
|
|
60725
|
+
const session = await this.loadDeliverableSession(id);
|
|
60726
|
+
const credential = requireDeliveryCredentials(input);
|
|
60727
|
+
if (principal) session.initiatingPrincipal = JSON.stringify(principal);
|
|
60728
|
+
await this.requireRuntimeArtifact(session);
|
|
60683
60729
|
let pairingCode;
|
|
60684
60730
|
try {
|
|
60685
60731
|
pairingCode = this.decryptVerifier(session);
|
|
@@ -62427,6 +62473,41 @@ var import_plugins_backend_sdk18 = require("@attraccess/plugins-backend-sdk");
|
|
|
62427
62473
|
// backend/diagnostics.service.ts
|
|
62428
62474
|
var import_common11 = require("@nestjs/common");
|
|
62429
62475
|
var import_plugins_backend_sdk17 = require("@attraccess/plugins-backend-sdk");
|
|
62476
|
+
function hasDraftChanges(draft, latest) {
|
|
62477
|
+
return !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash({ metadata: draft.presetProvenance ? JSON.parse(draft.presetProvenance) : null }) !== configurationHash({ metadata: latest.presetProvenance ? JSON.parse(latest.presetProvenance) : null }));
|
|
62478
|
+
}
|
|
62479
|
+
function configurationRejections(latest, runtime) {
|
|
62480
|
+
return latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [];
|
|
62481
|
+
}
|
|
62482
|
+
function configurationSummary(draft, latest, applied, runtime, revisionMismatch) {
|
|
62483
|
+
const validationErrors = draft ? validateSnapshot(JSON.parse(draft.snapshot)) : [];
|
|
62484
|
+
return {
|
|
62485
|
+
draftUpdatedAt: draft?.updatedAt ?? null,
|
|
62486
|
+
draftChanged: hasDraftChanges(draft, latest),
|
|
62487
|
+
validationErrorCount: validationErrors.length,
|
|
62488
|
+
// Codes originate in our validator. Omit messages and dynamic paths, which can include arbitrary draft values.
|
|
62489
|
+
validationCodes: [...new Set(validationErrors.map((error) => error.code))].slice(0, 50),
|
|
62490
|
+
validationErrors: safeValidationSummaries(validationErrors),
|
|
62491
|
+
rejectionErrors: configurationRejections(latest, runtime),
|
|
62492
|
+
publishedRevision: latest?.revision ?? null,
|
|
62493
|
+
publishedState: latest?.state ?? null,
|
|
62494
|
+
appliedRevision: applied?.revision ?? null,
|
|
62495
|
+
reportedRevision: runtime.revision ?? null,
|
|
62496
|
+
revisionMismatch,
|
|
62497
|
+
rejected: latest?.state === "rejected"
|
|
62498
|
+
};
|
|
62499
|
+
}
|
|
62500
|
+
function runtimeStreamSummary(runtime) {
|
|
62501
|
+
return {
|
|
62502
|
+
sequenceGaps: runtime.activeStream ? runtime.sequenceGaps : null,
|
|
62503
|
+
activeStream: runtime.activeStream ?? null,
|
|
62504
|
+
trackingExhausted: runtime.trackingExhausted,
|
|
62505
|
+
stateConnected: runtime.connected ?? null,
|
|
62506
|
+
stateHardwareAvailable: runtime.hardwareAvailable ?? null,
|
|
62507
|
+
stateSourceAt: runtime.stateSourceAt ?? null,
|
|
62508
|
+
sequenceExplanation: runtime.activeStream ? "Gaps are scoped to boot stream ID and message category; duplicates and retired streams are ignored." : "Legacy payloads have no source envelope; sequence gaps and source freshness are unavailable."
|
|
62509
|
+
};
|
|
62510
|
+
}
|
|
62430
62511
|
function own(values, key) {
|
|
62431
62512
|
return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
|
|
62432
62513
|
}
|
|
@@ -62560,7 +62641,6 @@ var WagoDiagnosticsService = class {
|
|
|
62560
62641
|
applied?.revision ?? null,
|
|
62561
62642
|
Object.fromEntries(appliedSnapshot?.logicalChannels.map((channel) => [channel.id, channel.capabilities]) ?? [])
|
|
62562
62643
|
);
|
|
62563
|
-
const validationErrors = draft ? validateSnapshot(JSON.parse(draft.snapshot)) : [];
|
|
62564
62644
|
return {
|
|
62565
62645
|
controllerId,
|
|
62566
62646
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -62572,28 +62652,8 @@ var WagoDiagnosticsService = class {
|
|
|
62572
62652
|
protocolVersion: controller.protocolVersion,
|
|
62573
62653
|
capabilities: JSON.parse(controller.capabilities).slice(0, 64),
|
|
62574
62654
|
incompatible: !!controller.compatibilityError,
|
|
62575
|
-
|
|
62576
|
-
|
|
62577
|
-
trackingExhausted: runtime.trackingExhausted,
|
|
62578
|
-
stateConnected: runtime.connected ?? null,
|
|
62579
|
-
stateHardwareAvailable: runtime.hardwareAvailable ?? null,
|
|
62580
|
-
stateSourceAt: runtime.stateSourceAt ?? null,
|
|
62581
|
-
sequenceExplanation: runtime.activeStream ? "Gaps are scoped to boot stream ID and message category; duplicates and retired streams are ignored." : "Legacy payloads have no source envelope; sequence gaps and source freshness are unavailable.",
|
|
62582
|
-
configuration: {
|
|
62583
|
-
draftUpdatedAt: draft?.updatedAt ?? null,
|
|
62584
|
-
draftChanged: !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash({ metadata: draft.presetProvenance ? JSON.parse(draft.presetProvenance) : null }) !== configurationHash({ metadata: latest.presetProvenance ? JSON.parse(latest.presetProvenance) : null })),
|
|
62585
|
-
validationErrorCount: validationErrors.length,
|
|
62586
|
-
// Codes originate in our validator. Omit messages and dynamic paths, which can include arbitrary draft values.
|
|
62587
|
-
validationCodes: [...new Set(validationErrors.map((error) => error.code))].slice(0, 50),
|
|
62588
|
-
validationErrors: safeValidationSummaries(validationErrors),
|
|
62589
|
-
rejectionErrors: latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [],
|
|
62590
|
-
publishedRevision: latest?.revision ?? null,
|
|
62591
|
-
publishedState: latest?.state ?? null,
|
|
62592
|
-
appliedRevision: applied?.revision ?? null,
|
|
62593
|
-
reportedRevision: runtime.revision ?? null,
|
|
62594
|
-
revisionMismatch,
|
|
62595
|
-
rejected: latest?.state === "rejected"
|
|
62596
|
-
},
|
|
62655
|
+
...runtimeStreamSummary(runtime),
|
|
62656
|
+
configuration: configurationSummary(draft, latest, applied, runtime, revisionMismatch),
|
|
62597
62657
|
hardwareReadiness: "unknown",
|
|
62598
62658
|
hardwareReadinessReason: "Reported hardware availability is shown when supplied; it does not prove physical I/O readiness. Applied configuration and cached output state are not physical proof.",
|
|
62599
62659
|
channels: ((appliedSnapshot ?? snapshot)?.logicalChannels ?? []).slice(0, 256).map((channel) => {
|
|
@@ -63015,12 +63075,23 @@ var WagoFlowService = class {
|
|
|
63015
63075
|
return;
|
|
63016
63076
|
}
|
|
63017
63077
|
const channels = await this.channels(controller.id);
|
|
63078
|
+
const stream = this.admitEvent(controller, event, eventTime);
|
|
63079
|
+
if (!stream) return;
|
|
63080
|
+
if (event.category === "state") {
|
|
63081
|
+
this.applyState(controller, event, eventTime, stream, channels);
|
|
63082
|
+
} else if ("channelId" in event) {
|
|
63083
|
+
const channel = channels.find((channel2) => channel2.id === event.channelId);
|
|
63084
|
+
if (!channel || event.category === "measurement" && !channel.capabilities.includes("measurement")) return;
|
|
63085
|
+
this.store(controller, event.channelId, event, event.category === "measurement" ? event.value : event);
|
|
63086
|
+
}
|
|
63087
|
+
}
|
|
63088
|
+
admitEvent(controller, event, eventTime) {
|
|
63018
63089
|
let stream = this.streams.get(controller.id);
|
|
63019
|
-
if (stream?.exhausted) return;
|
|
63090
|
+
if (stream?.exhausted) return void 0;
|
|
63020
63091
|
if (!stream || stream.active !== event.streamId) {
|
|
63021
63092
|
if (event.category !== "state" || stream?.retired.has(event.streamId) || stream && (!event.connected || Date.now() - eventTime > STALE_AFTER_MS2 || eventTime <= stream.latestSourceTime)) {
|
|
63022
63093
|
this.context.logger.warn(`Ignoring unestablished or retired WAGO stream for ${controller.hardwareId}`);
|
|
63023
|
-
return;
|
|
63094
|
+
return void 0;
|
|
63024
63095
|
}
|
|
63025
63096
|
if (stream && stream.retired.size >= MAX_RETIRED_STREAMS) {
|
|
63026
63097
|
stream.exhausted = true;
|
|
@@ -63028,7 +63099,7 @@ var WagoFlowService = class {
|
|
|
63028
63099
|
this.context.logger.warn(
|
|
63029
63100
|
`WAGO stream history exhausted for ${controller.hardwareId}; refusing further samples`
|
|
63030
63101
|
);
|
|
63031
|
-
return;
|
|
63102
|
+
return void 0;
|
|
63032
63103
|
}
|
|
63033
63104
|
const retired = stream?.retired ?? /* @__PURE__ */ new Set();
|
|
63034
63105
|
if (stream) retired.add(stream.active);
|
|
@@ -63045,7 +63116,7 @@ var WagoFlowService = class {
|
|
|
63045
63116
|
const previous = stream.sequences.get(event.category);
|
|
63046
63117
|
if (previous !== void 0 && event.sequence <= previous) {
|
|
63047
63118
|
this.context.logger.warn(`Ignoring duplicate or out-of-order WAGO event for ${controller.hardwareId}`);
|
|
63048
|
-
return;
|
|
63119
|
+
return void 0;
|
|
63049
63120
|
}
|
|
63050
63121
|
if (previous !== void 0 && event.sequence > previous + 1)
|
|
63051
63122
|
this.context.logger.warn(
|
|
@@ -63053,32 +63124,28 @@ var WagoFlowService = class {
|
|
|
63053
63124
|
);
|
|
63054
63125
|
stream.sequences.set(event.category, event.sequence);
|
|
63055
63126
|
stream.latestSourceTime = Math.max(stream.latestSourceTime, eventTime);
|
|
63056
|
-
|
|
63057
|
-
|
|
63058
|
-
|
|
63059
|
-
|
|
63060
|
-
|
|
63061
|
-
|
|
63062
|
-
|
|
63063
|
-
|
|
63064
|
-
|
|
63065
|
-
|
|
63066
|
-
|
|
63067
|
-
|
|
63068
|
-
|
|
63069
|
-
|
|
63070
|
-
|
|
63071
|
-
|
|
63072
|
-
|
|
63073
|
-
|
|
63074
|
-
|
|
63075
|
-
|
|
63076
|
-
|
|
63077
|
-
|
|
63078
|
-
} else if ("channelId" in event) {
|
|
63079
|
-
const channel = channels.find((channel2) => channel2.id === event.channelId);
|
|
63080
|
-
if (!channel || event.category === "measurement" && !channel.capabilities.includes("measurement")) return;
|
|
63081
|
-
this.store(controller, event.channelId, event, event.category === "measurement" ? event.value : event);
|
|
63127
|
+
return stream;
|
|
63128
|
+
}
|
|
63129
|
+
applyState(controller, event, eventTime, stream, channels) {
|
|
63130
|
+
const wasUnavailable = this.offlineControllers.has(controller.id) || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id) || stream.stateTimestamp !== void 0 && Date.now() - stream.stateTimestamp > STALE_AFTER_MS2;
|
|
63131
|
+
stream.stateTimestamp = eventTime;
|
|
63132
|
+
if (event.connected) this.offlineControllers.delete(controller.id);
|
|
63133
|
+
else this.offlineControllers.add(controller.id);
|
|
63134
|
+
if (event.readiness?.hardwareAvailable === false) this.unavailableHardware.add(controller.id);
|
|
63135
|
+
else if (event.readiness?.hardwareAvailable === true) this.unavailableHardware.delete(controller.id);
|
|
63136
|
+
const applied = this.appliedConfigurations.get(controller.id);
|
|
63137
|
+
if (applied && event.revision === applied.revision && event.contentHash === applied.contentHash)
|
|
63138
|
+
this.unavailableConfiguration.delete(controller.id);
|
|
63139
|
+
else this.unavailableConfiguration.add(controller.id);
|
|
63140
|
+
const invalidateSamples = wasUnavailable || !event.connected || this.unavailableHardware.has(controller.id) || this.unavailableConfiguration.has(controller.id);
|
|
63141
|
+
if (invalidateSamples) stream.sampleNotBefore = Math.max(stream.sampleNotBefore, eventTime);
|
|
63142
|
+
for (const state of this.cache.values()) {
|
|
63143
|
+
if (state.controllerId !== controller.id) continue;
|
|
63144
|
+
if (invalidateSamples || state.category === "state") state.invalidated = true;
|
|
63145
|
+
}
|
|
63146
|
+
for (const channel of channels) {
|
|
63147
|
+
const value = channel.capabilities.includes("input") && Object.hasOwn(event.inputs ?? {}, channel.id) ? event.inputs[channel.id] : channel.capabilities.includes("output") && Object.hasOwn(event.outputs, channel.id) ? event.outputs[channel.id] : void 0;
|
|
63148
|
+
if (typeof value === "boolean") this.store(controller, channel.id, event, value);
|
|
63082
63149
|
}
|
|
63083
63150
|
}
|
|
63084
63151
|
store(controller, channelId2, event, value) {
|