@attraccess/plugin-wago 0.1.0-nightly.953.1 → 0.1.0-nightly.954.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
|
@@ -661,9 +661,9 @@ var require_vary = __commonJS({
|
|
|
661
661
|
if (!field2) {
|
|
662
662
|
throw new TypeError("field argument is required");
|
|
663
663
|
}
|
|
664
|
-
var
|
|
665
|
-
for (var j = 0; j <
|
|
666
|
-
if (!FIELD_NAME_REGEXP.test(
|
|
664
|
+
var fields2 = !Array.isArray(field2) ? parse(String(field2)) : field2;
|
|
665
|
+
for (var j = 0; j < fields2.length; j++) {
|
|
666
|
+
if (!FIELD_NAME_REGEXP.test(fields2[j])) {
|
|
667
667
|
throw new TypeError("field argument contains an invalid header name");
|
|
668
668
|
}
|
|
669
669
|
}
|
|
@@ -672,14 +672,14 @@ var require_vary = __commonJS({
|
|
|
672
672
|
}
|
|
673
673
|
var val = header;
|
|
674
674
|
var vals = parse(header.toLowerCase());
|
|
675
|
-
if (
|
|
675
|
+
if (fields2.indexOf("*") !== -1 || vals.indexOf("*") !== -1) {
|
|
676
676
|
return "*";
|
|
677
677
|
}
|
|
678
|
-
for (var i = 0; i <
|
|
679
|
-
var fld =
|
|
678
|
+
for (var i = 0; i < fields2.length; i++) {
|
|
679
|
+
var fld = fields2[i].toLowerCase();
|
|
680
680
|
if (vals.indexOf(fld) === -1) {
|
|
681
681
|
vals.push(fld);
|
|
682
|
-
val = val ? val + ", " +
|
|
682
|
+
val = val ? val + ", " + fields2[i] : fields2[i];
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
return val;
|
|
@@ -2092,11 +2092,11 @@ var require_codes = __commonJS({
|
|
|
2092
2092
|
var require_statuses = __commonJS({
|
|
2093
2093
|
"../../../node_modules/.pnpm/statuses@2.0.2/node_modules/statuses/index.js"(exports2, module2) {
|
|
2094
2094
|
"use strict";
|
|
2095
|
-
var
|
|
2095
|
+
var codes2 = require_codes();
|
|
2096
2096
|
module2.exports = status;
|
|
2097
|
-
status.message =
|
|
2098
|
-
status.code = createMessageToStatusCodeMap(
|
|
2099
|
-
status.codes = createStatusCodeList(
|
|
2097
|
+
status.message = codes2;
|
|
2098
|
+
status.code = createMessageToStatusCodeMap(codes2);
|
|
2099
|
+
status.codes = createStatusCodeList(codes2);
|
|
2100
2100
|
status.redirect = {
|
|
2101
2101
|
300: true,
|
|
2102
2102
|
301: true,
|
|
@@ -2116,17 +2116,17 @@ var require_statuses = __commonJS({
|
|
|
2116
2116
|
503: true,
|
|
2117
2117
|
504: true
|
|
2118
2118
|
};
|
|
2119
|
-
function createMessageToStatusCodeMap(
|
|
2119
|
+
function createMessageToStatusCodeMap(codes3) {
|
|
2120
2120
|
var map = {};
|
|
2121
|
-
Object.keys(
|
|
2122
|
-
var message =
|
|
2121
|
+
Object.keys(codes3).forEach(function forEachCode(code) {
|
|
2122
|
+
var message = codes3[code];
|
|
2123
2123
|
var status2 = Number(code);
|
|
2124
2124
|
map[message.toLowerCase()] = status2;
|
|
2125
2125
|
});
|
|
2126
2126
|
return map;
|
|
2127
2127
|
}
|
|
2128
|
-
function createStatusCodeList(
|
|
2129
|
-
return Object.keys(
|
|
2128
|
+
function createStatusCodeList(codes3) {
|
|
2129
|
+
return Object.keys(codes3).map(function mapCode(code) {
|
|
2130
2130
|
return Number(code);
|
|
2131
2131
|
});
|
|
2132
2132
|
}
|
|
@@ -2358,8 +2358,8 @@ var require_http_errors = __commonJS({
|
|
|
2358
2358
|
Object.defineProperty(func, "name", desc);
|
|
2359
2359
|
}
|
|
2360
2360
|
}
|
|
2361
|
-
function populateConstructorExports(exports3,
|
|
2362
|
-
|
|
2361
|
+
function populateConstructorExports(exports3, codes2, HttpError) {
|
|
2362
|
+
codes2.forEach(function forEachCode(code) {
|
|
2363
2363
|
var CodeError;
|
|
2364
2364
|
var name = toIdentifier(statuses.message[code]);
|
|
2365
2365
|
switch (codeClass(code)) {
|
|
@@ -36301,7 +36301,7 @@ var require_multipart = __commonJS({
|
|
|
36301
36301
|
const fieldsLimit = limits2 && typeof limits2.fields === "number" ? limits2.fields : Infinity;
|
|
36302
36302
|
const partsLimit = limits2 && typeof limits2.parts === "number" ? limits2.parts : Infinity;
|
|
36303
36303
|
let parts = -1;
|
|
36304
|
-
let
|
|
36304
|
+
let fields2 = 0;
|
|
36305
36305
|
let files = 0;
|
|
36306
36306
|
let skipPart = false;
|
|
36307
36307
|
this._fileEndsLeft = 0;
|
|
@@ -36387,7 +36387,7 @@ var require_multipart = __commonJS({
|
|
|
36387
36387
|
}
|
|
36388
36388
|
);
|
|
36389
36389
|
} else {
|
|
36390
|
-
if (
|
|
36390
|
+
if (fields2 === fieldsLimit) {
|
|
36391
36391
|
if (!hitFieldsLimit) {
|
|
36392
36392
|
hitFieldsLimit = true;
|
|
36393
36393
|
this.emit("fieldsLimit");
|
|
@@ -36395,7 +36395,7 @@ var require_multipart = __commonJS({
|
|
|
36395
36395
|
skipPart = true;
|
|
36396
36396
|
return;
|
|
36397
36397
|
}
|
|
36398
|
-
++
|
|
36398
|
+
++fields2;
|
|
36399
36399
|
if (this.listenerCount("field") === 0) {
|
|
36400
36400
|
skipPart = true;
|
|
36401
36401
|
return;
|
|
@@ -38635,7 +38635,7 @@ var require_destroy = __commonJS({
|
|
|
38635
38635
|
var require_errors = __commonJS({
|
|
38636
38636
|
"../../../node_modules/.pnpm/readable-stream@3.6.2/node_modules/readable-stream/errors.js"(exports2, module2) {
|
|
38637
38637
|
"use strict";
|
|
38638
|
-
var
|
|
38638
|
+
var codes2 = {};
|
|
38639
38639
|
function createErrorType(code, message, Base) {
|
|
38640
38640
|
if (!Base) {
|
|
38641
38641
|
Base = Error;
|
|
@@ -38654,7 +38654,7 @@ var require_errors = __commonJS({
|
|
|
38654
38654
|
}
|
|
38655
38655
|
NodeError.prototype.name = Base.name;
|
|
38656
38656
|
NodeError.prototype.code = code;
|
|
38657
|
-
|
|
38657
|
+
codes2[code] = NodeError;
|
|
38658
38658
|
}
|
|
38659
38659
|
function oneOf(expected, thing) {
|
|
38660
38660
|
if (Array.isArray(expected)) {
|
|
@@ -38727,7 +38727,7 @@ var require_errors = __commonJS({
|
|
|
38727
38727
|
return "Unknown encoding: " + arg;
|
|
38728
38728
|
}, TypeError);
|
|
38729
38729
|
createErrorType("ERR_STREAM_UNSHIFT_AFTER_END_EVENT", "stream.unshift() after end event");
|
|
38730
|
-
module2.exports.codes =
|
|
38730
|
+
module2.exports.codes = codes2;
|
|
38731
38731
|
}
|
|
38732
38732
|
});
|
|
38733
38733
|
|
|
@@ -41732,11 +41732,11 @@ var require_multer = __commonJS({
|
|
|
41732
41732
|
this.defParamCharset = options.defParamCharset || "latin1";
|
|
41733
41733
|
this.fileFilter = options.fileFilter || allowAll;
|
|
41734
41734
|
}
|
|
41735
|
-
Multer.prototype._makeMiddleware = function(
|
|
41735
|
+
Multer.prototype._makeMiddleware = function(fields2, fileStrategy) {
|
|
41736
41736
|
function setup() {
|
|
41737
41737
|
var fileFilter = this.fileFilter;
|
|
41738
41738
|
var filesLeft = /* @__PURE__ */ Object.create(null);
|
|
41739
|
-
|
|
41739
|
+
fields2.forEach(function(field2) {
|
|
41740
41740
|
if (typeof field2.maxCount === "number") {
|
|
41741
41741
|
filesLeft[field2.name] = field2.maxCount;
|
|
41742
41742
|
} else {
|
|
@@ -41767,8 +41767,8 @@ var require_multer = __commonJS({
|
|
|
41767
41767
|
Multer.prototype.array = function(name, maxCount) {
|
|
41768
41768
|
return this._makeMiddleware([{ name, maxCount }], "ARRAY");
|
|
41769
41769
|
};
|
|
41770
|
-
Multer.prototype.fields = function(
|
|
41771
|
-
return this._makeMiddleware(
|
|
41770
|
+
Multer.prototype.fields = function(fields2) {
|
|
41771
|
+
return this._makeMiddleware(fields2, "OBJECT");
|
|
41772
41772
|
};
|
|
41773
41773
|
Multer.prototype.none = function() {
|
|
41774
41774
|
return this._makeMiddleware([], "NONE");
|
|
@@ -51633,7 +51633,7 @@ module.exports = __toCommonJS(plugin_exports);
|
|
|
51633
51633
|
|
|
51634
51634
|
// backend/wago.controller.ts
|
|
51635
51635
|
var import_common8 = require("@nestjs/common");
|
|
51636
|
-
var
|
|
51636
|
+
var import_plugins_backend_sdk13 = require("@attraccess/plugins-backend-sdk");
|
|
51637
51637
|
|
|
51638
51638
|
// backend/wago.service.ts
|
|
51639
51639
|
var import_node_crypto3 = require("node:crypto");
|
|
@@ -52047,6 +52047,16 @@ var WAGO_PRESETS = [
|
|
|
52047
52047
|
description: "A monitored digital input foundation."
|
|
52048
52048
|
}
|
|
52049
52049
|
];
|
|
52050
|
+
function applyPreset(snapshot, application) {
|
|
52051
|
+
const preset = application && WAGO_PRESETS.find((item) => item.id === application.presetId);
|
|
52052
|
+
if (!preset) throw new Error("unknown WAGO preset");
|
|
52053
|
+
const channel = presetChannel(application);
|
|
52054
|
+
const existingIndex = snapshot.logicalChannels.findIndex((item) => item.id === application.channelId);
|
|
52055
|
+
const logicalChannels = [...snapshot.logicalChannels];
|
|
52056
|
+
if (existingIndex === -1) logicalChannels.push(channel);
|
|
52057
|
+
else logicalChannels[existingIndex] = channel;
|
|
52058
|
+
return { ...snapshot, logicalChannels };
|
|
52059
|
+
}
|
|
52050
52060
|
function canonicalSnapshot(snapshot) {
|
|
52051
52061
|
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
|
|
52052
52062
|
throw new Error("configuration snapshot must be an object");
|
|
@@ -52066,7 +52076,7 @@ function parseConfigurationReport(value) {
|
|
|
52066
52076
|
if (!Number.isSafeInteger(report.revision) || report.revision < 1 || typeof report.contentHash !== "string" || !report.contentHash.trim() || report.errors !== void 0 && !Array.isArray(report.errors))
|
|
52067
52077
|
return null;
|
|
52068
52078
|
const errors = report.errors ?? [];
|
|
52069
|
-
if (!errors.every(isConfigurationValidationError)) return null;
|
|
52079
|
+
if (!Array.isArray(errors) || !errors.every(isConfigurationValidationError)) return null;
|
|
52070
52080
|
return { revision: report.revision, contentHash: report.contentHash, errors };
|
|
52071
52081
|
}
|
|
52072
52082
|
function validateSnapshot(snapshot) {
|
|
@@ -52097,7 +52107,7 @@ function validateSnapshot(snapshot) {
|
|
|
52097
52107
|
exactKeys(point, path, ["id", "hardwareProfile", "channel"], errors);
|
|
52098
52108
|
addId(point.id, `${path}.id`, pointIds, errors);
|
|
52099
52109
|
enumValue(point.hardwareProfile, `${path}.hardwareProfile`, HARDWARE_PROFILES, errors);
|
|
52100
|
-
if (!Number.isSafeInteger(point.channel) || point.channel < 0)
|
|
52110
|
+
if (typeof point.channel !== "number" || !Number.isSafeInteger(point.channel) || point.channel < 0)
|
|
52101
52111
|
errors.push({
|
|
52102
52112
|
path: `${path}.channel`,
|
|
52103
52113
|
code: "invalid_channel",
|
|
@@ -52337,6 +52347,35 @@ function validateProfile(value, capabilities, path, errors) {
|
|
|
52337
52347
|
})
|
|
52338
52348
|
);
|
|
52339
52349
|
}
|
|
52350
|
+
function presetChannel(application) {
|
|
52351
|
+
const base = {
|
|
52352
|
+
id: application.channelId,
|
|
52353
|
+
physicalPointId: application.physicalPointId,
|
|
52354
|
+
profile: application.presetId,
|
|
52355
|
+
disconnectPolicy: { mode: application.presetId === "generic-monitored-input" ? "hold" : "immediate" }
|
|
52356
|
+
};
|
|
52357
|
+
switch (application.presetId) {
|
|
52358
|
+
case "metered-switched-load":
|
|
52359
|
+
return { ...base, capabilities: ["output", "measurement"], measurement: { unit: "watt", scale: 1, offset: 0 } };
|
|
52360
|
+
case "pulsed-lock-bank":
|
|
52361
|
+
return { ...base, capabilities: ["output", "pulse"], pulse: { durationMs: 500 } };
|
|
52362
|
+
case "guarded-enable-request":
|
|
52363
|
+
if (!application.guardChannelId) throw new Error("guarded enable requests require a guard channel");
|
|
52364
|
+
return {
|
|
52365
|
+
...base,
|
|
52366
|
+
capabilities: ["output", "guard"],
|
|
52367
|
+
guard: { channelId: application.guardChannelId, when: "on" }
|
|
52368
|
+
};
|
|
52369
|
+
case "generic-monitored-input":
|
|
52370
|
+
return { ...base, capabilities: ["input"] };
|
|
52371
|
+
case "generic-digital-output":
|
|
52372
|
+
return {
|
|
52373
|
+
...base,
|
|
52374
|
+
capabilities: application.feedbackChannelId ? ["output", "feedback"] : ["output"],
|
|
52375
|
+
...application.feedbackChannelId ? { feedback: { channelId: application.feedbackChannelId, expected: "match", timeoutMs: 1e3 } } : {}
|
|
52376
|
+
};
|
|
52377
|
+
}
|
|
52378
|
+
}
|
|
52340
52379
|
function referenceError(path, type, id) {
|
|
52341
52380
|
return { path, code: "missing_reference", message: `${type} ${String(id)} does not exist in this snapshot` };
|
|
52342
52381
|
}
|
|
@@ -52348,6 +52387,143 @@ function sortValue(value) {
|
|
|
52348
52387
|
);
|
|
52349
52388
|
}
|
|
52350
52389
|
|
|
52390
|
+
// backend/configuration-digital.ts
|
|
52391
|
+
var DIGITAL_TERMINALS = [
|
|
52392
|
+
...Array.from({ length: 4 }, (_, channel) => ({ channel, label: `DO${channel + 1}`, direction: "output" })),
|
|
52393
|
+
...Array.from({ length: 8 }, (_, index) => ({
|
|
52394
|
+
channel: index + 4,
|
|
52395
|
+
label: `DI${index + 1}`,
|
|
52396
|
+
direction: "input"
|
|
52397
|
+
}))
|
|
52398
|
+
];
|
|
52399
|
+
|
|
52400
|
+
// backend/configuration-editor.ts
|
|
52401
|
+
function editorMetadata(value) {
|
|
52402
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("editor metadata must be an object");
|
|
52403
|
+
const { names, presets } = value;
|
|
52404
|
+
if (!names || typeof names !== "object" || Array.isArray(names) || !Object.values(names).every((name) => typeof name === "string" && name.trim().length > 0 && name.length <= 120))
|
|
52405
|
+
throw new Error("channel names must contain 1\u2013120 characters");
|
|
52406
|
+
if (!Array.isArray(presets) || !presets.every(
|
|
52407
|
+
(preset) => preset && WAGO_PRESETS.some((item) => item.id === preset.presetId) && typeof preset.channelId === "string" && typeof preset.physicalPointId === "string"
|
|
52408
|
+
))
|
|
52409
|
+
throw new Error("preset provenance must contain valid preset applications");
|
|
52410
|
+
return { names, presets };
|
|
52411
|
+
}
|
|
52412
|
+
function validateEditorSnapshot(snapshot) {
|
|
52413
|
+
const errors = validateSnapshot(snapshot);
|
|
52414
|
+
if (errors.length) return errors;
|
|
52415
|
+
const value = snapshot;
|
|
52416
|
+
const terminals = /* @__PURE__ */ new Set();
|
|
52417
|
+
const assignedPoints = /* @__PURE__ */ new Set();
|
|
52418
|
+
value.physicalPoints.forEach((point, index) => {
|
|
52419
|
+
if (point.hardwareProfile !== "751-9301") return;
|
|
52420
|
+
if (!DIGITAL_TERMINALS.some((terminal) => terminal.channel === point.channel))
|
|
52421
|
+
errors.push({
|
|
52422
|
+
path: `physicalPoints[${index}].channel`,
|
|
52423
|
+
code: "unsupported_terminal",
|
|
52424
|
+
message: "select DO1\u2013DO4 or DI1\u2013DI8"
|
|
52425
|
+
});
|
|
52426
|
+
if (terminals.has(point.channel))
|
|
52427
|
+
errors.push({
|
|
52428
|
+
path: `physicalPoints[${index}].channel`,
|
|
52429
|
+
code: "duplicate_terminal",
|
|
52430
|
+
message: "each digital terminal can have only one physical assignment"
|
|
52431
|
+
});
|
|
52432
|
+
terminals.add(point.channel);
|
|
52433
|
+
});
|
|
52434
|
+
value.logicalChannels.forEach((channel, index) => {
|
|
52435
|
+
const path = `logicalChannels[${index}]`;
|
|
52436
|
+
const point = value.physicalPoints.find((point2) => point2.id === channel.physicalPointId);
|
|
52437
|
+
if (point?.hardwareProfile === "751-9301") {
|
|
52438
|
+
const terminal = DIGITAL_TERMINALS.find((terminal2) => terminal2.channel === point.channel);
|
|
52439
|
+
if (terminal && (!channel.capabilities.includes(terminal.direction) || channel.capabilities.includes(terminal.direction === "input" ? "output" : "input")))
|
|
52440
|
+
errors.push({
|
|
52441
|
+
path: `${path}.physicalPointId`,
|
|
52442
|
+
code: "direction_mismatch",
|
|
52443
|
+
message: `${terminal.label} requires an ${terminal.direction} channel`
|
|
52444
|
+
});
|
|
52445
|
+
if (assignedPoints.has(point.id))
|
|
52446
|
+
errors.push({
|
|
52447
|
+
path: `${path}.physicalPointId`,
|
|
52448
|
+
code: "duplicate_assignment",
|
|
52449
|
+
message: "each digital terminal can be assigned to only one logical channel"
|
|
52450
|
+
});
|
|
52451
|
+
assignedPoints.add(point.id);
|
|
52452
|
+
}
|
|
52453
|
+
for (const capability of ["pulse", "guard", "feedback"]) {
|
|
52454
|
+
if (channel.capabilities.includes(capability) && !channel[capability])
|
|
52455
|
+
errors.push({
|
|
52456
|
+
path: `${path}.${capability}`,
|
|
52457
|
+
code: "required_field",
|
|
52458
|
+
message: `configure ${capability} settings for this capability`
|
|
52459
|
+
});
|
|
52460
|
+
}
|
|
52461
|
+
if (channel.guard) {
|
|
52462
|
+
const input = value.logicalChannels.find((item) => item.id === channel.guard?.channelId);
|
|
52463
|
+
if (!input || input.id === channel.id || !input.capabilities.includes("input"))
|
|
52464
|
+
errors.push({
|
|
52465
|
+
path: `${path}.guard.channelId`,
|
|
52466
|
+
code: "invalid_guard_channel",
|
|
52467
|
+
message: "guard must reference a distinct input channel"
|
|
52468
|
+
});
|
|
52469
|
+
}
|
|
52470
|
+
});
|
|
52471
|
+
return errors;
|
|
52472
|
+
}
|
|
52473
|
+
function previewConfigurationPreset(snapshot, application) {
|
|
52474
|
+
const errors = validateSnapshot(snapshot);
|
|
52475
|
+
if (errors.length) throw new Error(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
|
|
52476
|
+
const candidate = applyPreset(snapshot, application);
|
|
52477
|
+
return {
|
|
52478
|
+
draftHash: configurationHash(snapshot),
|
|
52479
|
+
snapshot: candidate,
|
|
52480
|
+
diff: configurationDiff(snapshot, candidate),
|
|
52481
|
+
errors: validateEditorSnapshot(candidate)
|
|
52482
|
+
};
|
|
52483
|
+
}
|
|
52484
|
+
function selectPresetChanges(snapshot, application, paths, expectedHash) {
|
|
52485
|
+
const preview = previewConfigurationPreset(snapshot, application);
|
|
52486
|
+
if (expectedHash !== preview.draftHash) throw new Error("draft changed; preview the preset again");
|
|
52487
|
+
if (!Array.isArray(paths) || paths.some((path) => !preview.diff.some((change) => change.path === path)))
|
|
52488
|
+
throw new Error("unknown preset change");
|
|
52489
|
+
const selected = new Set(paths);
|
|
52490
|
+
function copy(previous, current, path) {
|
|
52491
|
+
if (selected.has(path)) return current;
|
|
52492
|
+
if (Array.isArray(previous) && Array.isArray(current))
|
|
52493
|
+
return Array.from(
|
|
52494
|
+
{ length: Math.max(previous.length, current.length) },
|
|
52495
|
+
(_, index) => copy(previous[index], current[index], `${path}[${index}]`)
|
|
52496
|
+
).filter((item) => item !== void 0);
|
|
52497
|
+
if (previous && current && typeof previous === "object" && typeof current === "object" && !Array.isArray(previous) && !Array.isArray(current)) {
|
|
52498
|
+
const before = previous;
|
|
52499
|
+
const after = current;
|
|
52500
|
+
return Object.fromEntries(
|
|
52501
|
+
[.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].map((key) => [key, copy(before[key], after[key], `${path}.${key}`)]).filter(([, value]) => value !== void 0)
|
|
52502
|
+
);
|
|
52503
|
+
}
|
|
52504
|
+
return previous;
|
|
52505
|
+
}
|
|
52506
|
+
const candidate = copy(snapshot, preview.snapshot, "$");
|
|
52507
|
+
const errors = validateEditorSnapshot(candidate);
|
|
52508
|
+
if (errors.length) throw new Error(errors.map((error) => `${error.path}: ${error.message}`).join("; "));
|
|
52509
|
+
return candidate;
|
|
52510
|
+
}
|
|
52511
|
+
function configurationImpacts(previous, current) {
|
|
52512
|
+
return (previous?.logicalChannels ?? []).flatMap((channel) => {
|
|
52513
|
+
const next = current.logicalChannels.find((item) => item.id === channel.id);
|
|
52514
|
+
const oldPoint = previous?.physicalPoints.find((point) => point.id === channel.physicalPointId);
|
|
52515
|
+
const newPoint = current.physicalPoints.find((point) => point.id === next?.physicalPointId);
|
|
52516
|
+
if (next && configurationHash({ channel, point: oldPoint }) === configurationHash({ channel: next, point: newPoint }))
|
|
52517
|
+
return [];
|
|
52518
|
+
return [
|
|
52519
|
+
{
|
|
52520
|
+
channelId: channel.id,
|
|
52521
|
+
message: next ? "Channel behavior or physical assignment changed. Existing flow commands may behave differently." : "Channel removed. Existing flow references may fail."
|
|
52522
|
+
}
|
|
52523
|
+
];
|
|
52524
|
+
});
|
|
52525
|
+
}
|
|
52526
|
+
|
|
52351
52527
|
// backend/wago-configuration-draft.entity.ts
|
|
52352
52528
|
var import_plugins_backend_sdk4 = require("@attraccess/plugins-backend-sdk");
|
|
52353
52529
|
var WagoConfigurationDraft = class {
|
|
@@ -52382,14 +52558,42 @@ WagoConfigurationDraft = __decorateClass([
|
|
|
52382
52558
|
(0, import_plugins_backend_sdk4.Entity)({ name: "plugin_wago_configuration_drafts" })
|
|
52383
52559
|
], WagoConfigurationDraft);
|
|
52384
52560
|
|
|
52385
|
-
// backend/
|
|
52561
|
+
// backend/configuration-flow-references.ts
|
|
52386
52562
|
var import_plugins_backend_sdk5 = require("@attraccess/plugins-backend-sdk");
|
|
52563
|
+
async function configurationFlowImpacts(context, controllerId, previous, current) {
|
|
52564
|
+
const impacts = configurationImpacts(previous, current);
|
|
52565
|
+
const affected = new Set(impacts.map((impact) => impact.channelId));
|
|
52566
|
+
for (const channel of previous?.logicalChannels ?? []) {
|
|
52567
|
+
if (!affected.has(channel.id) && (channel.guard && affected.has(channel.guard.channelId) || channel.feedback && affected.has(channel.feedback.channelId))) {
|
|
52568
|
+
affected.add(channel.id);
|
|
52569
|
+
impacts.push({ channelId: channel.id, message: "An input used by this channel as a guard or feedback changed." });
|
|
52570
|
+
}
|
|
52571
|
+
}
|
|
52572
|
+
const nodes = await context.dataSource.getRepository(import_plugins_backend_sdk5.ResourceFlowNode).createQueryBuilder("node").select(["node.id", "node.resourceId", "node.type", "node.data"]).where("node.type LIKE :type", { type: "plugin.wago.%" }).andWhere("node.data ->> 'controllerId' = :controllerId", { controllerId }).getMany();
|
|
52573
|
+
for (const node of nodes) {
|
|
52574
|
+
const channelId = node.data?.channelId;
|
|
52575
|
+
if (String(node.type) !== "plugin.wago.command" || typeof channelId !== "string" || !channelId) continue;
|
|
52576
|
+
const message = "Publication changes the configuration revision. Reopen and save this channel's command nodes after it applies.";
|
|
52577
|
+
const impact = impacts.find((item) => item.channelId === channelId);
|
|
52578
|
+
if (impact) {
|
|
52579
|
+
if (!impact.message.includes(message)) impact.message += ` ${message}`;
|
|
52580
|
+
} else impacts.push({ channelId, message });
|
|
52581
|
+
}
|
|
52582
|
+
return impacts.map((impact) => ({
|
|
52583
|
+
...impact,
|
|
52584
|
+
references: nodes.filter((node) => node.data?.channelId === impact.channelId).map((node) => ({ resourceId: node.resourceId, nodeId: node.id, nodeType: node.type }))
|
|
52585
|
+
}));
|
|
52586
|
+
}
|
|
52587
|
+
|
|
52588
|
+
// backend/wago-configuration-revision.entity.ts
|
|
52589
|
+
var import_plugins_backend_sdk6 = require("@attraccess/plugins-backend-sdk");
|
|
52387
52590
|
var WagoConfigurationRevision = class {
|
|
52388
52591
|
constructor() {
|
|
52389
52592
|
__publicField(this, "id");
|
|
52390
52593
|
__publicField(this, "controllerId");
|
|
52391
52594
|
__publicField(this, "revision");
|
|
52392
52595
|
__publicField(this, "snapshot");
|
|
52596
|
+
__publicField(this, "presetProvenance");
|
|
52393
52597
|
__publicField(this, "contentHash");
|
|
52394
52598
|
__publicField(this, "state");
|
|
52395
52599
|
__publicField(this, "rejectionErrors");
|
|
@@ -52398,39 +52602,42 @@ var WagoConfigurationRevision = class {
|
|
|
52398
52602
|
}
|
|
52399
52603
|
};
|
|
52400
52604
|
__decorateClass([
|
|
52401
|
-
(0,
|
|
52605
|
+
(0, import_plugins_backend_sdk6.PrimaryGeneratedColumn)({ type: "integer" })
|
|
52402
52606
|
], WagoConfigurationRevision.prototype, "id", 2);
|
|
52403
52607
|
__decorateClass([
|
|
52404
|
-
(0,
|
|
52608
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "integer", name: "controller_id" })
|
|
52405
52609
|
], WagoConfigurationRevision.prototype, "controllerId", 2);
|
|
52406
52610
|
__decorateClass([
|
|
52407
|
-
(0,
|
|
52611
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "integer" })
|
|
52408
52612
|
], WagoConfigurationRevision.prototype, "revision", 2);
|
|
52409
52613
|
__decorateClass([
|
|
52410
|
-
(0,
|
|
52614
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "text" })
|
|
52411
52615
|
], WagoConfigurationRevision.prototype, "snapshot", 2);
|
|
52412
52616
|
__decorateClass([
|
|
52413
|
-
(0,
|
|
52617
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "text", name: "preset_provenance", nullable: true })
|
|
52618
|
+
], WagoConfigurationRevision.prototype, "presetProvenance", 2);
|
|
52619
|
+
__decorateClass([
|
|
52620
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "content_hash" })
|
|
52414
52621
|
], WagoConfigurationRevision.prototype, "contentHash", 2);
|
|
52415
52622
|
__decorateClass([
|
|
52416
|
-
(0,
|
|
52623
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar" })
|
|
52417
52624
|
], WagoConfigurationRevision.prototype, "state", 2);
|
|
52418
52625
|
__decorateClass([
|
|
52419
|
-
(0,
|
|
52626
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "text", name: "rejection_errors", nullable: true })
|
|
52420
52627
|
], WagoConfigurationRevision.prototype, "rejectionErrors", 2);
|
|
52421
52628
|
__decorateClass([
|
|
52422
|
-
(0,
|
|
52629
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "published_at" })
|
|
52423
52630
|
], WagoConfigurationRevision.prototype, "publishedAt", 2);
|
|
52424
52631
|
__decorateClass([
|
|
52425
|
-
(0,
|
|
52632
|
+
(0, import_plugins_backend_sdk6.Column)({ type: "varchar", name: "reported_at", nullable: true })
|
|
52426
52633
|
], WagoConfigurationRevision.prototype, "reportedAt", 2);
|
|
52427
52634
|
WagoConfigurationRevision = __decorateClass([
|
|
52428
|
-
(0,
|
|
52635
|
+
(0, import_plugins_backend_sdk6.Entity)({ name: "plugin_wago_configuration_revisions" })
|
|
52429
52636
|
], WagoConfigurationRevision);
|
|
52430
52637
|
|
|
52431
52638
|
// backend/wago-command-handler.ts
|
|
52432
52639
|
var import_node_crypto2 = require("node:crypto");
|
|
52433
|
-
var
|
|
52640
|
+
var import_plugins_backend_sdk7 = require("@attraccess/plugins-backend-sdk");
|
|
52434
52641
|
var DEFAULT_COMMAND_TIMEOUT_SECONDS = 30;
|
|
52435
52642
|
var MAX_COMMAND_TIMEOUT_SECONDS = 300;
|
|
52436
52643
|
var WagoCommandError = class extends Error {
|
|
@@ -52457,7 +52664,8 @@ var WagoCommandHandler = class {
|
|
|
52457
52664
|
const draft = await this.dependencies.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId });
|
|
52458
52665
|
try {
|
|
52459
52666
|
const draftMatchesAppliedRevision = typeof draft?.snapshot === "string" && configurationHash(JSON.parse(draft.snapshot)) === revision?.contentHash;
|
|
52460
|
-
const
|
|
52667
|
+
const provenance = revision?.presetProvenance ?? (draftMatchesAppliedRevision ? draft?.presetProvenance : null);
|
|
52668
|
+
const storedNames = JSON.parse(provenance ?? "null")?.editor?.names;
|
|
52461
52669
|
if (storedNames && typeof storedNames === "object" && !Array.isArray(storedNames)) names = storedNames;
|
|
52462
52670
|
} catch {
|
|
52463
52671
|
}
|
|
@@ -52601,6 +52809,7 @@ var WagoCommandHandler = class {
|
|
|
52601
52809
|
throw new WagoCommandError(`WAGO controller ${controllerId} has no MQTT server`, "transport-dispatch");
|
|
52602
52810
|
const settings = await this.dependencies.getSettings();
|
|
52603
52811
|
const id = (0, import_node_crypto2.randomUUID)();
|
|
52812
|
+
this.dependencies.onCommand?.(controllerId, channelId, id);
|
|
52604
52813
|
const command = JSON.stringify({
|
|
52605
52814
|
id,
|
|
52606
52815
|
expiresAt: new Date(Date.now() + acknowledgementTimeoutSeconds * 1e3).toISOString(),
|
|
@@ -52618,6 +52827,7 @@ var WagoCommandHandler = class {
|
|
|
52618
52827
|
{ qos: 1, retain: false }
|
|
52619
52828
|
);
|
|
52620
52829
|
} catch (error) {
|
|
52830
|
+
this.dependencies.onCommandFailure?.(id, "dispatch-failed");
|
|
52621
52831
|
const dispatchError = new WagoCommandError(
|
|
52622
52832
|
`Failed to publish WAGO command: ${String(error)}`,
|
|
52623
52833
|
"transport-dispatch"
|
|
@@ -52657,7 +52867,7 @@ var WagoCommandHandler = class {
|
|
|
52657
52867
|
}
|
|
52658
52868
|
}
|
|
52659
52869
|
async references(controllerId, channelId, resourceId) {
|
|
52660
|
-
const nodes = await this.dependencies.context.dataSource.getRepository(
|
|
52870
|
+
const nodes = await this.dependencies.context.dataSource.getRepository(import_plugins_backend_sdk7.ResourceFlowNode).createQueryBuilder("node").select(["node.id", "node.resourceId"]).where("node.type = :type", { type: "plugin.wago.command" }).andWhere("node.resourceId <> :resourceId", { resourceId }).andWhere("node.data ->> 'controllerId' = :controllerId", { controllerId }).andWhere("node.data ->> 'channelId' = :channelId", { channelId }).getMany();
|
|
52661
52871
|
return nodes.map((node) => `resource ${node.resourceId} / node ${node.id}`);
|
|
52662
52872
|
}
|
|
52663
52873
|
parse(config) {
|
|
@@ -52739,6 +52949,8 @@ var WagoCommandHandler = class {
|
|
|
52739
52949
|
pending.resolve();
|
|
52740
52950
|
}
|
|
52741
52951
|
reject(id, error) {
|
|
52952
|
+
if (error instanceof WagoCommandError && error.kind === "acknowledgement-timeout")
|
|
52953
|
+
this.dependencies.onCommandFailure?.(id, "timeout");
|
|
52742
52954
|
const pending = this.pending.get(id);
|
|
52743
52955
|
if (!pending) return;
|
|
52744
52956
|
this.pending.delete(id);
|
|
@@ -52757,6 +52969,349 @@ function positiveInteger(value) {
|
|
|
52757
52969
|
return Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
52758
52970
|
}
|
|
52759
52971
|
|
|
52972
|
+
// backend/diagnostics-envelope.ts
|
|
52973
|
+
var DIAGNOSTIC_CATEGORIES = [
|
|
52974
|
+
"heartbeat",
|
|
52975
|
+
"state",
|
|
52976
|
+
"measurements",
|
|
52977
|
+
"faults",
|
|
52978
|
+
"acknowledgements",
|
|
52979
|
+
"configuration/reported"
|
|
52980
|
+
];
|
|
52981
|
+
var MILLI_UNITS = ["milliampere", "millivolt", "milliwatt", "milliwatt-hour", "millipercent"];
|
|
52982
|
+
var CANONICAL_UNITS = [...MILLI_UNITS, "ampere", "volt", "watt", "watt-hour", "percent"];
|
|
52983
|
+
function sourceTime(value) {
|
|
52984
|
+
if (typeof value !== "string") return null;
|
|
52985
|
+
const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,3})?(Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
|
|
52986
|
+
if (!match || Number(match[4] ?? 0) > 23 || Number(match[5] ?? 0) > 59) return null;
|
|
52987
|
+
const time = Date.parse(value);
|
|
52988
|
+
if (!Number.isFinite(time)) return null;
|
|
52989
|
+
const offset = match[2] === "Z" ? 0 : (match[3] === "+" ? 1 : -1) * (Number(match[4]) * 60 + Number(match[5])) * 6e4;
|
|
52990
|
+
return new Date(time + offset).toISOString().slice(0, 19) === match[1] ? time : null;
|
|
52991
|
+
}
|
|
52992
|
+
function emptyStream() {
|
|
52993
|
+
return { lastSourceTime: 0, retiredStreams: [], watermarks: {}, sequenceGaps: 0, trackingExhausted: false };
|
|
52994
|
+
}
|
|
52995
|
+
function canonicalEnvelope(data, category) {
|
|
52996
|
+
return "timestamp" in data || "streamId" in data || category !== "heartbeat" && "sequence" in data;
|
|
52997
|
+
}
|
|
52998
|
+
function validEnvelope(data, now) {
|
|
52999
|
+
const time = sourceTime(data.timestamp);
|
|
53000
|
+
return time !== null && time <= now && typeof data.streamId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.streamId) && Number.isSafeInteger(data.sequence) && data.sequence >= 1;
|
|
53001
|
+
}
|
|
53002
|
+
function admitEnvelope(state, data, category, now) {
|
|
53003
|
+
const stream = data.streamId.toLowerCase();
|
|
53004
|
+
const timestamp = sourceTime(data.timestamp);
|
|
53005
|
+
const sequence = data.sequence;
|
|
53006
|
+
if (state.retiredStreams.includes(stream) || state.trackingExhausted) return "rejected";
|
|
53007
|
+
const restart = state.activeStream !== stream;
|
|
53008
|
+
if (restart) {
|
|
53009
|
+
if (!(category === "heartbeat" || category === "state" && data.connected === true) || now - timestamp > 9e4 || timestamp < state.lastSourceTime)
|
|
53010
|
+
return "rejected";
|
|
53011
|
+
if (state.retiredStreams.length >= 16) {
|
|
53012
|
+
state.trackingExhausted = true;
|
|
53013
|
+
return "rejected";
|
|
53014
|
+
}
|
|
53015
|
+
}
|
|
53016
|
+
const previous = restart ? void 0 : state.watermarks[category];
|
|
53017
|
+
if (previous && (sequence <= previous.sequence || timestamp < previous.timestamp)) return "rejected";
|
|
53018
|
+
if (restart) {
|
|
53019
|
+
if (state.activeStream) state.retiredStreams.push(state.activeStream);
|
|
53020
|
+
state.activeStream = stream;
|
|
53021
|
+
state.watermarks = {};
|
|
53022
|
+
}
|
|
53023
|
+
state.sequenceGaps = Math.min(
|
|
53024
|
+
Number.MAX_SAFE_INTEGER,
|
|
53025
|
+
state.sequenceGaps + Math.max(0, sequence - (previous?.sequence ?? 0) - 1)
|
|
53026
|
+
);
|
|
53027
|
+
state.watermarks[category] = { sequence, timestamp };
|
|
53028
|
+
state.lastSourceTime = Math.max(state.lastSourceTime, timestamp);
|
|
53029
|
+
return restart ? "restart" : "accepted";
|
|
53030
|
+
}
|
|
53031
|
+
|
|
53032
|
+
// backend/diagnostics-validation.ts
|
|
53033
|
+
var fields = new Set(
|
|
53034
|
+
"version physicalPoints logicalChannels id physicalPointId hardwareProfile channel profile capabilities disconnectPolicy mode timeoutMs range minimum maximum pulse durationMs guard channelId when feedback expected measurement unit scale offset".split(
|
|
53035
|
+
" "
|
|
53036
|
+
)
|
|
53037
|
+
);
|
|
53038
|
+
var codes = new Set(
|
|
53039
|
+
"invalid_snapshot unsupported_version invalid_channel invalid_collection invalid_object unknown_field required_field invalid_id duplicate_id unsupported_value invalid_capabilities unsupported_capability duplicate_capability unsupported_field invalid_range invalid_duration invalid_measurement invalid_feedback_channel invalid_timeout missing_capability unknown_reference invalid_reference content_hash_mismatch revision_mismatch hardware_unavailable".split(
|
|
53040
|
+
" "
|
|
53041
|
+
)
|
|
53042
|
+
);
|
|
53043
|
+
function safeValidationSummaries(value) {
|
|
53044
|
+
if (!Array.isArray(value)) return [];
|
|
53045
|
+
return value.slice(0, 50).map((item) => {
|
|
53046
|
+
const error = item && typeof item === "object" ? item : {};
|
|
53047
|
+
const path = typeof error.path === "string" && error.path.length <= 256 ? error.path : "$";
|
|
53048
|
+
const tokens = path.replace(/\[(\d{1,5})\]/g, ".$1").split(".");
|
|
53049
|
+
const safe = tokens[0] === "$" && tokens.slice(1).every((token) => fields.has(token) || /^\d{1,5}$/.test(token));
|
|
53050
|
+
return {
|
|
53051
|
+
path: safe ? path : "$",
|
|
53052
|
+
code: codes.has(error.code) ? error.code : "validation_error"
|
|
53053
|
+
};
|
|
53054
|
+
});
|
|
53055
|
+
}
|
|
53056
|
+
|
|
53057
|
+
// backend/diagnostics-store.ts
|
|
53058
|
+
function freshness(timestamp, now = Date.now(), maxAge = 9e4) {
|
|
53059
|
+
if (!timestamp) return "missing";
|
|
53060
|
+
const time = sourceTime(timestamp);
|
|
53061
|
+
if (time === null) return "invalid";
|
|
53062
|
+
if (time > now) return "future";
|
|
53063
|
+
return now - time > maxAge ? "stale" : "fresh";
|
|
53064
|
+
}
|
|
53065
|
+
var MAX_CONTROLLERS = 256;
|
|
53066
|
+
var MAX_CHANNELS = 256;
|
|
53067
|
+
var RETENTION_MS = 15 * 6e4;
|
|
53068
|
+
var faultCodes = /* @__PURE__ */ new Set([
|
|
53069
|
+
"measurement_read_failed",
|
|
53070
|
+
"device_write_failed",
|
|
53071
|
+
"feedback_mismatch",
|
|
53072
|
+
"feedback_read_failed"
|
|
53073
|
+
]);
|
|
53074
|
+
function identifier(value) {
|
|
53075
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= 256 && !Array.from(value).some((character) => character.charCodeAt(0) < 32);
|
|
53076
|
+
}
|
|
53077
|
+
var WagoDiagnosticsStore = class {
|
|
53078
|
+
constructor(now = Date.now) {
|
|
53079
|
+
__publicField(this, "now", now);
|
|
53080
|
+
__publicField(this, "controllers", /* @__PURE__ */ new Map());
|
|
53081
|
+
__publicField(this, "commands", /* @__PURE__ */ new Map());
|
|
53082
|
+
}
|
|
53083
|
+
prune() {
|
|
53084
|
+
const cutoff = this.now() - RETENTION_MS;
|
|
53085
|
+
for (const [id, state] of this.controllers) if (state.touched < cutoff) this.controllers.delete(id);
|
|
53086
|
+
for (const [id, command] of this.commands) if (command.at < cutoff) this.commands.delete(id);
|
|
53087
|
+
}
|
|
53088
|
+
read(id) {
|
|
53089
|
+
this.prune();
|
|
53090
|
+
const state = this.controllers.get(id);
|
|
53091
|
+
const copy = state ? JSON.parse(JSON.stringify(state)) : {
|
|
53092
|
+
touched: 0,
|
|
53093
|
+
inputs: {},
|
|
53094
|
+
outputs: {},
|
|
53095
|
+
measurements: {},
|
|
53096
|
+
cumulativeMeasurements: {},
|
|
53097
|
+
faults: {},
|
|
53098
|
+
acknowledgements: {},
|
|
53099
|
+
events: [],
|
|
53100
|
+
...emptyStream()
|
|
53101
|
+
};
|
|
53102
|
+
const cutoff = this.now() - RETENTION_MS;
|
|
53103
|
+
for (const field2 of [
|
|
53104
|
+
"inputs",
|
|
53105
|
+
"outputs",
|
|
53106
|
+
"measurements",
|
|
53107
|
+
"cumulativeMeasurements",
|
|
53108
|
+
"faults",
|
|
53109
|
+
"acknowledgements"
|
|
53110
|
+
]) {
|
|
53111
|
+
copy[field2] = Object.assign(/* @__PURE__ */ Object.create(null), copy[field2]);
|
|
53112
|
+
}
|
|
53113
|
+
for (const collection2 of [
|
|
53114
|
+
copy.cumulativeMeasurements,
|
|
53115
|
+
copy.inputs,
|
|
53116
|
+
copy.outputs,
|
|
53117
|
+
copy.measurements,
|
|
53118
|
+
copy.faults,
|
|
53119
|
+
copy.acknowledgements
|
|
53120
|
+
]) {
|
|
53121
|
+
for (const key of Object.keys(collection2))
|
|
53122
|
+
if (Date.parse(collection2[key].receivedAt) < cutoff) delete collection2[key];
|
|
53123
|
+
}
|
|
53124
|
+
copy.events = copy.events.filter((event) => Date.parse(event.receivedAt) >= cutoff);
|
|
53125
|
+
if (copy.rejection && Date.parse(copy.rejection.receivedAt) < cutoff) delete copy.rejection;
|
|
53126
|
+
return copy;
|
|
53127
|
+
}
|
|
53128
|
+
command(controllerId, channelId, id) {
|
|
53129
|
+
this.prune();
|
|
53130
|
+
if (!identifier(channelId) || !identifier(id)) return;
|
|
53131
|
+
if (!this.controllers.has(controllerId) && this.controllers.size >= MAX_CONTROLLERS) return;
|
|
53132
|
+
const state = this.read(controllerId);
|
|
53133
|
+
state.touched = this.now();
|
|
53134
|
+
this.controllers.set(controllerId, state);
|
|
53135
|
+
const oldest = this.commands.keys().next().value;
|
|
53136
|
+
if (this.commands.size >= 1024 && oldest !== void 0) this.commands.delete(oldest);
|
|
53137
|
+
this.commands.set(id, { controllerId, channelId, at: this.now() });
|
|
53138
|
+
}
|
|
53139
|
+
commandFailed(id, status) {
|
|
53140
|
+
this.prune();
|
|
53141
|
+
const command = this.commands.get(id);
|
|
53142
|
+
if (!command) return;
|
|
53143
|
+
this.commands.delete(id);
|
|
53144
|
+
const state = this.controllers.get(command.controllerId);
|
|
53145
|
+
if (!state) return;
|
|
53146
|
+
state.touched = this.now();
|
|
53147
|
+
state.acknowledgements[command.channelId] = { id, status, receivedAt: new Date(this.now()).toISOString() };
|
|
53148
|
+
while (Object.keys(state.acknowledgements).length > MAX_CHANNELS)
|
|
53149
|
+
delete state.acknowledgements[Object.keys(state.acknowledgements)[0]];
|
|
53150
|
+
}
|
|
53151
|
+
canTrack(id) {
|
|
53152
|
+
this.prune();
|
|
53153
|
+
return this.controllers.has(id) || this.controllers.size < MAX_CONTROLLERS || [...this.controllers.values()].some((state) => !state.activeStream);
|
|
53154
|
+
}
|
|
53155
|
+
ingest(id, kind, payload) {
|
|
53156
|
+
this.prune();
|
|
53157
|
+
if (payload.length > 65536) return false;
|
|
53158
|
+
let data;
|
|
53159
|
+
try {
|
|
53160
|
+
data = JSON.parse(payload.toString("utf8"));
|
|
53161
|
+
} catch {
|
|
53162
|
+
return false;
|
|
53163
|
+
}
|
|
53164
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
53165
|
+
if (!DIAGNOSTIC_CATEGORIES.includes(kind)) return false;
|
|
53166
|
+
const canonical = canonicalEnvelope(data, kind);
|
|
53167
|
+
if (canonical && !validEnvelope(data, this.now())) return false;
|
|
53168
|
+
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
53169
|
+
if (canonical && kind === "state" && data.readiness !== void 0 && (!isObject(data.readiness) || data.readiness.hardwareAvailable !== void 0 && typeof data.readiness.hardwareAvailable !== "boolean"))
|
|
53170
|
+
return false;
|
|
53171
|
+
if (canonical && kind === "state" && !(data.contentHash === null || typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash)))
|
|
53172
|
+
return false;
|
|
53173
|
+
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)))
|
|
53174
|
+
return false;
|
|
53175
|
+
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(
|
|
53176
|
+
(value) => typeof value === "boolean"
|
|
53177
|
+
)))
|
|
53178
|
+
return false;
|
|
53179
|
+
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))))
|
|
53180
|
+
return false;
|
|
53181
|
+
if (kind === "faults" && !identifier(data.channelId)) return false;
|
|
53182
|
+
if (kind === "acknowledgements" && (!identifier(data.id) || !["accepted", "duplicate", "rejected"].includes(data.status)))
|
|
53183
|
+
return false;
|
|
53184
|
+
const oldest = [...this.controllers].find(([, value]) => !value.activeStream)?.[0];
|
|
53185
|
+
if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest === void 0) return false;
|
|
53186
|
+
if (!this.controllers.has(id) && this.controllers.size >= MAX_CONTROLLERS && oldest !== void 0)
|
|
53187
|
+
this.controllers.delete(oldest);
|
|
53188
|
+
const state = this.read(id);
|
|
53189
|
+
if (!canonical && kind === "heartbeat" && typeof data.sequence === "number") {
|
|
53190
|
+
if (data.sequence < (state.legacyHeartbeatSequence ?? 0)) return false;
|
|
53191
|
+
state.legacyHeartbeatSequence = data.sequence;
|
|
53192
|
+
}
|
|
53193
|
+
if (canonical) {
|
|
53194
|
+
if (kind === "measurements" && state.measurementAfter !== void 0 && sourceTime(data.timestamp) <= state.measurementAfter)
|
|
53195
|
+
return false;
|
|
53196
|
+
const previousStream = state.activeStream;
|
|
53197
|
+
const admission = admitEnvelope(state, data, kind, this.now());
|
|
53198
|
+
if (admission === "rejected") {
|
|
53199
|
+
if (state.trackingExhausted) {
|
|
53200
|
+
state.connected = false;
|
|
53201
|
+
this.controllers.set(id, state);
|
|
53202
|
+
}
|
|
53203
|
+
return false;
|
|
53204
|
+
}
|
|
53205
|
+
if (admission === "restart") {
|
|
53206
|
+
state.measurementAfter = sourceTime(data.timestamp) - (previousStream ? 0 : 1);
|
|
53207
|
+
state.inputs = /* @__PURE__ */ Object.create(null);
|
|
53208
|
+
state.outputs = /* @__PURE__ */ Object.create(null);
|
|
53209
|
+
state.measurements = /* @__PURE__ */ Object.create(null);
|
|
53210
|
+
state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
|
|
53211
|
+
state.connected = void 0;
|
|
53212
|
+
state.heartbeatAt = void 0;
|
|
53213
|
+
state.rejection = void 0;
|
|
53214
|
+
state.hardwareAvailable = void 0;
|
|
53215
|
+
state.stateSourceAt = void 0;
|
|
53216
|
+
state.revision = void 0;
|
|
53217
|
+
state.contentHash = void 0;
|
|
53218
|
+
}
|
|
53219
|
+
} else if (state.activeStream && !["heartbeat", "configuration/reported"].includes(kind)) {
|
|
53220
|
+
if (kind === "state" && data.connected === false) {
|
|
53221
|
+
state.connected = false;
|
|
53222
|
+
state.measurementAfter = this.now();
|
|
53223
|
+
state.stateSourceAt = void 0;
|
|
53224
|
+
this.controllers.set(id, state);
|
|
53225
|
+
}
|
|
53226
|
+
return false;
|
|
53227
|
+
}
|
|
53228
|
+
state.touched = this.now();
|
|
53229
|
+
const receivedAt = new Date(this.now()).toISOString();
|
|
53230
|
+
const metadata = {
|
|
53231
|
+
sourceAt: canonical ? data.timestamp : null,
|
|
53232
|
+
receivedAt,
|
|
53233
|
+
streamId: canonical ? state.activeStream : null,
|
|
53234
|
+
sequence: canonical ? data.sequence : null
|
|
53235
|
+
};
|
|
53236
|
+
if (kind === "heartbeat") state.heartbeatAt = canonical ? data.timestamp : receivedAt;
|
|
53237
|
+
if (kind === "state") {
|
|
53238
|
+
const contentHash = typeof data.contentHash === "string" && /^[0-9a-f]{64}$/i.test(data.contentHash) ? data.contentHash : void 0;
|
|
53239
|
+
const hardwareAvailable = isObject(data.readiness) && typeof data.readiness.hardwareAvailable === "boolean" ? data.readiness.hardwareAvailable : void 0;
|
|
53240
|
+
if (state.connected !== data.connected || state.revision !== data.revision || state.contentHash !== contentHash || state.hardwareAvailable !== hardwareAvailable) {
|
|
53241
|
+
if (canonical || state.stateSourceAt)
|
|
53242
|
+
state.measurementAfter = canonical ? sourceTime(data.timestamp) : this.now();
|
|
53243
|
+
state.measurements = /* @__PURE__ */ Object.create(null);
|
|
53244
|
+
state.cumulativeMeasurements = /* @__PURE__ */ Object.create(null);
|
|
53245
|
+
}
|
|
53246
|
+
state.inputs = /* @__PURE__ */ Object.create(null);
|
|
53247
|
+
state.outputs = /* @__PURE__ */ Object.create(null);
|
|
53248
|
+
if (typeof data.connected === "boolean") state.connected = data.connected;
|
|
53249
|
+
state.revision = Number.isSafeInteger(data.revision) ? data.revision : void 0;
|
|
53250
|
+
state.contentHash = contentHash;
|
|
53251
|
+
state.hardwareAvailable = hardwareAvailable;
|
|
53252
|
+
state.stateSourceAt = canonical ? data.timestamp : void 0;
|
|
53253
|
+
if (data.outputs && typeof data.outputs === "object" && !Array.isArray(data.outputs)) {
|
|
53254
|
+
for (const [channelId, value] of Object.entries(data.outputs).slice(0, MAX_CHANNELS)) {
|
|
53255
|
+
if (identifier(channelId) && typeof value === "boolean")
|
|
53256
|
+
state.outputs[channelId] = { kind: "output", value, ...metadata };
|
|
53257
|
+
}
|
|
53258
|
+
}
|
|
53259
|
+
if (isObject(data.inputs))
|
|
53260
|
+
for (const [channelId, value] of Object.entries(data.inputs).slice(0, MAX_CHANNELS)) {
|
|
53261
|
+
if (identifier(channelId) && typeof value === "boolean")
|
|
53262
|
+
state.inputs[channelId] = { kind: "input", value, ...metadata };
|
|
53263
|
+
}
|
|
53264
|
+
}
|
|
53265
|
+
if (kind === "measurements" && identifier(data.channelId) && typeof data.value === "number" && Number.isFinite(data.value) && (canonical ? CANONICAL_UNITS : ["ampere", "volt", "watt", "percent"]).includes(data.unit)) {
|
|
53266
|
+
const collection2 = canonical && data.kind === "cumulative" ? state.cumulativeMeasurements : state.measurements;
|
|
53267
|
+
collection2[data.channelId] = {
|
|
53268
|
+
kind: "measurement",
|
|
53269
|
+
value: data.value,
|
|
53270
|
+
unit: data.unit,
|
|
53271
|
+
...canonical ? { measurementKind: data.kind } : {},
|
|
53272
|
+
...metadata
|
|
53273
|
+
};
|
|
53274
|
+
}
|
|
53275
|
+
if (kind === "faults" && identifier(data.channelId))
|
|
53276
|
+
state.faults[data.channelId] = {
|
|
53277
|
+
code: faultCodes.has(data.code) ? data.code : "runtime_fault",
|
|
53278
|
+
receivedAt
|
|
53279
|
+
};
|
|
53280
|
+
if (kind === "acknowledgements" && identifier(data.id) && ["accepted", "duplicate", "rejected"].includes(data.status)) {
|
|
53281
|
+
const command = this.commands.get(data.id);
|
|
53282
|
+
if (command?.controllerId === id) {
|
|
53283
|
+
state.acknowledgements[command.channelId] = {
|
|
53284
|
+
id: data.id,
|
|
53285
|
+
status: data.status,
|
|
53286
|
+
receivedAt
|
|
53287
|
+
};
|
|
53288
|
+
this.commands.delete(data.id);
|
|
53289
|
+
}
|
|
53290
|
+
}
|
|
53291
|
+
if (kind === "configuration/reported" && Number.isSafeInteger(data.revision) && typeof data.contentHash === "string")
|
|
53292
|
+
state.rejection = {
|
|
53293
|
+
revision: data.revision,
|
|
53294
|
+
contentHash: data.contentHash,
|
|
53295
|
+
receivedAt,
|
|
53296
|
+
errors: safeValidationSummaries(data.errors)
|
|
53297
|
+
};
|
|
53298
|
+
for (const collection2 of [
|
|
53299
|
+
state.cumulativeMeasurements,
|
|
53300
|
+
state.inputs,
|
|
53301
|
+
state.outputs,
|
|
53302
|
+
state.measurements,
|
|
53303
|
+
state.faults,
|
|
53304
|
+
state.acknowledgements
|
|
53305
|
+
]) {
|
|
53306
|
+
while (Object.keys(collection2).length > MAX_CHANNELS) delete collection2[Object.keys(collection2)[0]];
|
|
53307
|
+
}
|
|
53308
|
+
state.events.push({ kind, receivedAt });
|
|
53309
|
+
state.events = state.events.slice(-50);
|
|
53310
|
+
this.controllers.set(id, state);
|
|
53311
|
+
return true;
|
|
53312
|
+
}
|
|
53313
|
+
};
|
|
53314
|
+
|
|
52760
53315
|
// backend/wago.service.ts
|
|
52761
53316
|
var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
|
|
52762
53317
|
var STALE_AFTER_MS = 9e4;
|
|
@@ -52771,6 +53326,7 @@ var MqttSubscriptionError = class extends Error {
|
|
|
52771
53326
|
var WagoService = class {
|
|
52772
53327
|
constructor(context) {
|
|
52773
53328
|
__publicField(this, "context", context);
|
|
53329
|
+
__publicField(this, "diagnostics", new WagoDiagnosticsStore());
|
|
52774
53330
|
__publicField(this, "controllers");
|
|
52775
53331
|
__publicField(this, "settings");
|
|
52776
53332
|
__publicField(this, "enrollments");
|
|
@@ -52788,7 +53344,9 @@ var WagoService = class {
|
|
|
52788
53344
|
controllers: () => this.controllers,
|
|
52789
53345
|
claimedController: (id) => this.claimedController(id),
|
|
52790
53346
|
getSettings: () => this.getSettings(),
|
|
52791
|
-
appliedRevision: (id) => this.appliedRevision(id)
|
|
53347
|
+
appliedRevision: (id) => this.appliedRevision(id),
|
|
53348
|
+
onCommand: (controllerId, channelId, id) => this.diagnostics.command(controllerId, channelId, id),
|
|
53349
|
+
onCommandFailure: (id, status) => this.diagnostics.commandFailed(id, status)
|
|
52792
53350
|
}));
|
|
52793
53351
|
__publicField(this, "claimConfigurationLock", Promise.resolve());
|
|
52794
53352
|
__publicField(this, "subscriptionRebuild", Promise.resolve());
|
|
@@ -52852,7 +53410,7 @@ var WagoService = class {
|
|
|
52852
53410
|
runtimeVersion: controller.runtimeVersion,
|
|
52853
53411
|
capabilities: controller.capabilities,
|
|
52854
53412
|
lastSequence: controller.lastSequence,
|
|
52855
|
-
lastHeartbeatAt: controller.lastHeartbeatAt,
|
|
53413
|
+
lastHeartbeatAt: this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt,
|
|
52856
53414
|
lastSeenAt: controller.lastSeenAt,
|
|
52857
53415
|
compatibilityError: controller.compatibilityError,
|
|
52858
53416
|
createdAt: controller.createdAt,
|
|
@@ -52898,13 +53456,40 @@ var WagoService = class {
|
|
|
52898
53456
|
await this.claimedController(controllerId);
|
|
52899
53457
|
return this.drafts.findOneBy({ controllerId });
|
|
52900
53458
|
}
|
|
52901
|
-
async saveDraft(controllerId, snapshot) {
|
|
52902
|
-
return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot));
|
|
53459
|
+
async saveDraft(controllerId, snapshot, metadata) {
|
|
53460
|
+
return this.withConfigurationLock(controllerId, () => this.saveDraftWhileLocked(controllerId, snapshot, metadata));
|
|
53461
|
+
}
|
|
53462
|
+
presets() {
|
|
53463
|
+
return WAGO_PRESETS;
|
|
52903
53464
|
}
|
|
52904
|
-
async
|
|
53465
|
+
async previewPreset(controllerId, application, snapshot) {
|
|
52905
53466
|
const draft = await this.getDraft(controllerId);
|
|
52906
|
-
|
|
52907
|
-
|
|
53467
|
+
const source = snapshot ?? (draft ? JSON.parse(draft.snapshot) : { version: 1, physicalPoints: [], logicalChannels: [] });
|
|
53468
|
+
try {
|
|
53469
|
+
return previewConfigurationPreset(source, application);
|
|
53470
|
+
} catch (error) {
|
|
53471
|
+
throw new import_common.BadRequestException(error instanceof Error ? error.message : "invalid preset");
|
|
53472
|
+
}
|
|
53473
|
+
}
|
|
53474
|
+
async applyPreset(controllerId, application, selectedPaths, previewedDraftHash, snapshot) {
|
|
53475
|
+
return this.withConfigurationLock(controllerId, async () => {
|
|
53476
|
+
const draft = await this.getDraft(controllerId);
|
|
53477
|
+
const source = snapshot ?? (draft ? JSON.parse(draft.snapshot) : { version: 1, physicalPoints: [], logicalChannels: [] });
|
|
53478
|
+
let candidate;
|
|
53479
|
+
try {
|
|
53480
|
+
candidate = selectPresetChanges(source, application, selectedPaths, previewedDraftHash);
|
|
53481
|
+
} catch (error) {
|
|
53482
|
+
throw new import_common.BadRequestException(error instanceof Error ? error.message : "invalid preset");
|
|
53483
|
+
}
|
|
53484
|
+
if (snapshot) return { snapshot: canonicalSnapshot(candidate) };
|
|
53485
|
+
return this.saveDraftWhileLocked(controllerId, candidate);
|
|
53486
|
+
});
|
|
53487
|
+
}
|
|
53488
|
+
async validateDraft(controllerId, snapshot) {
|
|
53489
|
+
const draft = await this.getDraft(controllerId);
|
|
53490
|
+
if (!draft && snapshot === void 0)
|
|
53491
|
+
throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
53492
|
+
const errors = validateEditorSnapshot(snapshot === void 0 && draft ? JSON.parse(draft.snapshot) : snapshot);
|
|
52908
53493
|
return { valid: errors.length === 0, errors };
|
|
52909
53494
|
}
|
|
52910
53495
|
async revisionsFor(controllerId, offset = 0, limit = 20) {
|
|
@@ -52922,7 +53507,8 @@ var WagoService = class {
|
|
|
52922
53507
|
"state",
|
|
52923
53508
|
"rejectionErrors",
|
|
52924
53509
|
"publishedAt",
|
|
52925
|
-
"reportedAt"
|
|
53510
|
+
"reportedAt",
|
|
53511
|
+
"presetProvenance"
|
|
52926
53512
|
],
|
|
52927
53513
|
skip: pageOffset,
|
|
52928
53514
|
take: pageLimit
|
|
@@ -52932,35 +53518,93 @@ var WagoService = class {
|
|
|
52932
53518
|
async reviewDraft(controllerId) {
|
|
52933
53519
|
return this.withConfigurationLock(controllerId, () => this.reviewDraftWhileLocked(controllerId));
|
|
52934
53520
|
}
|
|
52935
|
-
async publishDraft(controllerId) {
|
|
52936
|
-
return this.withConfigurationLock(
|
|
53521
|
+
async publishDraft(controllerId, force = false, reviewedHash) {
|
|
53522
|
+
return this.withConfigurationLock(
|
|
53523
|
+
controllerId,
|
|
53524
|
+
() => this.publishDraftWhileLocked(controllerId, force, reviewedHash)
|
|
53525
|
+
);
|
|
52937
53526
|
}
|
|
52938
|
-
async rollback(controllerId, revision) {
|
|
53527
|
+
async rollback(controllerId, revision, force = false, sourceHash, currentHash, draftHash, impactHash) {
|
|
52939
53528
|
return this.withConfigurationLock(controllerId, async () => {
|
|
53529
|
+
await this.claimedController(controllerId);
|
|
52940
53530
|
const source = await this.revisions.findOneBy({ controllerId, revision });
|
|
52941
53531
|
if (!source) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
|
|
52942
|
-
await this.
|
|
53532
|
+
const current = await this.latestRevision(controllerId);
|
|
53533
|
+
const draft = await this.drafts.findOneBy({ controllerId });
|
|
53534
|
+
if (draftHash !== this.draftIdentity(draft) || sourceHash !== source.contentHash || currentHash !== this.revisionIdentity(current))
|
|
53535
|
+
throw new import_common.ConflictException("configuration changed since rollback preview; preview and confirm again");
|
|
53536
|
+
const snapshot = JSON.parse(source.snapshot);
|
|
53537
|
+
const errors = validateEditorSnapshot(snapshot);
|
|
53538
|
+
if (errors.length) throw new import_common.ConflictException({ message: "rollback configuration is invalid", errors });
|
|
53539
|
+
await this.requireImpactAcknowledgement(controllerId, current, snapshot, force, impactHash);
|
|
53540
|
+
await this.saveDraftWhileLocked(
|
|
53541
|
+
controllerId,
|
|
53542
|
+
snapshot,
|
|
53543
|
+
(source.presetProvenance ? JSON.parse(source.presetProvenance).editor : null) ?? { names: {}, presets: [] }
|
|
53544
|
+
);
|
|
52943
53545
|
await this.reviewDraftWhileLocked(controllerId);
|
|
52944
|
-
return this.publishDraftWhileLocked(controllerId);
|
|
53546
|
+
return this.publishDraftWhileLocked(controllerId, force);
|
|
52945
53547
|
});
|
|
52946
53548
|
}
|
|
52947
53549
|
async previewRevision(controllerId, revision) {
|
|
52948
|
-
|
|
52949
|
-
|
|
52950
|
-
|
|
52951
|
-
|
|
52952
|
-
|
|
52953
|
-
|
|
52954
|
-
|
|
52955
|
-
|
|
52956
|
-
|
|
53550
|
+
return this.withConfigurationLock(controllerId, async () => {
|
|
53551
|
+
await this.claimedController(controllerId);
|
|
53552
|
+
const selected = await this.revisions.findOneBy({ controllerId, revision });
|
|
53553
|
+
if (!selected) throw new import_common.NotFoundException(`WAGO configuration revision ${revision} not found`);
|
|
53554
|
+
const [current] = await this.revisions.find({ where: { controllerId }, order: { revision: "DESC" }, take: 1 });
|
|
53555
|
+
const draft = await this.drafts.findOneBy({ controllerId });
|
|
53556
|
+
const impacts = await configurationFlowImpacts(
|
|
53557
|
+
this.context,
|
|
53558
|
+
controllerId,
|
|
53559
|
+
current ? JSON.parse(current.snapshot) : null,
|
|
53560
|
+
JSON.parse(selected.snapshot)
|
|
53561
|
+
);
|
|
53562
|
+
return {
|
|
53563
|
+
draftHash: this.draftIdentity(draft),
|
|
53564
|
+
impacts,
|
|
53565
|
+
revision: selected,
|
|
53566
|
+
current: current ?? null,
|
|
53567
|
+
diff: configurationDiff(current ? JSON.parse(current.snapshot) : null, JSON.parse(selected.snapshot)),
|
|
53568
|
+
metadataDiff: configurationDiff(
|
|
53569
|
+
this.metadataFromProvenance(current?.presetProvenance),
|
|
53570
|
+
this.metadataFromProvenance(selected.presetProvenance)
|
|
53571
|
+
),
|
|
53572
|
+
impactHash: configurationHash({ impacts })
|
|
53573
|
+
};
|
|
53574
|
+
});
|
|
53575
|
+
}
|
|
53576
|
+
draftIdentity(draft) {
|
|
53577
|
+
return configurationHash(draft ? { snapshot: draft.snapshot, metadata: draft.presetProvenance ?? null } : null);
|
|
53578
|
+
}
|
|
53579
|
+
revisionIdentity(revision) {
|
|
53580
|
+
return revision ? `${revision.revision}:${revision.contentHash}` : null;
|
|
53581
|
+
}
|
|
53582
|
+
reviewIdentity(draft, impacts) {
|
|
53583
|
+
return configurationHash({ draft: this.draftIdentity(draft), impacts });
|
|
52957
53584
|
}
|
|
52958
|
-
|
|
53585
|
+
metadataFromProvenance(provenance) {
|
|
53586
|
+
if (!provenance) return { names: {}, presets: [] };
|
|
53587
|
+
try {
|
|
53588
|
+
return editorMetadata(JSON.parse(provenance).editor);
|
|
53589
|
+
} catch {
|
|
53590
|
+
return { names: {}, presets: [] };
|
|
53591
|
+
}
|
|
53592
|
+
}
|
|
53593
|
+
async saveDraftWhileLocked(controllerId, snapshot, metadata) {
|
|
52959
53594
|
await this.claimedController(controllerId);
|
|
53595
|
+
let provenance;
|
|
53596
|
+
if (metadata !== void 0) {
|
|
53597
|
+
try {
|
|
53598
|
+
provenance = JSON.stringify({ editor: editorMetadata(metadata) });
|
|
53599
|
+
} catch (error) {
|
|
53600
|
+
throw new import_common.BadRequestException(error instanceof Error ? error.message : "invalid editor metadata");
|
|
53601
|
+
}
|
|
53602
|
+
}
|
|
52960
53603
|
const serialized = canonicalSnapshot(snapshot);
|
|
52961
53604
|
const existing = await this.drafts.findOneBy({ controllerId });
|
|
52962
53605
|
const draft = existing ?? this.drafts.create({ controllerId, snapshot: serialized, reviewedHash: null, updatedAt: "" });
|
|
52963
53606
|
draft.snapshot = serialized;
|
|
53607
|
+
if (provenance !== void 0) draft.presetProvenance = provenance;
|
|
52964
53608
|
draft.reviewedHash = null;
|
|
52965
53609
|
draft.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
52966
53610
|
return this.drafts.save(draft);
|
|
@@ -52970,28 +53614,66 @@ var WagoService = class {
|
|
|
52970
53614
|
const draft = await this.drafts.findOneBy({ controllerId });
|
|
52971
53615
|
if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
52972
53616
|
const previous = await this.latestRevision(controllerId);
|
|
52973
|
-
|
|
53617
|
+
const impacts = await configurationFlowImpacts(
|
|
53618
|
+
this.context,
|
|
53619
|
+
controllerId,
|
|
53620
|
+
previous ? JSON.parse(previous.snapshot) : null,
|
|
53621
|
+
JSON.parse(draft.snapshot)
|
|
53622
|
+
);
|
|
53623
|
+
draft.reviewedHash = this.reviewIdentity(draft, impacts);
|
|
52974
53624
|
await this.drafts.save(draft);
|
|
52975
53625
|
const diff = configurationDiff(previous ? JSON.parse(previous.snapshot) : null, JSON.parse(draft.snapshot));
|
|
52976
|
-
|
|
53626
|
+
const metadataDiff = configurationDiff(
|
|
53627
|
+
this.metadataFromProvenance(previous?.presetProvenance),
|
|
53628
|
+
this.metadataFromProvenance(draft.presetProvenance)
|
|
53629
|
+
);
|
|
53630
|
+
return {
|
|
53631
|
+
draft,
|
|
53632
|
+
previous,
|
|
53633
|
+
changed: diff.length > 0 || metadataDiff.length > 0,
|
|
53634
|
+
diff,
|
|
53635
|
+
metadataDiff,
|
|
53636
|
+
impacts
|
|
53637
|
+
};
|
|
52977
53638
|
}
|
|
52978
|
-
async
|
|
53639
|
+
async requireImpactAcknowledgement(controllerId, previous, snapshot, force, expectedImpactHash) {
|
|
53640
|
+
const impacts = await configurationFlowImpacts(
|
|
53641
|
+
this.context,
|
|
53642
|
+
controllerId,
|
|
53643
|
+
previous ? JSON.parse(previous.snapshot) : null,
|
|
53644
|
+
snapshot
|
|
53645
|
+
);
|
|
53646
|
+
if (impacts.length && !force)
|
|
53647
|
+
throw new import_common.ConflictException({ message: "acknowledge potential flow impacts before publishing", impacts });
|
|
53648
|
+
if (force && expectedImpactHash !== void 0 && expectedImpactHash !== configurationHash({ impacts }))
|
|
53649
|
+
throw new import_common.ConflictException({
|
|
53650
|
+
message: "flow impacts changed since preview; preview and confirm again",
|
|
53651
|
+
impacts
|
|
53652
|
+
});
|
|
53653
|
+
return impacts;
|
|
53654
|
+
}
|
|
53655
|
+
async publishDraftWhileLocked(controllerId, force = false, reviewedHash) {
|
|
52979
53656
|
const controller = await this.claimedController(controllerId);
|
|
52980
53657
|
const draft = await this.drafts.findOneBy({ controllerId });
|
|
52981
53658
|
if (!draft) throw new import_common.NotFoundException(`WAGO controller ${controllerId} has no configuration draft`);
|
|
52982
|
-
const validation =
|
|
53659
|
+
const validation = validateEditorSnapshot(JSON.parse(draft.snapshot));
|
|
52983
53660
|
if (validation.length)
|
|
52984
53661
|
throw new import_common.ConflictException({ message: "configuration draft is invalid", errors: validation });
|
|
52985
53662
|
const contentHash = configurationHash(JSON.parse(draft.snapshot));
|
|
52986
|
-
if (draft.reviewedHash !== contentHash)
|
|
52987
|
-
throw new import_common.ConflictException("review the current configuration draft before publishing it");
|
|
52988
53663
|
const previous = await this.latestRevision(controllerId);
|
|
52989
|
-
|
|
53664
|
+
const impacts = await this.requireImpactAcknowledgement(controllerId, previous, JSON.parse(draft.snapshot), force);
|
|
53665
|
+
const reviewIdentity = this.reviewIdentity(draft, impacts);
|
|
53666
|
+
if (reviewedHash !== void 0 && reviewedHash !== reviewIdentity)
|
|
53667
|
+
throw new import_common.ConflictException("draft changed since your review; review it again");
|
|
53668
|
+
if (draft.reviewedHash !== reviewIdentity)
|
|
53669
|
+
throw new import_common.ConflictException("review the current configuration draft before publishing it");
|
|
53670
|
+
if (previous?.state === "pending" && previous.contentHash === contentHash && (previous.presetProvenance ?? null) === (draft.presetProvenance ?? null))
|
|
52990
53671
|
return this.publishRevision(controller, previous);
|
|
52991
53672
|
const revision = this.revisions.create({
|
|
52992
53673
|
controllerId,
|
|
52993
53674
|
revision: (previous?.revision ?? 0) + 1,
|
|
52994
53675
|
snapshot: draft.snapshot,
|
|
53676
|
+
presetProvenance: draft.presetProvenance ?? null,
|
|
52995
53677
|
contentHash,
|
|
52996
53678
|
state: "pending",
|
|
52997
53679
|
rejectionErrors: null,
|
|
@@ -53307,6 +53989,7 @@ var WagoService = class {
|
|
|
53307
53989
|
});
|
|
53308
53990
|
const generation = this.activeSubscriptionGeneration + 1;
|
|
53309
53991
|
const replacements = [];
|
|
53992
|
+
const retainedStates = /* @__PURE__ */ new Map();
|
|
53310
53993
|
try {
|
|
53311
53994
|
for (const serverId of serverIds) {
|
|
53312
53995
|
replacements.push(
|
|
@@ -53333,7 +54016,10 @@ var WagoService = class {
|
|
|
53333
54016
|
if (!this.isActiveSubscriptionGeneration(generation)) return;
|
|
53334
54017
|
const hardwareId = configurationReportedHardwareId(settings.operationalPrefix, message.topic);
|
|
53335
54018
|
const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
|
|
53336
|
-
if (controller)
|
|
54019
|
+
if (controller) {
|
|
54020
|
+
this.diagnostics.ingest(controller.id, "configuration/reported", message.payload);
|
|
54021
|
+
this.enqueueConfigurationReport(controller.id, message.payload);
|
|
54022
|
+
}
|
|
53337
54023
|
}
|
|
53338
54024
|
)
|
|
53339
54025
|
);
|
|
@@ -53342,7 +54028,10 @@ var WagoService = class {
|
|
|
53342
54028
|
if (!this.isActiveSubscriptionGeneration(generation)) return;
|
|
53343
54029
|
const hardwareId = acknowledgementHardwareId(settings.operationalPrefix, message.topic);
|
|
53344
54030
|
const controller = hardwareId ? controllersByHardwareId.get(hardwareId) : void 0;
|
|
53345
|
-
if (controller)
|
|
54031
|
+
if (controller) {
|
|
54032
|
+
this.diagnostics.ingest(controller.id, "acknowledgements", message.payload);
|
|
54033
|
+
this.onCommandAcknowledgement(controller.id, message.payload);
|
|
54034
|
+
}
|
|
53346
54035
|
})
|
|
53347
54036
|
);
|
|
53348
54037
|
if (this.destroyed) {
|
|
@@ -53350,6 +54039,20 @@ var WagoService = class {
|
|
|
53350
54039
|
return;
|
|
53351
54040
|
}
|
|
53352
54041
|
for (const controller of claimedControllers) {
|
|
54042
|
+
for (const suffix of ["state", "measurements", "faults"]) {
|
|
54043
|
+
replacements.push(
|
|
54044
|
+
await this.subscribeMqtt(
|
|
54045
|
+
serverId,
|
|
54046
|
+
`${settings.operationalPrefix}/v1/controllers/${controller.hardwareId}/${suffix}`,
|
|
54047
|
+
(message) => {
|
|
54048
|
+
if (this.isActiveSubscriptionGeneration(generation))
|
|
54049
|
+
this.diagnostics.ingest(controller.id, suffix, message.payload);
|
|
54050
|
+
else if (!this.destroyed && suffix === "state" && message.payload.length <= 65536)
|
|
54051
|
+
retainedStates.set(controller.id, Buffer.from(message.payload));
|
|
54052
|
+
}
|
|
54053
|
+
)
|
|
54054
|
+
);
|
|
54055
|
+
}
|
|
53353
54056
|
replacements.push(
|
|
53354
54057
|
await this.subscribeMqtt(
|
|
53355
54058
|
serverId,
|
|
@@ -53375,6 +54078,7 @@ var WagoService = class {
|
|
|
53375
54078
|
return;
|
|
53376
54079
|
}
|
|
53377
54080
|
this.activeSubscriptionGeneration = generation;
|
|
54081
|
+
retainedStates.forEach((payload, controllerId) => this.diagnostics.ingest(controllerId, "state", payload));
|
|
53378
54082
|
this.unsubscribe();
|
|
53379
54083
|
this.subscriptions.push(...replacements);
|
|
53380
54084
|
}
|
|
@@ -53466,15 +54170,35 @@ var WagoService = class {
|
|
|
53466
54170
|
return;
|
|
53467
54171
|
}
|
|
53468
54172
|
if (heartbeat.hardwareId !== hardwareId) return;
|
|
54173
|
+
let rawHeartbeat;
|
|
54174
|
+
try {
|
|
54175
|
+
rawHeartbeat = JSON.parse(payload.toString("utf8"));
|
|
54176
|
+
} catch {
|
|
54177
|
+
return;
|
|
54178
|
+
}
|
|
54179
|
+
const canonical = canonicalEnvelope(rawHeartbeat, "heartbeat");
|
|
53469
54180
|
const controller = await this.controllers.findOneBy({ hardwareId });
|
|
53470
|
-
if (!controller || controller.trustState !== "claimed" || heartbeat.sequence !== void 0 && heartbeat.sequence < controller.lastSequence)
|
|
54181
|
+
if (!controller || controller.trustState !== "claimed" || !canonical && heartbeat.sequence !== void 0 && heartbeat.sequence < controller.lastSequence)
|
|
53471
54182
|
return;
|
|
53472
54183
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
54184
|
+
const canTrackDiagnostics = this.diagnostics.canTrack(controller.id);
|
|
54185
|
+
const admitted = this.diagnostics.ingest(controller.id, "heartbeat", payload);
|
|
54186
|
+
const heartbeatAt = admitted ? this.diagnostics.read(controller.id).heartbeatAt : typeof rawHeartbeat.timestamp === "string" ? rawHeartbeat.timestamp : void 0;
|
|
54187
|
+
const persistedHeartbeatAt = sourceTime(controller.lastHeartbeatAt);
|
|
54188
|
+
if (canonical && (!validEnvelope(rawHeartbeat, Date.now()) || canTrackDiagnostics && !admitted || !heartbeatAt || persistedHeartbeatAt !== null && sourceTime(heartbeatAt) < persistedHeartbeatAt))
|
|
54189
|
+
return;
|
|
54190
|
+
const metadataChanged = controller.protocolVersion !== heartbeat.protocolVersion || controller.runtimeVersion !== heartbeat.runtimeVersion || controller.capabilities !== JSON.stringify(heartbeat.capabilities) || controller.compatibilityError !== compatibilityError(heartbeat);
|
|
54191
|
+
if (controller.lastHeartbeatAt && freshness(controller.lastSeenAt, Date.now(), 3e4) === "fresh" && !metadataChanged)
|
|
54192
|
+
return;
|
|
53473
54193
|
controller.protocolVersion = heartbeat.protocolVersion;
|
|
53474
54194
|
controller.runtimeVersion = heartbeat.runtimeVersion;
|
|
53475
54195
|
controller.capabilities = JSON.stringify(heartbeat.capabilities);
|
|
53476
|
-
|
|
53477
|
-
|
|
54196
|
+
if (!canonical) {
|
|
54197
|
+
controller.lastSequence = this.diagnostics.read(controller.id).legacyHeartbeatSequence ?? controller.lastSequence;
|
|
54198
|
+
controller.lastHeartbeatAt = now;
|
|
54199
|
+
} else {
|
|
54200
|
+
controller.lastHeartbeatAt = heartbeatAt;
|
|
54201
|
+
}
|
|
53478
54202
|
controller.lastSeenAt = now;
|
|
53479
54203
|
controller.compatibilityError = compatibilityError(heartbeat);
|
|
53480
54204
|
controller.updatedAt = now;
|
|
@@ -53585,7 +54309,8 @@ var WagoService = class {
|
|
|
53585
54309
|
}
|
|
53586
54310
|
connectivity(controller) {
|
|
53587
54311
|
if (controller.trustState === "untrusted") return "untrusted";
|
|
53588
|
-
|
|
54312
|
+
const heartbeatAt = this.diagnostics.read(controller.id).heartbeatAt ?? controller.lastHeartbeatAt;
|
|
54313
|
+
return freshness(heartbeatAt, Date.now(), STALE_AFTER_MS) === "fresh" ? "online" : "stale";
|
|
53589
54314
|
}
|
|
53590
54315
|
async appliedRevision(controllerId) {
|
|
53591
54316
|
const [revision] = await this.revisions.find({
|
|
@@ -53757,10 +54482,10 @@ var import_node_fs2 = require("node:fs");
|
|
|
53757
54482
|
var import_promises4 = require("node:fs/promises");
|
|
53758
54483
|
var import_node_os2 = require("node:os");
|
|
53759
54484
|
var import_node_path2 = require("node:path");
|
|
53760
|
-
var
|
|
54485
|
+
var import_plugins_backend_sdk12 = require("@attraccess/plugins-backend-sdk");
|
|
53761
54486
|
|
|
53762
54487
|
// backend/wago-commissioning-session.entity.ts
|
|
53763
|
-
var
|
|
54488
|
+
var import_plugins_backend_sdk8 = require("@attraccess/plugins-backend-sdk");
|
|
53764
54489
|
var WagoCommissioningSession = class {
|
|
53765
54490
|
constructor() {
|
|
53766
54491
|
__publicField(this, "id");
|
|
@@ -53792,85 +54517,85 @@ var WagoCommissioningSession = class {
|
|
|
53792
54517
|
}
|
|
53793
54518
|
};
|
|
53794
54519
|
__decorateClass([
|
|
53795
|
-
(0,
|
|
54520
|
+
(0, import_plugins_backend_sdk8.PrimaryGeneratedColumn)({ type: "integer" })
|
|
53796
54521
|
], WagoCommissioningSession.prototype, "id", 2);
|
|
53797
54522
|
__decorateClass([
|
|
53798
|
-
(0,
|
|
54523
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "hardware_id" })
|
|
53799
54524
|
], WagoCommissioningSession.prototype, "hardwareId", 2);
|
|
53800
54525
|
__decorateClass([
|
|
53801
|
-
(0,
|
|
54526
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "mqtt_server_id" })
|
|
53802
54527
|
], WagoCommissioningSession.prototype, "mqttServerId", 2);
|
|
53803
54528
|
__decorateClass([
|
|
53804
|
-
(0,
|
|
54529
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "target_host" })
|
|
53805
54530
|
], WagoCommissioningSession.prototype, "targetHost", 2);
|
|
53806
54531
|
__decorateClass([
|
|
53807
|
-
(0,
|
|
54532
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "host_key_fingerprint" })
|
|
53808
54533
|
], WagoCommissioningSession.prototype, "hostKeyFingerprint", 2);
|
|
53809
54534
|
__decorateClass([
|
|
53810
|
-
(0,
|
|
54535
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "firmware_baseline" })
|
|
53811
54536
|
], WagoCommissioningSession.prototype, "firmwareBaseline", 2);
|
|
53812
54537
|
__decorateClass([
|
|
53813
|
-
(0,
|
|
54538
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "controller_name", nullable: true })
|
|
53814
54539
|
], WagoCommissioningSession.prototype, "controllerName", 2);
|
|
53815
54540
|
__decorateClass([
|
|
53816
|
-
(0,
|
|
54541
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar" })
|
|
53817
54542
|
], WagoCommissioningSession.prototype, "state", 2);
|
|
53818
54543
|
__decorateClass([
|
|
53819
|
-
(0,
|
|
54544
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "enrollment_expires_at", nullable: true })
|
|
53820
54545
|
], WagoCommissioningSession.prototype, "enrollmentExpiresAt", 2);
|
|
53821
54546
|
__decorateClass([
|
|
53822
|
-
(0,
|
|
54547
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "enrollment_id", nullable: true })
|
|
53823
54548
|
], WagoCommissioningSession.prototype, "enrollmentId", 2);
|
|
53824
54549
|
__decorateClass([
|
|
53825
|
-
(0,
|
|
54550
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "pairing_code", nullable: true })
|
|
53826
54551
|
], WagoCommissioningSession.prototype, "pairingCode", 2);
|
|
53827
54552
|
__decorateClass([
|
|
53828
|
-
(0,
|
|
54553
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "delivery_token", nullable: true })
|
|
53829
54554
|
], WagoCommissioningSession.prototype, "deliveryToken", 2);
|
|
53830
54555
|
__decorateClass([
|
|
53831
|
-
(0,
|
|
54556
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "text", name: "initiating_principal", nullable: true })
|
|
53832
54557
|
], WagoCommissioningSession.prototype, "initiatingPrincipal", 2);
|
|
53833
54558
|
__decorateClass([
|
|
53834
|
-
(0,
|
|
54559
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "runtime_artifact_digest", nullable: true })
|
|
53835
54560
|
], WagoCommissioningSession.prototype, "runtimeArtifactDigest", 2);
|
|
53836
54561
|
__decorateClass([
|
|
53837
|
-
(0,
|
|
54562
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "management_controller_id", nullable: true })
|
|
53838
54563
|
], WagoCommissioningSession.prototype, "managementControllerId", 2);
|
|
53839
54564
|
__decorateClass([
|
|
53840
|
-
(0,
|
|
54565
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "docker_provision_token", nullable: true })
|
|
53841
54566
|
], WagoCommissioningSession.prototype, "dockerProvisionToken", 2);
|
|
53842
54567
|
__decorateClass([
|
|
53843
|
-
(0,
|
|
54568
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "docker_provision_state", nullable: true })
|
|
53844
54569
|
], WagoCommissioningSession.prototype, "dockerProvisionState", 2);
|
|
53845
54570
|
__decorateClass([
|
|
53846
|
-
(0,
|
|
54571
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "text", name: "platform_report", nullable: true })
|
|
53847
54572
|
], WagoCommissioningSession.prototype, "platformReport", 2);
|
|
53848
54573
|
__decorateClass([
|
|
53849
|
-
(0,
|
|
54574
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "codesys_state", nullable: true })
|
|
53850
54575
|
], WagoCommissioningSession.prototype, "codesysState", 2);
|
|
53851
54576
|
__decorateClass([
|
|
53852
|
-
(0,
|
|
54577
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "integer", name: "progress_percent", nullable: true })
|
|
53853
54578
|
], WagoCommissioningSession.prototype, "progressPercent", 2);
|
|
53854
54579
|
__decorateClass([
|
|
53855
|
-
(0,
|
|
54580
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "progress_step", nullable: true })
|
|
53856
54581
|
], WagoCommissioningSession.prototype, "progressStep", 2);
|
|
53857
54582
|
__decorateClass([
|
|
53858
|
-
(0,
|
|
54583
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "progress_detail", nullable: true })
|
|
53859
54584
|
], WagoCommissioningSession.prototype, "progressDetail", 2);
|
|
53860
54585
|
__decorateClass([
|
|
53861
|
-
(0,
|
|
54586
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "audit_log" })
|
|
53862
54587
|
], WagoCommissioningSession.prototype, "auditLog", 2);
|
|
53863
54588
|
__decorateClass([
|
|
53864
|
-
(0,
|
|
54589
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "failure_reason", nullable: true })
|
|
53865
54590
|
], WagoCommissioningSession.prototype, "failureReason", 2);
|
|
53866
54591
|
__decorateClass([
|
|
53867
|
-
(0,
|
|
54592
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "created_at" })
|
|
53868
54593
|
], WagoCommissioningSession.prototype, "createdAt", 2);
|
|
53869
54594
|
__decorateClass([
|
|
53870
|
-
(0,
|
|
54595
|
+
(0, import_plugins_backend_sdk8.Column)({ type: "varchar", name: "updated_at" })
|
|
53871
54596
|
], WagoCommissioningSession.prototype, "updatedAt", 2);
|
|
53872
54597
|
WagoCommissioningSession = __decorateClass([
|
|
53873
|
-
(0,
|
|
54598
|
+
(0, import_plugins_backend_sdk8.Entity)({ name: "plugin_wago_commissioning_sessions" })
|
|
53874
54599
|
], WagoCommissioningSession);
|
|
53875
54600
|
|
|
53876
54601
|
// backend/wago-commissioning-verification.ts
|
|
@@ -54268,13 +54993,13 @@ async function verifyRuntimeSignature(file, armor, trustedKey = WAGO_RUNTIME_REL
|
|
|
54268
54993
|
if (!match) invalid();
|
|
54269
54994
|
const packet = Buffer.from(match[1].replace(/\s/g, ""), "base64");
|
|
54270
54995
|
if (packet.length < 10 || packet.subarray(0, 6).toString() !== "SSHSIG" || packet.readUInt32BE(6) !== 1) invalid();
|
|
54271
|
-
const
|
|
54272
|
-
const key =
|
|
54273
|
-
const namespace =
|
|
54274
|
-
const reserved =
|
|
54275
|
-
const algorithm2 =
|
|
54276
|
-
const signature = reader(
|
|
54277
|
-
|
|
54996
|
+
const fields2 = reader(packet.subarray(10));
|
|
54997
|
+
const key = fields2.string();
|
|
54998
|
+
const namespace = fields2.string();
|
|
54999
|
+
const reserved = fields2.string();
|
|
55000
|
+
const algorithm2 = fields2.string();
|
|
55001
|
+
const signature = reader(fields2.string());
|
|
55002
|
+
fields2.end();
|
|
54278
55003
|
if (!key.equals(Buffer.from(trustedKey, "base64")) || namespace.toString() !== "attraccess-wago-runtime" || reserved.length)
|
|
54279
55004
|
invalid();
|
|
54280
55005
|
if (!["sha256", "sha512"].includes(algorithm2.toString()) || signature.string().toString() !== "ssh-ed25519")
|
|
@@ -54739,7 +55464,7 @@ WagoRuntimeArtifactsService = __decorateClass([
|
|
|
54739
55464
|
|
|
54740
55465
|
// backend/wago-commissioning-readiness.ts
|
|
54741
55466
|
var import_common5 = require("@nestjs/common");
|
|
54742
|
-
var
|
|
55467
|
+
var import_plugins_backend_sdk9 = require("@attraccess/plugins-backend-sdk");
|
|
54743
55468
|
var WagoCommissioningReadiness = class {
|
|
54744
55469
|
constructor(context) {
|
|
54745
55470
|
__publicField(this, "context", context);
|
|
@@ -54803,14 +55528,14 @@ var WagoCommissioningReadiness = class {
|
|
|
54803
55528
|
};
|
|
54804
55529
|
WagoCommissioningReadiness = __decorateClass([
|
|
54805
55530
|
(0, import_common5.Injectable)(),
|
|
54806
|
-
__decorateParam(0, (0, import_common5.Inject)(
|
|
55531
|
+
__decorateParam(0, (0, import_common5.Inject)(import_plugins_backend_sdk9.PLUGIN_CONTEXT))
|
|
54807
55532
|
], WagoCommissioningReadiness);
|
|
54808
55533
|
|
|
54809
55534
|
// backend/wago-commissioning-lease.ts
|
|
54810
55535
|
var import_node_crypto8 = require("node:crypto");
|
|
54811
55536
|
|
|
54812
55537
|
// backend/wago-commissioning-lease.entity.ts
|
|
54813
|
-
var
|
|
55538
|
+
var import_plugins_backend_sdk10 = require("@attraccess/plugins-backend-sdk");
|
|
54814
55539
|
var WagoCommissioningLeaseEntity = class {
|
|
54815
55540
|
constructor() {
|
|
54816
55541
|
__publicField(this, "fingerprintHash");
|
|
@@ -54821,22 +55546,22 @@ var WagoCommissioningLeaseEntity = class {
|
|
|
54821
55546
|
}
|
|
54822
55547
|
};
|
|
54823
55548
|
__decorateClass([
|
|
54824
|
-
(0,
|
|
55549
|
+
(0, import_plugins_backend_sdk10.PrimaryColumn)({ type: "varchar", name: "fingerprint_hash", length: 64 })
|
|
54825
55550
|
], WagoCommissioningLeaseEntity.prototype, "fingerprintHash", 2);
|
|
54826
55551
|
__decorateClass([
|
|
54827
|
-
(0,
|
|
55552
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "varchar", name: "owner" })
|
|
54828
55553
|
], WagoCommissioningLeaseEntity.prototype, "owner", 2);
|
|
54829
55554
|
__decorateClass([
|
|
54830
|
-
(0,
|
|
55555
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "lease_until" })
|
|
54831
55556
|
], WagoCommissioningLeaseEntity.prototype, "leaseUntil", 2);
|
|
54832
55557
|
__decorateClass([
|
|
54833
|
-
(0,
|
|
55558
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "operation_until" })
|
|
54834
55559
|
], WagoCommissioningLeaseEntity.prototype, "operationUntil", 2);
|
|
54835
55560
|
__decorateClass([
|
|
54836
|
-
(0,
|
|
55561
|
+
(0, import_plugins_backend_sdk10.Column)({ type: "bigint", name: "recovery_after" })
|
|
54837
55562
|
], WagoCommissioningLeaseEntity.prototype, "recoveryAfter", 2);
|
|
54838
55563
|
WagoCommissioningLeaseEntity = __decorateClass([
|
|
54839
|
-
(0,
|
|
55564
|
+
(0, import_plugins_backend_sdk10.Entity)({ name: "plugin_wago_commissioning_lease" })
|
|
54840
55565
|
], WagoCommissioningLeaseEntity);
|
|
54841
55566
|
|
|
54842
55567
|
// backend/wago-commissioning-lease.ts
|
|
@@ -55012,7 +55737,7 @@ function createWagoCommissioningLeaseService(context) {
|
|
|
55012
55737
|
}
|
|
55013
55738
|
|
|
55014
55739
|
// backend/wago-management.entity.ts
|
|
55015
|
-
var
|
|
55740
|
+
var import_plugins_backend_sdk11 = require("@attraccess/plugins-backend-sdk");
|
|
55016
55741
|
var WagoManagementEntity = class {
|
|
55017
55742
|
constructor() {
|
|
55018
55743
|
__publicField(this, "controllerId");
|
|
@@ -55023,22 +55748,22 @@ var WagoManagementEntity = class {
|
|
|
55023
55748
|
}
|
|
55024
55749
|
};
|
|
55025
55750
|
__decorateClass([
|
|
55026
|
-
(0,
|
|
55751
|
+
(0, import_plugins_backend_sdk11.PrimaryColumn)({ type: "integer", name: "controller_id" })
|
|
55027
55752
|
], WagoManagementEntity.prototype, "controllerId", 2);
|
|
55028
55753
|
__decorateClass([
|
|
55029
|
-
(0,
|
|
55754
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "text", name: "metadata_json", nullable: true })
|
|
55030
55755
|
], WagoManagementEntity.prototype, "metadataJson", 2);
|
|
55031
55756
|
__decorateClass([
|
|
55032
|
-
(0,
|
|
55757
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "text", name: "encrypted_private_key", nullable: true, select: false })
|
|
55033
55758
|
], WagoManagementEntity.prototype, "encryptedPrivateKey", 2);
|
|
55034
55759
|
__decorateClass([
|
|
55035
|
-
(0,
|
|
55760
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "varchar", name: "lease_owner", nullable: true })
|
|
55036
55761
|
], WagoManagementEntity.prototype, "leaseOwner", 2);
|
|
55037
55762
|
__decorateClass([
|
|
55038
|
-
(0,
|
|
55763
|
+
(0, import_plugins_backend_sdk11.Column)({ type: "bigint", name: "lease_until", default: 0 })
|
|
55039
55764
|
], WagoManagementEntity.prototype, "leaseUntil", 2);
|
|
55040
55765
|
WagoManagementEntity = __decorateClass([
|
|
55041
|
-
(0,
|
|
55766
|
+
(0, import_plugins_backend_sdk11.Entity)({ name: "plugin_wago_management" })
|
|
55042
55767
|
], WagoManagementEntity);
|
|
55043
55768
|
|
|
55044
55769
|
// backend/wago-management-inspection.ts
|
|
@@ -55503,7 +56228,7 @@ var transitionStates = [
|
|
|
55503
56228
|
"recovery_required"
|
|
55504
56229
|
];
|
|
55505
56230
|
var exceptionNames = ["wbm_exposed", "other_services_exposed", "unqualified_privileges"];
|
|
55506
|
-
var
|
|
56231
|
+
var identifier2 = () => (0, import_node_crypto10.randomBytes)(16).toString("hex");
|
|
55507
56232
|
var REVIEW_MS = 3e5;
|
|
55508
56233
|
var LEASE_MS = 30 * 6e4;
|
|
55509
56234
|
var ManagementError = class extends Error {
|
|
@@ -55570,7 +56295,7 @@ var WagoManagementService = class {
|
|
|
55570
56295
|
record3.support = this.adapter.qualify(record3.inspection, input.mode).support;
|
|
55571
56296
|
if (input.mode === "key_only" && (!record3.exceptions.includes("unqualified_privileges") || record3.inspection.wbm !== "not_observed" && !record3.exceptions.includes("wbm_exposed") || record3.inspection.otherManagement !== "not_observed" && !record3.exceptions.includes("other_services_exposed")))
|
|
55572
56297
|
throw new ManagementError("invalid_request");
|
|
55573
|
-
record3.reviewToken =
|
|
56298
|
+
record3.reviewToken = identifier2();
|
|
55574
56299
|
record3.reviewedAt = this.now();
|
|
55575
56300
|
record3.state = "reviewed";
|
|
55576
56301
|
await this.save(record3, owner, guard);
|
|
@@ -55603,7 +56328,7 @@ var WagoManagementService = class {
|
|
|
55603
56328
|
if (this.secrets.decrypt(record2.encryptedPrivateKey) !== key.privateKey) throw new Error();
|
|
55604
56329
|
record2.keyFingerprint = key.fingerprint;
|
|
55605
56330
|
record2.transaction = {
|
|
55606
|
-
id:
|
|
56331
|
+
id: identifier2(),
|
|
55607
56332
|
target: record2.target,
|
|
55608
56333
|
username: input.temporarySsh.username,
|
|
55609
56334
|
deadline: this.now() + 15e4
|
|
@@ -55659,7 +56384,7 @@ var WagoManagementService = class {
|
|
|
55659
56384
|
});
|
|
55660
56385
|
}
|
|
55661
56386
|
async verify(record2, privateKey, guard) {
|
|
55662
|
-
const nonce =
|
|
56387
|
+
const nonce = identifier2();
|
|
55663
56388
|
const tx = this.transaction(record2);
|
|
55664
56389
|
const proof = await this.remote(guard, () => this.adapter.verifyKey(tx, privateKey, nonce));
|
|
55665
56390
|
if (proof.nonce !== nonce || !proof.keyOnly || proof.hostKeyFingerprint !== tx.target.hostKeyFingerprint || proof.keyFingerprint !== record2.keyFingerprint || !Number.isSafeInteger(proof.uid) || proof.uid <= 0 || proof.uid !== this.inspection(record2).uid || !proof.managementOperationSucceeded)
|
|
@@ -55731,7 +56456,7 @@ var WagoManagementService = class {
|
|
|
55731
56456
|
}
|
|
55732
56457
|
async locked(controllerId, fingerprint2, action) {
|
|
55733
56458
|
validId(controllerId);
|
|
55734
|
-
const owner =
|
|
56459
|
+
const owner = identifier2();
|
|
55735
56460
|
let acquired = false;
|
|
55736
56461
|
let mutationStarted = false;
|
|
55737
56462
|
try {
|
|
@@ -57342,7 +58067,7 @@ printf '%s\\n' "$ATTRACCESS_SSH_PASSWORD"
|
|
|
57342
58067
|
};
|
|
57343
58068
|
WagoCommissioningService = __decorateClass([
|
|
57344
58069
|
(0, import_common6.Injectable)(),
|
|
57345
|
-
__decorateParam(0, (0, import_common6.Inject)(
|
|
58070
|
+
__decorateParam(0, (0, import_common6.Inject)(import_plugins_backend_sdk12.PLUGIN_CONTEXT)),
|
|
57346
58071
|
__decorateParam(1, (0, import_common6.Inject)(WagoService)),
|
|
57347
58072
|
__decorateParam(2, (0, import_common6.Optional)()),
|
|
57348
58073
|
__decorateParam(2, (0, import_common6.Inject)(WagoRuntimeArtifactsService)),
|
|
@@ -57777,17 +58502,23 @@ var WagoControllerApi = class {
|
|
|
57777
58502
|
}
|
|
57778
58503
|
previewPreset(id, body) {
|
|
57779
58504
|
if (!body?.application) throw new import_common8.BadRequestException("application is required");
|
|
57780
|
-
return this.wago.previewPreset(id, body.application);
|
|
58505
|
+
return this.wago.previewPreset(id, body.application, body.snapshot);
|
|
57781
58506
|
}
|
|
57782
58507
|
applyPreset(id, body) {
|
|
57783
58508
|
if (!body?.application) throw new import_common8.BadRequestException("application is required");
|
|
57784
|
-
return this.wago.applyPreset(
|
|
58509
|
+
return this.wago.applyPreset(
|
|
58510
|
+
id,
|
|
58511
|
+
body.application,
|
|
58512
|
+
body.selectedPaths ?? [],
|
|
58513
|
+
body.previewedDraftHash ?? "",
|
|
58514
|
+
body.snapshot
|
|
58515
|
+
);
|
|
57785
58516
|
}
|
|
57786
58517
|
saveDraft(id, body) {
|
|
57787
|
-
return this.wago.saveDraft(id, body?.snapshot);
|
|
58518
|
+
return this.wago.saveDraft(id, body?.snapshot, body?.metadata);
|
|
57788
58519
|
}
|
|
57789
|
-
validateDraft(id) {
|
|
57790
|
-
return this.wago.validateDraft(id);
|
|
58520
|
+
validateDraft(id, body) {
|
|
58521
|
+
return this.wago.validateDraft(id, body?.snapshot);
|
|
57791
58522
|
}
|
|
57792
58523
|
reviewDraft(id) {
|
|
57793
58524
|
return this.wago.reviewDraft(id);
|
|
@@ -57795,23 +58526,31 @@ var WagoControllerApi = class {
|
|
|
57795
58526
|
revisions(id, offset, limit) {
|
|
57796
58527
|
return this.wago.revisionsFor(id, Number(offset), Number(limit));
|
|
57797
58528
|
}
|
|
57798
|
-
publishDraft(id, request) {
|
|
58529
|
+
publishDraft(id, request, body) {
|
|
57799
58530
|
return this.audit.run(
|
|
57800
58531
|
wagoAuditPrincipal(request),
|
|
57801
58532
|
id,
|
|
57802
58533
|
"publication",
|
|
57803
|
-
{},
|
|
57804
|
-
() => this.wago.publishDraft(id),
|
|
58534
|
+
body?.force === true ? { force: true } : {},
|
|
58535
|
+
() => this.wago.publishDraft(id, body?.force === true, body?.reviewedHash),
|
|
57805
58536
|
(published) => ({ revision: published.revision })
|
|
57806
58537
|
);
|
|
57807
58538
|
}
|
|
57808
|
-
rollback(id, revision, request) {
|
|
58539
|
+
rollback(id, revision, request, body) {
|
|
57809
58540
|
return this.audit.run(
|
|
57810
58541
|
wagoAuditPrincipal(request),
|
|
57811
58542
|
id,
|
|
57812
58543
|
"rollback",
|
|
57813
|
-
{ sourceRevision: revision },
|
|
57814
|
-
() => this.wago.rollback(
|
|
58544
|
+
body?.force === true ? { sourceRevision: revision, force: true } : { sourceRevision: revision },
|
|
58545
|
+
() => this.wago.rollback(
|
|
58546
|
+
id,
|
|
58547
|
+
revision,
|
|
58548
|
+
body?.force === true,
|
|
58549
|
+
body?.sourceHash,
|
|
58550
|
+
body?.currentHash,
|
|
58551
|
+
body?.draftHash,
|
|
58552
|
+
body?.impactHash
|
|
58553
|
+
),
|
|
57815
58554
|
(published) => ({ revision: published.revision })
|
|
57816
58555
|
);
|
|
57817
58556
|
}
|
|
@@ -57823,61 +58562,61 @@ __decorateClass([
|
|
|
57823
58562
|
(0, import_common8.Get)("controllers")
|
|
57824
58563
|
], WagoControllerApi.prototype, "list", 1);
|
|
57825
58564
|
__decorateClass([
|
|
57826
|
-
(0,
|
|
58565
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57827
58566
|
(0, import_common8.Get)("settings")
|
|
57828
58567
|
], WagoControllerApi.prototype, "settings", 1);
|
|
57829
58568
|
__decorateClass([
|
|
57830
|
-
(0,
|
|
58569
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57831
58570
|
(0, import_common8.Post)("settings"),
|
|
57832
58571
|
__decorateParam(0, (0, import_common8.Body)())
|
|
57833
58572
|
], WagoControllerApi.prototype, "setSettings", 1);
|
|
57834
58573
|
__decorateClass([
|
|
57835
|
-
(0,
|
|
58574
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57836
58575
|
(0, import_common8.Get)("commissioning/support")
|
|
57837
58576
|
], WagoControllerApi.prototype, "commissioningSupport", 1);
|
|
57838
58577
|
__decorateClass([
|
|
57839
|
-
(0,
|
|
58578
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57840
58579
|
(0, import_common8.Get)("commissioning/sessions"),
|
|
57841
58580
|
__decorateParam(0, (0, import_common8.Query)("limit")),
|
|
57842
58581
|
__decorateParam(1, (0, import_common8.Query)("offset"))
|
|
57843
58582
|
], WagoControllerApi.prototype, "commissioningSessions", 1);
|
|
57844
58583
|
__decorateClass([
|
|
57845
|
-
(0,
|
|
58584
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57846
58585
|
(0, import_common8.Post)("commissioning/sessions"),
|
|
57847
58586
|
__decorateParam(0, (0, import_common8.Body)())
|
|
57848
58587
|
], WagoControllerApi.prototype, "createCommissioningSession", 1);
|
|
57849
58588
|
__decorateClass([
|
|
57850
|
-
(0,
|
|
58589
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57851
58590
|
(0, import_common8.Post)("commissioning/sessions/:id/confirm-host-key"),
|
|
57852
58591
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57853
58592
|
__decorateParam(1, (0, import_common8.Body)())
|
|
57854
58593
|
], WagoControllerApi.prototype, "confirmCommissioningHostKey", 1);
|
|
57855
58594
|
__decorateClass([
|
|
57856
|
-
(0,
|
|
58595
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57857
58596
|
(0, import_common8.Post)("commissioning/sessions/:id/deliver"),
|
|
57858
58597
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57859
58598
|
__decorateParam(1, (0, import_common8.Body)()),
|
|
57860
58599
|
__decorateParam(2, (0, import_common8.Req)())
|
|
57861
58600
|
], WagoControllerApi.prototype, "deliverCommissioningSession", 1);
|
|
57862
58601
|
__decorateClass([
|
|
57863
|
-
(0,
|
|
58602
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57864
58603
|
(0, import_common8.Post)("commissioning/sessions/:id/recover"),
|
|
57865
58604
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57866
58605
|
__decorateParam(1, (0, import_common8.Body)()),
|
|
57867
58606
|
__decorateParam(2, (0, import_common8.Req)())
|
|
57868
58607
|
], WagoControllerApi.prototype, "recoverCommissioningSession", 1);
|
|
57869
58608
|
__decorateClass([
|
|
57870
|
-
(0,
|
|
58609
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57871
58610
|
(0, import_common8.Get)("commissioning/sessions/:id/verification"),
|
|
57872
58611
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
57873
58612
|
], WagoControllerApi.prototype, "commissioningVerification", 1);
|
|
57874
58613
|
__decorateClass([
|
|
57875
|
-
(0,
|
|
58614
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57876
58615
|
(0, import_common8.Post)("commissioning/sessions/:id/revoke"),
|
|
57877
58616
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
57878
58617
|
], WagoControllerApi.prototype, "revokeCommissioningSession", 1);
|
|
57879
58618
|
__decorateClass([
|
|
57880
|
-
(0,
|
|
58619
|
+
(0, import_plugins_backend_sdk13.Auth)("system.settings.manage"),
|
|
57881
58620
|
(0, import_common8.Delete)("commissioning/sessions/:id"),
|
|
57882
58621
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
57883
58622
|
], WagoControllerApi.prototype, "removeCommissioningSession", 1);
|
|
@@ -57916,7 +58655,8 @@ __decorateClass([
|
|
|
57916
58655
|
], WagoControllerApi.prototype, "saveDraft", 1);
|
|
57917
58656
|
__decorateClass([
|
|
57918
58657
|
(0, import_common8.Post)("controllers/:id/configuration/validate"),
|
|
57919
|
-
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe))
|
|
58658
|
+
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
58659
|
+
__decorateParam(1, (0, import_common8.Body)())
|
|
57920
58660
|
], WagoControllerApi.prototype, "validateDraft", 1);
|
|
57921
58661
|
__decorateClass([
|
|
57922
58662
|
(0, import_common8.Post)("controllers/:id/configuration/review"),
|
|
@@ -57931,13 +58671,15 @@ __decorateClass([
|
|
|
57931
58671
|
__decorateClass([
|
|
57932
58672
|
(0, import_common8.Post)("controllers/:id/configuration/publish"),
|
|
57933
58673
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57934
|
-
__decorateParam(1, (0, import_common8.Req)())
|
|
58674
|
+
__decorateParam(1, (0, import_common8.Req)()),
|
|
58675
|
+
__decorateParam(2, (0, import_common8.Body)())
|
|
57935
58676
|
], WagoControllerApi.prototype, "publishDraft", 1);
|
|
57936
58677
|
__decorateClass([
|
|
57937
58678
|
(0, import_common8.Post)("controllers/:id/configuration/rollback/:revision"),
|
|
57938
58679
|
__decorateParam(0, (0, import_common8.Param)("id", import_common8.ParseIntPipe)),
|
|
57939
58680
|
__decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe)),
|
|
57940
|
-
__decorateParam(2, (0, import_common8.Req)())
|
|
58681
|
+
__decorateParam(2, (0, import_common8.Req)()),
|
|
58682
|
+
__decorateParam(3, (0, import_common8.Body)())
|
|
57941
58683
|
], WagoControllerApi.prototype, "rollback", 1);
|
|
57942
58684
|
__decorateClass([
|
|
57943
58685
|
(0, import_common8.Get)("controllers/:id/configuration/revisions/:revision/preview"),
|
|
@@ -57945,7 +58687,7 @@ __decorateClass([
|
|
|
57945
58687
|
__decorateParam(1, (0, import_common8.Param)("revision", import_common8.ParseIntPipe))
|
|
57946
58688
|
], WagoControllerApi.prototype, "previewRevision", 1);
|
|
57947
58689
|
WagoControllerApi = __decorateClass([
|
|
57948
|
-
(0,
|
|
58690
|
+
(0, import_plugins_backend_sdk13.Auth)("resources.update"),
|
|
57949
58691
|
(0, import_common8.Controller)("wago"),
|
|
57950
58692
|
__decorateParam(0, (0, import_common8.Inject)(WagoService)),
|
|
57951
58693
|
__decorateParam(1, (0, import_common8.Inject)(WagoCommissioningService)),
|
|
@@ -57954,7 +58696,7 @@ WagoControllerApi = __decorateClass([
|
|
|
57954
58696
|
|
|
57955
58697
|
// backend/wago-artifacts.controller.ts
|
|
57956
58698
|
var import_common9 = require("@nestjs/common");
|
|
57957
|
-
var
|
|
58699
|
+
var import_plugins_backend_sdk14 = require("@attraccess/plugins-backend-sdk");
|
|
57958
58700
|
var import_platform_express = __toESM(require_platform_express());
|
|
57959
58701
|
var import_promises5 = require("node:fs/promises");
|
|
57960
58702
|
var import_node_path3 = require("node:path");
|
|
@@ -58172,7 +58914,7 @@ __decorateClass([
|
|
|
58172
58914
|
__decorateParam(0, (0, import_common9.UploadedFiles)())
|
|
58173
58915
|
], WagoArtifactsController.prototype, "import", 1);
|
|
58174
58916
|
WagoArtifactsController = __decorateClass([
|
|
58175
|
-
(0,
|
|
58917
|
+
(0, import_plugins_backend_sdk14.Auth)("system.settings.manage"),
|
|
58176
58918
|
(0, import_common9.Controller)("wago/runtime-artifacts"),
|
|
58177
58919
|
__decorateParam(0, (0, import_common9.Inject)(WagoRuntimeArtifactsService))
|
|
58178
58920
|
], WagoArtifactsController);
|
|
@@ -58642,6 +59384,167 @@ WagoFlowService = __decorateClass([
|
|
|
58642
59384
|
__decorateParam(0, (0, import_common10.Inject)(PLUGIN_CONTEXT4))
|
|
58643
59385
|
], WagoFlowService);
|
|
58644
59386
|
|
|
59387
|
+
// backend/diagnostics.controller.ts
|
|
59388
|
+
var import_common12 = require("@nestjs/common");
|
|
59389
|
+
var import_plugins_backend_sdk16 = require("@attraccess/plugins-backend-sdk");
|
|
59390
|
+
|
|
59391
|
+
// backend/diagnostics.service.ts
|
|
59392
|
+
var import_common11 = require("@nestjs/common");
|
|
59393
|
+
var import_plugins_backend_sdk15 = require("@attraccess/plugins-backend-sdk");
|
|
59394
|
+
function own(values, key) {
|
|
59395
|
+
return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
|
|
59396
|
+
}
|
|
59397
|
+
function metadataFromProvenance(provenance) {
|
|
59398
|
+
if (!provenance) return { names: {}, presets: [] };
|
|
59399
|
+
try {
|
|
59400
|
+
return editorMetadata(JSON.parse(provenance).editor);
|
|
59401
|
+
} catch {
|
|
59402
|
+
return { names: {}, presets: [] };
|
|
59403
|
+
}
|
|
59404
|
+
}
|
|
59405
|
+
function diagnosticReferences(nodes, channelIds, revision, capabilities) {
|
|
59406
|
+
return nodes.map((node) => {
|
|
59407
|
+
const channelId = typeof node.data.channelId === "string" ? node.data.channelId : "";
|
|
59408
|
+
const control = node.type === "plugin.wago.command";
|
|
59409
|
+
return {
|
|
59410
|
+
nodeId: node.id,
|
|
59411
|
+
resourceId: node.resourceId,
|
|
59412
|
+
channelId,
|
|
59413
|
+
control,
|
|
59414
|
+
href: `/resources/${node.resourceId}/flows`,
|
|
59415
|
+
invalid: !channelIds.includes(channelId) || control && (node.data.expectedConfigurationRevision !== revision || capabilities && (!own(capabilities, channelId)?.includes("output") || node.data.action === "pulse" && !own(capabilities, channelId)?.includes("pulse"))),
|
|
59416
|
+
conflict: control && nodes.some(
|
|
59417
|
+
(other) => other.type === "plugin.wago.command" && other.resourceId !== node.resourceId && other.data.channelId === channelId
|
|
59418
|
+
)
|
|
59419
|
+
};
|
|
59420
|
+
});
|
|
59421
|
+
}
|
|
59422
|
+
var WagoDiagnosticsService = class {
|
|
59423
|
+
constructor(context, wago) {
|
|
59424
|
+
__publicField(this, "context", context);
|
|
59425
|
+
__publicField(this, "wago", wago);
|
|
59426
|
+
}
|
|
59427
|
+
async get(controllerId) {
|
|
59428
|
+
const controller = await this.context.getRepository(WagoController).findOneBy({ id: controllerId });
|
|
59429
|
+
if (!controller) throw new import_common11.NotFoundException("WAGO controller not found");
|
|
59430
|
+
const [draft, latest, applied, nodes] = await Promise.all([
|
|
59431
|
+
this.context.getRepository(WagoConfigurationDraft).findOneBy({ controllerId }),
|
|
59432
|
+
this.context.getRepository(WagoConfigurationRevision).findOne({ where: { controllerId }, order: { revision: "DESC" } }),
|
|
59433
|
+
this.context.getRepository(WagoConfigurationRevision).findOne({ where: { controllerId, state: "applied" }, order: { revision: "DESC" } }),
|
|
59434
|
+
this.context.dataSource.getRepository(import_plugins_backend_sdk15.ResourceFlowNode).createQueryBuilder("node").where("node.type LIKE :type", { type: "plugin.wago.%" }).andWhere("node.data ->> 'controllerId' = :controllerId", { controllerId }).take(1001).getMany()
|
|
59435
|
+
]);
|
|
59436
|
+
const runtime = this.wago.diagnostics.read(controllerId);
|
|
59437
|
+
const snapshot = latest ? JSON.parse(latest.snapshot) : null;
|
|
59438
|
+
const appliedSnapshot = applied ? JSON.parse(applied.snapshot) : null;
|
|
59439
|
+
const heartbeatAt = runtime.heartbeatAt ?? controller.lastHeartbeatAt;
|
|
59440
|
+
const heartbeatFreshness = freshness(heartbeatAt);
|
|
59441
|
+
const revisionMismatch = !!latest && (applied?.revision !== latest.revision || runtime.revision !== latest.revision || runtime.activeStream !== void 0 && runtime.contentHash !== latest.contentHash);
|
|
59442
|
+
const connected = runtime.connected === false ? false : heartbeatFreshness === "fresh" || freshness(runtime.stateSourceAt) === "fresh";
|
|
59443
|
+
const references = diagnosticReferences(
|
|
59444
|
+
nodes.slice(0, 1e3),
|
|
59445
|
+
appliedSnapshot?.logicalChannels.map((channel) => channel.id) ?? [],
|
|
59446
|
+
applied?.revision ?? null,
|
|
59447
|
+
Object.fromEntries(appliedSnapshot?.logicalChannels.map((channel) => [channel.id, channel.capabilities]) ?? [])
|
|
59448
|
+
);
|
|
59449
|
+
const validationErrors = draft ? validateSnapshot(JSON.parse(draft.snapshot)) : [];
|
|
59450
|
+
return {
|
|
59451
|
+
controllerId,
|
|
59452
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
59453
|
+
name: controller.name ?? controller.hardwareId,
|
|
59454
|
+
connectivity: controller.trustState !== "claimed" ? "untrusted" : runtime.connected === false ? "disconnected" : connected ? "online" : "stale",
|
|
59455
|
+
heartbeatAt,
|
|
59456
|
+
heartbeatFreshness,
|
|
59457
|
+
runtimeVersion: controller.runtimeVersion,
|
|
59458
|
+
protocolVersion: controller.protocolVersion,
|
|
59459
|
+
capabilities: JSON.parse(controller.capabilities).slice(0, 64),
|
|
59460
|
+
incompatible: !!controller.compatibilityError,
|
|
59461
|
+
sequenceGaps: runtime.activeStream ? runtime.sequenceGaps : null,
|
|
59462
|
+
activeStream: runtime.activeStream ?? null,
|
|
59463
|
+
trackingExhausted: runtime.trackingExhausted,
|
|
59464
|
+
stateConnected: runtime.connected ?? null,
|
|
59465
|
+
stateHardwareAvailable: runtime.hardwareAvailable ?? null,
|
|
59466
|
+
stateSourceAt: runtime.stateSourceAt ?? null,
|
|
59467
|
+
sequenceExplanation: runtime.activeStream ? "Gaps are scoped to boot UUID and message category; duplicates and retired streams are ignored." : "Legacy payloads have no source envelope; sequence gaps and source freshness are unavailable.",
|
|
59468
|
+
configuration: {
|
|
59469
|
+
draftUpdatedAt: draft?.updatedAt ?? null,
|
|
59470
|
+
draftChanged: !!draft && (!latest || configurationHash(JSON.parse(draft.snapshot)) !== latest.contentHash || configurationHash(metadataFromProvenance(draft.presetProvenance)) !== configurationHash(metadataFromProvenance(latest.presetProvenance))),
|
|
59471
|
+
validationErrorCount: validationErrors.length,
|
|
59472
|
+
// Codes originate in our validator. Omit messages and dynamic paths, which can include arbitrary draft values.
|
|
59473
|
+
validationCodes: [...new Set(validationErrors.map((error) => error.code))].slice(0, 50),
|
|
59474
|
+
validationErrors: safeValidationSummaries(validationErrors),
|
|
59475
|
+
rejectionErrors: latest?.rejectionErrors ? safeValidationSummaries(JSON.parse(latest.rejectionErrors)) : latest && runtime.rejection?.revision === latest.revision && runtime.rejection.contentHash === latest.contentHash ? runtime.rejection.errors : [],
|
|
59476
|
+
publishedRevision: latest?.revision ?? null,
|
|
59477
|
+
publishedState: latest?.state ?? null,
|
|
59478
|
+
appliedRevision: applied?.revision ?? null,
|
|
59479
|
+
reportedRevision: runtime.revision ?? null,
|
|
59480
|
+
revisionMismatch,
|
|
59481
|
+
rejected: latest?.state === "rejected"
|
|
59482
|
+
},
|
|
59483
|
+
hardwareReadiness: "unknown",
|
|
59484
|
+
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.",
|
|
59485
|
+
channels: (snapshot?.logicalChannels ?? []).slice(0, 256).map((channel) => {
|
|
59486
|
+
const values = [
|
|
59487
|
+
channel.capabilities.includes("input") ? own(runtime.inputs, channel.id) : void 0,
|
|
59488
|
+
channel.capabilities.includes("output") ? own(runtime.outputs, channel.id) : void 0,
|
|
59489
|
+
channel.capabilities.includes("measurement") ? own(runtime.measurements, channel.id) : void 0,
|
|
59490
|
+
channel.capabilities.includes("measurement") ? own(runtime.cumulativeMeasurements, channel.id) : void 0
|
|
59491
|
+
].filter((value) => value !== void 0);
|
|
59492
|
+
const samples = values.map((value) => {
|
|
59493
|
+
const sourceFreshness = freshness(value.sourceAt);
|
|
59494
|
+
const availabilityReason = controller.trustState !== "claimed" ? "untrusted" : controller.compatibilityError ? "incompatible-runtime" : runtime.trackingExhausted ? "stream-tracking-exhausted" : runtime.connected !== true || !connected ? "disconnected-or-unknown" : runtime.hardwareAvailable === false ? "hardware-unavailable" : revisionMismatch || !applied ? "configuration-mismatch" : own(runtime.faults, channel.id) ? "recent-fault" : freshness(runtime.stateSourceAt) !== "fresh" ? "state-source-unavailable-or-stale" : value.streamId !== runtime.activeStream ? "old-or-legacy-stream" : sourceFreshness !== "fresh" ? `source-${sourceFreshness}` : "current";
|
|
59495
|
+
return { ...value, sourceFreshness, current: availabilityReason === "current", availabilityReason };
|
|
59496
|
+
});
|
|
59497
|
+
return {
|
|
59498
|
+
id: channel.id,
|
|
59499
|
+
profile: channel.profile,
|
|
59500
|
+
capabilities: channel.capabilities,
|
|
59501
|
+
disconnectPolicy: channel.disconnectPolicy,
|
|
59502
|
+
safeState: channel.capabilities.includes("output") ? "off (runtime default)" : "not applicable",
|
|
59503
|
+
samples,
|
|
59504
|
+
current: samples.length > 0 && samples.every((value) => value.current),
|
|
59505
|
+
fault: own(runtime.faults, channel.id) ?? null,
|
|
59506
|
+
acknowledgement: own(runtime.acknowledgements, channel.id) ?? null
|
|
59507
|
+
};
|
|
59508
|
+
}),
|
|
59509
|
+
faults: Object.entries(runtime.faults).map(([channelId, fault]) => ({ channelId, ...fault })),
|
|
59510
|
+
references,
|
|
59511
|
+
referencesTruncated: nodes.length > 1e3,
|
|
59512
|
+
events: runtime.events,
|
|
59513
|
+
limitations: [
|
|
59514
|
+
"Hardware readiness remains unknown. Legacy samples have no source time and are never current.",
|
|
59515
|
+
"Diagnostics retain at most 256 channels, 50 recent events and 15 minutes in this process.",
|
|
59516
|
+
"Resource-page slots are not integrated. Flow links open the affected resource flow; node IDs identify the exact nodes.",
|
|
59517
|
+
"The MQTT SDK exposes neither retained-message flags nor broker connection events. Source age is bounded to 90 seconds; immediate broker-disconnect detection is unavailable.",
|
|
59518
|
+
"Stream tracking keeps at most 16 retired boot UUIDs per controller and six category counters, and fails closed when exhausted. Replayed source times never become receipt times."
|
|
59519
|
+
]
|
|
59520
|
+
};
|
|
59521
|
+
}
|
|
59522
|
+
};
|
|
59523
|
+
WagoDiagnosticsService = __decorateClass([
|
|
59524
|
+
(0, import_common11.Injectable)(),
|
|
59525
|
+
__decorateParam(0, (0, import_common11.Inject)(/* @__PURE__ */ Symbol.for("attraccess.plugin.context"))),
|
|
59526
|
+
__decorateParam(1, (0, import_common11.Inject)(WagoService))
|
|
59527
|
+
], WagoDiagnosticsService);
|
|
59528
|
+
|
|
59529
|
+
// backend/diagnostics.controller.ts
|
|
59530
|
+
var WagoDiagnosticsController = class {
|
|
59531
|
+
constructor(diagnostics) {
|
|
59532
|
+
__publicField(this, "diagnostics", diagnostics);
|
|
59533
|
+
}
|
|
59534
|
+
get(id) {
|
|
59535
|
+
return this.diagnostics.get(id);
|
|
59536
|
+
}
|
|
59537
|
+
};
|
|
59538
|
+
__decorateClass([
|
|
59539
|
+
(0, import_common12.Get)("controllers/:id/diagnostics"),
|
|
59540
|
+
__decorateParam(0, (0, import_common12.Param)("id", import_common12.ParseIntPipe))
|
|
59541
|
+
], WagoDiagnosticsController.prototype, "get", 1);
|
|
59542
|
+
WagoDiagnosticsController = __decorateClass([
|
|
59543
|
+
(0, import_plugins_backend_sdk16.Auth)("resources.update"),
|
|
59544
|
+
(0, import_common12.Controller)("wago"),
|
|
59545
|
+
__decorateParam(0, (0, import_common12.Inject)(WagoDiagnosticsService))
|
|
59546
|
+
], WagoDiagnosticsController);
|
|
59547
|
+
|
|
58645
59548
|
// backend/plugin.ts
|
|
58646
59549
|
var PLUGIN_CONTEXT5 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
|
|
58647
59550
|
var WagoPluginModule = class {
|
|
@@ -58698,7 +59601,7 @@ var plugin = {
|
|
|
58698
59601
|
flowService = new WagoFlowService(context);
|
|
58699
59602
|
return {
|
|
58700
59603
|
module: WagoPluginModule,
|
|
58701
|
-
controllers: [WagoControllerApi, WagoArtifactsController],
|
|
59604
|
+
controllers: [WagoControllerApi, WagoArtifactsController, WagoDiagnosticsController],
|
|
58702
59605
|
providers: [
|
|
58703
59606
|
{ provide: PLUGIN_CONTEXT5, useValue: context },
|
|
58704
59607
|
WagoService,
|
|
@@ -58706,6 +59609,7 @@ var plugin = {
|
|
|
58706
59609
|
WagoArtifactUploadInterceptor,
|
|
58707
59610
|
WagoCommissioningReadiness,
|
|
58708
59611
|
WagoCommissioningService,
|
|
59612
|
+
WagoDiagnosticsService,
|
|
58709
59613
|
{ provide: WagoFlowService, useValue: flowService }
|
|
58710
59614
|
]
|
|
58711
59615
|
};
|